webgl:绘制多个圆圈的最快方法

kin*_*ike 1 glsl webgl

我目前正在绘制数千个圆,实例化一个圆形几何图形(许多三角形)。

在此处输入图片说明

或者,我可以简单地实例一个四边形(2 个三角形),但在片段着色器中使用距离函数和discard.

在此处输入图片说明

哪种方法会更快?-- 绘制许多三角形是否比在片段着色器中进行的计算更昂贵?

gma*_*man 5

最快的方法可能取决于 GPU 和许多其他因素,例如您如何绘制圆圈、2D、3D,您是否将它们混合,您是否使用 z 缓冲区等......但总的来说,三角形较少比多快,少像素比多快。所以......,我们真正能做的就是尝试。

首先让我们只绘制带纹理的四边形,不进行混合。首先,我似乎总是从 WebGL 获得不一致的性能,但在我对 GPU 的测试中,我使用实例化在这个 300x150 画布中以 60fps 获得了 20k-30k 四边形

function main() {
  const gl = document.querySelector('canvas').getContext('webgl');
  const ext = gl.getExtension('ANGLE_instanced_arrays');
  if (!ext) {
    return alert('need ANGLE_instanced_arrays');
  }
  twgl.addExtensionsToContext(gl);
  
  const vs = `
  attribute float id;
  attribute vec4 position;
  attribute vec2 texcoord;
  
  uniform float time;
  
  varying vec2 v_texcoord;
  varying vec4 v_color;
  
  void main() {
    float o = id + time;
    gl_Position = position + vec4(
        vec2(
             fract(o * 0.1373),
             fract(o * 0.5127)) * 2.0 - 1.0,
        0, 0);
        
    v_texcoord = texcoord;
    v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
  }`;
  
  const fs = `
  precision mediump float;
  varying vec2 v_texcoord;
  varying vec4 v_color;
  uniform sampler2D tex;
  void main() {
    gl_FragColor = texture2D(tex, v_texcoord) * v_color;
  }
  `; 
  
  // compile shaders, link program, look up locations
  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

  const maxCount = 250000;
  const ids = new Float32Array(maxCount);
  for (let i = 0; i < ids.length; ++i) {
    ids[i] = i;
  }
  const x = 16 / 300 * 2;
  const y = 16 / 150 * 2;
  
  const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
    position: {
      numComponents: 2,
      data: [
       -x, -y,
        x, -y,
       -x,  y,
       -x,  y,
        x, -y,
        x,  y,
    	],
    },
    texcoord: [
        0, 1,
        1, 1,
        0, 0,
        0, 0,
        1, 1,
        1, 0,    
    ],
    id: {
      numComponents: 1,
      data: ids,
      divisor: 1,
    }
  });
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  
  {
    const ctx = document.createElement('canvas').getContext('2d');
    ctx.canvas.width = 32;
    ctx.canvas.height = 32;
    ctx.fillStyle = 'white';
    ctx.beginPath();
    ctx.arc(16, 16, 15, 0, Math.PI * 2);
    ctx.fill();
    const tex = twgl.createTexture(gl, { src: ctx.canvas });
  }
  
  const fpsElem = document.querySelector('#fps');
  const countElem = document.querySelector('#count');
  
  let count;  
  function getCount() {
    count = Math.min(maxCount, parseInt(countElem.value));
  }
  
  countElem.addEventListener('input', getCount);
  getCount();
  
  const maxHistory = 60;
  const fpsHistory = new Array(maxHistory).fill(0);
  let historyNdx = 0;
  let historyTotal = 0;
  
  let then = 0;
  function render(now) {
    const deltaTime = now - then;
    then = now;
    
    historyTotal += deltaTime - fpsHistory[historyNdx];
    fpsHistory[historyNdx] = deltaTime;
    historyNdx = (historyNdx + 1) % maxHistory;
    
    fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
    
    gl.useProgram(programInfo.program);
    twgl.setUniforms(programInfo, {time: now * 0.001});
    ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
Run Code Online (Sandbox Code Playgroud)
canvas { display: block; border: 1px solid black; }
Run Code Online (Sandbox Code Playgroud)
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
Run Code Online (Sandbox Code Playgroud)

而且我使用重复几何而不是实例化以 60fps 获得相同的性能。这让我感到惊讶,因为 7-8 年前,当我测试重复几何图形时,速度提高了 20-30%。无论是因为现在拥有更好的 GPU 还是更好的驱动程序,还是我不知道的原因。

function main() {
  const gl = document.querySelector('canvas').getContext('webgl');
  
  const vs = `
  attribute float id;
  attribute vec4 position;
  attribute vec2 texcoord;
  
  uniform float time;
  
  varying vec2 v_texcoord;
  varying vec4 v_color;
  
  void main() {
    float o = id + time;
    gl_Position = position + vec4(
        vec2(
             fract(o * 0.1373),
             fract(o * 0.5127)) * 2.0 - 1.0,
        0, 0);
        
    v_texcoord = texcoord;
    v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
  }`;
  
  const fs = `
  precision mediump float;
  varying vec2 v_texcoord;
  varying vec4 v_color;
  uniform sampler2D tex;
  void main() {
    gl_FragColor = texture2D(tex, v_texcoord) * v_color;
  }
  `; 
  
  // compile shaders, link program, look up locations
  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

  const maxCount = 250000;
  const x = 16 / 300 * 2;
  const y = 16 / 150 * 2;
  
  const quadPositions = [
     -x, -y,
      x, -y,
     -x,  y,
     -x,  y,
      x, -y,
      x,  y,
  ];
  const quadTexcoords = [
      0, 1,
      1, 1,
      0, 0,
      0, 0,
      1, 1,
      1, 0,    
  ];
  const positions = new Float32Array(maxCount * 2 * 6);
  const texcoords = new Float32Array(maxCount * 2 * 6);
  for (let i = 0; i < maxCount; ++i) {
    const off = i * 2 * 6;
    positions.set(quadPositions, off);
    texcoords.set(quadTexcoords, off);
  }
  const ids = new Float32Array(maxCount * 6);
  for (let i = 0; i < ids.length; ++i) {
    ids[i] = i / 6 | 0;
  }
      
  const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
    position: {
      numComponents: 2,
      data: positions,
    },
    texcoord: texcoords,
    id: {
      numComponents: 1,
      data: ids,
    }
  });
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  
  {
    const ctx = document.createElement('canvas').getContext('2d');
    ctx.canvas.width = 32;
    ctx.canvas.height = 32;
    ctx.fillStyle = 'white';
    ctx.beginPath();
    ctx.arc(16, 16, 15, 0, Math.PI * 2);
    ctx.fill();
    const tex = twgl.createTexture(gl, { src: ctx.canvas });
  }
  
  const fpsElem = document.querySelector('#fps');
  const countElem = document.querySelector('#count');
  
  let count;  
  function getCount() {
    count = Math.min(maxCount, parseInt(countElem.value));
  }
  
  countElem.addEventListener('input', getCount);
  getCount();
  
  const maxHistory = 60;
  const fpsHistory = new Array(maxHistory).fill(0);
  let historyNdx = 0;
  let historyTotal = 0;
  
  let then = 0;
  function render(now) {
    const deltaTime = now - then;
    then = now;
    
    historyTotal += deltaTime - fpsHistory[historyNdx];
    fpsHistory[historyNdx] = deltaTime;
    historyNdx = (historyNdx + 1) % maxHistory;
    
    fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
    
    gl.useProgram(programInfo.program);
    twgl.setUniforms(programInfo, {time: now * 0.001});
    gl.drawArrays(gl.TRIANGLES, 0, 6 * count);
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
Run Code Online (Sandbox Code Playgroud)
canvas { display: block; border: 1px solid black; }
Run Code Online (Sandbox Code Playgroud)
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
Run Code Online (Sandbox Code Playgroud)

接下来是纹理或在片段着色器中计算一个圆。

function main() {
  const gl = document.querySelector('canvas').getContext('webgl');
  const ext = gl.getExtension('ANGLE_instanced_arrays');
  if (!ext) {
    return alert('need ANGLE_instanced_arrays');
  }
  twgl.addExtensionsToContext(gl);
  
  const vs = `
  attribute float id;
  attribute vec4 position;
  attribute vec2 texcoord;
  
  uniform float time;
  
  varying vec2 v_texcoord;
  varying vec4 v_color;
  
  void main() {
    float o = id + time;
    gl_Position = position + vec4(
        vec2(
             fract(o * 0.1373),
             fract(o * 0.5127)) * 2.0 - 1.0,
        0, 0);
        
    v_texcoord = texcoord;
    v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
  }`;
  
  const fs = `
  precision mediump float;
  varying vec2 v_texcoord;
  varying vec4 v_color;
  void main() {
    gl_FragColor = mix(
       v_color, 
       vec4(0), 
       step(1.0, length(v_texcoord.xy * 2. - 1.)));
  }
  `; 
  
  // compile shaders, link program, look up locations
  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

  const maxCount = 250000;
  const ids = new Float32Array(maxCount);
  for (let i = 0; i < ids.length; ++i) {
    ids[i] = i;
  }
  const x = 16 / 300 * 2;
  const y = 16 / 150 * 2;
  
  const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
    position: {
      numComponents: 2,
      data: [
       -x, -y,
        x, -y,
       -x,  y,
       -x,  y,
        x, -y,
        x,  y,
    	],
    },
    texcoord: [
        0, 1,
        1, 1,
        0, 0,
        0, 0,
        1, 1,
        1, 0,    
    ],
    id: {
      numComponents: 1,
      data: ids,
      divisor: 1,
    }
  });
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  
  const fpsElem = document.querySelector('#fps');
  const countElem = document.querySelector('#count');
  
  let count;  
  function getCount() {
    count = Math.min(maxCount, parseInt(countElem.value));
  }
  
  countElem.addEventListener('input', getCount);
  getCount();
  
  const maxHistory = 60;
  const fpsHistory = new Array(maxHistory).fill(0);
  let historyNdx = 0;
  let historyTotal = 0;
  
  let then = 0;
  function render(now) {
    const deltaTime = now - then;
    then = now;
    
    historyTotal += deltaTime - fpsHistory[historyNdx];
    fpsHistory[historyNdx] = deltaTime;
    historyNdx = (historyNdx + 1) % maxHistory;
    
    fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
    
    gl.useProgram(programInfo.program);
    twgl.setUniforms(programInfo, {time: now * 0.001});
    ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
Run Code Online (Sandbox Code Playgroud)
canvas { display: block; border: 1px solid black; }
Run Code Online (Sandbox Code Playgroud)
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
Run Code Online (Sandbox Code Playgroud)

我没有得到可测量的差异。试试你的圈子功能

function main() {
  const gl = document.querySelector('canvas').getContext('webgl');
  const ext = gl.getExtension('ANGLE_instanced_arrays');
  if (!ext) {
    return alert('need ANGLE_instanced_arrays');
  }
  twgl.addExtensionsToContext(gl);
  
  const vs = `
  attribute float id;
  attribute vec4 position;
  attribute vec2 texcoord;
  
  uniform float time;
  
  varying vec2 v_texcoord;
  varying vec4 v_color;
  
  void main() {
    float o = id + time;
    gl_Position = position + vec4(
        vec2(
             fract(o * 0.1373),
             fract(o * 0.5127)) * 2.0 - 1.0,
        0, 0);
        
    v_texcoord = texcoord;
    v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
  }`;
  
  const fs = `
  precision mediump float;
  varying vec2 v_texcoord;
  varying vec4 v_color;
  
  float circle(in vec2 st, in float radius) {
    vec2 dist = st - vec2(0.5);
    return 1.0 - smoothstep(
       radius - (radius * 0.01),
       radius +(radius * 0.01),
       dot(dist, dist) * 4.0);
  }
  
  void main() {
    gl_FragColor = mix(
       vec4(0), 
       v_color, 
       circle(v_texcoord, 1.0));
  }
  `; 
  
  // compile shaders, link program, look up locations
  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

  const maxCount = 250000;
  const ids = new Float32Array(maxCount);
  for (let i = 0; i < ids.length; ++i) {
    ids[i] = i;
  }
  const x = 16 / 300 * 2;
  const y = 16 / 150 * 2;
  
  const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
    position: {
      numComponents: 2,
      data: [
       -x, -y,
        x, -y,
       -x,  y,
       -x,  y,
        x, -y,
        x,  y,
    	],
    },
    texcoord: [
        0, 1,
        1, 1,
        0, 0,
        0, 0,
        1, 1,
        1, 0,    
    ],
    id: {
      numComponents: 1,
      data: ids,
      divisor: 1,
    }
  });
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  
  const fpsElem = document.querySelector('#fps');
  const countElem = document.querySelector('#count');
  
  let count;  
  function getCount() {
    count = Math.min(maxCount, parseInt(countElem.value));
  }
  
  countElem.addEventListener('input', getCount);
  getCount();
  
  const maxHistory = 60;
  const fpsHistory = new Array(maxHistory).fill(0);
  let historyNdx = 0;
  let historyTotal = 0;
  
  let then = 0;
  function render(now) {
    const deltaTime = now - then;
    then = now;
    
    historyTotal += deltaTime - fpsHistory[historyNdx];
    fpsHistory[historyNdx] = deltaTime;
    historyNdx = (historyNdx + 1) % maxHistory;
    
    fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
    
    gl.useProgram(programInfo.program);
    twgl.setUniforms(programInfo, {time: now * 0.001});
    ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
Run Code Online (Sandbox Code Playgroud)
canvas { display: block; border: 1px solid black; }
Run Code Online (Sandbox Code Playgroud)
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
Run Code Online (Sandbox Code Playgroud)

我再次得到没有可测量的差异。注意:就像我上面说的那样,我在 WebGL 中得到了非常不一致的结果。当我运行第一次测试时,我以 60fps 的速度获得了 28k。当我跑第二个时,我得到了 23k。我很惊讶,因为我预计第二个会更快,所以我又跑了第一个,只得到了 23k。最后一个我得到了 29k,再次感到惊讶,但后来我回去做了前一个并得到了 29k。基本上这意味着在 WebGL 中测试时间几乎是不可能的。鉴于一切都是多过程的,因此有很多活动部件,获得恒定的结果似乎是不可能的。

可以尝试丢弃

function main() {
  const gl = document.querySelector('canvas').getContext('webgl');
  const ext = gl.getExtension('ANGLE_instanced_arrays');
  if (!ext) {
    return alert('need ANGLE_instanced_arrays');
  }
  twgl.addExtensionsToContext(gl);
  
  const vs = `
  attribute float id;
  attribute vec4 position;
  attribute vec2 texcoord;
  
  uniform float time;
  
  varying vec2 v_texcoord;
  varying vec4 v_color;
  
  void main() {
    float o = id + time;
    gl_Position = position + vec4(
        vec2(
             fract(o * 0.1373),
             fract(o * 0.5127)) * 2.0 - 1.0,
        0, 0);
        
    v_texcoord = texcoord;
    v_color = vec4(fract(vec3(id) * vec3(0.127, 0.373, 0.513)), 1);
  }`;
  
  const fs = `
  precision mediump float;
  varying vec2 v_texcoord;
  varying vec4 v_color;
  
  float circle(in vec2 st, in float radius) {
    vec2 dist = st - vec2(0.5);
    return 1.0 - smoothstep(
       radius - (radius * 0.01),
       radius +(radius * 0.01),
       dot(dist, dist) * 4.0);
  }
  
  void main() {
    if (circle(v_texcoord, 1.0) < 0.5) {
      discard;
    }
    gl_FragColor = v_color;
  }
  `; 
  
  // compile shaders, link program, look up locations
  const programInfo = twgl.createProgramInfo(gl, [vs, fs]);

  const maxCount = 250000;
  const ids = new Float32Array(maxCount);
  for (let i = 0; i < ids.length; ++i) {
    ids[i] = i;
  }
  const x = 16 / 300 * 2;
  const y = 16 / 150 * 2;
  
  const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
    position: {
      numComponents: 2,
      data: [
       -x, -y,
        x, -y,
       -x,  y,
       -x,  y,
        x, -y,
        x,  y,
    	],
    },
    texcoord: [
        0, 1,
        1, 1,
        0, 0,
        0, 0,
        1, 1,
        1, 0,    
    ],
    id: {
      numComponents: 1,
      data: ids,
      divisor: 1,
    }
  });
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  
  const fpsElem = document.querySelector('#fps');
  const countElem = document.querySelector('#count');
  
  let count;  
  function getCount() {
    count = Math.min(maxCount, parseInt(countElem.value));
  }
  
  countElem.addEventListener('input', getCount);
  getCount();
  
  const maxHistory = 60;
  const fpsHistory = new Array(maxHistory).fill(0);
  let historyNdx = 0;
  let historyTotal = 0;
  
  let then = 0;
  function render(now) {
    const deltaTime = now - then;
    then = now;
    
    historyTotal += deltaTime - fpsHistory[historyNdx];
    fpsHistory[historyNdx] = deltaTime;
    historyNdx = (historyNdx + 1) % maxHistory;
    
    fpsElem.textContent = (1000 / (historyTotal / maxHistory)).toFixed(1);
    
    gl.useProgram(programInfo.program);
    twgl.setUniforms(programInfo, {time: now * 0.001});
    ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, count);
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
Run Code Online (Sandbox Code Playgroud)
canvas { display: block; border: 1px solid black; }
Run Code Online (Sandbox Code Playgroud)
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
<div>fps: <span id="fps"></span></div>
<div>count: <input type="number" id="count" min="0" max="1000000" value="25000"></div>
Run Code Online (Sandbox Code Playgroud)

