Aca*_*uza 5 performance html5 gpu canvas webgl
如果两者都使用硬件加速(GPU)执行代码,为什么WebGL比Canvas最快?
我的意思是,我想知道为什么从代码到处理器的链处于较低水平。
怎么了?Canvas / WebGL直接与驱动程序进行通信,然后与视频卡进行通信?
Canvas 较慢,因为它是通用的,因此很难优化到与优化 WebGL 相同的水平。让我们举一个简单的例子,用 绘制一个实心圆arc
。
Canvas 实际上也运行在 GPU 之上,并使用与 WebGL 相同的 API。那么,画圆的时候canvas有什么作用呢?使用 canvas 2d 在 JavaScript 中绘制圆的最小代码是
ctx.beginPath():
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.fill();
Run Code Online (Sandbox Code Playgroud)
你可以在内部想象最简单的实现是
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 也有它的着色器)。我们还必须创建一个缓冲区并用三角形填充它作为圆,(注意我们已经节省了时间,因为我们跳过了中间的点缓冲区)。然后我们可以调用gl.drawArrays
或gl.drawElements
绘制我们的圆圈。如果我们想画第二个圆圈?我们只是更新制服并gl.drawArrays
再次调用跳过所有其他步骤。
ctx.beginPath():
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.fill();
Run Code Online (Sandbox Code Playgroud)
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]);
Run Code Online (Sandbox Code Playgroud)
一些开发人员可能会看到并认为 Canvas 缓存缓冲区,因此它可以在第二次绘制调用中重用这些点。这可能是真的,但我有点怀疑。为什么?由于画布 api 的通用性。fill
,完成所有实际工作的函数不知道点的内部缓冲区中有什么。你可以打电话arc
,然后moveTo
,lineTo
,然后arc
再次,然后调用fill
。当我们到达 时,所有这些点都将在点的内部缓冲区中fill
。
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
Run Code Online (Sandbox Code Playgroud)
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();
Run Code Online (Sandbox Code Playgroud)
换句话说,填充需要始终查看所有点。另一件事,我怀疑 arc 试图优化大小。如果你arc
用半径 2调用它可能比你用半径 2000 调用它生成的点少。画布可能缓存点,但考虑到命中率可能很小,这似乎不太可能。
在任何情况下,关键是 WebGL 让您在较低级别进行控制,允许您跳过画布无法跳过的步骤。它还允许您重用画布无法重用的数据。
事实上,如果我们知道我们想要绘制 10000 个动画圆,我们甚至在 WebGL 中还有其他选择。我们可以生成 10000 个圆的点,这是一个有效的选项。我们也可以使用实例化。这两种技术都比画布快得多,因为在画布中我们必须调用arc
10000 次,而且以一种或另一种方式,它必须在每一帧中为 10000 个圆生成点,而不是在开始时只生成一次,它必须调用gl.drawXXX
10000 次而不是一次。
当然相反的是canvas很容易。绘制圆圈需要 3 行代码。在 WebGL 中,因为您需要设置和编写着色器,所以它可能至少需要 60 行代码。事实上,上面的例子大约有 60 行,不包括编译和链接着色器的代码(约 10 行)。除此之外,画布还支持变换、图案、渐变、蒙版等。我们必须在 WebGL 中添加更多行代码来添加所有选项。因此,canvas 基本上是通过 WebGL 来换取易用性以换取速度。