为什么WebGL比Canvas更快?

13

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

我的意思是,我想知道从代码到处理器的低级链路为什么会出现这种情况。

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


WebGL是OpenGL的子集,与DirectX无关。此外,我不知道如何回答这个问题,因为WebGL通过<canvas>元素工作,所以我不知道你想将其与什么进行比较... - jplatte
我认为你应该删除DirectX部分,因为基于DX版本和GPU,我们会得到不同的结果。此外,在Linux中没有DX,但我们仍然有画布和GL。所以,它更应该是WEB - 浏览器 - 驱动程序 - GPU。如果浏览器使用DX,则需要安装它(我没有看到这个要求),并且怀疑MS是否允许嵌入DX(这是否可能?)。 - DaGhostman Dimitrov
@DaGhostmanDimitrov 在 Windows 机器上,WebGL 是通过 DirectX 执行的(自从 XP SP2 以来已经包含在 Windows 中),使用 ANGLE 项目 - LJᛃ
@jPlatte请看我上面的评论。 - LJᛃ
@LJ_1102 我看起来很蠢吗...谢谢,我不知道那个 :) - DaGhostman Dimitrov
@LJ_1102 这个问题似乎已经完全改写了,但无论如何 - 感谢您提供的信息,我也不知道这一点。 - jplatte
2个回答

17
Canvas因为是通用的,所以难以像WebGL一样进行优化,因此速度较慢。让我们举一个简单的例子,使用arc绘制一个实心圆。
实际上,Canvas也可以使用与WebGL相同的API在GPU上运行。那么,当你绘制一个圆时,Canvas需要做什么呢?使用canvas 2d在JavaScript中绘制一个圆的最小代码为:
ctx.beginPath():
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.fill();

你可以想象,在内部,最简单的实现方式是:
  1. beginPath 创建一个缓冲区 (gl.bufferData)
  2. arc 生成三角形点以绘制圆,并使用 gl.bufferData 上传。
  3. fill 调用 gl.drawArraysgl.drawElements
但请等一下...根据我们对 GL 工作原理的了解,如果我们调用 stroke 而不是 fill,则在第2步时canvas无法生成这些点,因为我们需要针对实心圆 (fill) 和空心圆 (stroke) 的不同点集。所以,真正发生的事情更像是:
  1. beginPath 创建或重置某些内部缓冲区
  2. arc 将生成构成圆的点放入内部缓冲区
  3. fill 获取该内部缓冲区中的点,为该内部缓冲区中的点生成正确的三角形集合到一个 GL 缓冲区中,然后使用 gl.bufferData 上传它们,最后调用 gl.drawArraysgl.drawElements
如果我们要绘制两个圆,那么同样的步骤可能会重复。
让我们将其与在WebGL中所做的进行比较。当然,在WebGL中,我们必须编写自己的着色器(Canvas也有其着色器)。我们还必须创建一个缓冲区并填充它以形成一个圆的三角形(请注意,我们已经节省了时间,因为跳过了中间点的缓冲区)。然后,我们可以调用gl.drawArraysgl.drawElements来绘制我们的圆。如果我们想要绘制第二个圆?我们只需更新一个uniform并再次调用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>

换句话说,fill需要始终查看所有点。另一件事,我怀疑弧尝试优化大小。如果你用半径2调用arc,它可能生成的点比用半径2000调用要少。画布可能会缓存这些点,但考虑到命中率很小,这似乎不太可能。
无论如何,重点是WebGL让你在更低的级别上控制,允许你跳过画布无法跳过的步骤。它也允许您重复使用画布无法重复使用的数据。
事实上,如果我们知道我们想要绘制10000个动画圆,我们甚至有其他选项可以在WebGL中使用。我们可以为10000个圆生成点,这是一个有效的选择。我们还可以使用实例化。这两种技术都比画布快得多,因为在画布中,我们必须调用arc 10000次,而且不管怎样,它每帧都必须为10000个圆生成点,而不仅仅是在开始时生成一次,并且必须调用gl.drawXXX 10000次,而不仅仅是一次。
当然,相反的是,画布很容易。绘制圆只需3行代码。在WebGL中,因为您需要设置和编写着色器,所以它可能需要至少60行代码。事实上,上面的示例大约有60行,不包括编译和链接着色器的代码(约10行)。除此之外,画布还支持变换、图案、渐变、蒙版等等。所有这些选项在WebGL中都需要添加更多的代码行数。因此,画布基本上是为了简便易用而在WebGL上进行速度交易。

4
Canvas不像OpenGL/WebGL一样执行一系列的处理层来将顶点和索引转换为三角形,并在硬件上进行纹理和光照处理...这是速度差异的根本原因...Canvas对应此类公式的所有计算都在CPU上完成,只有最终渲染发送到图形硬件...当尝试在Canvas上合成/动画化大量这样的顶点时,速度差异特别明显与WebGL相比...
遗憾的是,我们即将听到现代OpenGL替代品——Vulkan的公开宣布:Vulkan,其使命包括以比OpenCL/CUDA更加普通的方式公开通用计算,并加入使用多核处理器的技术,这可能会将类似于Canvas的处理转移到硬件上。

许多实现中,包括Safari和Chrome,画布处理已经在硬件上完成。 - gman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接