对半透明对象实施深度测试

时间:2018-06-06 12:34:10

标签: c++ opengl-es webgl transparency depth-buffer

过去两天我一直在小心地浏览互联网,以了解半透明物体的深度测试。我已经阅读了关于这个主题的多篇论文/教程,理论上我相信我理解它是如何工作的。但是,它们都没有给我实际的示例代码。

我对半透明物体的深度测试有三个要求:

  1. 它应该是独立的订单。

  2. 如果相同对象的两个四边形相互交叉,它应该有效。两者都是半透明的。想象一下从上面看时看起来像X的草对象:

  3. enter image description here

    1. 它应该在建筑物的窗口rgba(0, 1, 0, 0.5)后面,但在背景物体rgba(0, 0, 1, 0.5)前面正确渲染半透明的玩家rgba(1, 0, 0, 1)
    2. enter image description here

      最左边的线是我想象光线/颜色在透过半透明物体朝向相机时的变化

      最后的想法

      我怀疑最好的方法是做深度剥离,但我仍然缺乏一些实施/例子。我倾向于这种方法,因为游戏是2.5D,因为它可能会对性能造成危险(许多层要剥离),所以不需要两个以上的半透明物体来“剥离”。

      我已经熟悉framebuffers以及如何对它们进行编码(用它们做一些后处理效果)。我将使用它们,对吧?

      opengl的大部分知识来自this tutorial,但它分别涵盖了深度测试和半透明度。遗憾的是,他根本没有涵盖与订单无关的透明度(参见混合页面的底部)。

      最后,请不要只在理论上回答。例如

        

      绘制不透明,绘制透明,再次绘制不透明等等。

      我理想的答案将包含缓冲区配置的代码,每个过程的着色器和屏幕截图,并解释它的作用。

      使用的编程语言也不太重要,只要它使用OpenGL 4或更新版本。非opengl代码可以是伪的(我不关心你如何对数组进行排序或创建GLFW窗口)。

      编辑:

      我正在更新我的问题,只是举例说明我的代码的当前状态。此示例首先绘制半透明播放器(绿色),然后绘制不透明背景(红色),然后绘制半透明窗口(蓝色)。但是,深度应该通过正方形的Z位置而不是它的绘制顺序来计算。

      (function() {
         // your page initialization code here
         // the DOM will be available here
        var script = document.createElement('script');
        script.onload = function () {
          main();
        };
        script.src = 'https://mdn.github.io/webgl-examples/tutorial/gl-matrix.js';
        document.head.appendChild(script); //or something of the likes
      })();
      
      //
      // Start here
      //
      function main() {
        const canvas = document.querySelector('#glcanvas');
        const gl = canvas.getContext('webgl', {alpha:false});
      
        // If we don't have a GL context, give up now
      
        if (!gl) {
          alert('Unable to initialize WebGL. Your browser or machine may not support it.');
          return;
        }
      
        // Vertex shader program
      
        const vsSource = `
          attribute vec4 aVertexPosition;
          attribute vec4 aVertexColor;
      
          uniform mat4 uModelViewMatrix;
          uniform mat4 uProjectionMatrix;
      
          varying lowp vec4 vColor;
      
          void main(void) {
            gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
            vColor = aVertexColor;
          }
        `;
      
        // Fragment shader program
      
        const fsSource = `
          varying lowp vec4 vColor;
      
          void main(void) {
            gl_FragColor = vColor;
          }
        `;
      
        // Initialize a shader program; this is where all the lighting
        // for the vertices and so forth is established.
        const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
      
        // Collect all the info needed to use the shader program.
        // Look up which attributes our shader program is using
        // for aVertexPosition, aVevrtexColor and also
        // look up uniform locations.
        const programInfo = {
          program: shaderProgram,
          attribLocations: {
            vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
            vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
          },
          uniformLocations: {
            projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
            modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
          },
        };
      
        // Here's where we call the routine that builds all the
        // objects we'll be drawing.
        const buffers = initBuffers(gl);
      
        // Draw the scene
        drawScene(gl, programInfo, buffers);
      }
      
      //
      // initBuffers
      //
      // Initialize the buffers we'll need. For this demo, we just
      // have one object -- a simple two-dimensional square.
      //
      function initBuffers(gl) {
        // Create a buffer for the square's positions.
      
        const positionBuffer0 = gl.createBuffer();
      
        // Select the positionBuffer as the one to apply buffer
        // operations to from here out.
      
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer0);
      
        // Now create an array of positions for the square.
      
        var positions = [
           0.5,  0.5,
          -0.5,  0.5,
           0.5, -0.5,
          -0.5, -0.5,
        ];
      
        // Now pass the list of positions into WebGL to build the
        // shape. We do this by creating a Float32Array from the
        // JavaScript array, then use it to fill the current buffer.
      
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
      
        // Now set up the colors for the vertices
      
        var colors = [
          0.0,  1.0,  0.0,  0.5,    // white
          0.0,  1.0,  0.0,  0.5,    // red
          0.0,  1.0,  0.0,  0.5,    // green
          0.0,  1.0,  0.0,  0.5,    // blue
        ];
      
        const colorBuffer0 = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer0);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
      
      
      
        // Create a buffer for the square's positions.
      
        const positionBuffer1 = gl.createBuffer();
      
        // Select the positionBuffer as the one to apply buffer
        // operations to from here out.
      
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer1);
      
        // Now create an array of positions for the square.
      
        positions = [
           2.0,  0.4,
          -2.0,  0.4,
           2.0, -2.0,
          -2.0, -2.0,
        ];
      
        // Now pass the list of positions into WebGL to build the
        // shape. We do this by creating a Float32Array from the
        // JavaScript array, then use it to fill the current buffer.
      
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
      
        // Now set up the colors for the vertices
      
        colors = [
          1.0,  0.0,  0.0,  1.0,    // white
          1.0,  0.0,  0.0,  1.0,    // red
          1.0,  0.0,  0.0,  1.0,    // green
          1.0,  0.0,  0.0,  1.0,    // blue
        ];
      
        const colorBuffer1 = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer1);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
        
        // Create a buffer for the square's positions.
      
        const positionBuffer2 = gl.createBuffer();
      
        // Select the positionBuffer as the one to apply buffer
        // operations to from here out.
      
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer2);
      
        // Now create an array of positions for the square.
      
        positions = [
           1.0,  1.0,
          -0.0,  1.0,
           1.0, -1.0,
          -0.0, -1.0,
        ];
      
        // Now pass the list of positions into WebGL to build the
        // shape. We do this by creating a Float32Array from the
        // JavaScript array, then use it to fill the current buffer.
      
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
      
        // Now set up the colors for the vertices
      
        colors = [
          0.0,  0.0,  1.0,  0.5,    // white
          0.0,  0.0,  1.0,  0.5,    // red
          0.0,  0.0,  1.0,  0.5,    // green
          0.0,  0.0,  1.0,  0.5,    // blue
        ];
      
        const colorBuffer2 = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer2);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
      
      
      
        return {
          position0: positionBuffer0,
          color0: colorBuffer0,
          position1: positionBuffer1,
          color1: colorBuffer1,
          position2: positionBuffer2,
          color2: colorBuffer2,
        };
      }
      
      //
      // Draw the scene.
      //
      function drawScene(gl, programInfo, buffers) {
        gl.clearColor(0.0, 0.0, 0.0, 1.0);  // Clear to black, fully opaque
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        //gl.clearDepth(1.0);                 // Clear everything
        gl.disable(gl.DEPTH_TEST)
        gl.enable(gl.BLEND)
        gl.blendEquation(gl.FUNC_ADD)
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
      
        // Clear the canvas before we start drawing on it.
      
      
        // Create a perspective matrix, a special matrix that is
        // used to simulate the distortion of perspective in a camera.
        // Our field of view is 45 degrees, with a width/height
        // ratio that matches the display size of the canvas
        // and we only want to see objects between 0.1 units
        // and 100 units away from the camera.
      
        const fieldOfView = 45 * Math.PI / 180;   // in radians
        const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
        const zNear = 0.1;
        const zFar = 100.0;
        const projectionMatrix = mat4.create();
      
        // note: glmatrix.js always has the first argument
        // as the destination to receive the result.
        mat4.perspective(projectionMatrix,
                         fieldOfView,
                         aspect,
                         zNear,
                         zFar);
      
        // Set the drawing position to the "identity" point, which is
        // the center of the scene.
        const modelViewMatrix = mat4.create();
      
        // Now move the drawing position a bit to where we want to
        // start drawing the square.
      
        mat4.translate(modelViewMatrix,     // destination matrix
                       modelViewMatrix,     // matrix to translate
                       [-0.0, 0.0, -6.0]);  // amount to translate
      
        function drawSquare(positionbuffer, colorbuffer) {
        // Tell WebGL how to pull out the positions from the position
        // buffer into the vertexPosition attribute
        {
          const numComponents = 2;
          const type = gl.FLOAT;
          const normalize = false;
          const stride = 0;
          const offset = 0;
          gl.bindBuffer(gl.ARRAY_BUFFER, positionbuffer);
          gl.vertexAttribPointer(
              programInfo.attribLocations.vertexPosition,
              numComponents,
              type,
              normalize,
              stride,
              offset);
          gl.enableVertexAttribArray(
              programInfo.attribLocations.vertexPosition);
        }
      
        // Tell WebGL how to pull out the colors from the color buffer
        // into the vertexColor attribute.
        {
          const numComponents = 4;
          const type = gl.FLOAT;
          const normalize = false;
          const stride = 0;
          const offset = 0;
          gl.bindBuffer(gl.ARRAY_BUFFER, colorbuffer);
          gl.vertexAttribPointer(
              programInfo.attribLocations.vertexColor,
              numComponents,
              type,
              normalize,
              stride,
              offset);
          gl.enableVertexAttribArray(
              programInfo.attribLocations.vertexColor);
        }
      
        // Tell WebGL to use our program when drawing
      
        gl.useProgram(programInfo.program);
      
        // Set the shader uniforms
      
        gl.uniformMatrix4fv(
            programInfo.uniformLocations.projectionMatrix,
            false,
            projectionMatrix);
        gl.uniformMatrix4fv(
            programInfo.uniformLocations.modelViewMatrix,
            false,
            modelViewMatrix);
      
        {
          const offset = 0;
          const vertexCount = 4;
          gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount);
        }
        }
        drawSquare(buffers.position0, buffers.color0); // Player
        drawSquare(buffers.position1, buffers.color1); // Background
        drawSquare(buffers.position2, buffers.color2); // Window
      }
      
      //
      // Initialize a shader program, so WebGL knows how to draw our data
      //
      function initShaderProgram(gl, vsSource, fsSource) {
        const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
        const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
      
        // Create the shader program
      
        const shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vertexShader);
        gl.attachShader(shaderProgram, fragmentShader);
        gl.linkProgram(shaderProgram);
      
        // If creating the shader program failed, alert
      
        if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
          alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram));
          return null;
        }
      
        return shaderProgram;
      }
      
      //
      // creates a shader of the given type, uploads the source and
      // compiles it.
      //
      function loadShader(gl, type, source) {
        const shader = gl.createShader(type);
      
        // Send the source to the shader object
      
        gl.shaderSource(shader, source);
      
        // Compile the shader program
      
        gl.compileShader(shader);
      
        // See if it compiled successfully
      
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
          gl.deleteShader(shader);
          return null;
        }
      
        return shader;
      }
      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title></title>
      </head>
      <body>
        <canvas id="glcanvas" width="640" height="480"></canvas>
      
      </body>
      </html>

