在后处理系统中,特效(Effect) 负责数据层的维护,管线(Pass)负责渲染逻辑的编写,在管线中调用 getBlendEffect 方法可以拿到经过 全局/局部 混合后的最终后处理数据。
引擎内置了 PostProcessUberPass,搭配 BloomEffect 和 TonemappingEffect 的数据使用,如果想要自定义后处理特效,我们需要新建一个 Pass,然后根据是否需要融合数据来创建 Effect。
这里就简单地实现一个灰度图的后处理效果吧~
我们先创建一个脚本,接下去我们要在这个脚本文件里面编写我们的自定义后处理 Shader 和 Pass。
我们参考添加后处理方式添加全局或者局部后处理组件,并且将脚本挂到这个实体上面:
算法没有特殊的, 需要注意 renderer_BlitTexture
这个内置变量就是上一个 Pass 的后处理渲染结果,在此处就是 Bloom 和 Tonemapping 后的结果,我们针对这个结果进行了灰度显示。
const customShader = Shader.create(
"Gray Scale Shader",
`
attribute vec4 POSITION_UV;
varying vec2 v_uv;
void main() {
gl_Position = vec4(POSITION_UV.xy, 0.0, 1.0);
v_uv = POSITION_UV.zw;
}
`,
`
varying vec2 v_uv;
uniform sampler2D renderer_BlitTexture;
void main(){
vec4 color = texture2D(renderer_BlitTexture, v_uv);
float grayScale = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
gl_FragColor = vec4(vec3(grayScale), 1.0);
}
`
);
我们新建一个 Pass,在 onRender 钩子 里面直接 Blitter 到屏幕,然后将这个 Pass 添加到引擎中。
class CustomPass extends PostProcessPass {
private _blitMaterial: Material;
constructor(engine: Engine) {
super(engine);
this._blitMaterial = new Material(this.engine, customShader);
// 我们这里关掉深度测试和深度写入,保证每次 Blit 都能覆盖到屏幕上。
const depthState = this._blitMaterial.renderState.depthState;
depthState.enabled = false;
depthState.writeEnabled = false;
}
onRender(_, srcTexture: Texture2D, dst: RenderTarget): void {
Blitter.blitTexture(this.engine, srcTexture, dst, undefined, undefined, this._blitMaterial, 0);
}
}
const customPass = new CustomPass(engine);
engine.addPostProcessPass(customPass);
Pass 的执行顺序默认在 Uber Pass 的后面执行,即 PostProcessPassEvent.AfterUber
,我们也可以手动修改管线的执行顺序:
customPass.event = PostProcessPassEvent.BeforeUber;
Pass 是否生效默认是根据 Pass 的 isActive 来决定的,我们也可以修改生效逻辑,比如强度是否大于 0 :
class CustomPass extends PostProcessPass {
override isValid(postProcessManager: PostProcessManager): boolean {
if (!this.isActive) {
return false;
}
const customEffectBlend = postProcessManager.getBlendEffect(CustomEffect);
return customEffectBlend?.intensity > 0;
}
}
上述 2、3 步骤已经能够自定义后处理效果,这里再来一个进阶版的融合数据。
拿 intensity
举例,我们定义一个 CustomEffect
,专门用来融合强度,需要融合的数据也很简单,引擎已经封装了一系列后处理参数,如浮点类型参数。
class CustomEffect extends PostProcessEffect {
intensity = new PostProcessEffectFloatParameter(0.8);
}
// 将这个 effect 添加到后处理组件中,postProcess 可以是单独新建的,
// 也可以是跟 Bloom 等 effect 同一个,具体看融合的需求~
postProcess.addEffect(CustomEffect);
定义好数据后,需要在自定义 Pass 的 onRender
钩子中,改成获取融合数据:
class CustomPass extends PostProcessPass {
onRender(camera: Camera, srcTexture: Texture2D, dst: RenderTarget): void {
const postProcessManager = camera.scene.postProcessManager;
const customEffectBlend = postProcessManager.getBlendEffect(CustomEffect);
if (customEffectBlend) {
this._blitMaterial.shaderData.setFloat("u_intensity", customEffectBlend.intensity.value);
}
Blitter.blitTexture(this.engine, srcTexture, dst, undefined, undefined, this._blitMaterial, 0);
}
}
可以看到,我们在自定义管线的 onRender
钩子中,不断设置融合后的强度,然后在 shader 中消费这个数据即可:
const customShader = Shader.create(
"Gray Scale Shader",
`
......
`,
`
......
uniform float u_intensity;
void main(){
......
gl_FragColor = vec4(mix(color.rgb, vec3(grayScale), u_intensity), 1.0);
}
`
);
如果你的后处理组件是局部模式,我们还可以通过 Blend Distance 来设置相机靠近碰撞体多少距离时开始混合: