之前写的一个类似Photoshop中的图层的 内发光 效果
管线:built-in 管线
unity 版本:2019.4.30f1
思路
- 得到内轮廓边缘:只要某个像素的 kernal size 范围内的,如 5x5 的 kernal size,只要某个 pixel(x,y) 像素不是全黑,并且 kernal size 核内的 sum 值小于 kernal size 平方,都算是边缘
- 将内轮廓边缘模糊
- 将模糊的边缘与 _SrcTex 做像素交集才显示
Shader
// jave.lin 2021/02/25
Shader "PP/InnerGlowPP"
{
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;
}
// inner glow
Sampler2D _InnerGlowOrginTex;
float4 _InnerGlowOrginTex_TexelSize;
F _InnerGlowKernelSize;
F _InnerGlowSize;
struct a2v_inner_glow
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f_inner_glow
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f_inner_glow vert_inner_glow (a2v_inner_glow v) {
v2f_inner_glow o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag_inner_glow(v2f_inner_glow i) : SV_Target
{
fixed originV = 0;
F sum = 0;
int start = -_InnerGlowKernelSize;
int count = _InnerGlowKernelSize + 1;
int delta = count - start;
int threhold = delta * delta;
for (int x = start; x < count; x++)
{
for (int y = start; y < count; y++)
{
float2 temp_uv = i.uv + float2(x, y) * _InnerGlowOrginTex_TexelSize.xy * _InnerGlowSize;
fixed v = tex2D(_InnerGlowOrginTex, temp_uv);
if (x == 0 && y == 0) originV = v;
sum += v;
}
}
return originV > 0 && sum < threhold ? 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;
F _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 _InnerGlowColor;
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;
blur_col *= (mask_col > 0 ? 1 : 0);
fixed4 combinedCol = saturate(tex2D(_SrcTex, i.uv));
combinedCol.rgb += (blur_col * _InnerGlowColor.a) * _InnerGlowColor.rgb;
return combinedCol;
}
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 // inner glow 1
{
ColorMask R
CGPROGRAM
#pragma vertex vert_inner_glow
#pragma fragment frag_inner_glow
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
Camera PP 脚本
// jave.lin 2021/02/25
// 绘制外发光后效
// 临时效果,时间关系,未优化
using UnityEngine;
using UnityEngine.Rendering;
public class InnerGlowPP : PostEffectBasic
{
private static int _InnerGlowKernelSize_hash = Shader.PropertyToID("_InnerGlowKernelSize");
private static int _InnerGlowSize_hash = Shader.PropertyToID("_InnerGlowSize");
private static int _InnerGlowColor_hash = Shader.PropertyToID("_InnerGlowColor");
private static int _InnerGlowOrginTex_hash = Shader.PropertyToID("_InnerGlowOrginTex");
private static int _BlurOrginTex_hash = Shader.PropertyToID("_BlurOrginTex");
private static int _BlurSize_hash = Shader.PropertyToID("_BlurSize");
private static int _BlurTex_hash = Shader.PropertyToID("_BlurTex");
private static int _MaskTex_hash = Shader.PropertyToID("_MaskTex");
private static int _SrcTex_hash = Shader.PropertyToID("_SrcTex");
[Header("绘制材质")]
public Material mat;
[Header("DownSample 降采等级")]
public int down_sample_level = 4;
[Range(1, 4)]
[Header("高斯模糊的次数")]
public int iterations = 4;
[Header("核大小")]
[Range(0.0f, 2.0f)]
public int innerGlowKernelSize = 1;
[Header("内发光大小")]
[Range(0.0f, 10.0f)]
public float innerGlowSize = 2.0f;
[Header("模糊边界大小:每次模糊采样纹素距离的缩放因数")]
[Range(0.2f, 3.0f)]
public float blur_size = 0.2f;
[Header("内发光颜色")]
public Color glow_color = Color.red;
private CommandBuffer cmdBuffer;
private Camera cam;
protected override void Start()
{
base.Start();
cmdBuffer = new CommandBuffer();
cmdBuffer.name = "InnerGlowPPCmdBuffer";
cam = GetComponent<Camera>();
}
private void OnDestroy()
{
if (cmdBuffer != null)
{
cmdBuffer.Clear();
cmdBuffer.Dispose();
cmdBuffer = null;
}
cam.targetTexture = null;
cam = null;
}
protected override void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (!IsSupported || InnerGlowManager.instance.Count == 0)
{
GraphicsUtil.SkipOnImage(src, dest);
return;
}
if (mat == null)
{
Debug.LogError("InnerGlowPP.mat == null");
GraphicsUtil.SkipOnImage(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;
ShowGlow(src, dest, sw, sh, rw, rh, mat);
}
private void ShowGlow(RenderTexture src, RenderTexture dest, int sw, int sh, int rw, int rh, Material usingMat)
{
// create RT
var inner_glow_mask_rt = RenderTexture.GetTemporary(sw, sh, 0, RenderTextureFormat.R8);
inner_glow_mask_rt.filterMode = FilterMode.Bilinear;
inner_glow_mask_rt.name = $"InnerGlowPP.inner_glow_mask_rt_{sw}x{sh}";
cmdBuffer.Clear();
cmdBuffer.SetRenderTarget(inner_glow_mask_rt);
cmdBuffer.ClearRenderTarget(false, true, Color.black);
InnerGlowManager.instance.Update2CmdBuffer2Draw(cmdBuffer, usingMat, 0);
// execute cmd buffer, Draw To RT
Graphics.ExecuteCommandBuffer(cmdBuffer);
// inner glow
var inner_glow_rt = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
inner_glow_rt.filterMode = FilterMode.Bilinear;
inner_glow_rt.name = $"InnerGlowPP.inner_glow_rt_{rw}x{rh}";
usingMat.SetInt(_InnerGlowKernelSize_hash, innerGlowKernelSize);
usingMat.SetFloat(_InnerGlowSize_hash, innerGlowSize);
usingMat.SetTexture(_InnerGlowOrginTex_hash, inner_glow_mask_rt);
Graphics.Blit(null, inner_glow_rt, usingMat, 1);
// blur
var blur_rt = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
blur_rt.filterMode = FilterMode.Bilinear;
blur_rt.name = $"InnerGlowPP.blur_rt_{rw}x{rh}";
var rt0 = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
rt0.filterMode = FilterMode.Bilinear;
rt0.name = $"InnerGlowPP.rt0_{rw}x{rh}";
// 先将远 blur_rt 复制到rt0
// references : https://blog.youkuaiyun.com/linjf520/article/details/104940213
Graphics.Blit(inner_glow_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 = $"InnerGlowPP.rt1.1_{rw}x{rh}";
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 = $"InnerGlowPP.rt1.2_{rw}x{rh}";
// 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, inner_glow_mask_rt);
usingMat.SetTexture(_BlurTex_hash, blur_rt);
usingMat.SetTexture(_SrcTex_hash, src);
usingMat.SetColor(_InnerGlowColor_hash, glow_color);
Graphics.Blit(null, dest, usingMat, 4);
RenderTexture.ReleaseTemporary(inner_glow_mask_rt);
RenderTexture.ReleaseTemporary(inner_glow_rt);
RenderTexture.ReleaseTemporary(blur_rt);
}
}
Outline Renderer 提取器
挂载在对应的 Root 下,会自动提取底下所有的 renderer
// jave.lin 2021/09/24
// InnerGlow 后效绘制的 Renderer 提取组件
using System.Collections.Generic;
using UnityEngine;
public class InnerGlowRendererExtractor : MonoBehaviour
{
public bool extractEveryFrame = false;
//private List<Renderer> renderers;
public List<Renderer> renderers; // 暂时 public 便于 inspector 中查看
private int instID;
private void Awake()
{
instID = GetInstanceID();
renderers = ListPoolUtil<Renderer>.FromPool();
}
private void Start()
{
UpdateRenderers();
}
private void Update()
{
if (extractEveryFrame)
{
UpdateRenderers();
}
}
private void OnDestroy()
{
if (renderers != null)
{
ListPoolUtil<Renderer>.ToPool(renderers);
renderers = null;
}
}
private void UpdateRenderers()
{
var mrList = ListPoolUtil<MeshRenderer>.FromPool();
gameObject.GetComponentsInChildren<MeshRenderer>(false, mrList);
var smrList = ListPoolUtil<SkinnedMeshRenderer>.FromPool();
gameObject.GetComponentsInChildren<SkinnedMeshRenderer>(false, smrList);
renderers.Clear();
renderers.AddRange(mrList);
renderers.AddRange(smrList);
ListPoolUtil<MeshRenderer>.ToPool(mrList);
ListPoolUtil<SkinnedMeshRenderer>.ToPool(smrList);
InnerGlowManager.instance.Remove(instID);
InnerGlowManager.instance.Add(instID, renderers);
}
}
OutlineManager
// jave.lin 2021/02/25
// 内发光的管理
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class InnerGlowManager : MonoSingleton<InnerGlowManager>
{
private Stack<List<Renderer>> listPool = new Stack<List<Renderer>>();
private Stack<InnerGlowElement> innerGlowElementPool = new Stack<InnerGlowElement>();
private List<InnerGlowElement> innerGlowElementList = new List<InnerGlowElement>();
private Dictionary<int, InnerGlowElement> innerGlowElementDict_key_instid = new Dictionary<int, InnerGlowElement>();
private Dictionary<GameObject, InnerGlowElement> innerGlowElementDict_key_go = new Dictionary<GameObject, InnerGlowElement>();
public int Count => innerGlowElementList.Count;
private void OnDestroy()
{
if (listPool != null)
{
foreach (var item in listPool)
{
item.Clear();
}
listPool.Clear();
listPool = null;
}
if (innerGlowElementPool != null)
{
innerGlowElementPool.Clear();
innerGlowElementPool = null;
}
if (listPool != null)
{
listPool.Clear();
listPool = null;
}
}
public bool Contains(GameObject go)
{
foreach (var glowElement in innerGlowElementList)
{
if (glowElement.go == go)
{
return true;
}
}
return false;
}
public void Add(int instID, List<Renderer> renderers)
{
InnerGlowElement e = innerGlowElementPool.Count > 0 ? innerGlowElementPool.Pop() : null;
if (e == null)
{
e = new InnerGlowElement
{
optType = eInnerGlowOptType.SpecialRenderers,
instID = instID,
go = null,
renderers = new List<Renderer>(renderers),
ignoreActive = false
};
}
else
{
e.optType = eInnerGlowOptType.SpecialRenderers;
e.instID = instID;
e.go = null;
e.renderers.AddRange(renderers);
e.ignoreActive = false;
}
innerGlowElementDict_key_instid[instID] = e;
innerGlowElementList.Add(e);
//Log.logError($"InnerGlowManager.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);
InnerGlowElement e = innerGlowElementPool.Count > 0 ? innerGlowElementPool.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 InnerGlowElement { optType = eInnerGlowOptType.SpecialGO, instID = -1, go = go, renderers = list, ignoreActive = ignoreActive };
}
else
{
e.optType = eInnerGlowOptType.SpecialGO;
e.instID = -1;
e.go = go;
e.renderers = list;
e.ignoreActive = ignoreActive;
}
innerGlowElementDict_key_go[go] = e;
innerGlowElementList.Add(e);
}
public void Remove(GameObject go)
{
if (go == null)
{
return;
}
for (int i = 0; i < innerGlowElementList.Count; i++)
{
var e = innerGlowElementList[i];
if (e.go == null)
{
_Reclyle(e);
innerGlowElementList.RemoveAt(i);
continue;
}
if (e.go == go)
{
_Reclyle(e);
innerGlowElementList.RemoveAt(i);
return;
}
}
innerGlowElementDict_key_go.Remove(go);
}
public void Remove(int instID)
{
if (innerGlowElementDict_key_instid.Remove(instID))
{
for (int i = 0; i < innerGlowElementList.Count; i++)
{
var e = innerGlowElementList[i];
if (e.instID == instID)
{
_Reclyle(e);
innerGlowElementList.RemoveAt(i);
break;
}
}
}
//Log.logError($"InnerGlowManager.Remove instID:{instID}");
}
public void Clear()
{
if (innerGlowElementList.Count > 0)
{
foreach (var e in innerGlowElementList)
{
_Reclyle(e);
}
innerGlowElementList.Clear();
}
innerGlowElementList.Clear();
innerGlowElementDict_key_go.Clear();
}
public void Update2CmdBuffer2Draw(CommandBuffer cmdBuffer, Material material, int pass = -1)
{
for (int i = innerGlowElementList.Count - 1; i > -1; i--)
{
var e = innerGlowElementList[i];
switch (e.optType)
{
case eInnerGlowOptType.SpecialGO:
if (e.go == null)
{
_Reclyle(e);
innerGlowElementList.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 eInnerGlowOptType.SpecialRenderers:
for (int j = 0; j < e.renderers.Count; j++)
{
var r = e.renderers[j];
if (r == null || r.gameObject == null)
{
_Reclyle(e);
innerGlowElementList.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(InnerGlowElement e)
{
if (e.optType == eInnerGlowOptType.SpecialGO)
{
innerGlowElementDict_key_go.Remove(e.go);
}
else
{
innerGlowElementDict_key_instid.Remove(e.instID);
}
e.renderers.Clear();
listPool.Push(e.renderers);
innerGlowElementPool.Push(e);
}
}
public enum eInnerGlowOptType
{
SpecialGO,
SpecialRenderers,
}
public class InnerGlowElement
{
public eInnerGlowOptType 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;
}
效果
原图
加上外发光效果
GIF,颜色可以调整外发光大小,还有透明度
下面是 外发光 + 内发光 的效果