模糊效果是游戏项目开发中最简单也是最常见的效果之一,什么高斯模糊,运动模糊,镜头景深之类的效果通通都属于模糊的范畴,只是根据最终的显示不同,算法上略有区别而已,当然除了后期效果,模糊还有很多广泛的应用,比如优化实时阴影
模糊的原理
个人理解就是一句话,让像素失去它原本的细节信息
通俗的解释是一个像素原本的颜色乘以一个权重值,(范围在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);
}
}
}
文章评论