【问题标题】:webgl: fastest approach to drawing many circleswebgl:绘制许多圆圈的最快方法
【发布时间】:2019-10-12 12:24:34
【问题描述】:

我目前正在绘制数千个圆圈,实例化 circle geometry(许多三角形)。

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

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

【问题讨论】:

    标签: glsl webgl


    【解决方案1】:

    最快的方法可能取决于 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();
    canvas { display: block; border: 1px solid black; }
    <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>

    而且我使用重复几何而不是实例化获得了 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();
    canvas { display: block; border: 1px solid black; }
    <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>

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

    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();
    canvas { display: block; border: 1px solid black; }
    <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>

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

    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();
    canvas { display: block; border: 1px solid black; }
    <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>

    我再次没有得到可测量的差异。注意:就像我上面所说的,我在 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();
    canvas { display: block; border: 1px solid black; }
    <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>

    鉴于时间不一致,我无法确定,但我的印象是丢弃速度较慢。 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',
    });
    

    选项有“默认”、“低功耗”、“高性能”。 “默认”的意思是“让浏览器决定”,但最终它们都意味着“让浏览器决定”。无论如何,上面的设置对我来说并没有改变 Firefox 中的任何内容。

    【讨论】:

    • 很有趣... -- 感谢您付出所有努力来测试所有这些!我想也许有一个明确的答案(我对 3d 图形的内部工作原理不太了解),但如果是“它取决于太多东西,所以很难真正知道”的情况,我会可能停止尝试在这里优化。我已经根据需要(卸载)加载圆圈表示的数据,因此我想尽量保持显示的圆圈总数尽可能低。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-07-11
    • 2015-02-09
    • 2013-02-04
    • 2020-09-14
    • 2016-04-10
    • 1970-01-01
    • 2015-12-15
    相关资源
    最近更新 更多