之前写的一个类似Photoshop中的图层的 外发光 效果,其实准确的来说,我这个应该叫:基于像素颜色的外轮廓的描边,和描边的思路很像而已
管线:built-in 管线
unity 版本:2019.4.30f1
思路
只要某个像素的 kernal size 范围内的,如 5x5 的 kernal size,只要某个 pixel(x,y) 像素作为的 5x5 范围内的像素有一个不是为全黑的像素,都算是边缘
Shader
// jave.lin 2021/09/24
Shader "PP/OutlinePP"
{
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;
int _OutlineKernelSize;
float _OutlineSize;
struct a2v_expand
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f_expand
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f_expand vert_expand (a2v_expand v) {
v2f_expand o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag_expand(v2f_expand i) : SV_Target
{
fixed sum = 0;
int start = -_OutlineKernelSize;
int count = _OutlineKernelSize + 1;
for (int x = start; x < count; x++)
{
for (int y = start; y < count; y++)
{
float2 temp_uv = i.uv + float2(x, y) * _ExpandOrginTex_TexelSize.xy * _OutlineSize;
sum += tex2D(_ExpandOrginTex, temp_uv);
}
}
return sum != 0 ? 1 : 0;
}
/// final /
Sampler2D _ExpandTex;
Sampler2D _SrcTex;
fixed4 _OutlineColor;
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 expand_col = tex2D(_ExpandTex, i.uv).r;
fixed origin_col = tex2D(_ExpandOrginTex, i.uv).r;
fixed value = saturate(expand_col - origin_col);
fixed4 combinedCol = saturate(tex2D(_SrcTex, i.uv));
combinedCol.rgb += (value * _OutlineColor.a) * _OutlineColor.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 // expand 1
{
ColorMask R
CGPROGRAM
#pragma vertex vert_expand
#pragma fragment frag_expand
ENDCG
}
Pass // final 2
{
CGPROGRAM
#pragma vertex vert_final
#pragma fragment frag_final
ENDCG
}
}
}
CSharp
Camera PP 脚本
// jave.lin 2021/09/24
// 外轮廓 后效
using UnityEngine;
using UnityEngine.Rendering;
public class OutlinePP : PostEffectBasic
{
private static int _ExpandOrginTex = Shader.PropertyToID("_ExpandOrginTex");
private static int _ExpandTex = Shader.PropertyToID("_ExpandTex");
private static int _SrcTex = Shader.PropertyToID("_SrcTex");
private static int _OutlineKernelSize = Shader.PropertyToID("_OutlineKernelSize");
private static int _OutlineSize = Shader.PropertyToID("_OutlineSize");
private static int _OutlineColor = Shader.PropertyToID("_OutlineColor");
[Header("材质")]
public Material mat;
[Header("轮廓核大小")]
[Range(0.0f, 2.0f)]
public int outlineKernelSize = 1;
[Header("轮廓大小")]
[Range(0.0f, 10.0f)]
public float outlineSize = 5.0f;
[Header("轮廓颜色")]
public Color outline_color = Color.red;
private CommandBuffer cmdBuffer;
private Camera cam;
protected override void Start()
{
base.Start();
cmdBuffer = new CommandBuffer();
cmdBuffer.name = "OutlinePPCmdBuffer";
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 || OutlineManager.instance.Count == 0)
{
GraphicsUtil.SkipOnImage(src, dest);
return;
}
if (mat == null)
{
Debug.LogError("Outline.mat == null");
GraphicsUtil.SkipOnImage(src, dest);
return;
}
ShowOutline(src, dest, mat);
}
private void ShowOutline(RenderTexture src, RenderTexture dest, Material usingMat)
{
var curCamRT = cam.targetTexture;
var rw = curCamRT == null ? Screen.width : curCamRT.width;
var rh = curCamRT == null ? Screen.height : curCamRT.height;
// create RT
var expandOrginRT = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
expandOrginRT.filterMode = FilterMode.Bilinear;
expandOrginRT.name = "OutlinePP.expandOrginRT";
cmdBuffer.Clear();
cmdBuffer.SetRenderTarget(expandOrginRT);
cmdBuffer.ClearRenderTarget(false, true, Color.black);
OutlineManager.instance.Update2CmdBuffer2Draw(cmdBuffer, usingMat, 0);
// execute cmd buffer, Draw To RT
Graphics.ExecuteCommandBuffer(cmdBuffer);
// expand
var expandRT = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
expandRT.filterMode = FilterMode.Bilinear;
expandRT.name = "OutlinePP.expandRT";
usingMat.SetInt(_OutlineKernelSize, outlineKernelSize);
usingMat.SetFloat(_OutlineSize, outlineSize);
usingMat.SetTexture(_ExpandOrginTex, expandOrginRT);
Graphics.Blit(null, expandRT, usingMat, 1);
// final
usingMat.SetTexture(_ExpandTex, expandRT);
usingMat.SetTexture(_SrcTex, src);
usingMat.SetColor(_OutlineColor, outline_color);
Graphics.Blit(null, dest, usingMat, 2);
RenderTexture.ReleaseTemporary(expandOrginRT);
RenderTexture.ReleaseTemporary(expandRT);
}
}
Outline Renderer 提取器
挂载在对应的 Root 下,会自动提取底下所有的 renderer
// jave.lin 2021/09/24
// Outline 后效绘制的 Renderer 提取组件
using System.Collections.Generic;
using UnityEngine;
public class OutlineRendererExtractor : 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);
OutlineManager.instance.Remove(instID);
OutlineManager.instance.Add(instID, renderers);
}
}
OutlineManager
// jave.lin 2021/02/25
// 外发光的管理
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class OutlineManager : MonoSingleton<OutlineManager>
{
private Stack<List<Renderer>> listPool = new Stack<List<Renderer>>();
private Stack<OutlineElement> outlineElementPool = new Stack<OutlineElement>();
private List<OutlineElement> outlineElementList = new List<OutlineElement>();
private Dictionary<int, OutlineElement> outlineElementDict_key_instid = new Dictionary<int, OutlineElement>();
private Dictionary<GameObject, OutlineElement> outlineElementDict_key_go = new Dictionary<GameObject, OutlineElement>();
public int Count => outlineElementList.Count;
private void OnDestroy()
{
if (listPool != null)
{
foreach (var item in listPool)
{
item.Clear();
}
listPool.Clear();
listPool = null;
}
if (outlineElementPool != null)
{
outlineElementPool.Clear();
outlineElementPool = null;
}
if (listPool != null)
{
listPool.Clear();
listPool = null;
}
}
public bool Contains(GameObject go)
{
foreach (var glowElement in outlineElementList)
{
if (glowElement.go == go)
{
return true;
}
}
return false;
}
public void Add(int instID, List<Renderer> renderers)
{
OutlineElement e = outlineElementPool.Count > 0 ? outlineElementPool.Pop() : null;
if (e == null)
{
e = new OutlineElement
{
optType = eOutlineOptType.SpecialRenderers,
instID = instID,
go = null,
renderers = new List<Renderer>(renderers),
ignoreActive = false
};
}
else
{
e.optType = eOutlineOptType.SpecialRenderers;
e.instID = instID;
e.go = null;
e.renderers.AddRange(renderers);
e.ignoreActive = false;
}
outlineElementDict_key_instid[instID] = e;
outlineElementList.Add(e);
//Log.logError($"OutlineManager.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);
OutlineElement e = outlineElementPool.Count > 0 ? outlineElementPool.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 OutlineElement { optType = eOutlineOptType.SpecialGO, instID = -1, go = go, renderers = list, ignoreActive = ignoreActive };
}
else
{
e.optType = eOutlineOptType.SpecialGO;
e.instID = -1;
e.go = go;
e.renderers = list;
e.ignoreActive = ignoreActive;
}
outlineElementDict_key_go[go] = e;
outlineElementList.Add(e);
}
public void Remove(GameObject go)
{
if (go == null)
{
return;
}
for (int i = 0; i < outlineElementList.Count; i++)
{
var e = outlineElementList[i];
if (e.go == null)
{
_Reclyle(e);
outlineElementList.RemoveAt(i);
continue;
}
if (e.go == go)
{
_Reclyle(e);
outlineElementList.RemoveAt(i);
return;
}
}
outlineElementDict_key_go.Remove(go);
}
public void Remove(int instID)
{
if (outlineElementDict_key_instid.Remove(instID))
{
for (int i = 0; i < outlineElementList.Count; i++)
{
var e = outlineElementList[i];
if (e.instID == instID)
{
_Reclyle(e);
outlineElementList.RemoveAt(i);
break;
}
}
}
//Log.logError($"OutlineManager.Remove instID:{instID}");
}
public void Clear()
{
if (outlineElementList.Count > 0)
{
foreach (var e in outlineElementList)
{
_Reclyle(e);
}
outlineElementList.Clear();
}
outlineElementList.Clear();
outlineElementDict_key_go.Clear();
}
public void Update2CmdBuffer2Draw(CommandBuffer cmdBuffer, Material material, int pass = -1)
{
for (int i = outlineElementList.Count - 1; i > -1; i--)
{
var e = outlineElementList[i];
switch (e.optType)
{
case eOutlineOptType.SpecialGO:
if (e.go == null)
{
_Reclyle(e);
outlineElementList.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 eOutlineOptType.SpecialRenderers:
for (int j = 0; j < e.renderers.Count; j++)
{
var r = e.renderers[j];
if (r == null || r.gameObject == null)
{
_Reclyle(e);
outlineElementList.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 OutlineOptType : {e.optType}");
break;
}
}
}
private void _Reclyle(OutlineElement e)
{
if (e.optType == eOutlineOptType.SpecialGO)
{
outlineElementDict_key_go.Remove(e.go);
}
else
{
outlineElementDict_key_instid.Remove(e.instID);
}
e.renderers.Clear();
listPool.Push(e.renderers);
outlineElementPool.Push(e);
}
}
public enum eOutlineOptType
{
SpecialGO,
SpecialRenderers,
}
public class OutlineElement
{
public eOutlineOptType 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,颜色可以调整外发光大小,还有透明度