Unity屏幕后处理之局部发光效果
大家好,我是阿赵
之前介绍了CommandBuffer的用法。这里来举一个实际应用的例子——模型局部发光
上面是这个例子的实际效果。我可以根据需要,控制某个部位的模型发光,比如只是剑发光、只是头发发光,或者只是身体发光。
注意最后一张图,发光的物体是受到遮挡关系的影响的,比如身体发光了,但腰带并没有发光,这是特意的,为了看出这个不是全屏后处理,而是可以有遮挡关系的。
要实现这样的效果,做法有很多,但核心的部分分为这么几步:
以只是剑发光为例,
如果以身体为例
我们只看第一张遮罩贴图,可以看到没有指定发光的物体会挡住发光物体,所以那个部分是不会发光的。
由于这个例子涉及到了卡通渲染的材质,具体的卡通渲染做法可以参考我之前的文章,这里为了简化问题,就用两个球和一个立方体来做这个演示
从上面的渲染过程可以看出来,这个效果的实现步骤分为三步
1.先不渲染要发光的物体,单独用CommandBuffer渲染发光物体,然后使用摄像机深度图去裁剪单独渲染的物体。
2.把需要渲染的物体和遮挡关系的物体全部用代码收集,然后通过CommandBuffer逐个渲染,发光的物体渲染成白色,不发光的物体渲染成黑色
3.加多一个摄像机,使用指定的shader去渲染场景里面的物体,发光的物体渲染成白色,不发光的物体渲染成黑色
我在网上看了几篇文章,发现都是使用第一种方式做的。但第一种方式我感觉是有问题的,深度图如果不包含发光物体,那么深度的信息就会有误,导致发光物体一直会被不发光物体遮盖,做不到发光物体穿插在两个不发光物体之间。
所以我并没有使用深度图的做法,而是使用了另外两种方法各自实现了一遍,具体的做法在下面会说。
这一步是比较简单的,高斯模糊的算法我只会会写单独的文章介绍,也不一定是用高斯模糊去做,模糊的算法有很多种。
这一步没什么好说的,只是把高斯模糊图和原图相加而已,当然也可以做混合,给一个权重值,控制高斯贴图叠加时的权重。
第一步黑白遮罩图是关键,不但可以控制是否发光,还可以控制发光的强度。
下面就重点来说一下黑白遮罩图的做法:
using UnityEngine;
using UnityEngine.Rendering;
public class DrawObjs : MonoBehaviour
{
public Camera cam;
public MeshRenderer[] renders;
private RenderTexture rt1;
private RenderTexture rt2;
private RenderTexture rt3;
public Material blurMat;
public Material comMat;
public Material whiteMat;
public Material blackMat;
private CommandBuffer cmd1;
public int blurDownSample = 1;
public int downSample = 1;
public int iterations = 1;
public float blurSpread = 1;
// Start is called before the first frame update
void Start()
{
}
void OnEnable()
{
rt1 = RenderTexture.GetTemporary(Screen.width >> blurDownSample, Screen.height >> blurDownSample, 24, RenderTextureFormat.Default);
rt2 = RenderTexture.GetTemporary(Screen.width >> blurDownSample, Screen.height >> blurDownSample, 24, RenderTextureFormat.Default);
rt2.filterMode = FilterMode.Bilinear;
rt3 = RenderTexture.GetTemporary(Screen.width >> blurDownSample, Screen.height >> blurDownSample, 24, RenderTextureFormat.Default);
rt3.filterMode = FilterMode.Bilinear;
DrawObjects();
}
void OnDisable()
{
if(rt1)
{
RenderTexture.ReleaseTemporary(rt1);
rt1 = null;
}
if(rt2)
{
RenderTexture.ReleaseTemporary(rt2);
rt2 = null;
}
if (rt3)
{
RenderTexture.ReleaseTemporary(rt3);
rt3 = null;
}
if (cmd1!=null)
{
cmd1.Dispose();
cmd1 = null;
}
}
// Update is called once per frame
void Update()
{
}
private void DrawObjects()
{
if (renders == null || renders.Length == 0)
{
return;
}
cmd1 = new CommandBuffer();
cmd1.name = "AzhaoDrawLightObj";
cmd1.SetRenderTarget(rt1);
cmd1.ClearRenderTarget(true, true, Color.clear);
//渲染黑白物体
for (int i = 0; i < renders.Length; i++)
{
Renderer item = renders[i];
if (item.gameObject.activeInHierarchy == false || item.enabled == false)
{
continue;
}
if(item.gameObject.layer == 8)
{
cmd1.DrawRenderer(item, whiteMat);
}
else
{
cmd1.DrawRenderer(item, blackMat);
}
}
//高斯模糊
cmd1.Blit(rt1, rt2);
for (int i = 0; i < iterations; i++)
{
blurMat.SetFloat("_BlurSize", 1.0f + i * blurSpread);
cmd1.Blit(rt2, rt3, blurMat, 0);
cmd1.Blit(rt3, rt2, blurMat, 1);
}
cmd1.Blit(rt2, rt1);
//把结果放到合成材质球的指定贴图
comMat.SetTexture("_AddTex", rt1);
//添加到摄像机的CameraEvent.BeforeImageEffects事件渲染
cam.AddCommandBuffer(CameraEvent.BeforeImageEffects, cmd1);
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Graphics.Blit(source, destination, comMat);
}
}
需要注意的几个点:
(1)RenderTexture和CommandBuffer都必须记得有创建就有回收
(2)在Enable的时候,创建CommandBuffer,并给他添加到摄像机的BeforeImageEffects。
(3)使用CommandBuffer.DrawRenderer方法,逐个渲染指定的物体
(4)我给发光物体指定了layer是8,所以如果layer是8的物体,就使用白色的材质球渲染,其他物体使用黑色的材质球渲染
(5)高斯模糊部分可以先忽略
(6)把高斯模糊后的图片存到合成用的材质球的_AddTex贴图
(7)在OnRenderImage生命周期,把原来屏幕的渲染图和刚才的_AddTex贴图做叠加,得到最后的发光效果。
正常的Unlit/Color就行,只是要一个颜色。创建一黑一白两个材质球,根据黑白的需要调整颜色
Shader "azhao/GaussianBlur" {
Properties{
_MainTex("Base (RGB)", 2D) = "white" {}
_BlurSize("Blur Size", Float) = 1.0
_Color("Color",Color) = (1,1,1,1)
}
SubShader{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
float4 _Color;
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 j = 1; j < 3; j++) {
sum += tex2D(_MainTex, i.uv[j * 2 - 1]).rgb * weight[j];
sum += tex2D(_MainTex, i.uv[j * 2]).rgb * weight[j];
}
sum *= _Color;
return fixed4(sum, 1.0);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
Shader "azhao/Composite"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_AddTex("AddTex",2D) = "black"{}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Cull Off
ZWrite Off
ZTest Always
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;Q
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _AddTex;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
half4 frag (v2f i) : SV_Target
{
// sample the texture
half4 col = tex2D(_MainTex, i.uv);
half4 addCol = tex2D(_AddTex, i.uv);
half4 final = half4(saturate( col.rgb + addCol.rgb), 1);
return final;
}
ENDCG
}
}
}
实现过程简单,不需要对原物体的材质进行修改,只需要用代码收集需要渲染的物体到脚本就可以。
(1)对比起深度图的做法,物体渲染多了一遍,不顾深度图也有它本身的消耗
(2)由于物体不一定在摄像机的视锥范围内,所以需要自己去做剔除,不然会渲染多了很多摄像机看不见的物体。
(3)由于只有黑白两个材质球,所以变亮的物体的亮度是统一的,不能逐个调整。
1.复制一个摄像机在主摄像机下面,完全和主摄像机重叠,fov也和主摄像机一样,于是这
个摄像机看到的范围是和主摄像机一样的。
2.设置这个摄像机
把摄像机的Enable关掉,然后把摄像机的ClearFlags改成纯色,背景色改成黑色,去掉AudioListener
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class DrawLightByCam : MonoBehaviour
{
public Camera mainCam;
public Camera subCam;
public Shader blackShader;
public RenderTexture rt1;
public RenderTexture rt2;
public RenderTexture rt3;
private CommandBuffer cmd1;
public Material blurMat;
public Material comMat;
public int downSample = 1;
public int iterations = 1;
public float blurSpread = 1;
// Start is called before the first frame update
void Start()
{
subCam.enabled = false;
}
private void OnEnable()
{
rt1 = RenderTexture.GetTemporary(Screen.width>>downSample, Screen.height >> downSample, 16, RenderTextureFormat.Default);
rt2 = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 16, RenderTextureFormat.Default);
rt3 = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 16, RenderTextureFormat.Default);
subCam.targetTexture = rt1;
DrawObjects();
}
private void OnDisable()
{
if(rt1)
{
RenderTexture.ReleaseTemporary(rt1);
rt1 = null;
}
if (rt2)
{
RenderTexture.ReleaseTemporary(rt2);
rt2 = null;
}
if (rt3)
{
RenderTexture.ReleaseTemporary(rt3);
rt3 = null;
}
if (cmd1 != null)
{
cmd1.Dispose();
cmd1 = null;
}
}
// Update is called once per frame
void Update()
{
subCam.RenderWithShader(blackShader, "RenderType");
}
private void DrawObjects()
{
cmd1 = new CommandBuffer();
cmd1.name = "drawLightObj";
cmd1.SetRenderTarget(rt2);
cmd1.ClearRenderTarget(true, true, Color.clear);
//高斯模糊
cmd1.Blit(rt1, rt2);
for (int i = 0; i < iterations; i++)
{
blurMat.SetFloat("_BlurSize", 1.0f + i * blurSpread);
cmd1.Blit(rt2, rt3, blurMat, 0);
cmd1.Blit(rt3, rt2, blurMat, 1);
}
//把结果放到合成材质球的指定贴图
comMat.SetTexture("_AddTex", rt2);
//添加到摄像机的CameraEvent.BeforeImageEffects事件渲染
mainCam.AddCommandBuffer(CameraEvent.BeforeImageEffects, cmd1);
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
Graphics.Blit(source, destination, comMat);
}
}
(1)复制出来的摄像机要关闭Enable ,因为我们在Update的时候主动渲染它
(2)在Update的时候调用subCam.RenderWithShader(blackShader, “RenderType”);,指定一个shader来渲染场景里面的物体,把物体渲染成黑白色。
(3)其他步骤和上面第一种方法一样
Shader "azhao/OnlyColor"
{
Properties
{
_MaskColor("Color",Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
float4 _MaskColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
half4 frag (v2f i) : SV_Target
{
return _MaskColor;
}
ENDCG
}
}
}
只需要在原始Shader的属性里面加一个遮罩颜色就够了
_MaskColor(“MaskColor”,Color) = (1,1,1,1)
模糊shader和合成shader参考第一种方法。
(1)不需要计算哪些物体需要渲染,不需要计算物体是否在摄像机的视锥内
(2)由于黑白遮罩颜色是记录在每个模型的_MaskColor上的,所以可以逐个模型指定发光的强度和变化。
(1)渲染的内容比第一种方法要多
(2)需要修改原模型的shader