前几天接了个需求,给自己参与的Unity项目添加一个屏幕后处理泛光效果,要求只能是灯有泛光效果,而其它不发光的物体不能出现泛光的效果。在写这个特效的时候遇到了一些坑,并且在网上查找解决方案的时候网上往往说的都是全屏的泛光效果,而对于需要分层的泛光效果能找到的资料很少。因此在这里记录一下,一方面加强一下自己的记忆另一方面希望能帮到有同样需求的同学。下面展示一组从LearnOpenGL盗来的没有泛光效果与添加了泛光效果的对比图:
从这组对比图中我们可以体会到,泛光可以极大提升场景中的光照效果。有泛光效果的场景让我们有一种灯真的在发光的感觉。
泛光效果一种比较简单的屏幕后处理的特效,可以分为以下几步实现:1.在一张纹理A中提取屏幕中的光亮区域,存储在一张渲染纹理B中;2.对B渲染纹理进行高斯模糊处理,得到纹理C;3.将经过高斯模糊处理的纹理C与纹理A进行混合,将结果输出即可。
下面我们来实现一遍上面的步骤,先实现shader部分(看到的代码排版如果不整齐的话可以看最后的截图):
提取光亮部分其实就是将采样得到的颜色转换为亮度,如果该亮度大于我们设定的阈值,则该点为一个亮点
Shader "Unlit/Bloom"
{
Properties
{
_MainTex("输入的渲染纹理", 2D) = "white" {}
_Bloom("高斯模糊后的较亮区域", 2D) = "black" {}
_LuminanceThreshold("用于提取较亮区域的阈值", Float) = 0.5
_BlurSize("用于控制不同迭代之间的高斯模糊的模糊区域范围", Float) = 1.0
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;
struct v2f
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
// 顶点着色器
v2f vertExtractBright(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
// 将传入的颜色转换为亮度
fixed luminance(fixed4 color)
{
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
// 片元着色器
fixed4 fragExtractBright(v2f i) : SV_Target
{
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
return c * val;
}
struct v2fBloom
{
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};
// 混合高斯模糊后的光亮区域纹理与原纹理的顶点着色器
v2fBloom vertBloom(appdata_img v)
{
v2fBloom o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0.0)
{
o.uv.w = 1.0 - o.uv.w;
}
#endif
return o;
}
// 混合高斯模糊后的光亮区域纹理与原纹理的片元着色器
fixed4 fragBloom(v2fBloom i) : SV_Target
{
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass // Pass0 提取光亮区域
{
CGPROGRAM
#pragma vertex vertExtractBright // 指定该Pass的顶点着色器
#pragma fragment fragExtractBright // 指定该Pass的片元着色器
ENDCG
}
UsePass "Unlit/GaussianBlur/GAUSSIAN_BLUR_VERTICAL" // Pass1 对纹理进行纵向的高斯模糊的处理
UsePass "Unlit/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL" // Pass2 对纹理进行横向的高斯模糊的处理
Pass // Pass3 将进行过高斯模糊的光亮区域与原纹理混合
{
CGPROGRAM
#pragma vertex vertBloom // 指定该Pass的顶点着色器
#pragma fragment fragBloom // 指定该Pass的片元着色器
ENDCG
}
}
FallBack Off
}
高斯模糊的shader代码如下(看到的代码排版如果不整齐的话可以看最后的截图):
Shader "Unlit/GaussianBlur"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
// 纵向模糊的顶点着色器
v2f vertBlurVertical(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
// 横向模糊的顶点着色器
v2f vertBlurHorizontal(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
// 片元着色器
fixed4 fragBlur(v2f i) : SV_Target {
float weight[3] = {0.4026, 0.2442, 0.0545};
// 纹理采样
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for (int it = 1; it < 3; it++) {
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1.0);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass { // Pass0 纵向模糊
NAME "GAUSSIAN_BLUR_VERTICAL" // 给该Pass命名,方便代码的重用
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass { // Pass1 横向模糊
NAME "GAUSSIAN_BLUR_HORIZONTAL" // 给该Pass命名,方便代码的重用
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
下面是c#代码部分(看到的代码排版如果不整齐的话可以看最后的截图):
首先我们创建用于挂在在主摄像机上的脚本:Bloom
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Effect
{
public class Bloom : PostEffectsBase
{
[Header("泛光效果的着色器")]
[SerializeField]
private Shader bloomShader;
///
/// 泛光效果的材质
///
private Material bloomMaterial = null;
///
/// 泛光效果的材质
///
public Material material
{
get
{
bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
return bloomMaterial;
}
}
[Header("高斯模糊的迭代次数,次数越多越模糊,但性能越低")]
[SerializeField]
[Range(0, 10)]
private int iterations = 3;
[Header("模糊范围(采样的距离的倍数)")]
[SerializeField]
[Range(0.2f, 3.0f)]
private float blurSpread = 1.0f;
[Header("缩放系数(创建的RenderTexture的宽高为屏幕大小的多少分之一)")]
[SerializeField]
[Range(1, 8)]
private int downSample = 2;
[Header("亮度的范围,如果一个像素亮度大于等于该值时认为该像素点需要进行泛光处理,在不开启HDR的情况下亮度范围不会大于1")]
[SerializeField]
[Range(0.0f, 4.0f)]
private float luminanceThreshold = 0.0f;
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
int iWidth = source.width / downSample;
int iHeight = source.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(iWidth, iHeight, 0);
buffer0.filterMode = FilterMode.Bilinear;
// 提取较亮区域部分(调用bloomShader中的Pass0)
Graphics.Blit(source, buffer0, material, 0);
// 进行高斯模糊(调用bloomShader中的Pass1和Pass2)
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(iWidth, iHeight, 0);
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(iWidth, iHeight, 0);
Graphics.Blit(buffer0, buffer1, material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
// 将高斯模糊后的纹理与原纹理进行混合(调用bloomShader中的Pass3)
material.SetTexture("_Bloom", buffer0);
Graphics.Blit(source, destination, material, 3);
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(source, destination);
}
}
}
}
Bloom继承于PostEffectBase
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Effect
{
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class PostEffectsBase : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
CheckResources();
OnStart();
}
protected virtual void OnStart()
{
}
protected void CheckResources()
{
if (CheckSupport() == false)
{
NotSupported();
}
}
protected bool CheckSupport()
{
if (SystemInfo.supportsImageEffects == false)
{
return false;
}
return true;
}
protected void NotSupported()
{
enabled = false;
}
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material)
{
if (shader == null)
{
return null;
}
if (shader.isSupported && material && material.shader == shader)
{
return material;
}
if (shader.isSupported == false)
{
return null;
}
else
{
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
return material;
}
}
}
}
然后我们搭建一个有吊灯模型的简单场景,将天空盒与平行光去掉
在主摄像机上添加Bloom脚本
然后我们就可以看到灯上的泛光效果了
但是这时候有个问题,如果我添加了一个白色的瓷盘到该场景中,则白色的瓷盘也有泛光效果
接下来我们就要想办法只让吊灯有泛光效果。这点相信很多人和我想到的是同一个办法:我们创建一个摄像机,它专门用来拍摄吊灯所在的Layer,然后获取该摄像机的渲染目标,再对该渲染目标进行亮度的提取。但是在这里我踩了一个坑,我创建了一个专门用于拍摄吊灯的摄像机之后,又在该摄像机上加了一个继承于Bloom的脚本,然后直接在该脚本的OnRenderImage中获取该摄像机的渲染目标,提取亮度,然后再进行高斯模糊与最后的混合操作。但是不知道为什么,在该脚本中获取的渲染目标总是不能应用到该脚本的OnRenderImage中。然后我转换了思路,直接在主摄像机上获取该摄像机的渲染目标,结果是可用的。
下面是修改过后的Bloom脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Effect
{
public class Bloom : PostEffectsBase
{
[Header("泛光效果的着色器")]
[SerializeField]
private Shader bloomShader;
[Header("泛光摄像机要在哪些层级中取亮度区域")]
[SerializeField]
private int[] CullingLayer;
///
/// 泛光效果的材质
///
private Material bloomMaterial = null;
///
/// 泛光效果的材质
///
public Material material
{
get
{
bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
return bloomMaterial;
}
}
[Header("高斯模糊的迭代次数,次数越多越模糊,但性能越低")]
[SerializeField]
[Range(0, 10)]
private int iterations = 3;
[Header("模糊范围(采样的距离的倍数)")]
[SerializeField]
[Range(0.2f, 3.0f)]
private float blurSpread = 1.0f;
[Header("缩放系数(创建的RenderTexture的宽高为屏幕大小的多少分之一)")]
[SerializeField]
[Range(1, 8)]
private int downSample = 2;
[Header("亮度的范围,如果一个像素亮度大于等于该值时认为该像素点需要进行泛光处理,在不开启HDR的情况下亮度范围不会大于1")]
[SerializeField]
[Range(0.0f, 4.0f)]
private float luminanceThreshold = 0.0f;
///
/// 特效摄像机上的渲染目标
///
private RenderTexture targetRenderTexture;
///
/// 当前的泛光摄像机
///
private Camera currBloomCamera;
protected override void OnStart()
{
base.OnStart();
if (targetRenderTexture == null)
{
targetRenderTexture = RenderTexture.GetTemporary(Screen.width, Screen.height, 16);
}
// 创建特效摄像机
int iTargetLayer = 0;
for (int i = 0; i < CullingLayer.Length; i++)
{
iTargetLayer |= 1 << CullingLayer[i];
}
currBloomCamera = CreateBloomCamera(iTargetLayer);
}
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null && targetRenderTexture != null && currBloomCamera != null)
{
currBloomCamera.Render(); // 强行进行渲染
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
int iWidth = source.width / downSample;
int iHeight = source.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(iWidth, iHeight, 0);
buffer0.filterMode = FilterMode.Bilinear;
// 提取较亮区域部分(调用Shader中的Pass0)
Graphics.Blit(targetRenderTexture, buffer0, material, 0);
// 进行高斯模糊(调用Shader中的Pass1和Pass2)
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(iWidth, iHeight, 0);
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(iWidth, iHeight, 0);
Graphics.Blit(buffer0, buffer1, material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
// 将高斯模糊后的纹理与原纹理进行混合(调用Shader中的Pass3)
material.SetTexture("_Bloom", buffer0);
Graphics.Blit(source, destination, material, 3);
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(source, destination);
}
}
///
/// 创建泛光摄像机
///
/// 摄像机照射的层
///
private Camera CreateBloomCamera(int iCullingLayer)
{
if (currBloomCamera != null)
{
return currBloomCamera;
}
// 创建新的专门用于生成亮度纹理的摄像机
GameObject goBloomCamera = new GameObject();
// 设置新摄像机的父节点与位置
goBloomCamera.transform.parent = transform;
goBloomCamera.transform.position = transform.position;
// 不激活该摄像机
goBloomCamera.SetActive(false);
currBloomCamera = goBloomCamera.AddComponent();
// 将新创建的摄像机设置为与
currBloomCamera.CopyFrom(GetComponent());
// 设置摄像机要照射的层
currBloomCamera.cullingMask = iCullingLayer;
// 摄像机每次清空为固定的颜色
currBloomCamera.clearFlags = CameraClearFlags.SolidColor;
currBloomCamera.backgroundColor = Color.black;
// 设置渲染目标
currBloomCamera.targetTexture = targetRenderTexture;
return currBloomCamera;
}
private void OnDestroy()
{
if (targetRenderTexture != null)
{
RenderTexture.ReleaseTemporary(targetRenderTexture);
targetRenderTexture = null;
}
if (currBloomCamera != null)
{
Destroy(currBloomCamera.gameObject);
currBloomCamera = null;
}
bloomShader = null;
CullingLayer = null;
bloomMaterial = null;
}
}
}
下面我们在Layers上添加一个bloomEffectLayer层,该层对应的数字为8:
我们将吊灯的Layer设置为bloomEffectLayer:
接下来设置Bloom中的参数:
最后是效果图,白色的瓷盘现在已经不再有泛光效果了
现在还是有坑没有填,如果吊灯前面有物体挡着的话,我们会看到这个物体上会出现吊灯的泛光效果。解决这个办法我想到的有两种:1.使用深度,比较主摄像机与新建的摄像机在某个片元上的深度是否一致;2.比较主摄像机与新建的摄像机在某个片元上的颜色或者亮度是否相差很小。如果他们的答案都是否,则在最初的提取亮度部分就不再提取该片元上的亮度,这只是我的设想,还有待于实现。
代码截图: