如果两者都使用硬件加速(GPU)来执行代码,为什么WebGL比Canvas更快?
我的意思是,我想知道为什么在低级别,从代码到处理器的链条。
怎么了? Canvas / WebGL直接与驱动程序通信,然后与视频卡通信?
Canvas不会执行一系列处理层来将顶点和索引集转换为三角形,然后像OpenGL / WebGL一样在硬件中给出纹理和光照......这就是这种速度差异的根本原因... Canvas这些配方的对应物都是在CPU上完成的,只有最终的渲染发送到图形硬件......当尝试在Canvas上对WebGL进行合成/动画时,速度差异特别明显......
唉,我们正在听取公开宣布现代替代OpenGL的消息:Vulkan的职责范围包括以比OpenCL / CUDA更为行人的方式公开通用计算,以及烘焙使用可能只是转移的多核处理器Canvas喜欢处理硬件
Canvas速度较慢,因为它是通用的,因此难以优化到可以优化WebGL的相同级别。让我们举一个简单的例子,用arc
绘制一个实心圆。
Canvas实际上运行在GPU之上,使用与WebGL相同的API。那么,当你画一个圆圈时画布必须做什么?使用canvas 2d在JavaScript中绘制圆圈的最小代码是
ctx.beginPath():
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.fill();
你可以在内部想象最简单的实现
beginPath
创建缓冲区(gl.bufferData
)arc
生成三角形的点,这些三角形形成一个圆圈并使用gl.bufferData
上传。fill
称gl.drawArrays
或gl.drawElements
但是等一下......知道我们对GL如何工作的了解画布不能在第2步生成点,因为如果我们调用stroke
而不是fill
然后根据我们对GL如何工作的了解我们需要一组不同的点对于实心圆(填充)与圆形轮廓(笔划)。所以,真正发生的事情更像是
beginPath
创建或重置一些内部缓冲区arc
生成将圆圈放入内部缓冲区的点fill
获取内部缓冲区中的点,为该内部缓冲区中的点生成正确的三角形集合到GL缓冲区中,使用gl.bufferData
上传它们,调用gl.drawArrays
或gl.drawElements
如果我们想绘制2个圆圈会怎样?可能会重复相同的步骤。
让我们将它与我们在WebGL中的操作进行比较。当然在WebGL中我们必须编写自己的着色器(Canvas has its shaders as well)。我们还必须创建一个缓冲区并用圆形三角形填充它(注意我们已经节省了时间,因为我们跳过了点的中间缓冲区)。然后我们可以打电话给gl.drawArrays
或gl.drawElements
画我们的圈子。如果我们想绘制第二个圆圈?我们只是更新制服并再次调用gl.drawArrays
跳过所有其他步骤。
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
时,所有这些点都将在点的内部缓冲区中。
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>
换句话说,填充需要始终查看所有点。另一件事,我怀疑arc试图优化尺寸。如果你调用半径为2的arc
它可能比你用半径2000调用它产生更少的点数。可能画布缓存点但是如果命中率可能很小似乎不太可能。
在任何情况下,重点是WebGL让你在较低级别控制,允许你跳过画布无法跳过的步骤。它还允许您重用canvas无法重用的数据。
事实上,如果我们知道我们想绘制10000个动画圆圈,我们甚至在WebGL中有其他选项。我们可以为10000个圆圈生成积分,这是一个有效的选项。我们也可以使用实例化。这两种技术都比帆布快得多,因为在画布中我们必须调用arc
10000次,不管怎样,它必须为每一帧生成10000个圆圈的点,而不是在开始时只生成一次,它会有拨打gl.drawXXX
10000次而不是一次。
当然,相反的是帆布很容易。绘制圆圈需要3行代码。在WebGL中,因为您需要设置和编写着色器,所以它可能需要至少60行代码。实际上上面的例子大约是60行,不包括编译和链接着色器的代码(~10行)。在该画布的顶部支持变换,模式,渐变,蒙版等。我们必须在WebGL中添加更多行代码。因此,canvas基本上比WebGL更易于使用。