在上一篇文章中,麒麟子给大家分享了如何在 Cocos Creator 3.8 中的自定义管线中,添加属于自己的后期效果 Shader。 但基于 BlitScreen 的方案,我们只能编写最简单后效 Shader,如果我们想要支持更多复杂的 Shader,比如模糊、景深等等效果,就需要配合代码才能实现。
今天麒麟子就用高斯模糊来演示如何编写一个多 Pass 的后效 Shader。
高斯模糊的涉及到的内容比较多,什么正态分布、高斯函数、高斯卷积核。
如果对数学没兴趣,上面这些统统不用管。
简单来说,高斯模糊就是把图片上的每一个像素都用下面的流程处理一遍。
直白来说,就是一个简单的加权求和:采样目标像素的同时,再采样一些周围的像素;并且每一个像素给一个权重(权重和为1.0)。最终像素值 = 所有(像素 x 权重)的和。
如果想要效果好,就多迭代几次,迭代次数越多,画面越好,但性能开销越高。
我们创建一个 Cocos Shader 文件,命名为 “gaussian-blur.effect” 然后编写下面的内容。
CCEffect %{
techniques:
- passes:
- vert: blur-hor-vs
frag: blur-fs
pass: blur-x
depthStencilState:
depthTest: false
depthWrite: false
- vert: blur-vert-vs
frag: blur-fs
pass: blur-y
depthStencilState:
depthTest: false
depthWrite: false
}%
CCProgram blur-hor-vs %{
//...
}%
CCProgram blur-hor-vs %{
//...
}%
CCProgram blur-fs %{
//...
}%
//为了方便文章阅读
//完整 Shader 被放到了文末
可以看到,整个 Cocos Shader 只有两个 Pass,一个用来水平模糊,一个用来竖直模糊。
为了不影响阅读,完整 Shader 放在了文末。
注意:Cocos Creator 3.8.0 版本如果新增了后效 Shader,需要重启编辑器才能识别。后面版本中会优化这个流程。
Cocos Creator 3.8 中,只需要在节点上添加对应的后期效果组件,就可以启动对应的后期效果。
自定义后期管线提供了一个 postProcess.PostProcessingSetting
组件类,我们可以通过继承它类来实现后效参数的可视化界面配置。
通过它自定义出来的后期效果 ,是完全可以达到和内置的后期效果一样的使用体验的。
新建一个 TS 脚本文件,起名为 “GaussianBlur.ts”,然后输入以下代码。
代码片段1:
import { _decorator, gfx, postProcess, Material, EffectAsset, renderer, rendering, Vec4 } from 'cc';
const { Format } = gfx
const { ccclass, property, menu, executeInEditMode } = _decorator;
@ccclass('GaussianBlur')
@menu('PostProcess/GaussianBlur')
@executeInEditMode
export class GaussianBlur extends postProcess.PostProcessSetting{
@property(EffectAsset)
_effectAsset: EffectAsset | undefined
@property(EffectAsset)
get effect () {
return this._effectAsset;
}
set effect (v) {
this._effectAsset = v;
if(this._effectAsset == null){
this._material = null;
}
else{
if(this._material == null){
this._material = new Material();
}
this._material.reset({effectAsset:this._effectAsset});
}
this.updateMaterial();
}
@property
iterations = 3;
@property
get blurRadius(){
return this._blurParams.x;
}
set blurRadius(v){
this._blurParams.x = v;
this.updateMaterial();
}
private _material:Material;
public get material():Material{
return this._material;
}
@property
private _blurParams:Vec4 = new Vec4(1.0,0.0,0.0,0.0);
public get blurParams():Vec4{
return this._blurParams;
}
updateMaterial(){
if(!this._material){
return;
}
this._material.setProperty('blurParams', this.blurParams);
}
protected start(): void {
if(this._effectAsset){
this._material = new Material();
this._material.initialize({effectAsset:this._effectAsset});
this._material.setProperty('blurParams', this.blurParams);
}
}
}
接下来,我们就可以通过“添加组件“按钮,把 PostProcess/GansussianBlur
添加到后处理结点上了。
可以看到,在 Inspector 面板上,它接受 3 个参数。
effect
:用于指定用于这个后期效果的 Shader 文件iterations
:用于指定需要迭代多少次,迭代次数越多越模糊blurRadius
:采样临近像素时的偏移,偏移量越大越模糊这个时候是没有任何效果的,因为还没有实现对应的渲染代码。
Cocos Creator 3.8 中的后处理管线是基于新版自定义管线的,而新版的自定义管线是基于 RenderGraph 架构的。
你可以简单地认为,我们想要实现的单个后处理效果,对应的就是渲染流程图上的一个节点。
后处理管线提供了 postProcess.SettingPass
给我们,用来编写自己的后处理渲染效果。(也可以叫,自定义后处理节点的资源和行为)。
接下来,我们来做真正的渲染实现。 我们需要继承自定义管线中的 postProcess.SettingPass
类,并实现并要的代码。
get setting
:获取配置信息,对应的就是上面实现的界面组件checkEnable
:用于判断此后效是否开启name
:后效的名字,一般保持和类名一致即可outputNames
:最终输出的 RT 数组。(临时用的 RT 不用放在这里)render
:用于执行渲染流程完整编码如下。
代码片段2:
export class GaussianBlurPass extends postProcess.SettingPass {
get setting () { return this.getSetting(GaussianBlur); }
checkEnable (camera: renderer.scene.Camera) {
let enable = super.checkEnable(camera);
if (postProcess.disablePostProcessForDebugView()) {
enable = false;
}
return enable && this.setting.material != null;
}
name = 'GaussianBlurPass';
outputNames = ['GaussianBlurMap'];
public render (camera: renderer.scene.Camera, ppl: rendering.Pipeline): void {
const setting = this.setting;
if(!setting.material){
return;
}
let passContext = this.context;
passContext.material = setting.material;
const cameraID = this.getCameraUniqueID(camera);
const cameraName = `Camera${cameraID}`;
const passViewport = passContext.passViewport;
passContext.clearBlack();
const format = Format.RGBA8;
let input = this.lastPass!.slotName(camera, 0);
for(let i = 0; i < setting.iterations; ++i){
passContext
.updatePassViewPort()
.addRenderPass(`blur-x`, `blur-x${cameraID}`)
.setPassInput(input, 'outputResultMap')
.addRasterView('GaussianBlurMap_TMP', format)
.blitScreen(0)
.version();
passContext
.updatePassViewPort()
.addRenderPass(`blur-y`, `blur-y${cameraID}`)
.setPassInput('GaussianBlurMap_TMP', 'outputResultMap')
.addRasterView(this.slotName(camera), format)
.blitScreen(1)
.version();
input = this.slotName(camera);
}
}
}
接下来,我们主要看看 render
处理了哪些事情。
每一个 SettingPass
就是一个绘制节点。而节点的数据,我们存在了 context
中。
render
函数会每帧执行,所以需要调用 context.clearBack()
来清理背景。
然后,我们要将材质设置给 context
。
模糊的处理,需要使用上一个处理流程结束后的画面内容。因此,我们使用 this.lastPass.slotName(camera,0);
来获取。
一切准备就绪后,就进入到了绘制环节。
这里,我们使用了 iterations 属性来控制总共要迭代的次数。迭代一次,绘制流程就会走一遍。
我们来看看,绘制流程中每一步操作的用途。
updatePassViewPort:这个函数用来指定相对分辨率大小,这个根据算法需求来指定就行。如果要保持和后台缓冲区一样大,传入 1.0 即可。
addRenderPass:这个函数用来告诉管线,需要执行一次绘制流程。
setPassInput:如果有用到自定义管线中的 RT 资源(比如上一次执行的结果),则需要在这里指定,方便自定义管线对资源进行管理。
addRasterView:可以简单理解为,输出结果
blitScreen:执行绘制
version:无实际意义,可以忽略。
如果只是写好了上面的代码,不进行添加,也是不会生效的。
我们在文件末尾加上下面代码。
代码片段3:
let builder = rendering.getCustomPipeline('Custom') as postProcess.PostProcessBuilder;
if (builder) {
builder.insertPass(new GaussianBlurPass(),postProcess.BlitScreenPass);
}
首先,我们获取到了 Custom 管线,然后把我们新写的效果添加进去。
回到编辑器中,调节参数,就可以看到我们新写的模糊效果生效咯。
为了方便大家理解后处理的渲染流程,麒麟子简单说明一些关键的源码。
后效相关的类,大多都在 postProcess
下,建议先从 “cc” 引入 postProcess
,再使用。
开启后处理管线后,开启了后处理管线效果的摄像机,会自动生成 RT,并设置它的 targetTexture
。
后效管线基于 RenderGraph 管线架构, Cocos 引擎中使用的 RenderGraph 是基于数据驱动的,会每帧收集需要的 RT 资源,并做统一管理。 因此不需要自己再手工新建 RT。
后处理效果的执行,不是按界面添加的顺序来的,而是按照处于数组中的顺序来的。我们通过源码可以看到它的内部顺序如下:
// pipeline related
this.addPass(new HBAOPass());
this.addPass(new ToneMappingPass());
// user post-processing
this.addPass(new TAAPass());
this.addPass(new FxaaPass());
this.addPass(new ColorGradingPass());
this.addPass(new BlitScreenPass());
this.addPass(new BloomPass());
// final output
this.addPass(new FSRPass()); // fsr should be final
this.addPass(forwardFinal);
后处理效果在渲染时,管线会遍历这个数组,依次执行可用的后处理效果。
for (let i = 0; i < passes.length; i++) {
const pass = passes[i];
if (!pass.checkEnable(camera)) {
continue;
}
if (i === (passes.length - 1)) {
passContext.isFinalPass = true;
}
pass.lastPass = lastPass;
pass.render(camera, ppl);
lastPass = pass;
}
**源码位置:**engine/cocos/rendering/post-process/post-process-builder.ts
在将自己定义的后效 Shader 添加到管线中时,需要注意几个问题:
ForwardFinal
之前,否则没有效果。所以 builder.addPass
不建议使用builder.insterPass
添加新的后效时,如果新后效与旧的后效重名,会先移除旧的后效builder.insterPass
会将新的后效插入到第二个参数类型指定的后效后面,通常情况下,建议使用 postProcess.BlitScreenPass
。后效会多次读写帧缓存,对 GPU 带宽和像素填充率,纹理填充率要求高。 在中低端机型上很可能造成性能锐减。
做好高、中、低端机型管理 ,在不同档次的机型上,开启对应的后效组合,确保性能和效果的平衡。
许多单 Pass 的后效,可以合成一个。 比如 ColorGrading,FXAA,Vignette,FinalPass 可以合成一个。这样可以减少 BlitScreen 次数,提升性能。
HBAO 能够极大地提升空间关系,但也是性能开销大户,谨慎使用。
使用后处理效果能极大地提升画面质感,但也需要注意后处理效果带来的额外内存开销和填充率开销。
后效对低端机型很不友好,请做好在低端机上根据性能测试结果做好分档。
另外,后效的种类繁多,且根据具体的项目需求,又可以做特殊优化。因此引擎只能内置常见的后效供大家使用,更多后效果需求,还得开发者们自己来。
希望今天的分享能够对大家有帮助,谢谢大家!
新建一个 TS 脚本,将代码片段1、代码片段2、代码片段3 复制到这个脚本中即可。
新建一个 Cocos Shader 文件,将下面 Shader 代码复制到文件中即可。
CCEffect %{
techniques:
- passes:
- vert: blur-hor-vs
frag: blur-fs
pass: blur-x
depthStencilState:
depthTest: false
depthWrite: false
- vert: blur-vert-vs
frag: blur-fs
pass: blur-y
depthStencilState:
depthTest: false
depthWrite: false
}%
CCProgram blur-hor-vs %{
precision highp float;
#include <legacy/input-standard>
#include <builtin/uniforms/cc-global>
#include <common/common-define>
uniform MyConstants {
vec4 blurParams;
};
out vec2 v_uv;
out vec2 v_uv1;
out vec2 v_uv2;
out vec2 v_uv3;
out vec2 v_uv4;
void main () {
StandardVertInput In;
CCVertInput(In);
CC_HANDLE_GET_CLIP_FLIP(In.position.xy);
gl_Position = In.position;
gl_Position.y = gl_Position.y;
v_uv = a_texCoord;
vec2 texelSize = cc_nativeSize.zw;
float blurOffsetX = blurParams.x * texelSize.x;
v_uv1 = v_uv + vec2(blurOffsetX * 1.0, 0.0);
v_uv2 = v_uv - vec2(blurOffsetX * 1.0, 0.0);
v_uv3 = v_uv + vec2(blurOffsetX * 2.0, 0.0);
v_uv4 = v_uv - vec2(blurOffsetX * 2.0, 0.0);
}
}%
CCProgram blur-vert-vs %{
precision highp float;
#include <legacy/input-standard>
#include <builtin/uniforms/cc-global>
#include <common/common-define>
uniform MyConstants {
vec4 blurParams;
};
out vec2 v_uv;
out vec2 v_uv1;
out vec2 v_uv2;
out vec2 v_uv3;
out vec2 v_uv4;
void main () {
StandardVertInput In;
CCVertInput(In);
CC_HANDLE_GET_CLIP_FLIP(In.position.xy);
gl_Position = In.position;
gl_Position.y = gl_Position.y;
v_uv = a_texCoord;
vec2 texelSize = cc_nativeSize.zw;
float blurOffsetY = blurParams.x * texelSize.y;
v_uv1 = v_uv + vec2(0.0, blurOffsetY * 1.0);
v_uv2 = v_uv - vec2(0.0, blurOffsetY * 1.0);
v_uv3 = v_uv + vec2(0.0, blurOffsetY * 2.0);
v_uv4 = v_uv - vec2(0.0, blurOffsetY * 2.0);
}
}%
CCProgram blur-fs %{
precision highp float;
#include <builtin/uniforms/cc-global>
in vec2 v_uv;
in vec2 v_uv1;
in vec2 v_uv2;
in vec2 v_uv3;
in vec2 v_uv4;
#pragma rate outputResultMap pass
uniform sampler2D outputResultMap;
layout(location = 0) out vec4 fragColor;
void main () {
vec3 weights = vec3(0.4026,0.2442,0.0545);
vec3 sum = texture(outputResultMap, v_uv).rgb * weights.x;
sum += texture(outputResultMap, v_uv1).rgb * weights.y;
sum += texture(outputResultMap, v_uv2).rgb * weights.y;
sum += texture(outputResultMap, v_uv3).rgb * weights.z;
sum += texture(outputResultMap, v_uv4).rgb * weights.z;
fragColor = vec4(sum, 1.0);
}
}%
深耕游戏引擎与游戏开发15年,每一滴干货都源自商业项目实践。
用技术资源赋能行业商机,提供实用解决方案、项目分析、技术指导与干货教程!
欢迎私信!