问题描述
如果两者都使用硬件加速(GPU)来执行代码,为什么WebGL比Canvas最快?
If both use hardware acceleration (GPU) to execute code, why WebGL is so most faster than Canvas?
我的意思是,我想知道为什么从代码到处理器的链处于较低水平.
I mean, I want to know why at low level, the chain from the code to the processor.
会发生什么?Canvas/WebGL直接与驱动程序进行通讯,然后与视频卡进行通讯?
What happens? Canvas/WebGL comunicates directly with Drivers and then with Video Card?
推荐答案
画布较慢,因为它是通用的,因此很难将其优化到可以优化WebGL的水平.让我们举一个简单的例子,用 arc绘制一个实心圆
.
Canvas is slower because it's generic and therefore is hard to optimize to the same level that you can optimize WebGL. Let's take a simple example, drawing a solid circle with arc
.
Canvas实际上也使用与WebGL相同的API在GPU之上运行.那么,画圆时画布必须做什么?使用canvas 2d在JavaScript中绘制圆的最小代码为
Canvas actually runs on top of the GPU as well using the same APIs as WebGL. So, what does canvas have to do when you draw an circle? The minimum code to draw an circle in JavaScript using canvas 2d is
ctx.beginPath():
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.fill();
您可以想象内部最简单的实现是
You can imagine internally the simplest implementation is
-
beginPath
创建一个缓冲区(gl.bufferData
) -
arc
为形成圆的三角形生成点,并使用gl.bufferData
上传. -
fill
调用gl.drawArrays
或gl.drawElements
beginPath
creates a buffer (gl.bufferData
)arc
generates the points for triangles that make a circle and uploads withgl.bufferData
.fill
callsgl.drawArrays
orgl.drawElements
但是,请稍等...了解我们对GL工作原理的了解,在步骤2中无法生成点,因为如果我们调用 stroke
而不是 fill
然后,根据我们对GL的工作原理的了解,我们需要针对实心圆(填充)与圆轮廓(描边)的一组不同的点.因此,真正发生的事情更像是
But wait a minute ... knowing what we know about how GL works canvas can't generate the points at step 2 because if we call stroke
instead of fill
then based on what we know about how GL works we need a different set of points for a solid circle (fill) vs an outline of a circle (stroke). So, what really happens is something more like
-
beginPath
创建或重置一些内部缓冲区 -
arc
生成将一个圆圈圈入内部缓冲区的点 -
fill
获取该内部缓冲区中的点,为该内部缓冲区中的点生成正确的三角形集,并将其生成到GL缓冲区中,并使用gl.bufferData
上传它们,调用gl.drawArrays
或gl.drawElements
beginPath
creates or resets some internal bufferarc
generates the points that make a circle into the internal bufferfill
takes the points in that internal buffer, generates the correct set of triangles for the points in that internal buffer into a GL buffer, uploads them withgl.bufferData
, callsgl.drawArrays
orgl.drawElements
如果我们要绘制2个圆会发生什么?可能会重复相同的步骤.
What happens if we want to draw 2 circles? The same steps are likely repeated.
让我们将其与我们在WebGL中所做的进行比较.当然,在WebGL中,我们必须编写自己的着色器(画布具有着色器).我们还必须创建一个缓冲区,并用一个三角形填充该圆,(请注意,由于跳过了点的中间缓冲区,因此已经节省了时间).然后,我们可以调用 gl.drawArrays
或 gl.drawElements
绘制圆.如果我们想画第二个圆?我们只是更新了制服,然后再次跳过其他所有步骤,再次调用 gl.drawArrays
.
Let's compare that to what we would do in WebGL. Of course in WebGL we'd have to write our own shaders (Canvas has its shaders as well). We'd also have to create a buffer and fill it with the triangles for a circle, (note we already saved time as we skipped the intermediate buffer of points). We then can call gl.drawArrays
or gl.drawElements
to draw our circle. And if we want to draw a second circle? We just update a uniform and call gl.drawArrays
again skipping all the other steps.
const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 position;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * position;
}
`;
const fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, 'position');
const colorLoc = gl.getUniformLocation(program, 'u_color');
const matrixLoc = gl.getUniformLocation(program, 'u_matrix');
const positions = [];
const radius = 50;
const numEdgePoints = 64;
for (let i = 0; i < numEdgePoints; ++i) {
const angle0 = (i ) * Math.PI * 2 / numEdgePoints;
const angle1 = (i + 1) * Math.PI * 2 / numEdgePoints;
// make a triangle
positions.push(
0, 0,
Math.cos(angle0) * radius,
Math.sin(angle0) * radius,
Math.cos(angle1) * radius,
Math.sin(angle1) * radius,
);
}
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
gl.useProgram(program);
const projection = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
function drawCircle(x, y, color) {
const mat = m4.translate(projection, [x, y, 0]);
gl.uniform4fv(colorLoc, color);
gl.uniformMatrix4fv(matrixLoc, false, mat);
gl.drawArrays(gl.TRIANGLES, 0, numEdgePoints * 3);
}
drawCircle( 50, 75, [1, 0, 0, 1]);
drawCircle(150, 75, [0, 1, 0, 1]);
drawCircle(250, 75, [0, 0, 1, 1]);
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
一些开发人员可能会考虑这一点,并认为Canvas会缓存缓冲区,以便它可以重用第二次绘制调用中的点.可能是真的,但我对此表示怀疑.为什么?由于canvas API的通用性. fill
,完成所有实际工作的函数不知道点的内部缓冲区中的内容.您可以调用 arc
,然后依次调用 moveTo
, lineTo
,然后 arc
,然后调用 fill 代码>.当我们进入
fill
时,所有这些点都将位于点的内部缓冲区中.
Some devs might look at that and think Canvas caches the buffer so it can just reuse the points on the 2nd draw call. It's possible that's true but I kind of doubt it. Why? Because of the genericness of the canvas api. fill
, the function that does all the real work doesn't know what's in the internal buffer of points. You can call arc
, then moveTo
, lineTo
, then arc
again, then call fill
. All of those points will be in the internal buffer of points when we get to fill
.
const ctx = document.querySelector('canvas').getContext('2d');
ctx.beginPath();
ctx.moveTo(50, 30);
ctx.lineTo(100, 150);
ctx.arc(150, 75, 30, 0, Math.PI * 2);
ctx.fill();
<canvas></canvas>
换句话说,填充需要始终查看所有要点.另一件事,我怀疑弧试图优化大小.如果调用半径为2的 arc
,它产生的点数可能要比半径为2000的 arc
少.点可能是画布缓存点,但是鉴于命中率可能很小,这似乎不太可能
In other words, fill needs to always look at all the points. Another thing, I suspect arc tries to optimize for size. If you call arc
with a radius of 2 it probably generates less points than if you call it with a radius of 2000. It's possible canvas caches the points but given the hit rate would likely be small it seems unlikely.
无论如何,关键是WebGL让我们在较低的级别上进行控制,从而允许您跳过画布无法跳过的步骤.它还可以让您重用画布无法重用的数据.
In any case, the point is WebGL let's you take control at a lower level allowing you skip steps that canvas can't skip. It also lets you reuse data that canvas can't reuse.
实际上,如果我们知道要绘制10000个动画圆,我们甚至可以在WebGL中使用其他选项.我们可以为10000个圆生成点,这是有效的选择.我们也可以使用实例化.这两种技术都比画布快得多,因为在画布中,我们必须调用 arc
10000次,或者以一种或另一种方式,它必须在每帧中为10000个圆生成点,而不是一次首先,它必须调用 gl.drawXXX
10000次,而不是一次.
In fact if we know we want to draw 10000 animated circles we even have other options in WebGL. We could generate the points for 10000 circles which is a valid option. We could also use instancing. Both of those techniques would be vastly faster than canvas since in canvas we'd have to call arc
10000 times and one way or another it would have to generate points for 10000 circles every single frame instead of just once at the beginning and it would have to call gl.drawXXX
10000 times instead of just once.
当然相反,画布很容易.画圆需要3行代码.在WebGL中,由于需要设置和编写着色器,因此可能至少需要60行代码.实际上,上面的示例大约有60行,其中不包括编译和链接着色器的代码(约10行).在该画布的顶部,支持变换,图案,渐变,遮罩等.我们必须在WebGL中添加所有更多代码行才能添加所有选项.因此画布基本上是为了提高WebGL的速度而使用易用性.
Of course the converse is canvas is easy. Drawing the circle took 3 lines of code. In WebGL, because you need to setup and write shaders it probably takes at least 60 lines of code. In fact the example above is about 60 lines not including the code to compile and link shaders (~10 lines). On top of that canvas supports transforms, patterns, gradients, masks, etc. All options we'd have to add with lots more lines of code in WebGL. So canvas is basically trading ease of use for speed over WebGL.
这篇关于为什么WebGL比Canvas快?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!