使用ShaderPass自定义后处理

922 阅读4分钟

使用ShaderPass自定义后处理

ShaderPass的工作方式

名字ShaderPass中的Shader,其实就说明了ShaderPass的工作方式,就是通过ShaderWebGL的着色器程序),来处理“图片”。这里就有个问题,Shader是用来处理3D内容的,怎么用来处理图片呢?

其实很简单,可以给一个平面贴上一个纹理贴图,然后调整相机,刚好让这个纹理贴图占满整个屏幕,这样,片段着色器(fragment shader)就可以在光栅化之后,对每一个片段/采样点,或者说是像素进行处理。

Three.js也已经提供了这种在3D场景中渲染整张2D图片的工具FullScreenQuad,可以通过源码学习一下

FullScreenQuad源码

FullScreenQuad源码的重点,并不是class FullScreenQuad,而是在它上面定义的camerageometry

//examples/jsm/postprocess/Pass.js
const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );

const _geometry = new BufferGeometry();
_geometry.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
_geometry.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );

先看_geometry,它定义了三个顶点

  • position:这三个顶点的z坐标都是0,也就是在z = 0的平面上,找了三个点围成一个平面三角形。

1.PNG

  • uv:纹理映射,刚好在position[1,1]2{[-1, 1]}^2的正方形范围内,uv[0,1]2{[0, 1]}^2

2.png

_geometry创建一个三角形(Mesh),然后给它贴上一个纹理贴图,那么,在position[1,1]2{[-1, 1]}^2的范围内,刚好就是整张图片。

为了可以让这张图片渲染出来,还需要正交相机观察,在x轴和y轴观察的范围就是[1,1]2{[-1, 1]}^2: 回到上面定义相机的代码

const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );

// 第一个参数,表示left  = -1
// 第二个参数,表示right = 1
// 这两个参数定义了x轴方向的观察范围是[-1, 1]

// 第三个参数,表示top    = 1
// 第四个参数,表示bottom = -1
// 这两个参数定义了y轴方向的观察范围是[-1, 1]

// 第五个参数,表示near = 0
// 第六个参数,表示far = 1
// 这两个参数定义了z轴方向的观察范围是[0, 1]

可以看到,使用这里的_camera观察,就刚好可以把整张图片渲染到全屏。

接下来看class FullScreenQuad的代码:

class FullScreenQuad {

  constructor(material) {
    // 使用传入的material创建mesh,只有三个顶点,就是一个三角形
    this._mesh = new Mesh(_geometry, material);
  }

  render(renderer) {
    // 使用参数传入的renderer渲染这个三角形
    // 使用上面定义的_camera进行观察,结果是整张图片被渲染到整个屏幕
    renderer.render(this._mesh, _camera);
  }

  dispose() {
    this._mesh.geometry.dispose();
  }

  get material() {
    return this._mesh.material;
  }

  set material(value) {
    this._mesh.material = value;
  }
}

ShaderPass源码

class ShaderPass extends Pass {

  constructor(shader, textureID) {

    super();
    // 一个textureID,后续通过uniform传递给GPU
    // 这个id“指向”被处理(全屏渲染)的图片
    this.textureID = (textureID !== undefined) ? textureID : 'tDiffuse';

    // 下面这一大段代码 就是用来创建一个ShaderMaterial
    // if-else分支是为了处理传入的参数
    // 可以传入一个ShaderMaterial,或者是一个用来描述的对象
    if (shader instanceof ShaderMaterial) {
      this.uniforms = shader.uniforms;
      this.material = shader;
    } else if (shader) {
      this.uniforms = UniformsUtils.clone(shader.uniforms);
      this.material = new ShaderMaterial({
        defines: Object.assign({}, shader.defines),
        uniforms: this.uniforms,
        vertexShader: shader.vertexShader,
        fragmentShader: shader.fragmentShader
      });
    }

    // 用这个ShaderMaterial创建一个FullScreenQuad
    this.fsQuad = new FullScreenQuad(this.material);
  }

  render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) {

    // 使用textureID指向readBuffer(被处理的图片)
    if (this.uniforms[this.textureID]) {
      this.uniforms[this.textureID].value = readBuffer.texture;
    }

    // 在构造器中 已经完成了这里的material设置
    this.fsQuad.material = this.material;


    // 下面的代码就是渲染了
        // 1. 如果需要渲染到屏幕 设置renderTarget为null
        // 2. 如果不渲染到屏幕 设置renderTarget为writeBuffer
            //在构造器中 super(); 设置了needsSwap = true,这里不需要再设置
    if (this.renderToScreen) {
    
      renderer.setRenderTarget(null);
      this.fsQuad.render(renderer);
    } else {
        
      renderer.setRenderTarget(writeBuffer);
      // TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
      if (this.clear) renderer.clear(renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil);
      this.fsQuad.render(renderer);

    }

  }

  dispose() {
    this.material.dispose();
    this.fsQuad.dispose();
  }
}

ShaderPass会使用构造器传入的shader对图片进行处理,更具体的来说使用Fragment shader进行处理。

还是通过一段源码来理解,这里选择一个比较简单的用来进行伽马矫正的GammaCorrectionShader

const GammaCorrectionShader = {

	uniforms: {
    // 可以回过头看一下 上面的ShaderPass有一个textureID 默认值就是 tDiffuse
    // 在ShaderPass tDiffuse指向了readBuffer
		'tDiffuse': { value: null }

	},

  // 顶点着色器一般都是下面的写法
  // 1. 将uv坐标传递给下一阶段(光栅化
      // 一般情况下uv坐标不需要特殊处理 直接复制给vUv向下传递即可
      // 当然,在一些抗锯齿的后处理shader uv不是直接赋值给vUv
  // 2. 对顶点进行ModelView以及Projection变换
	vertexShader: /* glsl */`

		varying vec2 vUv;

		void main() {

			vUv = uv;
			gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

		}`,

  // 在片段着色器中,接收uniform传递来的tDiffuse
  //shaderpass会将tDiffuse指向readBuffer
	fragmentShader: /* glsl */`

		uniform sampler2D tDiffuse;

		varying vec2 vUv;

		void main() {
      // 对tDiffuse采样(读取readBuffer)
			vec4 tex = texture2D( tDiffuse, vUv );
      // 将颜色从线性空间转换到sRGB空间
			gl_FragColor = LinearTosRGB( tex );
		}`

};