鉴于时间不一致,我无法确定,但我的印象是丢弃速度较慢。IIRC 丢弃很慢,因为如果没有丢弃,GPU 甚至在执行片段着色器之前就知道它将更新 z 缓冲区,而与丢弃一样,它直到着色器执行之后才知道,这种差异意味着某些事情不能也要优化。

我要到此为止,因为要尝试的事物组合太多了。

我们可以尝试混合。混合通常也较慢,因为它必须混合(读取背景)但它比丢弃慢吗?我不知道。

你有深度测试吗?如果是这样,那么绘制顺序将很重要。

另一件要测试的事情是使用非四边形,如六边形或八边形,因为它们会通过片段着色器运行更少的像素。我怀疑你可能需要把圆圈放大才能看到,但如果我们有一个 100x100 像素的四边形,那就是 10k 像素。如果我们有完美的圆形几何图形,大约 pi*r^2 或 ~7853 或少 21% 的像素。六边形大约为 8740 像素或少 11%。介于两者之间的八边形。少画 11% 到 21% 的像素通常是一个胜利,但当然,对于六边形,你会多画 3 倍的三角形,如果是八边形,你会多画 4 倍。您基本上必须测试所有这些情况。

这指出了另一个问题,我相信你会在更大的画布上用更大的圆圈得到不同的相对结果,因为每个圆圈会有更多的像素,所以对于任何给定数量的圆圈,绘制更多的时间将花费在绘制像素和更少的计算顶点和/或更少的时间重新启动 GPU 来绘制下一个圆。

更新

在 Chrome 和 Firefox 上测试我在同一台机器上的 Chrome 中在所有情况下都得到了 60k-66k。鉴于 WebGL 本身几乎什么都不做,不知道为什么差异如此巨大。所有 4 个测试每帧只有一个绘制调用。但无论如何,至少在 2019-10 年,Chrome 在这种特殊情况下的速度是 Firefox 的两倍多

一个想法是我有一台双 GPU 笔记本电脑。当你创建上下文时,你可以通过传入powerPreference上下文创建属性来告诉 WebGL 你的目标是什么

const gl = document.createContext('webgl', {
  powerPreference: 'high-performance',
});
Run Code Online (Sandbox Code Playgroud)

选项是“默认”、“低功耗”、“高性能”。'default' 的意思是“让浏览器决定”,但最终所有的意思都是“让浏览器决定”。在任何情况下,上面的设置对我来说都没有改变 Firefox 中的任何内容。