高性能模糊算法 Dual Blur 在 2D Sprite 上的实现与应用丨Cocos Creator
引言:在游戏开发中,很多效果的实现都离不开图像模糊算法的运用。今天,一起来看看社区开发者「詠恆の承諾」是如何基于 RenderTexture 实现多 Pass Kawase Blur。
屏幕后处理效果(Screen Post Processing Effects)是游戏中实现屏幕特效的方法,有助于提升画面效果。图像模糊算法在后处理渲染领域占据着重要地位,泛光(Bloom)、镜头眩光光晕(Glare Lens Flare)、景深(Depth of Field)、体积光(Volume Ray)等许多效果都用到了图像模糊算法。所以说,后处理中所采用模糊算法的优劣,决定了后处理管线最终的渲染品质和消耗性能的多少。
后处理管线中会使用到十种模糊算法总结
前段时间,由于项目需要做一个背景模糊的功能,正巧之前看到了大城小胖在》" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">《如何重绘<江南百景图>》中对比了几种模糊算法,本着学习的态度,我决定尝试在 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 轴相反。
protectedrenderWithMaterial(srcRT:cc.RenderTexture,dstRT:cc.RenderTexture|cc.Material,material?:cc.Material,size?:cc.Size){//检查参数if(dstRTinstanceofcc.Material){material=dstRT;dstRT=newcc.RenderTexture();}//创建临时节点(用于渲染RenderTexture)consttempNode=newcc.Node();tempNode.setParent(cc.Canvas.instance.node);consttempSprite=tempNode.addComponent(cc.Sprite);tempSprite.sizeMode=cc.Sprite.SizeMode.RAW;tempSprite.trim=false;tempSprite.spriteFrame=newcc.SpriteFrame(srcRT);//获取图像宽高const{width,height}=size??{width:srcRT.width,height:srcRT.height};//初始化RenderTexture//如果截图内容中不包含Mask组件,可以不用传递第三个参数dstRT.initWithSize(width,height,cc.gfx.RB_FMT_S8);//更新材质if(materialinstanceofcc.Material){tempSprite.setMaterial(0,material);}//创建临时摄像机(用于渲染临时节点)constcameraNode=newcc.Node();cameraNode.setParent(tempNode);constcamera=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();//返回RenderTexturereturndstRT;}
提示!需要留意 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*Inittherendertexturewithsize.*!#zh*初始化rendertexture*@param{Number}[width]*@param{Number}[height]*@param{Number}[depthStencilFormat]*@methodinitWithSize*/initWithSize(width,height,depthStencilFormat){this.width=Math.floor(width||cc.visibleRect.width);this.height=Math.floor(height||cc.visibleRect.height);this._resetUnderlyingMipmaps();letopts={colors:[this._texture],};if(this._depthStencilBuffer)this._depthStencilBuffer.destroy();letdepthStencilBuffer;if(depthStencilFormat){depthStencilBuffer=newgfx.RenderBuffer(renderer.device,depthStencilFormat,width,height);if(depthStencilFormat===gfx.RB_FMT_D24S8){opts.depthStencil=depthStencilBuffer;}elseif(depthStencilFormat===gfx.RB_FMT_S8){opts.stencil=depthStencilBuffer;}elseif(depthStencilFormat===gfx.RB_FMT_D16){opts.depth=depthStencilBuffer;}}this._depthStencilBuffer=depthStencilBuffer;if(this._framebuffer)this._framebuffer.destroy();this._framebuffer=newgfx.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中基本上仅进行采样即可。
此外,为了支持合图也能使用,这里我修改了顶点数据。
//DualKawaseBlur(双重模糊)//教程地址:https://github.com/QianMo/X-PostProcessing-Library/tree/master/Assets/X-PostProcessing/Effects/DualKawaseBlurCCEffect%{techniques:-name:Downpasses:-name:Downvert:vs:Downfrag:fs:DownblendState:targets:-blend:truerasterizerState:cullMode:noneproperties:&proptexture:{value:white}resolution:{value:[1920,1080]}offset:{value:1,editor:{range:[0,100]}}alphaThreshold:{value:0.5}-name:Uppasses:-name:Upvert:vs:Upfrag:fs:UpblendState:targets:-blend:truerasterizerState:cullMode:noneproperties:*prop}%CCProgramvs%{precisionhighpfloat;#include#include invec3a_position;invec4a_color;outvec4v_color;#ifUSE_TEXTUREinvec2a_uv0;outvec2v_uv0;outvec4v_uv1;outvec4v_uv2;outvec4v_uv3;outvec4v_uv4;#endifuniformProperties{vec2resolution;floatoffset;};vec4Down(){vec4pos=vec4(a_position,1);#ifCC_USE_MODELpos=cc_matViewProj*cc_matWorld*pos;#elsepos=cc_matViewProj*pos;#endif#ifUSE_TEXTUREvec2uv=a_uv0;vec2texelSize=0.5/resolution;v_uv0=uv;v_uv1.xy=uv-texelSize*vec2(offset);//toprightv_uv1.zw=uv+texelSize*vec2(offset);//bottomleftv_uv2.xy=uv-vec2(texelSize.x,-texelSize.y)*vec2(offset);//toprightv_uv2.zw=uv+vec2(texelSize.x,-texelSize.y)*vec2(offset);//bottomleft#endifv_color=a_color;returnpos;}vec4Up(){vec4pos=vec4(a_position,1);#ifCC_USE_MODELpos=cc_matViewProj*cc_matWorld*pos;#elsepos=cc_matViewProj*pos;#endif#ifUSE_TEXTUREvec2uv=a_uv0;vec2texelSize=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;#endifv_color=a_color;returnpos;}}%CCProgramfs%{precisionhighpfloat;#include #include #include
有了 effect 后,需要创建2个材质分别对应 techniques中的 Down与 Up,示例代码中用 materialDown、materialUp来表示2个材质。
通过摄像机截图,得到初始 RT 后(纹理倒置),对初始 RT 进行降采样和模糊得到新的 RT,重复若干次后,对最后的 RT 进行相同次数的升采样和模糊,得到最终满足效果的 RT。当降采样 scale不为1时,设置 RT 尺寸时会自动向下取整,倒置最终效果会有黑边,iteration次数越大越明显,且 iteration存在上限,实际使用时可自行取舍。
/***模糊渲染*@paramoffset模糊半径*@paramiteration模糊迭代次数*@paramscale降采样缩放比例*/blur(offset:number,iteration:number,scale:number=0.5){//设置源结点、目标spriteconstspriteDst=this.spriteDst,nodeSrc=this.spriteSrc.node;//设置材质constmaterial=this.materialDown;this.materialDown.setProperty("resolution",cc.v2(nodeSrc.width,nodeSrc.height));this.materialDown.setProperty("offset",offset);this.materialUp.setProperty("resolution",cc.v2(nodeSrc.width,nodeSrc.height));this.materialUp.setProperty("offset",offset);//创建临时RenderTextureletsrcRT=newcc.RenderTexture(),lastRT=newcc.RenderTexture();//获取初始RenderTexturethis.getRenderTexture(nodeSrc,lastRT);//多Pass处理//注:由于 OpenGL 中的纹理是倒置的,所以双数 Pass 的出的图像是颠倒的//记录升降纹理时纹理尺寸letpyramid:[number,number][]=[],tw:number=lastRT.width,th:number=lastRT.height;//Downsamplefor(leti=0;i=0;i--){[lastRT,srcRT]=[srcRT,lastRT];this.renderWithMaterial(srcRT,lastRT,this.materialUp,cc.size(pyramid[i][0],pyramid[i][1]));}//使用经过处理的RenderTexturethis.renderTexture=lastRT;spriteDst.spriteFrame=newcc.SpriteFrame(this.renderTexture);//翻转纹理y轴spriteDst.spriteFrame.setFlipY(true);//销毁不用的临时RenderTexturesrcRT.destroy();}
以上是本次的分享,希望可以给大家一点启发和帮助!欢迎点击【阅读原文】前往论坛专贴一起讨论交流,完整代码见代码仓库:
https://github.com/RicardoZhou/CreatorStudy/tree/master/assets/Menu/Shader/DualBlur
参考资料
[1]如何重绘《江南百景图》,大城小胖
[2]基于 RenderTexture 实现多 Pass 的 Kawase Blur,陈皮皮
https://forum.cocos.org/t/topic/126481
[3]Creator丨实现单个 Node 截图的两种方式,渡鸦
[4]高品质后处理:十种图像模糊算法的总结与实现,毛星云
https://zhuanlan.zhihu.com/p/125744132
[5]自定义顶点格式,GT
https://forum.cocos.org/t/topic/95087
往期精彩相关阅读
-
世界热推荐:今晚7:00直播丨下一个突破...
今晚19:00,Cocos视频号直播马上点击【预约】啦↓↓↓在运营了三年... -
NFT周刊|Magic Eden宣布支持Polygon网...
Block-986在NFT这样的市场,每周都会有相当多项目起起伏伏。在过去... -
环球今亮点!头条观察 | DeFi的兴衰与...
在比特币得到机构关注之后,许多财务专家预测世界将因为加密货币的... -
重新审视合作,体育Crypto的可靠关系才能双赢
Block-987即使在体育Crypto领域,人们的目光仍然集中在FTX上。随着... -
简讯:前端单元测试,更进一步
前端测试@2022如果从2014年Jest的第一个版本发布开始计算,前端开发... -
焦点热讯:刘强东这波操作秀
近日,刘强东发布京东全员信,信中提到:自2023年1月1日起,逐步为...