文章目录
其实以前老早就想制作这个效果了,但是没有太多的空闲时间
那这次反正项目组需要这个效果,就顺手将 Demo 记录到 Blog
2021/03/03 刚刚好今天早上晨会分享了这个 外发光 的思路
我讲的比较简单的理解化的方式
该效果还是比较简单的,都是非常基础的东西,大神、大佬可以跳过。
思路
使用后效处理:
- 先将需要外发光的对象都 draw 到一张 RT(RenderTexture,这里因为颜色统一的,所以 RT 只要 Format为
R8
即可),假设 RT 名为:Mask_RT
- 再将
Mask_RT
Blit 一下,处理高斯模糊,将Mask_RT
高斯模糊后的内容存入到另一张 RT:Blur_RT
- 再将
Value_RT = Blur_RT - Mask_RT
,就是最终外发光的像素部分了 - 再用一个
FinalCol = _Color * Value_RT
就是最终颜色了
下面列出完整的 CSharp + ShaderLab 的代码
Shader Code - GlowZAlwaysPP.shader
// jave.lin 2021/02/25
// references : https://blog.csdn.net/linjf520/article/details/104940213
Shader "Game/PP/GlowZAlwaysPP"
{
CGINCLUDE
#include "UnityCG.cginc"
// pure col
float4 vert_pure_col(float4 vertex : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(vertex);
}
fixed4 frag_pure_col() : SV_Target
{
return 1;
}
// expand
sampler2D _ExpandOrginTex;
float4 _ExpandOrginTex_TexelSize;
float _GlowSize;
struct a2v_expand
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f_expand
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float4 uv01 : TEXCOORD1;
float4 uv23 : TEXCOORD2;
};
v2f_expand vert_expand(a2v_expand v) {
v2f_expand o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
float2 ts = _ExpandOrginTex_TexelSize.xy;
float2 offset1 = float2(1, 0);
float2 offset2 = float2(0, 1);
o.uv01.xy = v.uv + offset1 * ts * _GlowSize; // 左
o.uv01.zw = v.uv + offset1 * -ts * _GlowSize; // 右
o.uv23.xy = v.uv + offset2 * ts * _GlowSize; // 上
o.uv23.zw = v.uv + offset2 * -ts * _GlowSize; // 下
return o;
}
fixed4 frag_expand(v2f_expand i) : SV_Target
{
fixed sum = tex2D(_ExpandOrginTex, i.uv).r;
if (sum == 0)
sum = tex2D(_ExpandOrginTex, i.uv01.xy).r; // 左
if (sum == 0)
sum += tex2D(_ExpandOrginTex, i.uv01.zw).r; // 右
if (sum == 0)
sum += tex2D(_ExpandOrginTex, i.uv23.xy).r; // 上
if (sum == 0)
sum += tex2D(_ExpandOrginTex, i.uv23.zw).r; // 下
//if (sum == 0)
// sum = tex2D(_ExpandOrginTex, float2(i.uv01.xy.x, i.uv23.xy.y)).r; // 左 | 上
//if (sum == 0)
// sum += tex2D(_ExpandOrginTex, float2(i.uv01.zw.x, i.uv23.xy.y)).r; // 右 | 上
//if (sum == 0)
// sum = tex2D(_ExpandOrginTex, float2(i.uv01.xy.x, i.uv23.zw.y)).r; // 左 | 下
//if (sum == 0)
// sum += tex2D(_ExpandOrginTex, float2(i.uv01.zw.x, i.uv23.zw.y)).r; // 右 | 下
return sum != 0 ? 1 : 0;
}
/// blur /
struct a2v_blur
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f_blur
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float4 uv01 : TEXCOORD1;
float4 uv23 : TEXCOORD2;
};
sampler2D _BlurOrginTex;
float4 _BlurOrginTex_TexelSize;
float _BlurSize;
v2f_blur vert_blur_h(a2v_blur v) {
v2f_blur o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
float2 ts = _BlurOrginTex_TexelSize.xy;
float2 offset1 = float2(1, 0);
float2 offset2 = float2(2, 0);
o.uv01.xy = v.uv + offset1 * ts * _BlurSize; // 左1
o.uv01.zw = v.uv + offset1 * -ts * _BlurSize; // 右1
o.uv23.xy = v.uv + offset2 * ts * _BlurSize; // 左2
o.uv23.zw = v.uv + offset2 * -ts * _BlurSize; // 右2
return o;
}
v2f_blur vert_blur_v(a2v_blur v) {
v2f_blur o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
float2 ts = _BlurOrginTex_TexelSize.xy;
float2 offset1 = float2(0, 1);
float2 offset2 = float2(0, 2);
o.uv01.xy = v.uv + offset1 * ts * _BlurSize; // 上1
o.uv01.zw = v.uv + offset1 * -ts * _BlurSize; // 下1
o.uv23.xy = v.uv + offset2 * ts * _BlurSize; // 上2
o.uv23.zw = v.uv + offset2 * -ts * _BlurSize; // 下2
return o;
}
fixed4 frag_blur(v2f_blur i) : SV_Target{
fixed4 sum = tex2D(_BlurOrginTex, i.uv) * 0.4026;
sum += tex2D(_BlurOrginTex, i.uv01.xy) * 0.2442; // 左1 | 上1
sum += tex2D(_BlurOrginTex, i.uv01.zw) * 0.2442; // 右1 | 下1
sum += tex2D(_BlurOrginTex, i.uv23.xy) * 0.0545; // 左2 | 上2
sum += tex2D(_BlurOrginTex, i.uv23.zw) * 0.0545; // 右2 | 下2
return sum;
}
/// final /
sampler2D _MaskTex;
sampler2D _BlurTex;
sampler2D _SrcTex;
fixed4 _GlowColor;
struct a2v_final
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f_final
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f_final vert_final(a2v_final v) {
v2f_final o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag_final(v2f_final i) : SV_Target{
fixed blur_col = tex2D(_BlurTex, i.uv).r;
fixed mask_col = tex2D(_MaskTex, i.uv).r;
fixed value = saturate(blur_col - mask_col);
return saturate(tex2D(_SrcTex, i.uv) + value * _GlowColor);
}
ENDCG
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always
Pass // pure col 0
{
ColorMask R
CGPROGRAM
#pragma vertex vert_pure_col
#pragma fragment frag_pure_col
ENDCG
}
Pass // expand 1
{
ColorMask R
CGPROGRAM
#pragma vertex vert_expand
#pragma fragment frag_expand
ENDCG
}
Pass // blur h 2
{
ColorMask R
CGPROGRAM
#pragma vertex vert_blur_h
#pragma fragment frag_blur
ENDCG
}
Pass // blur v 3
{
ColorMask R
CGPROGRAM
#pragma vertex vert_blur_v
#pragma fragment frag_blur
ENDCG
}
Pass // final 4
{
CGPROGRAM
#pragma vertex vert_final
#pragma fragment frag_final
ENDCG
}
}
}
CSharp Code - GlowPP.cs
// jave.lin 2021/02/25
// 绘制外发光后效
// 临时效果,时间关系,未优化
using UnityEngine;
using UnityEngine.Rendering;
public class GlowPP : PostEffectBasic
{
private static int _GlowSize_hash = Shader.PropertyToID("_GlowSize");
private static int _BlurSize_hash = Shader.PropertyToID("_BlurSize");
private static int _GlowColor_hash = Shader.PropertyToID("_GlowColor");
private static int _ExpandOrginTex_hash = Shader.PropertyToID("_ExpandOrginTex");
private static int _BlurTex_hash = Shader.PropertyToID("_BlurTex");
private static int _BlurOrginTex_hash = Shader.PropertyToID("_BlurOrginTex");
private static int _MaskTex_hash = Shader.PropertyToID("_MaskTex");
private static int _SrcTex_hash = Shader.PropertyToID("_SrcTex");
private static int _ZLessTex_hash = Shader.PropertyToID("_ZLessTex");
private static int _ZGreaterTex_hash = Shader.PropertyToID("_ZGreaterTex");
private static int _ExpandTex_hash = Shader.PropertyToID("_ExpandTex");
[Header("ZGreater绘制材质")]
public Material z_greater_draw_mat;
[Header("ZAlways绘制材质")]
public Material z_always_draw_mat;
[Header("DownSample 降采等级")]
public int down_sample_level = 4;
[Range(1, 4)]
[Header("高斯模糊的次数")]
public int iterations = 4;
[Header("模糊边界大小:每次模糊采样纹素距离的缩放因数")]
[Range(0.2f, 3.0f)]
public float blur_size = 0.2f;
[Header("外发光边缘大小")]
[Range(0.0f, 2.0f)]
public float glow_size = 2;
[Header("外发光颜色")]
public Color glow_color = Color.red;
[Header("是否只显示 z greater 部分(暂时无效的参数)")]
public bool only_show_greater_glow = false;
private CommandBuffer cmdBuffer;
private Camera cam;
protected override void Start()
{
base.Start();
cmdBuffer = new CommandBuffer();
cmdBuffer.name = "GlowPPCmdBuffer";
cam = GetComponent<Camera>();
}
private void OnDestroy()
{
if (cmdBuffer != null)
{
cmdBuffer.Clear();
cmdBuffer.Dispose();
cmdBuffer = null;
}
cam = null;
}
protected override void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (!IsSupported || GlowManager.instance.Count == 0)
{
Graphics.Blit(src, dest);
return;
}
if (z_always_draw_mat == null)
{
Debug.LogError("GlowPP.z_always_draw_mat == null");
Graphics.Blit(src, dest);
return;
}
// clamp down_sample_level
if (down_sample_level <= 0) down_sample_level = 1;
var sw = Screen.width;
var sh = Screen.height;
var rw = sw / down_sample_level;
var rh = sh / down_sample_level;
//if (only_show_greater_glow)
//{
// // 还有 BUG,后续完善
// ShowGreaterGlow(src, dest, rw, rh, z_greater_draw_mat);
//}
//else
//{
ShowAlwaysGlow(src, dest, sw, sh, rw, rh, z_always_draw_mat);
//}
}
private void ShowAlwaysGlow(RenderTexture src, RenderTexture dest, int sw, int sh, int rw, int rh, Material usingMat)
{
// create RT
var glow_mask_rt = RenderTexture.GetTemporary(sw, sh, 0, RenderTextureFormat.R8);
glow_mask_rt.filterMode = FilterMode.Bilinear;
glow_mask_rt.name = "GlowPP.glow_mask_rt";
// back up actived rt
var src_rt = cam.targetTexture;
cmdBuffer.Clear();
cmdBuffer.SetRenderTarget(glow_mask_rt);
cmdBuffer.ClearRenderTarget(false, true, Color.black);
GlowManager.instance.Update2CmdBuffer2Draw(cmdBuffer, usingMat, 0);
// execute cmd buffer, Draw To RT
Graphics.ExecuteCommandBuffer(cmdBuffer);
// expand
var expand_rt = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
expand_rt.filterMode = FilterMode.Bilinear;
expand_rt.name = "GlowPP.expand_rt";
usingMat.SetFloat(_GlowSize_hash, glow_size);
usingMat.SetTexture(_ExpandOrginTex_hash, glow_mask_rt);
Graphics.Blit(null, expand_rt, usingMat, 1);
// blur
var blur_rt = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
blur_rt.filterMode = FilterMode.Bilinear;
blur_rt.name = "GlowPP.blur_rt";
var rt0 = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
rt0.filterMode = FilterMode.Bilinear;
rt0.name = "GlowPP.rt0";
// 先将远 blur_rt 复制到rt0
// references : https://blog.csdn.net/linjf520/article/details/104940213
Graphics.Blit(expand_rt, rt0);
for (int i = 0; i < iterations; i++)
{
usingMat.SetFloat(_BlurSize_hash, 1 + i * blur_size);
var rt1 = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
rt1.filterMode = FilterMode.Bilinear;
rt1.name = "GlowPP.rt1.1";
usingMat.SetTexture(_BlurOrginTex_hash, rt0);
// horizontal blur
Graphics.Blit(null, rt1, usingMat, 2);
RenderTexture.ReleaseTemporary(rt0);
rt0 = rt1;
rt1 = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
rt1.filterMode = FilterMode.Bilinear;
rt1.name = "GlowPP.rt1.2";
// vertical blur
usingMat.SetTexture(_BlurOrginTex_hash, rt0);
Graphics.Blit(null, rt1, usingMat, 3);
RenderTexture.ReleaseTemporary(rt0);
rt0 = rt1;
}
Graphics.Blit(rt0, blur_rt);
RenderTexture.ReleaseTemporary(rt0);
// final
usingMat.SetTexture(_MaskTex_hash, glow_mask_rt);
usingMat.SetTexture(_BlurTex_hash, blur_rt);
usingMat.SetTexture(_SrcTex_hash, src);
usingMat.SetColor(_GlowColor_hash, glow_color);
Graphics.Blit(null, dest, usingMat, 4);
// reset src rt
cam.targetTexture = src_rt;
RenderTexture.ReleaseTemporary(glow_mask_rt);
RenderTexture.ReleaseTemporary(expand_rt);
RenderTexture.ReleaseTemporary(blur_rt);
}
private void ShowGreaterGlow(RenderTexture src, RenderTexture dest, int rw, int rh, Material usingMat)
{
// create RT
var glow_mask_rt = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
glow_mask_rt.filterMode = FilterMode.Bilinear;
glow_mask_rt.name = "GlowPP.glow_mask_rt";
// back up actived rt
var src_rt = cam.targetTexture;
cmdBuffer.Clear();
cmdBuffer.SetRenderTarget(glow_mask_rt);
cmdBuffer.ClearRenderTarget(false, true, Color.black);
GlowManager.instance.Update2CmdBuffer2Draw(cmdBuffer, usingMat, 0);
// execute cmd buffer, Draw To RT
Graphics.ExecuteCommandBuffer(cmdBuffer);
// expand
var expand_rt = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
expand_rt.filterMode = FilterMode.Bilinear;
expand_rt.name = "GlowPP.expand_rt";
usingMat.SetFloat(_GlowSize_hash, glow_size);
usingMat.SetTexture(_ExpandOrginTex_hash, glow_mask_rt);
Graphics.Blit(null, expand_rt, usingMat, 1);
// blur
var blur_rt = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
blur_rt.filterMode = FilterMode.Bilinear;
blur_rt.name = "GlowPP.blur_rt";
var rt0 = RenderTexture.GetTemporary(rw, rh, 0);
rt0.filterMode = FilterMode.Bilinear;
rt0.name = "GlowPP.rt0";
// 先将远 blur_rt 复制到rt0
// references : https://blog.csdn.net/linjf520/article/details/104940213
Graphics.Blit(expand_rt, rt0);
for (int i = 0; i < iterations; i++)
{
usingMat.SetFloat(_BlurSize_hash, 1 + i * blur_size);
var rt1 = RenderTexture.GetTemporary(rw, rh, 0);
rt1.filterMode = FilterMode.Bilinear;
usingMat.SetTexture(_BlurOrginTex_hash, rt0);
// horizontal blur
Graphics.Blit(null, rt1, usingMat, 2);
RenderTexture.ReleaseTemporary(rt0);
rt0 = rt1;
rt1 = RenderTexture.GetTemporary(rw, rh, 0);
rt1.filterMode = FilterMode.Bilinear;
// vertical blur
usingMat.SetTexture(_BlurOrginTex_hash, rt0);
Graphics.Blit(null, rt1, usingMat, 3);
RenderTexture.ReleaseTemporary(rt0);
rt0 = rt1;
}
Graphics.Blit(rt0, blur_rt);
RenderTexture.ReleaseTemporary(rt0);
// final
usingMat.SetTexture(_MaskTex_hash, glow_mask_rt);
usingMat.SetTexture(_BlurTex_hash, blur_rt);
usingMat.SetTexture(_SrcTex_hash, src);
usingMat.SetColor(_GlowColor_hash, glow_color);
Graphics.Blit(null, dest, usingMat, 4);
// reset src rt
cam.targetTexture = src_rt;
RenderTexture.ReleaseTemporary(glow_mask_rt);
RenderTexture.ReleaseTemporary(expand_rt);
RenderTexture.ReleaseTemporary(blur_rt);
}
}
CSharp Code - GlowManager.cs
再写一个简单的Glow 管理器
// jave.lin 2021/02/25
// 外发光的管理
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class GlowManager : MonoSingleton<GlowManager>
{
private Stack<List<Renderer>> listPool = new Stack<List<Renderer>>();
private Stack<GlowElement> glowElementPool = new Stack<GlowElement>();
private List<GlowElement> glowElementList = new List<GlowElement>();
private Dictionary<int, GlowElement> glowElementDict_key_instid = new Dictionary<int, GlowElement>();
private Dictionary<GameObject, GlowElement> glowElementDict_key_go = new Dictionary<GameObject, GlowElement>();
public int Count => glowElementList.Count;
private void OnDestroy()
{
if (listPool != null)
{
foreach (var item in listPool)
{
item.Clear();
}
listPool.Clear();
listPool = null;
}
if (glowElementPool != null)
{
glowElementPool.Clear();
glowElementPool = null;
}
if (listPool != null)
{
listPool.Clear();
listPool = null;
}
}
public bool Contains(GameObject go)
{
foreach (var glowElement in glowElementList)
{
if (glowElement.go == go)
{
return true;
}
}
return false;
}
public void Add(int instID, List<Renderer> renderers)
{
GlowElement e = glowElementPool.Count > 0 ? glowElementPool.Pop() : null;
if (e == null)
{
e = new GlowElement
{
optType = eGlowOptType.SpecialRenderers,
instID = instID,
go = null,
renderers = renderers,
ignoreActive = false
};
}
else
{
e.optType = eGlowOptType.SpecialRenderers;
e.instID = instID;
e.go = null;
e.renderers = renderers;
e.ignoreActive = false;
}
glowElementDict_key_instid[instID] = e;
glowElementList.Add(e);
//Log.logError($"GlowManager.Add instID:{instID}");
}
public void Add(GameObject go, bool ignoreActive = false, int ignoreLayer = 0)
{
if (ignoreLayer == -1)
{
// culling everything
return;
}
if (go == null)
{
return;
}
if (Contains(go))
{
return;
}
var list = listPool.Count > 0 ? listPool.Pop() : new List<Renderer>();
go.GetComponentsInChildren<Renderer>(false, list);
GlowElement e = glowElementPool.Count > 0 ? glowElementPool.Pop() : null;
if (ignoreLayer != 0)
{
var count = list.Count;
for (int i = 0; i < count; i++)
{
if (list[i].gameObject.layer == ignoreLayer)
{
list.RemoveAt(i);
--i;
--count;
continue;
}
}
}
if (e == null)
{
e = new GlowElement {
optType = eGlowOptType.SpecialGO, instID = -1, go = go, renderers = list, ignoreActive = ignoreActive };
}
else
{
e.optType = eGlowOptType.SpecialGO;
e.instID = -1;
e.go = go;
e.renderers = list;
e.ignoreActive = ignoreActive;
}
glowElementDict_key_go[go] = e;
glowElementList.Add(e);
}
public void Remove(GameObject go)
{
if (go == null)
{
return;
}
for (int i = 0; i < glowElementList.Count; i++)
{
var e = glowElementList[i];
if (e.go == null)
{
_Reclyle(e);
glowElementList.RemoveAt(i);
continue;
}
if (e.go == go)
{
_Reclyle(e);
glowElementList.RemoveAt(i);
return;
}
}
glowElementDict_key_go.Remove(go);
}
public void Remove(int instID)
{
if (glowElementDict_key_instid.Remove(instID))
{
for (int i = 0; i < glowElementList.Count; i++)
{
var e = glowElementList[i];
if (e.instID == instID)
{
_Reclyle(e);
glowElementList.RemoveAt(i);
break;
}
}
}
//Log.logError($"GlowManager.Remove instID:{instID}");
}
public void Clear()
{
if (glowElementList.Count > 0)
{
foreach (var e in glowElementList)
{
_Reclyle(e);
}
glowElementList.Clear();
}
glowElementList.Clear();
glowElementDict_key_go.Clear();
}
public void Update2CmdBuffer2Draw(CommandBuffer cmdBuffer, Material material, int pass = -1)
{
for (int i = glowElementList.Count - 1; i > -1; i--)
{
var e = glowElementList[i];
switch (e.optType)
{
case eGlowOptType.SpecialGO:
if (e.go == null)
{
_Reclyle(e);
glowElementList.RemoveAt(i);
continue;
}
if (!e.ignoreActive)
{
if (!e.go.activeInHierarchy)
{
continue;
}
}
for (int j = 0; j < e.renderers.Count; j++)
{
var r = e.renderers[j];
var draw = r != null;
if (draw && !e.ignoreActive)
{
draw = r.enabled && r.gameObject.activeInHierarchy;
}
if (draw)
{
cmdBuffer.DrawRenderer(r, material, 0, pass);
}
}
break;
case eGlowOptType.SpecialRenderers:
for (int j = 0; j < e.renderers.Count; j++)
{
var r = e.renderers[j];
if (r == null || r.gameObject == null)
{
_Reclyle(e);
glowElementList.RemoveAt(i);
break;
}
var draw = true;
if (!e.ignoreActive)
{
draw = r.enabled && r.gameObject.activeInHierarchy;
}
if (draw)
{
cmdBuffer.DrawRenderer(r, material, 0, pass);
}
}
break;
default:
Debug.LogError($"Unimplements GlowOptType : {e.optType}");
break;
}
}
}
private void _Reclyle(GlowElement e)
{
if (e.optType == eGlowOptType.SpecialGO)
{
glowElementDict_key_go.Remove(e.go);
}
else
{
glowElementDict_key_instid.Remove(e.instID);
}
e.renderers.Clear();
listPool.Push(e.renderers);
glowElementPool.Push(e);
}
}
public enum eGlowOptType
{
SpecialGO,
SpecialRenderers,
}
public class GlowElement
{
public eGlowOptType optType;
public int instID; // key, when optType == Special Instance ID
public GameObject go; // key, when optType == Special GO
public List<Renderer> renderers;
public bool ignoreActive;
}
使用方式
// jave.lin 2021/02/25
// 测试外发光的管理的使用效果
using UnityEngine;
public class TestingPushToGlowMgr : MonoBehaviour
{
public GameObject[] goes;
// Start is called before the first frame update
private bool added = false;
private void AddGoes(GameObject[] goes)
{
foreach (var go in goes)
{
GlowManager.instance.Add(go, false);
}
added = true;
}
private void RemoveGoes(GameObject[] goes)
{
foreach (var go in goes)
{
GlowManager.instance.Remove(go);
}
added = false;
}
private void Start()
{
AddGoes(goes);
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
if (!added)
{
AddGoes(goes);
}
else
{
RemoveGoes(goes);
}
}
}
}
查看效果
Project
back up : TestingGlowPostProcessEffect
References
- Unity Shader PostProcessing - 6 - GaussianBlur 高斯模糊+CommandBuffer使用做一些其他的特效 - 这是我之前写的高斯模糊的后效
- UNITY3D场景物体外发光插件——HIGHLIGHTINGSYSTEM - 还没有参考,后续可以参考一下这个插件的做法和我的区别是怎么样的
- 后处理方式
- 【unityshader小实例】 轮廓外发光(光晕) 添加 pass 另外绘制 反 rim 的颜色即可
- 【Unity学习心得】Sprite外发光的制作