模糊效果是游戏项目开发中最简单也是最常见的效果之一,什么高斯模糊,运动模糊,镜头景深之类的效果通通都属于模糊的范畴,只是根据最终的显示不同,算法上略有区别而已,当然除了后期效果,模糊还有很多广泛的应用,比如优化实时阴影
模糊的原理
个人理解就是一句话,让像素失去它原本的细节信息
通俗的解释是一个像素原本的颜色乘以一个权重值,(范围在0,1之间),默认为1,无变化,乘完之后从周围的像素补齐剩余的权重,举个例子:自身像素颜色*权重值0.5,然后从左右各取一个像素颜色,分别乘以权重值0.25,最后把所有的颜色相加代替原本的颜色,我们可以说原本的像素丢失了50%的细节
权重的分配
权重的分配可以做为了一个模糊效果的调整参数,简单一点的操作是取周围N个像素做平均值,那么你得到的模糊会是一块一块的,类似马赛克的效果,如果我将靠近原本像素的权重值给高,然后以距离来分配权重,即越接近中心的像素权重越高,越边缘的像素权重越低,你会得到类似毛玻璃的效果,这是因为像素丢失的细节并不大,我们还是可以模模糊糊看到它的样子,而且由于权重的正态分布,边缘处是平滑的,而这种正态分布的方式又叫高斯分布,至此你已经学会了高斯模糊。
现在让我们扩展一下思维,前面说到模糊是取周围的N个像素来补齐我们丢失的细节,那么N到底是多少呢,事实上这并没有一个准确的数字,毕竟渲染的原则为渲染结果正确即合理,但是想想也知道,取的周围的像素越多,越模糊,计算量也越大,(这里给出UNITY官方的高斯模糊算法供大家参考,取左上角方向两个像素,右下角方向两个像素,中间像素权重0.4,然后以和中心点的距离分别为0.15和0.05)。另外模糊像素距离中心点位置(越远模糊越严重)
现在我们知道取填充像素的数量可变,那么我们是否控制一下这些像素的坐标呢,难道必须是周围N圈吗?显然不是的,比如我们可以根据角色或者相机移动的方向来换算出我们要模糊的“周围”像素位置,于是我们得到了运动模糊的效果(运动模糊的实现我之后的文章会详细说,这里只说原理)。
继续思考,上面我们说的模糊是针对整个画面的所有像素的来处理的,有没有一种可能,我们只对“部分”像素进行模糊操作呢,答案是肯定的,不过我们需要一个参考值来告诉我们哪些像素需要模糊,哪些不需要,说到这里,你们不会想到溶解效果的实现,给张控制图不就完了,说道控制图,那么相机的深度图算不算?当然算!我们可以根据深度图来使距离相机近的像素不模糊,距离远的像素模糊,于是我们得到的镜头效果-景深
实践
通过上面的几种方式你可能得到的模糊效果并不够“模糊”,特别是你处理了一张高清图的时候,这个时候我们需要对它多模糊几次,简单来说就是先把原图blit到临时RT1上,然后RT1 blit到临时RT2上,RT2再到RT1,反复模糊处理,那么画面也会越模糊,当然性能也越低,毕竟你pass了N回
优化总结
这里我们总结一下导致性能下降的几个点
1.模糊像素数量
2.模糊次数
3.临时RT大小(临时RT越小,处理的像素越少,反正是模糊处理)
4.因地制宜(根据不同使用情况,采用不同的模糊算法,在性能充足的时候可以满足美术同学的细节需求,性能不充足的情况简单模糊处理,甚至可以模糊后做为一个不动的背景图使用,不用实时渲染,这也是本文的最终目的,懂原理,然后对项目进行定制化渲染,而不是一套效果从头用到尾
最后给出我在项目中里实现的一个模糊效果供大家参考(暂且叫高斯模糊吧,其实不满足正态分布,效果是最终调出来的)先贴一下效果图
代码(项目使用URP渲染)
using System; using UnityEngine; using System.Collections.Generic; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using GameCore; namespace PF.URP.PostProcessing { [DisallowMultipleComponent] public class GaussianBlur : MonoBehaviour { private Material mMat; private bool mIsOpen; public void Open() { mIsOpen = true; CameraSlave.UI.Use(); CameraSlave.UI.camera.SetCullingMask("Temp3"); (CameraSlave.UI as CameraSlaveUI)?.SwitchEventSystem(); SetLayer(LayerMask.NameToLayer("Temp3")); PFPostProcessingMgr.Instance.AddPostProcessing(this); } public void Close() { if (mIsOpen) { PFPostProcessingMgr.Instance.RemovePostProcessing(this); SetLayer(LayerMask.NameToLayer("UI")); CameraSlave.UI.UnUse(); mIsOpen = false; } } private void SetLayer(int layer) { transform.gameObject.layer = layer; transform.SetChildLayer(layer); } [LuaInterface.NoToLua] public Material Material { get { if (mMat == null) { mMat = Resources.Load<Material>("GaussianBlur"); mMat = new Material(mMat); } return mMat; } } } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using GameCore; namespace PF.URP.PostProcessing { public class GaussianBlurFeature : ScriptableRendererFeature { public RenderPassEvent mEvent = RenderPassEvent.AfterRenderingPostProcessing; private GaussianBlurPass mPass; private GaussianBlur mBlur; public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { mBlur = PFPostProcessingMgr.Instance.GetPostProcessing<GaussianBlur>(); if (mBlur == null)//控制开关 return; if (renderingData.cameraData.camera != CameraEx._UICamera) return; var cameraColorTarget = renderer.cameraColorTarget; //设置当前需要后期的画面 mPass.Setup(cameraColorTarget, mBlur); //添加到渲染列表 renderer.EnqueuePass(mPass); } public override void Create() { mPass = new GaussianBlurPass(mEvent); } } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using GameCore; namespace PF.URP.PostProcessing { public class GaussianBlurPass : ScriptableRenderPass { private const string mCommandBufferName = "CommandBuffer_GaussianBlur"; private const string mTempTexName1 = "GaussianBlurPass Temp Texture1"; private const string mTempTexName2 = "GaussianBlurPass Temp Texture2"; private RenderTargetHandle mTempTex_Handle1; private RenderTargetHandle mTempTex_Handle2; private GaussianBlur mBlur; private FilterMode mFilterMode = FilterMode.Bilinear; private RenderTargetIdentifier mSourceRT_Id; public GaussianBlurPass(RenderPassEvent @event) { this.renderPassEvent = @event; mTempTex_Handle1.Init(mTempTexName1); mTempTex_Handle2.Init(mTempTexName2); } public void Setup(RenderTargetIdentifier sourceRT, GaussianBlur blur) { mSourceRT_Id = sourceRT; mBlur = blur; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd = CommandBufferPool.Get(mCommandBufferName); RenderImage(cmd, ref renderingData); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } private void RenderImage(CommandBuffer cmd, ref RenderingData renderingData) { RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor; opaqueDesc.width = Mathf.FloorToInt(Screen.width * 0.5f); opaqueDesc.height = Mathf.FloorToInt(Screen.height * 0.5f); //获取临时RT cmd.GetTemporaryRT(mTempTex_Handle1.id, opaqueDesc, mFilterMode); cmd.GetTemporaryRT(mTempTex_Handle2.id, opaqueDesc, mFilterMode); //1 mBlur.Material.SetVector("_OffsetSize", new Vector4(3, 4, 5, 6)); Blit(cmd, mSourceRT_Id, mTempTex_Handle1.Identifier(), mBlur.Material); //2 mBlur.Material.SetVector("_OffsetSize", new Vector4(6, 5, 4, 3)); Blit(cmd, mTempTex_Handle1.Identifier(), mTempTex_Handle2.Identifier(), mBlur.Material); //3 mBlur.Material.SetVector("_OffsetSize", new Vector4(3, 4, 5, 6)); Blit(cmd, mTempTex_Handle2.Identifier(), mTempTex_Handle1.Identifier(), mBlur.Material); //4 mBlur.Material.SetVector("_OffsetSize", new Vector4(6, 5, 4, 3)); Blit(cmd, mTempTex_Handle1.Identifier(), mTempTex_Handle2.Identifier(), mBlur.Material); //将处理后的RT赋值给相机RT Blit(cmd, mTempTex_Handle2.Identifier(), mSourceRT_Id); } public override void FrameCleanup(CommandBuffer cmd) { //释放临时RT cmd.ReleaseTemporaryRT(mTempTex_Handle1.id); cmd.ReleaseTemporaryRT(mTempTex_Handle2.id); } } }
文章评论