引言:在游戏开发中,很多效果的实现都离不开图像模糊算法的运用。今天,一起来看看社区开发者「詠恆の承諾」是如何基于 RenderTexture 实现多 Pass Kawase Blur。
屏幕后处理效果(Screen Post Processing Effects)是游戏中实现屏幕特效的方法,有助于提升画面效果。图像模糊算法在后处理渲染领域占据着重要地位,泛光(Bloom)、镜头眩光光晕(Glare Lens Flare)、景深(Depth of Field)、体积光(Volume Ray)等许多效果都用到了图像模糊算法。所以说,后处理中所采用模糊算法的优劣,决定了后处理管线最终的渲染品质和消耗性能的多少。
前段时间,由于项目需要做一个背景模糊的功能,正巧之前看到了大城小胖在《如何重绘<江南百景图>》中对比了几种模糊算法,本着学习的态度,我决定尝试在 Cocos Creator 2.4.x 中实现 Dual Blur(双重模糊)。
实现多 Pass
首先要解决的问题是:如何在 v2.4.x 中实现多 pass?
参考陈皮皮大佬的实现方案[2],基于 RenderTexture 实现多 Pass Kawase Blur。先将纹理渲染到 RenderTexture(下文简称 RT)上,再对得到的 RT 做单次模糊处理并得到新的 RT,重复此操作,将最后一个 RT 渲染到需要的 Sprite 中即可。
注意:每次渲染得到的 RT 是倒置的,渲染前的纹理 Y 轴相反。
protected renderWithMaterial(srcRT: cc.RenderTexture, dstRT: cc.RenderTexture | cc.Material, material?: cc.Material, size?: cc.Size) {
// 检查参数
if (dstRT instanceof cc.Material) {
material = dstRT;
dstRT = new cc.RenderTexture();
}
// 创建临时节点(用于渲染 RenderTexture)
const tempNode = new cc.Node();
tempNode.setParent(cc.Canvas.instance.node);
const tempSprite = tempNode.addComponent(cc.Sprite);
tempSprite.sizeMode = cc.Sprite.SizeMode.RAW;
tempSprite.trim = false;
tempSprite.spriteFrame = new cc.SpriteFrame(srcRT);
// 获取图像宽高
const { width, height } = size ?? { width: srcRT.width, height: srcRT.height };
// 初始化 RenderTexture
// 如果截图内容中不包含 Mask 组件,可以不用传递第三个参数
dstRT.initWithSize(width, height, cc.gfx.RB_FMT_S8);
// 更新材质
if (material instanceof cc.Material) {
tempSprite.setMaterial(0, material);
}
// 创建临时摄像机(用于渲染临时节点)
const cameraNode = new cc.Node();
cameraNode.setParent(tempNode);
const camera = cameraNode.addComponent(cc.Camera);
camera.clearFlags |= cc.Camera.ClearFlags.COLOR;
camera.backgroundColor = cc.color(0, 0, 0, 0);
// 根据屏幕适配方案,决定摄像机缩放比
// 还原sizeScale,zoomRatio取屏幕与RT宽高比
camera.zoomRatio = cc.winSize.height / srcRT.height;
// 将临时节点渲染到 RenderTexture 中
camera.targetTexture = dstRT;
camera.render(tempNode);
// 销毁临时对象
cameraNode.destroy();
tempNode.destroy();
// 返回 RenderTexture
return dstRT;
}
提示!需要留意 cc.RenderTexture.initWithSize(width, height, depthStencilFormat) 中的第3个参数,之前使用时我忽略了第3个参数,加上场景比较复杂,需要截图的结点中带有 Mask 组件,导致截图丢失了 Mask 组件所在结点之前的所有图片。
查看源码可知道,initWithSize 默认会清除深度缓冲区、模版缓冲区,depthStencilFormat 传入 gfx.RB_FMT_D16、gfx.RB_FMT_S8、gfx.RB_FMT_D24S8 时,则可以保留对应缓冲区。感谢鸦哥(渡鸦)的文章《实现单个 Node 截图的两种方式》[3],代码+注释太香了!
/**
* !#en
* Init the render texture with size.
* !#zh
* 初始化 render texture
* @param {Number} [width]
* @param {Number} [height]
* @param {Number} [depthStencilFormat]
* @method initWithSize
*/
initWithSize (width, height, depthStencilFormat) {
this.width = Math.floor(width || cc.visibleRect.width);
this.height = Math.floor(height || cc.visibleRect.height);
this._resetUnderlyingMipmaps();
let opts = {
colors: [ this._texture ],
};
if (this._depthStencilBuffer) this._depthStencilBuffer.destroy();
let depthStencilBuffer;
if (depthStencilFormat) {
depthStencilBuffer = new gfx.RenderBuffer(renderer.device, depthStencilFormat, width, height);
if (depthStencilFormat === gfx.RB_FMT_D24S8) {
opts.depthStencil = depthStencilBuffer;
}
else if (depthStencilFormat === gfx.RB_FMT_S8) {
opts.stencil = depthStencilBuffer;
}
else if (depthStencilFormat === gfx.RB_FMT_D16) {
opts.depth = depthStencilBuffer;
}
}
this._depthStencilBuffer = depthStencilBuffer;
if (this._framebuffer) this._framebuffer.destroy();
this._framebuffer = new gfx.FrameBuffer(renderer.device, width, height, opts);
this._packable = false;
this.loaded = true;
this.emit("load");
},
Dual Blur(双重模糊)
接下来只需实现 Dual Blur 算法即可。首先简单了解一下 Dual Blur,此处引用《高品质后处理:十种图像模糊算法的总结与实现》[4]一文。
Dual Kawase Blur,简称 Dual Blur,是一种衍生自 Kawase Blur 的模糊算法,其由两种不同的 Blur Kernel 构成。相较于 Kawase Blur 在两个大小相等的纹理之间进行乒乓 blit 的的思路,Dual Kawase Blur 的核心思路在于 blit 过程中进行降采样和升采样,即对 RT 进行了降采样以及升采样。
由于灵活的升降采样带来了 blit RT 所需计算量的减少等原因,Dual Kawase Blur 相对而言有更好的性能。下图是相同条件下几种模糊算法的性能对比,可以看到,Dual Kawase Blur 在其中具有最佳的性能表现。
为了带来更好的性能表现,可以将 uv 的偏移放在 Vert Shader 中进行,而 Fragment Shader 中基本上仅进行采样即可。
此外,为了支持合图也能使用,这里我修改了顶点数据。
// Dual Kawase Blur (双重模糊)
// 教程地址:https://github.com/QianMo/X-PostProcessing-Library/tree/master/Assets/X-PostProcessing/Effects/DualKawaseBlur
CCEffect %{
techniques:
- name: Down
passes:
- name: Down
vert: vs

frag: fs:Down
blendState:
targets:
- blend: true
rasterizerState:
cullMode: none
properties: &prop
texture: { value: white }
resolution: { value: [1920, 1080] }
offset: { value: 1, editor: { range: [0, 100] }}
alphaThreshold: { value: 0.5 }
- name: Up
passes:
- name: Up
vert: vs:Up
frag: fs:Up
blendState:
targets:
- blend: true
rasterizerState:
cullMode: none
properties: *prop
}%
CCProgram vs %{
precision highp float;
#include
#include
in vec3 a_position;
in vec4 a_color;
out vec4 v_color;
#if USE_TEXTURE
in vec2 a_uv0;
out vec2 v_uv0;
out vec4 v_uv1;
out vec4 v_uv2;
out vec4 v_uv3;
out vec4 v_uv4;
#endif
uniform Properties {
vec2 resolution;
float offset;
};
vec4 Down () {
vec4 pos = vec4(a_position, 1);
#if CC_USE_MODEL
pos = cc_matViewProj * cc_matWorld * pos;
#else
pos = cc_matViewProj * pos;
#endif
#if USE_TEXTURE
vec2 uv = a_uv0;
vec2 texelSize = 0.5 / resolution;
v_uv0 = uv;
v_uv1.xy = uv - texelSize * vec2(offset); //top right
v_uv1.zw = uv + texelSize * vec2(offset); //bottom left
v_uv2.xy = uv - vec2(texelSize.x, -texelSize.y) * vec2(offset); //top right
v_uv2.zw = uv + vec2(texelSize.x, -texelSize.y) * vec2(offset); //bottom left
#endif
v_color = a_color;
return pos;
}
vec4 Up () {
vec4 pos = vec4(a_position, 1);
#if CC_USE_MODEL
pos = cc_matViewProj * cc_matWorld * pos;
#else
pos = cc_matViewProj * pos;
#endif
#if USE_TEXTURE
vec2 uv = a_uv0;
vec2 texelSize = 0.5 / resolution;
v_uv0 = uv;
v_uv1.xy = uv + vec2(-texelSize.x * 2., 0) * offset;
v_uv1.zw = uv + vec2(-texelSize.x, texelSize.y) * offset;
v_uv2.xy = uv + vec2(0, texelSize.y * 2.) * offset;
v_uv2.zw = uv + texelSize * offset;
v_uv3.xy = uv + vec2(texelSize.x * 2., 0) * offset;
v_uv3.zw = uv + vec2(texelSize.x, -texelSize.y) * offset;
v_uv4.xy = uv + vec2(0, -texelSize.y * 2.) * offset;
v_uv4.zw = uv - texelSize * offset;
#endif
v_color = a_color;
return pos;
}
}%
CCProgram fs %{
precision highp float;
#include
#include
#include