【问题标题】:WebGL rendering outside of browser paint time浏览器绘制时间之外的 WebGL 渲染
【发布时间】:2025-09-21 06:20:02
【问题描述】:

我们正在构建一个具有一些高渲染负载对象的 WebGL 应用程序。有没有办法我们可以在浏览器绘制时间之外渲染这些对象,即在后台?我们不希望我们的 FPS 下降,并且可以打破我们的渲染过程(在帧之间分割)。

【问题讨论】:

    标签: webgl


    【解决方案1】:

    想到三个想法。

    1. 您可以通过帧缓冲区在多个帧上渲染到纹理,完成后将该纹理渲染到画布。

    const gl = document.querySelector('canvas').getContext('webgl');
    const vs = `
    attribute vec4 position;
    attribute vec2 texcoord;
    varying vec2 v_texcoord;
    void main() {
      gl_Position = position;
      v_texcoord = texcoord;
    }
    `;
    const fs = `
    precision highp float;
    uniform sampler2D tex;
    varying vec2 v_texcoord;
    void main() {
      gl_FragColor = texture2D(tex, v_texcoord);
    }
    `;
    
    // compile shader, link program, look up locations
    const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
    
    // gl.createBuffer, gl.bindBuffer, gl.bufferData
    const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
      position: {
        numComponents: 2,
        data: [
          -1, -1,
           1, -1,
          -1,  1,
          -1,  1,
           1, -1,
           1,  1,
        ],
      },
      texcoord: {
        numComponents: 2,
        data: [
           0,  0,
           1,  0,
           0,  1,
           0,  1,
           1,  0,
           1,  1,
        ],  
      },
    });
    
    // create a framebuffer with a texture and depth buffer
    // same size as canvas
    // gl.createTexture, gl.texImage2D, gl.createFramebuffer
    // gl.framebufferTexture2D
    const framebufferInfo = twgl.createFramebufferInfo(gl);
    
    const infoElem = document.querySelector('#info');
    
    const numDrawSteps = 16;
    let drawStep = 0;
    let time = 0;
    
    // draw over several frames. Return true when ready
    function draw() {
      // draw to texture
      // gl.bindFrambuffer, gl.viewport
      twgl.bindFramebufferInfo(gl, framebufferInfo);
      
      if (drawStep == 0) {
        // on the first step clear and record time
        gl.disable(gl.SCISSOR_TEST);
        gl.clearColor(0, 0, 0, 0);
        gl.clear(gl.COLOR_BUFFER_BIT  | gl.DEPTH_BUFFER_BIT);
        time = performance.now() * 0.001;
      }
      
    
      // this represents drawing something. 
      gl.enable(gl.SCISSOR_TEST);
      
      const halfWidth = framebufferInfo.width / 2;
      const halfHeight = framebufferInfo.height / 2;
      
      const a = time * 0.1 + drawStep
      const x = Math.cos(a      ) * halfWidth + halfWidth;
      const y = Math.sin(a * 1.3) * halfHeight + halfHeight;
    
      gl.scissor(x, y, 16, 16);
      gl.clearColor(
         drawStep / 16,
         drawStep / 6 % 1,
         drawStep / 3 % 1,
         1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      
      drawStep = (drawStep + 1) % numDrawSteps;
      return drawStep === 0;
    }
    
    let frameCount = 0;
    function render() {
      ++frameCount;
      infoElem.textContent = frameCount;
      
      if (draw()) {
        // draw to canvas
        // gl.bindFramebuffer, gl.viewport
        twgl.bindFramebufferInfo(gl, null);
        
        gl.disable(gl.DEPTH_TEST);
        gl.disable(gl.BLEND);
        gl.disable(gl.SCISSOR_TEST);
        
        gl.useProgram(programInfo.program);
        
        // gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
        twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
        
        // gl.uniform...
        twgl.setUniformsAndBindTextures(programInfo, {
          tex: framebufferInfo.attachments[0],
        });
        
        // draw the quad
        gl.drawArrays(gl.TRIANGLES, 0, 6);
      }
      
      requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
    <canvas></canvas>
    <div id="info"></div>
    <script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
    1. 您可以制作 2 个画布。不在 DOM 中的 webgl 画布。您在许多帧上对其进行渲染,完成后使用ctx.drawImage(webglCanvas, ...) 将其绘制到 2D 画布上,这与 #1 基本相同,只是您让浏览器“将该纹理渲染到画布”部分李>

    const ctx = document.querySelector('canvas').getContext('2d');
    const gl = document.createElement('canvas').getContext('webgl');
    const vs = `
    attribute vec4 position;
    attribute vec2 texcoord;
    varying vec2 v_texcoord;
    void main() {
      gl_Position = position;
      v_texcoord = texcoord;
    }
    `;
    const fs = `
    precision highp float;
    uniform sampler2D tex;
    varying vec2 v_texcoord;
    void main() {
      gl_FragColor = texture2D(tex, v_texcoord);
    }
    `;
    
    // compile shader, link program, look up locations
    const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
    
    const infoElem = document.querySelector('#info');
    
    const numDrawSteps = 16;
    let drawStep = 0;
    let time = 0;
    
    // draw over several frames. Return true when ready
    function draw() {  
      if (drawStep == 0) {
        // on the first step clear and record time
        gl.disable(gl.SCISSOR_TEST);
        gl.clearColor(0, 0, 0, 0);
        gl.clear(gl.COLOR_BUFFER_BIT  | gl.DEPTH_BUFFER_BIT);
        time = performance.now() * 0.001;
      }
      
    
      // this represents drawing something. 
      gl.enable(gl.SCISSOR_TEST);
      
      const halfWidth = gl.canvas.width / 2;
      const halfHeight = gl.canvas.height / 2;
      
      const a = time * 0.1 + drawStep
      const x = Math.cos(a      ) * halfWidth + halfWidth;
      const y = Math.sin(a * 1.3) * halfHeight + halfHeight;
    
      gl.scissor(x, y, 16, 16);
      gl.clearColor(
         drawStep / 16,
         drawStep / 6 % 1,
         drawStep / 3 % 1,
         1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      
      drawStep = (drawStep + 1) % numDrawSteps;
      return drawStep === 0;
    }
    
    let frameCount = 0;
    function render() {
      ++frameCount;
      infoElem.textContent = frameCount;
      
      if (draw()) {
        // draw to canvas
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        ctx.drawImage(gl.canvas, 0, 0);
      }
      
      requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
    <canvas></canvas>
    <div id="info"></div>
    <script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
    1. 您可以使用OffscreenCanvas 并在worker 中进行渲染。不过,这仅在 Chrome 中提供。

    请注意,如果您对 GPU 进行 DOS(给 GPU 太多工作),您仍然会影响主线程的响应能力,因为大多数 GPU 不支持抢先式多任务处理。因此,如果您有很多非常繁重的工作,请将其拆分为更小的任务。

    例如,如果您从 shadertoy.com 获取最重的着色器之一,它在以 1920x1080 渲染时以 0.5 fps 运行,即使在屏幕外,它也会强制整个机器以 0.5 fps 运行。要解决此问题,您需要在多个帧上渲染较小的部分。如果它以 0.5 fps 的速度运行,这表明您需要将其分成至少 120 个甚至更多的小部分,以保持主线程的响应速度,而在 120 个较小的部分中,您每 2 秒才能看到一次结果。

    事实上,尝试它会发现一些问题。 Here's Iq's Happy Jumping Example drawn over 960 frames。即使它每帧仅渲染 2160 像素(1920x1080 画布的 2 列),它仍然无法在我的 2018 年末 Macbook Air 上保持 60fps。问题可能是场景的某些部分必须深度递归,并且无法事先知道场景的哪些部分将是。使用有符号距离场的 shadertoy 风格着色器更像是一种玩具(因此是 shaderTOY)而不是实际生产风格技术的一个原因。

    无论如何,关键是如果你给 GPU 做太多工作,你仍然会得到一台无响应的机器。

    【讨论】: