多目标渲染(MRT)

835 阅读4分钟

1-多渲染目标简介

多渲染目标(Multiple Render Targets, MRT)指的是在图形渲染过程中,同时将渲染结果写入多个不同的目标缓冲区(通常是纹理)。这种技术允许一个渲染操作产生多个输出,而不仅仅是传统的颜色缓冲区。

2-使用场景和优势

  1. 后处理效果

    MRT 可以用于实现复杂的后处理效果,如屏幕空间环境光遮蔽 (SSAO)、泛光 (Bloom)、运动模糊等,每种效果可能需要将结果输出到不同的缓冲区以便后续处理。

  2. 同时渲染多个视图

    在某些情况下,可能需要同时渲染多个视图(如立方体贴图的六个面),每个视图需要单独的渲染目标。

  3. 提高渲染效率

    MRT 可以减少渲染通道的数量,避免多次渲染同一场景以生成不同效果,从而提高渲染效率和性能。

3-实现方式

在 OpenGL 或 WebGL 中实现 MRT,通常通过以下步骤:

1.创建和绑定帧缓冲对象(Framebuffer Object, FBO):

创建一个帧缓冲对象,并绑定多个纹理附件作为渲染目标。每个附件对应一个渲染目标,可以是颜色附件、深度附件或者模板附件。

2.配置每个附件

对每个纹理附件进行设置,包括纹理的格式、尺寸、采样参数等。

3.渲染到多个目标

在渲染过程中,通过设置合适的渲染目标(如 gl.drawBuffers)来指定输出到多个纹理附件。

4.检查帧缓冲完整性

在所有设置完成后,确保帧缓冲对象的完整性,以确保能够正常渲染到指定的多个目标。

4-代码实例

当前示例是使用webgl2.0写的,实现了将一个程序对象的渲染结果分成两份,然后通过帧缓冲区输出到两个纹理对象中的功能。

后面我对两个纹理对象分别进行了展示,以便观察效果。

下面的代码都有详细的注释,大家可以通过注释理解其原理。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>layout</title>
  <style>
    body {
      margin: 0;
      overflow: hidden
    }
  </style>
</head>

<body>
  <canvas id="canvas"></canvas>
  <!-- 顶点着色器,#version 300 es 必须写在最前面,不可换行 -->
  <script id="vertexShader" type="x-shader/x-vertex">#version 300 es
    //uv坐标,(0,0)到(1,1)
    layout(location = 0) in vec2 a_Pin;
    //uv的插值,用于传递给着色器
    out vec2 v_Pin;
    void main(){
      //根据特定的uv,映射充满画布的顶点位置
      gl_Position = vec4(a_Pin*2.0-1.0,0.0,1.0);
      //为插值赋值
      v_Pin=a_Pin;
    }
  </script>
  <!-- 片元着色器,在FrameBuffer中绘图 -->
  <script id="fragmentShader1" type="x-shader/x-fragment">#version 300 es
    precision mediump float;
    //为纹理单元为0 的纹理对象写入的数据
    layout(location = 0) out float a;   
    //为纹理单元为1 的纹理对象写入的数据
    layout(location = 1) out vec4 b;
    void main() {
      //gl.R8
      a = 0.6;
      //gl.RGBA8
      b = vec4(0,0.7,0.9,1);
    }
  </script>
  <!-- 片元着色器,在canvas上绘图 -->
  <script id="fragmentShader2" type="x-shader/x-fragment">#version 300 es
    precision mediump float;
    //对应某一个纹理
    uniform sampler2D u_texture;
    //纹理UV
    in vec2 v_Pin;
    //输出颜色
    out vec4 color;
    void main() {
      //输出纹理中相应纹理插值的颜色
      color = texture(u_texture, v_Pin);
    }
  </script>
  
  <script type="module">
    const [width,height]=[window.innerWidth,window.innerHeight]
    const canvas = document.getElementById('canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const gl = canvas.getContext('webgl2');
    const vsSource =document.querySelector('#vertexShader').innerHTML
    const fsSource1 = document.querySelector('#fragmentShader1').innerHTML
    const fsSource2 = document.querySelector('#fragmentShader2').innerHTML

    // 在FrameBuffer中绘图的程序对象,此程序对象中的vsSource是个摆设,不用管
    const program1=createProgram(gl, vsSource, fsSource1)
    // 在canvas上绘图的程序对象
    const program2=createProgram(gl, vsSource, fsSource2)

    // 创建存储uv数据的缓冲区
    const sourceBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, sourceBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 1,0, 1,1, 0,1]), gl.STATIC_DRAW);
    // 获取顶点着色器中对于uv数据的变量
    const a_Pin = gl.getAttribLocation(program1, 'a_Pin');
    // 向着色器传递uv数据
    gl.enableVertexAttribArray(a_Pin);
    gl.vertexAttribPointer(a_Pin,2,gl.FLOAT,false,0,0);
    // 清理缓冲区
    gl.bindBuffer(gl.ARRAY_BUFFER, null);

    // 创建帧缓冲对象
    const framebuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER,framebuffer);
    // 创建纹理对象
    const texture0 = gl.createTexture();
    // 绑定纹理对象
    gl.bindTexture(gl.TEXTURE_2D, texture0);
    // 为纹理对象分配内存空间
    gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R8, width, height);
    // 将纹理附加到帧缓冲对象
    // 其作用是将一个纹理作为帧缓冲的一个附件,使得渲染操作可以直接将结果写入到这个纹理中
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture0, 0);
    // 再创建一个纹理对象,与上面同理
    const texture1 = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture1);
    gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, width, height);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.TEXTURE_2D, texture1, 0);
    // 多渲染目标(MRT)
    gl.drawBuffers( [gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]);
    // 应用program1
    gl.useProgram(program1);
    // 绘图,将绘制结果输出到帧缓冲区里的多个纹理对象中
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
    // 清理帧缓冲区
    gl.bindFramebuffer(gl.FRAMEBUFFER, null)

    // 应用program2
    gl.useProgram(program2)
    // 绑定texture0
    gl.bindTexture(gl.TEXTURE_2D, texture0);  
    // 渲染尺寸
    gl.viewport(0,0,width/2,height); 
    // 渲染
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
    // 与上同理
    gl.bindTexture(gl.TEXTURE_2D, texture1);  
    gl.viewport(width/2,0,width/2,height); 
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

    // 创建程序对象
    function createProgram(gl, vertexShaderSource, fragmentShaderSource) {
      let  vsh = gl.createShader( gl.VERTEX_SHADER );
      gl.shaderSource( vsh, vertexShaderSource );
      gl.compileShader( vsh );
      let  fsh = gl.createShader( gl.FRAGMENT_SHADER );
      gl.shaderSource( fsh, fragmentShaderSource );
      gl.compileShader( fsh );
      let  program = gl.createProgram();
      gl.attachShader( program, vsh );
      gl.attachShader( program, fsh );
      gl.linkProgram( program );
      return program;
    }
  </script>
</body>

</html>

效果如下:

image-20240711225132675

总结

建议大家使用ChatGPT 辅助学习。

此文章的大部分文字都是我用ChatGPT生成的,它写得很好,我就直接拿来用了。

代码示例是我自己写的,目的是以最简洁的方式,让大家理解MRT 的核心原理。

关于MRT 的实际应用,我会在后面举例详解。