2 个答案:

答案 0 :(得分:1)

  

我对半透明物体的深度测试有三个要求

实际上很少有具有部分透明(实际混合)样本的自相交对象。自相交几何的常见情况是草和叶。然而,在这些情况下,草和树叶覆盖的实际区域不透明 - 它们是不透明的。

这里的常见解决方案是alpha测试。将叶子渲染为不透明(未混合)四边形(具有正常深度测试和写入),并丢弃具有不足α的片段(例如,因为它们在叶子之外)。由于此处的单个样本是不透明的,因此您可以免费获得订单独立性,因为深度测试的工作方式与您对不透明对象的预期相同。

如果你想要混合边缘,那么启用alpha-to-coverage并让多样本解析清理边缘。

对于你剩下的少量实际透明的东西,通常你需要在CPU上进行从前到后的排序,并在不透明的传递后进行渲染。

适当的OIT是可能的,但通常是相当昂贵的技术,所以我还没有看到任何人在学术环境之外实际使用它(至少在移动OpenGL ES实现上)。

答案 1 :(得分:1)

这似乎是the paper linked by ripi2正在做的事情

function main() {
  const m4 = twgl.m4;
  const gl = document.querySelector('canvas').getContext('webgl2', {alpha: false});
  if (!gl) {
    alert('need WebGL2');
    return;
  }
  const ext = gl.getExtension('EXT_color_buffer_float');
  if (!ext) {
    alert('EXT_color_buffer_float');
    return;
  }

  const vs = `
  #version 300 es
  layout(location=0) in vec4 position;
  uniform mat4 u_matrix;
  void main() {
    gl_Position = u_matrix * position;
  }
  `;

  const checkerFS = `
  #version 300 es
  precision highp float;

  uniform vec4 color1;
  uniform vec4 color2;

  out vec4 fragColor;

  void main() {
    ivec2 grid = ivec2(gl_FragCoord.xy) / 32;
    fragColor = mix(color1, color2, float((grid.x + grid.y) % 2));
  }
  `;

  const transparentFS = `
  #version 300 es
  precision highp float;
  uniform vec4 Ci;

  out vec4 fragData[2];

  float w(float z, float a) {
    return a * max(pow(10.0,-2.0),3.0*pow(10.0,3.0)*pow((1.0 - z), 3.));
  }

  void main() {
    float ai = Ci.a;
    float zi = gl_FragCoord.z;

    float wresult = w(zi, ai);
    fragData[0] = vec4(Ci.rgb * wresult, ai);
    fragData[1].r = ai * wresult;
  }
  `;

  const compositeFS = `
  #version 300 es
  precision highp float;
  uniform sampler2D ATexture;
  uniform sampler2D BTexture;

  out vec4 fragColor;

  void main() {
    vec4 accum = texelFetch(ATexture, ivec2(gl_FragCoord.xy), 0);
    float r = accum.a;
    accum.a = texelFetch(BTexture, ivec2(gl_FragCoord.xy), 0).r;
    fragColor = vec4(accum.rgb / clamp(accum.a, 1e-4, 5e4), r);
  }
  `;

  const checkerProgramInfo = twgl.createProgramInfo(gl, [vs, checkerFS]);
  const transparentProgramInfo = twgl.createProgramInfo(gl, [vs, transparentFS]);
  const compositeProgramInfo = twgl.createProgramInfo(gl, [vs, compositeFS]);

  const bufferInfo = twgl.primitives.createXYQuadBufferInfo(gl);

  const fbi = twgl.createFramebufferInfo(
    gl,
    [
      { internalFormat: gl.RGBA32F, minMag: gl.NEAREST },
      { internalFormat: gl.R32F, minMag: gl.NEAREST },
    ]);

  function render(time) {
    time *= 0.001;

    twgl.setBuffersAndAttributes(gl, transparentProgramInfo, bufferInfo);

    // drawOpaqueSurfaces();
    gl.useProgram(checkerProgramInfo.program);
    gl.disable(gl.BLEND);
    twgl.setUniforms(checkerProgramInfo, {
      color1: [.5, .5, .5, 1],
      color2: [.7, .7, .7, 1],
      u_matrix: m4.identity(),
    });
    twgl.drawBufferInfo(gl, bufferInfo);

    twgl.bindFramebufferInfo(gl, fbi);
    gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);
    gl.clearBufferfv(gl.COLOR, 0, new Float32Array([0, 0, 0, 1]));
    gl.clearBufferfv(gl.COLOR, 1, new Float32Array([1, 1, 1, 1]));

    gl.depthMask(false);
    gl.enable(gl.BLEND);
    gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ZERO, gl.ONE_MINUS_SRC_ALPHA);

    gl.useProgram(transparentProgramInfo.program);

    // drawTransparentSurfaces();
    const quads = [
       [ .4,  0,  0, .4],
       [ .4, .4,  0, .4],
       [  0, .4,  0, .4],
       [  0, .4, .4, .4],
       [  0, .0, .4, .4],
       [ .4, .0, .4, .4],
    ];
    quads.forEach((color, ndx) => {
      const u = ndx / (quads.length - 1);
      // change the order every second
      const v = ((ndx + time | 0) % quads.length) / (quads.length - 1);
      const xy = (u * 2 - 1) * .25;
      const z = (v * 2 - 1) * .25;
      let mat = m4.identity();
      mat = m4.translate(mat, [xy, xy, z]);
      mat = m4.scale(mat, [.3, .3, 1]);
      twgl.setUniforms(transparentProgramInfo, {
        Ci: color,
        u_matrix: mat,
      });
      twgl.drawBufferInfo(gl, bufferInfo);
    });

    twgl.bindFramebufferInfo(gl, null);
    gl.drawBuffers([gl.BACK]);

    gl.blendFunc(gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA);

    gl.useProgram(compositeProgramInfo.program);

    twgl.setUniforms(compositeProgramInfo, {
      ATexture: fbi.attachments[0],
      BTexture: fbi.attachments[1],
      u_matrix: m4.identity(),
    });

    twgl.drawBufferInfo(gl, bufferInfo);

    /* only needed if {alpha: false} not passed into getContext
    gl.colorMask(false, false, false, true);
    gl.clearColor(1, 1, 1, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.colorMask(true, true, true, true);
    */

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

