使用ShaderPass自定义后处理
ShaderPass的工作方式
名字ShaderPass中的Shader,其实就说明了ShaderPass的工作方式,就是通过Shader(WebGL的着色器程序),来处理“图片”。这里就有个问题,Shader是用来处理3D内容的,怎么用来处理图片呢?
其实很简单,可以给一个平面贴上一个纹理贴图,然后调整相机,刚好让这个纹理贴图占满整个屏幕,这样,片段着色器(fragment shader)就可以在光栅化之后,对每一个片段/采样点,或者说是像素进行处理。
Three.js也已经提供了这种在3D场景中渲染整张2D图片的工具FullScreenQuad,可以通过源码学习一下
FullScreenQuad源码
FullScreenQuad源码的重点,并不是class FullScreenQuad,而是在它上面定义的camera和geometry。
//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的平面上,找了三个点围成一个平面三角形。
uv:纹理映射,刚好在position在的正方形范围内,uv是
用_geometry创建一个三角形(Mesh),然后给它贴上一个纹理贴图,那么,在position为的范围内,刚好就是整张图片。
为了可以让这张图片渲染出来,还需要正交相机观察,在x轴和y轴观察的范围就是: 回到上面定义相机的代码
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 );
}`
};