为什么WebGL比Canvas更快?

问题描述 投票:4回答:2

如果两者都使用硬件加速(GPU)来执行代码,为什么WebGL比Canvas更快?

我的意思是,我想知道为什么在低级别,从代码到处理器的链条。

怎么了? Canvas / WebGL直接与驱动程序通信,然后与视频卡通信?

html5 performance canvas webgl gpu
2个回答
3
投票

Canvas不会执行一系列处理层来将顶点和索引集转换为三角形,然后像OpenGL / WebGL一样在硬件中给出纹理和光照......这就是这种速度差异的根本原因... Canvas这些配方的对应物都是在CPU上完成的,只有最终的渲染发送到图形硬件......当尝试在Canvas上对WebGL进行合成/动画时,速度差异特别明显......

唉,我们正在听取公开宣布现代替代OpenGL的消息:Vulkan的职责范围包括以比OpenCL / CUDA更为行人的方式公开通用计算,以及烘焙使用可能只是转移的多核处理器Canvas喜欢处理硬件


2
投票

Canvas速度较慢,因为它是通用的,因此难以优化到可以优化WebGL的相同级别。让我们举一个简单的例子,用arc绘制一个实心圆。

Canvas实际上运行在GPU之上,使用与WebGL相同的API。那么,当你画一个圆圈时画布必须做什么?使用canvas 2d在JavaScript中绘制圆圈的最小代码是

ctx.beginPath():
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.fill();

你可以在内部想象最简单的实现

  1. beginPath创建缓冲区(gl.bufferData
  2. arc生成三角形的点,这些三角形形成一个圆圈并使用gl.bufferData上传。
  3. fillgl.drawArraysgl.drawElements

但是等一下......知道我们对GL如何工作的了解画布不能在第2步生成点,因为如果我们调用stroke而不是fill然后根据我们对GL如何工作的了解我们需要一组不同的点对于实心圆(填充)与圆形轮廓(笔划)。所以,真正发生的事情更像是

  1. beginPath创建或重置一些内部缓冲区
  2. arc生成将圆圈放入内部缓冲区的点
  3. fill获取内部缓冲区中的点,为该内部缓冲区中的点生成正确的三角形集合到GL缓冲区中,使用gl.bufferData上传它们,调用gl.drawArraysgl.drawElements

如果我们想绘制2个圆圈会怎样?可能会重复相同的步骤。

让我们将它与我们在WebGL中的操作进行比较。当然在WebGL中我们必须编写自己的着色器(Canvas has its shaders as well)。我们还必须创建一个缓冲区并用圆形三角形填充它(注意我们已经节省了时间,因为我们跳过了点的中间缓冲区)。然后我们可以打电话给gl.drawArraysgl.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,然后是moveTolineTo,然后是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更易于使用。

© www.soinside.com 2019 - 2024. All rights reserved.