有些注意事项:

  • 它使用的是WebGL2,但它应该可以在WebGL1中使用,你必须更改着色器才能使用GLSL ES 1.0。
  • 它使用浮点纹理。本文提到你也可以使用半浮动纹理。请注意,即使是WebGL2,渲染为half和float纹理也是一个可选功能。我相信大多数移动硬件可以渲染到一半而不是浮动。
  • 它使用了论文中的权重等式10。文中有4个权重方程。 7,8,9和10.要执行7,8或9,您需要将视图空间z从顶点着色器传递到片段着色器
  • 它每秒都会切换绘图顺序

代码很简单。

它创建了3个着色器。一个是绘制一个棋盘,所以我们有一些不透明的东西,看到上面绘制的透明的东西。一个是透明对象着色器。最后是着色器将透明的东西复合到场景中。

接下来它产生2个纹理,浮点RGBA32F纹理和浮点R32F纹理(仅限红色通道)。它将它们附加到帧缓冲区。 (这都是在1函数twgl.createFramebufferInfo中完成的。默认情况下,该函数使纹理与画布大小相同。

我们制作一个从-1到+1的单个四元组

我们使用该四边形将棋盘画入画布

然后我们打开混合,设置混合方程式,如纸张所说,切换到渲染到我们的帧缓冲区,清除帧缓冲区。注意,它分别被清除为0,0,0,1和1。这是我们每个绘制缓冲区没有单独的混合函数的版本。如果切换到每个绘制缓冲区可以使用单独混合函数的版本,则需要清除不同的值并使用不同的着色器(参见纸张)

使用我们的透明度着色器,我们使用相同的四边形绘制每个纯色的6个矩形。我只是用纯色来保持简单。每个都处于不同的Z,Zs每秒都会改变,只是为了看到结果Z的变化。

在着色器中Ci是输入颜色。根据该论文,它有望成为预乘的α颜色。 fragData [0] is the "accumulate" texture and fragData [1] is the "revealage" texture and is only one channel, red. The w`函数表示论文中的等式10.

在绘制完所有6个四边形之后,我们切换回渲染到画布并使用合成着色器将透明度结果与非透明画布内容合成。

这是一个几何图形的例子。差异:

  • 它使用纸上的等式(7)代替(10)
  • 为了做正确的zbuffering,在进行不透明和透明渲染时需要共享深度缓冲区。所以有2帧缓冲区。一个缓冲区具有RGBA8 +深度,另一个缓冲区具有RGBA32F + R32F +深度。深度缓冲区是共享的。
  • 透明渲染器计算简单光照,然后将结果用作纸张的Ci
  • 将透明合成到不透明后,我们仍需要将opaque复制到画布中以查看结果

function main() {
  const m4 = twgl.m4;
  const v3 = twgl.v3;
  const gl = document.querySelector('canvas').getContext('webgl2', {alpha: false});
  if (!gl) {
    alert('need WebGL2');
    return;
  }
  const ext = gl.getExtension('EXT_color_buffer_float');
  if (!ext) {
    alert('EXT_color_buffer_float');
    return;
  }

  const vs = `
  #version 300 es
  layout(location=0) in vec4 position;
  layout(location=1) in vec3 normal;
  uniform mat4 u_projection;
  uniform mat4 u_modelView;
  
  out vec4 v_viewPosition;
  out vec3 v_normal;

  void main() {
    gl_Position = u_projection * u_modelView * position;
    v_viewPosition = u_modelView * position;
    v_normal = (u_modelView * vec4(normal, 0)).xyz;
  }
  `;

  const checkerFS = `
  #version 300 es
  precision highp float;

  uniform vec4 color1;
  uniform vec4 color2;

  out vec4 fragColor;

  void main() {
    ivec2 grid = ivec2(gl_FragCoord.xy) / 32;
    fragColor = mix(color1, color2, float((grid.x + grid.y) % 2));
  }
  `;
  
  const opaqueFS = `
  #version 300 es
  precision highp float;
  
  in vec4 v_viewPosition;
  in vec3 v_normal;
  
  uniform vec4 u_color;
  uniform vec3 u_lightDirection;
  
  out vec4 fragColor;
  
  void main() {
    float light = abs(dot(normalize(v_normal), u_lightDirection));
    fragColor = vec4(u_color.rgb * light, u_color.a);
  }
  `;

  const transparentFS = `
  #version 300 es
  precision highp float;
  uniform vec4 u_color;
  uniform vec3 u_lightDirection;
  
  in vec4 v_viewPosition;
  in vec3 v_normal;
  
  out vec4 fragData[2];

  // eq (7)
  float w(float z, float a) {
    return a * max(
      pow(10.0, -2.0),
      min(
        3.0 * pow(10.0, 3.0),
        10.0 /
        (pow(10.0, -5.0) + 
         pow(abs(z) / 5.0, 2.0) +
         pow(abs(z) / 200.0, 6.0)
        )
      )
    );
  }

  void main() {
    float light = abs(dot(normalize(v_normal), u_lightDirection));
    vec4 Ci = vec4(u_color.rgb * light, u_color.a);
  
    float ai = Ci.a;
    float zi = gl_FragCoord.z;

    float wresult = w(zi, ai);
    fragData[0] = vec4(Ci.rgb * wresult, ai);
    fragData[1].r = ai * wresult;
  }
  `;

  const compositeFS = `
  #version 300 es
  precision highp float;
  uniform sampler2D ATexture;
  uniform sampler2D BTexture;
  
  out vec4 fragColor;

  void main() {
    vec4 accum = texelFetch(ATexture, ivec2(gl_FragCoord.xy), 0);
    float r = accum.a;
    accum.a = texelFetch(BTexture, ivec2(gl_FragCoord.xy), 0).r;
    fragColor = vec4(accum.rgb / clamp(accum.a, 1e-4, 5e4), r);
  }
  `;
  
  const blitFS = `
  #version 300 es
  precision highp float;
  uniform sampler2D u_texture;
  
  out vec4 fragColor;

  void main() {
    fragColor = texelFetch(u_texture, ivec2(gl_FragCoord.xy), 0);
  }
  `;

  const checkerProgramInfo = twgl.createProgramInfo(gl, [vs, checkerFS]);
  const opaqueProgramInfo = twgl.createProgramInfo(gl, [vs, opaqueFS]);
  const transparentProgramInfo = twgl.createProgramInfo(gl, [vs, transparentFS]);
  const compositeProgramInfo = twgl.createProgramInfo(gl, [vs, compositeFS]);
  const blitProgramInfo = twgl.createProgramInfo(gl, [vs, blitFS]);

  const xyQuadVertexArrayInfo = makeVAO(checkerProgramInfo, twgl.primitives.createXYQuadBufferInfo(gl));
  const sphereVertexArrayInfo = makeVAO(transparentProgramInfo, twgl.primitives.createSphereBufferInfo(gl, 1, 16, 12));
  const cubeVertexArrayInfo = makeVAO(opaqueProgramInfo, twgl.primitives.createCubeBufferInfo(gl, 1, 1));
  
  function makeVAO(programInfo, bufferInfo) {
    return twgl.createVertexArrayInfo(gl, programInfo, bufferInfo);
  }
  
  // In order to do proper zbuffering we need to share
  // the depth buffer 
  
  const opaqueAttachments = [
    { internalFormat: gl.RGBA8, minMag: gl.NEAREST },
    { format: gl.DEPTH_COMPONENT16, minMag: gl.NEAREST },
  ];
  const opaqueFBI = twgl.createFramebufferInfo(gl, opaqueAttachments);
  
  const transparentAttachments = [
    { internalFormat: gl.RGBA32F, minMag: gl.NEAREST },
    { internalFormat: gl.R32F, minMag: gl.NEAREST },
    { format: gl.DEPTH_COMPONENT16, minMag: gl.NEAREST, attachment: opaqueFBI.attachments[1] },
  ];
  const transparentFBI = twgl.createFramebufferInfo(gl, transparentAttachments);

  function render(time) {
    time *= 0.001;

    if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
      // if the canvas is resized also resize the framebuffer
      // attachments (the depth buffer will be resized twice 
      // but I'm too lazy to fix it)
      twgl.resizeFramebufferInfo(gl, opaqueFBI, opaqueAttachments);
      twgl.resizeFramebufferInfo(gl, transparentFBI, transparentAttachments);
    }
    
    const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
    const fov = 45 * Math.PI / 180;
    const zNear = 0.1;
    const zFar = 500;
    
    const projection = m4.perspective(fov, aspect, zNear, zFar);
    const eye = [0, 0, -5];
    const target = [0, 0, 0];
    const up = [0, 1, 0];
    const camera = m4.lookAt(eye, target, up);
    const view = m4.inverse(camera);

    const lightDirection = v3.normalize([1, 3, 5]);

    twgl.bindFramebufferInfo(gl, opaqueFBI);
    gl.drawBuffers([gl.COLOR_ATTACHMENT0]);    
    gl.depthMask(true);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    gl.bindVertexArray(xyQuadVertexArrayInfo.vertexArrayObject);

    // drawOpaqueSurfaces();
    // draw checkerboard
    gl.useProgram(checkerProgramInfo.program);
    gl.disable(gl.DEPTH_TEST);
    gl.disable(gl.BLEND);
    twgl.setUniforms(checkerProgramInfo, {
      color1: [.5, .5, .5, 1],
      color2: [.7, .7, .7, 1],
      u_projection: m4.identity(),
      u_modelView: m4.identity(),
    });
    twgl.drawBufferInfo(gl, xyQuadVertexArrayInfo);

    // draw a cube with depth buffer
    gl.enable(gl.DEPTH_TEST);
    
    {
      gl.useProgram(opaqueProgramInfo.program);
      gl.bindVertexArray(cubeVertexArrayInfo.vertexArrayObject);
      let mat = view;
      mat = m4.rotateX(mat, time * .1);
      mat = m4.rotateY(mat, time * .2);
      mat = m4.scale(mat, [1.5, 1.5, 1.5]);
      twgl.setUniforms(opaqueProgramInfo, {
        u_color: [1, .5, .2, 1],
        u_lightDirection: lightDirection,
        u_projection: projection,
        u_modelView: mat,
      });    
      twgl.drawBufferInfo(gl, cubeVertexArrayInfo);
    }
    
    twgl.bindFramebufferInfo(gl, transparentFBI);
    gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);
    // these values change if using separate blend functions
    // per attachment (something WebGL2 does not support)
    gl.clearBufferfv(gl.COLOR, 0, new Float32Array([0, 0, 0, 1]));
    gl.clearBufferfv(gl.COLOR, 1, new Float32Array([1, 1, 1, 1]));

    gl.depthMask(false);  // don't write to depth buffer (but still testing)
    gl.enable(gl.BLEND);
    // this changes if using separate blend functions per attachment
    gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ZERO, gl.ONE_MINUS_SRC_ALPHA);

    gl.useProgram(transparentProgramInfo.program);
    gl.bindVertexArray(sphereVertexArrayInfo.vertexArrayObject);

    // drawTransparentSurfaces();
    const spheres = [
       [ .4,  0,  0, .4],
       [ .4, .4,  0, .4],
       [  0, .4,  0, .4],
       [  0, .4, .4, .4],
       [  0, .0, .4, .4],
       [ .4, .0, .4, .4],
    ];
    spheres.forEach((color, ndx) => {
      const u = ndx + 2;
      let mat = view;
      mat = m4.rotateX(mat, time * u * .1);
      mat = m4.rotateY(mat, time * u * .2);
      mat = m4.translate(mat, [0, 0, 1 + ndx * .1]);
      twgl.setUniforms(transparentProgramInfo, {
        u_color: color,
        u_lightDirection: lightDirection,
        u_projection: projection,
        u_modelView: mat,
      });
      twgl.drawBufferInfo(gl, sphereVertexArrayInfo);
    });

    // composite transparent results with opaque
    twgl.bindFramebufferInfo(gl, opaqueFBI);
    gl.drawBuffers([gl.COLOR_ATTACHMENT0]);

    gl.disable(gl.DEPTH_TEST);
    gl.blendFunc(gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA);

    gl.useProgram(compositeProgramInfo.program);
    gl.bindVertexArray(xyQuadVertexArrayInfo.vertexArrayObject);

    twgl.setUniforms(compositeProgramInfo, {
      ATexture: transparentFBI.attachments[0],
      BTexture: transparentFBI.attachments[1],
      u_projection: m4.identity(),
      u_modelView: m4.identity(),
    });

    twgl.drawBufferInfo(gl, xyQuadVertexArrayInfo);

    /* only needed if {alpha: false} not passed into getContext
    gl.colorMask(false, false, false, true);
    gl.clearColor(1, 1, 1, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.colorMask(true, true, true, true);
    */
    
    // draw opaque color buffer into canvas
    // could probably use gl.blitFramebuffer
    gl.disable(gl.BLEND);
    twgl.bindFramebufferInfo(gl, null);
    gl.useProgram(blitProgramInfo.program);
    gl.bindVertexArray(xyQuadVertexArrayInfo.vertexArrayObject);

    twgl.setUniforms(blitProgramInfo, {
      u_texture: opaqueFBI.attachments[0],
      u_projection: m4.identity(),
      u_modelView: m4.identity(),
    });
    twgl.drawBufferInfo(gl, xyQuadVertexArrayInfo);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

我发现它不是在最后两个步骤中使用标准OpenGL混合(复合然后是blit)我们可以更改复合着色器,因此需要3个纹理(ATexutre,BTexture,opaqueTexture)并在着色器中直接混合输出画布。那会更快。

function main() {
  const m4 = twgl.m4;
  const v3 = twgl.v3;
  const gl = document.querySelector('canvas').getContext('webgl2', {alpha: false});
  if (!gl) {
    alert('need WebGL2');
    return;
  }
  const ext = gl.getExtension('EXT_color_buffer_float');
  if (!ext) {
    alert('EXT_color_buffer_float');
    return;
  }

  const vs = `
  #version 300 es
  layout(location=0) in vec4 position;
  layout(location=1) in vec3 normal;
  uniform mat4 u_projection;
  uniform mat4 u_modelView;
  
  out vec4 v_viewPosition;
  out vec3 v_normal;

  void main() {
    gl_Position = u_projection * u_modelView * position;
    v_viewPosition = u_modelView * position;
    v_normal = (u_modelView * vec4(normal, 0)).xyz;
  }
  `;

  const checkerFS = `
  #version 300 es
  precision highp float;

  uniform vec4 color1;
  uniform vec4 color2;

  out vec4 fragColor;

  void main() {
    ivec2 grid = ivec2(gl_FragCoord.xy) / 32;
    fragColor = mix(color1, color2, float((grid.x + grid.y) % 2));
  }
  `;
  
  const opaqueFS = `
  #version 300 es
  precision highp float;
  
  in vec4 v_viewPosition;
  in vec3 v_normal;
  
  uniform vec4 u_color;
  uniform vec3 u_lightDirection;
  
  out vec4 fragColor;
  
  void main() {
    float light = abs(dot(normalize(v_normal), u_lightDirection));
    fragColor = vec4(u_color.rgb * light, u_color.a);
  }
  `;

  const transparentFS = `
  #version 300 es
  precision highp float;
  uniform vec4 u_color;
  uniform vec3 u_lightDirection;
  
  in vec4 v_viewPosition;
  in vec3 v_normal;
  
  out vec4 fragData[2];

  // eq (7)
  float w(float z, float a) {
    return a * max(
      pow(10.0, -2.0),
      min(
        3.0 * pow(10.0, 3.0),
        10.0 /
        (pow(10.0, -5.0) + 
         pow(abs(z) / 5.0, 2.0) +
         pow(abs(z) / 200.0, 6.0)
        )
      )
    );
  }

  void main() {
    float light = abs(dot(normalize(v_normal), u_lightDirection));
    vec4 Ci = vec4(u_color.rgb * light, u_color.a);
  
    float ai = Ci.a;
    float zi = gl_FragCoord.z;

    float wresult = w(zi, ai);
    fragData[0] = vec4(Ci.rgb * wresult, ai);
    fragData[1].r = ai * wresult;
  }
  `;

  const compositeFS = `
  #version 300 es
  precision highp float;
  uniform sampler2D ATexture;
  uniform sampler2D BTexture;
  uniform sampler2D opaqueTexture;
  
  out vec4 fragColor;

  void main() {
    vec4 accum = texelFetch(ATexture, ivec2(gl_FragCoord.xy), 0);
    float r = accum.a;
    accum.a = texelFetch(BTexture, ivec2(gl_FragCoord.xy), 0).r;
    vec4 transparentColor = vec4(accum.rgb / clamp(accum.a, 1e-4, 5e4), r);
    vec4 opaqueColor = texelFetch(opaqueTexture, ivec2(gl_FragCoord.xy), 0);
    //  gl.blendFunc(gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA);
    fragColor = transparentColor * (1. - r) + opaqueColor * r;
  }
  `;
  
  const checkerProgramInfo = twgl.createProgramInfo(gl, [vs, checkerFS]);
  const opaqueProgramInfo = twgl.createProgramInfo(gl, [vs, opaqueFS]);
  const transparentProgramInfo = twgl.createProgramInfo(gl, [vs, transparentFS]);
  const compositeProgramInfo = twgl.createProgramInfo(gl, [vs, compositeFS]);

  const xyQuadVertexArrayInfo = makeVAO(checkerProgramInfo, twgl.primitives.createXYQuadBufferInfo(gl));
  const sphereVertexArrayInfo = makeVAO(transparentProgramInfo, twgl.primitives.createSphereBufferInfo(gl, 1, 16, 12));
  const cubeVertexArrayInfo = makeVAO(opaqueProgramInfo, twgl.primitives.createCubeBufferInfo(gl, 1, 1));
  
  function makeVAO(programInfo, bufferInfo) {
    return twgl.createVertexArrayInfo(gl, programInfo, bufferInfo);
  }
  
  // In order to do proper zbuffering we need to share
  // the depth buffer 
  
  const opaqueAttachments = [
    { internalFormat: gl.RGBA8, minMag: gl.NEAREST },
    { format: gl.DEPTH_COMPONENT16, minMag: gl.NEAREST },
  ];
  const opaqueFBI = twgl.createFramebufferInfo(gl, opaqueAttachments);
  
  const transparentAttachments = [
    { internalFormat: gl.RGBA32F, minMag: gl.NEAREST },
    { internalFormat: gl.R32F, minMag: gl.NEAREST },
    { format: gl.DEPTH_COMPONENT16, minMag: gl.NEAREST, attachment: opaqueFBI.attachments[1] },
  ];
  const transparentFBI = twgl.createFramebufferInfo(gl, transparentAttachments);

  function render(time) {
    time *= 0.001;

    if (twgl.resizeCanvasToDisplaySize(gl.canvas)) {
      // if the canvas is resized also resize the framebuffer
      // attachments (the depth buffer will be resized twice 
      // but I'm too lazy to fix it)
      twgl.resizeFramebufferInfo(gl, opaqueFBI, opaqueAttachments);
      twgl.resizeFramebufferInfo(gl, transparentFBI, transparentAttachments);
    }
    
    const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
    const fov = 45 * Math.PI / 180;
    const zNear = 0.1;
    const zFar = 500;
    
    const projection = m4.perspective(fov, aspect, zNear, zFar);
    const eye = [0, 0, -5];
    const target = [0, 0, 0];
    const up = [0, 1, 0];
    const camera = m4.lookAt(eye, target, up);
    const view = m4.inverse(camera);

    const lightDirection = v3.normalize([1, 3, 5]);

    twgl.bindFramebufferInfo(gl, opaqueFBI);
    gl.drawBuffers([gl.COLOR_ATTACHMENT0]);    
    gl.depthMask(true);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    gl.bindVertexArray(xyQuadVertexArrayInfo.vertexArrayObject);

    // drawOpaqueSurfaces();
    // draw checkerboard
    gl.useProgram(checkerProgramInfo.program);
    gl.disable(gl.DEPTH_TEST);
    gl.disable(gl.BLEND);
    twgl.setUniforms(checkerProgramInfo, {
      color1: [.5, .5, .5, 1],
      color2: [.7, .7, .7, 1],
      u_projection: m4.identity(),
      u_modelView: m4.identity(),
    });
    twgl.drawBufferInfo(gl, xyQuadVertexArrayInfo);

    // draw a cube with depth buffer
    gl.enable(gl.DEPTH_TEST);
    
    {
      gl.useProgram(opaqueProgramInfo.program);
      gl.bindVertexArray(cubeVertexArrayInfo.vertexArrayObject);
      let mat = view;
      mat = m4.rotateX(mat, time * .1);
      mat = m4.rotateY(mat, time * .2);
      mat = m4.scale(mat, [1.5, 1.5, 1.5]);
      twgl.setUniforms(opaqueProgramInfo, {
        u_color: [1, .5, .2, 1],
        u_lightDirection: lightDirection,
        u_projection: projection,
        u_modelView: mat,
      });    
      twgl.drawBufferInfo(gl, cubeVertexArrayInfo);
    }
    
    twgl.bindFramebufferInfo(gl, transparentFBI);
    gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);
    // these values change if using separate blend functions
    // per attachment (something WebGL2 does not support)
    gl.clearBufferfv(gl.COLOR, 0, new Float32Array([0, 0, 0, 1]));
    gl.clearBufferfv(gl.COLOR, 1, new Float32Array([1, 1, 1, 1]));

    gl.depthMask(false);  // don't write to depth buffer (but still testing)
    gl.enable(gl.BLEND);
    // this changes if using separate blend functions per attachment
    gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ZERO, gl.ONE_MINUS_SRC_ALPHA);

    gl.useProgram(transparentProgramInfo.program);
    gl.bindVertexArray(sphereVertexArrayInfo.vertexArrayObject);

    // drawTransparentSurfaces();
    const spheres = [
       [ .4,  0,  0, .4],
       [ .4, .4,  0, .4],
       [  0, .4,  0, .4],
       [  0, .4, .4, .4],
       [  0, .0, .4, .4],
       [ .4, .0, .4, .4],
    ];
    spheres.forEach((color, ndx) => {
      const u = ndx + 2;
      let mat = view;
      mat = m4.rotateX(mat, time * u * .1);
      mat = m4.rotateY(mat, time * u * .2);
      mat = m4.translate(mat, [0, 0, 1 + ndx * .1]);
      twgl.setUniforms(transparentProgramInfo, {
        u_color: color,
        u_lightDirection: lightDirection,
        u_projection: projection,
        u_modelView: mat,
      });
      twgl.drawBufferInfo(gl, sphereVertexArrayInfo);
    });

    // composite transparent results with opaque
    twgl.bindFramebufferInfo(gl, null);

    gl.disable(gl.DEPTH_TEST);
    gl.disable(gl.BLEND);
    
    gl.useProgram(compositeProgramInfo.program);
    gl.bindVertexArray(xyQuadVertexArrayInfo.vertexArrayObject);

    twgl.setUniforms(compositeProgramInfo, {
      ATexture: transparentFBI.attachments[0],
      BTexture: transparentFBI.attachments[1],
      opaqueTexture: opaqueFBI.attachments[0],
      u_projection: m4.identity(),
      u_modelView: m4.identity(),
    });

    twgl.drawBufferInfo(gl, xyQuadVertexArrayInfo);

    /* only needed if {alpha: false} not passed into getContext
    gl.colorMask(false, false, false, true);
    gl.clearColor(1, 1, 1, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.colorMask(true, true, true, true);
    */
    
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
}
main();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

相关问题