Unity实现物体外发光描边效果方式有好几种,如重叠放大模型描边Pass、卷积核描边、屏幕后处理等。
HIightingSytem使用了屏幕后期效果实现,效果如下:
整理出核心代码如下,主要分为4个步骤。
1 ,根据场景上所有需要描边的物体轮廓,将它们画到一张RenderTexture上,其中黑色部分A通道值为0。实现着色器为HighlightingOpaque.Shader。
2,将RenderTexture模糊化,通过偏移uv混合周围像素的值,包括混合A通道的值。实现着色器为HighlightingBlur.Shader。
混合后的RGB通道的值
混合后A通道的值:
3,通过模板测试,裁剪和场景中描边模型重叠的像素,即在着色器中通过设置重叠部分A通道的值为0。实现着色器为HighlightingCut.Shader。
4,根据RenderTexture的A通道的值,和屏幕后的Source纹理进行和RenderTexture的RGB值进行差值混合,得出描边效果。
实现着色器为HighlightingComposit.Shader。
整理出核心代码如下:
OutlineObject.cs
OutlineObjectRender.cs
HighlightingOpaque.shader
HighlightingBlur.shader
HighlightingCut.shader
HighlightingComposite.shader
OutlineObject.cs
一个场景可能挂多个,必须挂在场景物体的有MeshRender/SkinMeshRender的节点上!
OutlineObjectRender.cs
必须挂在有MainCamera的节点上!
OutlineObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class OutlineObject : MonoBehaviour
{
public Color color = Color.red;
[HideInInspector]
public Renderer render;
[HideInInspector]
public Material objMat;
private void Start()
{
render = GetComponent<MeshRenderer>();
if (render == null)
render = GetComponent<SkinnedMeshRenderer>();
objMat = new Material(Shader.Find("Hidden/Highlighted/Opaque"));
OutlineObjectRender.Instance.AddOutlineObject(this);
}
}
OutlineObjectRender.cs
using UnityEngine;
using UnityEngine.Rendering;
using System.Collections.Generic;
public class OutlineObjectRender : MonoBehaviour
{
public static OutlineObjectRender Instance;
private List<OutlineObject> outlineObjs = new List<OutlineObject>();
private CommandBuffer buf;
//1,渲染OutlineObject对象成平面纹理;
private Material blurMat;//2,将步骤1纹理模糊化,模糊边缘向平面周边扩散;
private Material maskMat;//3,通过模板测试,不渲染步骤2纹理中和模型重叠的片元部分,只渲染模糊边框;
private Material combineMat;//4,将当前相机渲染的屏幕结果,与步骤3得出的轮廓纹理结合得出描边效果。
private RenderTextureDescriptor rtft;
private RenderTexture rt;
private RenderTargetIdentifier rtID;
private int blur0;
private int blur1;
private void Awake()
{
Instance = this;
blurMat = new Material(Shader.Find("Hidden/Highlighted/Blur"));
maskMat = new Material(Shader.Find("Hidden/Highlighted/Cut"));
combineMat = new Material(Shader.Find("Hidden/Highlighted/Composite"));
blur0 = Shader.PropertyToID("Blur0");
blur1 = Shader.PropertyToID("Blur1");
buf = new CommandBuffer();
Camera.main.AddCommandBuffer(CameraEvent.BeforeImageEffectsOpaque, buf);
}
public void AddOutlineObject(OutlineObject obj) {
if (this.outlineObjs.Contains(obj) == false)
{
this.outlineObjs.Add(obj);
}
}
public void RemoveOutlineObject(OutlineObject obj) {
if (this.outlineObjs.Contains(obj))
this.outlineObjs.Remove(obj);
}
private void OnPreRender()
{
if (rt == null)
{
Camera cam = GetComponent<Camera>();
rtft = new RenderTextureDescriptor(cam.pixelWidth, cam.pixelHeight, RenderTextureFormat.ARGB32, 24);
rtft.colorFormat = RenderTextureFormat.ARGB32;
rtft.sRGB = QualitySettings.activeColorSpace == ColorSpace.Linear;
rtft.useMipMap = false;
rtft.msaaSamples = 8;
rt = new RenderTexture(rtft);
rt.filterMode = FilterMode.Point;
rt.wrapMode = TextureWrapMode.Clamp;
if (!rt.Create())
{
Debug.LogError("Failed to create RenderTexture!");
}
rtID = new RenderTargetIdentifier(rt);
maskMat.SetFloat("_HighlightingFillAlpha", 0);
}
buf.Clear();
buf.SetRenderTarget(rtID);
buf.ClearRenderTarget(true, true, new Color(0, 0, 0, 0));
RenderTextureDescriptor desc = rtft;
desc.width = rt.width;
desc.height = rt.height;
desc.depthBufferBits = 0;
for (int i = 0; i < this.outlineObjs.Count; i++)
{
//第一步,将所有要描边的render GameObject用Opaque渲染成2D面颜色,Draw到buf里。
//所有的需要描边的Render对象都要在这里处理一下,用CommandBuff统一画到同一个Texture上,交给下面的步骤处理。
outlineObjs[i].objMat.SetColor("_HighlightingColor", outlineObjs[i].color);
buf.DrawRenderer(outlineObjs[i].render, outlineObjs[i].objMat);
}
//第二步,对buf的texture进行模糊。
//获取2张模板用于模糊处理。
buf.GetTemporaryRT(blur0, desc, FilterMode.Bilinear);
buf.GetTemporaryRT(blur1, desc, FilterMode.Bilinear);
RenderTargetIdentifier blurID0 = new RenderTargetIdentifier(blur0);
RenderTargetIdentifier blurID1 = new RenderTargetIdentifier(blur1);
//开始模糊循环之前,将rtID传到blurID。
buf.Blit(rtID, blurID0);
bool oddEven = true;
for (int i = 0; i < 4; i++)
{
float off = 1.5f + 0.5f * i;
buf.SetGlobalFloat(HighlightingSystem.ShaderPropertyID._HighlightingBlurOffset, off);
if (oddEven)
{
buf.Blit(blurID0, blurID1, blurMat);
}
else
{
buf.Blit(blurID1, blurID0, blurMat);
}
oddEven = !oddEven;
}
//第三步,对得到模糊纹理进行模板测试,去除和模型重叠的片元像素,通过
buf.Blit(oddEven ? blurID0 : blurID1, rtID, maskMat);
//
buf.SetGlobalTexture("_HighlightingBuffer", rtID);
buf.ReleaseTemporaryRT(blur0);
buf.ReleaseTemporaryRT(blur1);
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//第四步,与当前屏幕效果混合
Graphics.Blit(source, destination, combineMat);
}
}
HighlightingOpaque.shader
用于将场景上需要描边的物体渲染到RenderTexture上。
Shader "Hidden/Highlighted/Opaque"
{
Properties
{
[HideInInspector] _HighlightingColor ("", Color) = (1, 1, 1, 1)
}
SubShader
{
Lighting Off
Fog { Mode Off }
ZWrite Off // Manual depth test
ZTest Always // Manual depth test
Pass
{
Stencil
{
Ref 1
Comp Always
Pass Replace
ZFail Keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#pragma target 2.0
#pragma multi_compile __ HIGHLIGHTING_OVERLAY
#include "UnityCG.cginc"
uniform fixed4 _HighlightingColor;
#ifndef HIGHLIGHTING_OVERLAY
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
#endif
struct vs_input
{
float4 vertex : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct ps_input
{
float4 pos : SV_POSITION;
#ifndef HIGHLIGHTING_OVERLAY
float4 screen : TEXCOORD0;
#endif
};
ps_input vert(vs_input v)
{
ps_input o;
UNITY_SETUP_INSTANCE_ID(v);
o.pos = UnityObjectToClipPos(v.vertex);
#ifndef HIGHLIGHTING_OVERLAY
o.screen = ComputeScreenPos(o.pos);
COMPUTE_EYEDEPTH(o.screen.z);
#endif
return o;
}
fixed4 frag(ps_input i) : SV_Target
{
#ifndef HIGHLIGHTING_OVERLAY
float z = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screen));
float perspZ = LinearEyeDepth(z); // LinearEyeDepth automatically handles UNITY_REVERSED_Z case
#if defined(UNITY_REVERSED_Z)
z = 1 - z;
#endif
float orthoZ = _ProjectionParams.y + z * (_ProjectionParams.z - _ProjectionParams.y); // near + z * (far - near)
float sceneZ = lerp(perspZ, orthoZ, unity_OrthoParams.w);
clip(sceneZ - i.screen.z + 0.01);
#endif
return _HighlightingColor;
}
ENDCG
}
}
Fallback Off
}
HighlightingBlur.shader
将RenderTexture模糊化,并混合A通道的值。
Shader "Hidden/Highlighted/Blur"
{
Properties
{
[HideInInspector] _MainTex ("", 2D) = "" {}
[HideInInspector] _HighlightingIntensity ("", Range (0.25,0.5)) = 0.3
}
SubShader
{
Pass
{
ZTest Always
Cull Off
ZWrite Off
Lighting Off
Fog { Mode Off }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#pragma target 2.0
#pragma multi_compile DIAGONAL_DIRECTIONS STRAIGHT_DIRECTIONS ALL_DIRECTIONS
#include "UnityCG.cginc"
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform float4 _MainTex_TexelSize;
uniform float _HighlightingBlurOffset;
uniform half _HighlightingIntensity;
struct vs_input
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};
struct ps_input
{
float4 pos : SV_POSITION;
#if defined(ALL_DIRECTIONS)
float4 uv0 : TEXCOORD0;
float4 uv1 : TEXCOORD1;
float4 uv2 : TEXCOORD2;
float4 uv3 : TEXCOORD3;
#else
float4 uv0 : TEXCOORD0;
float4 uv1 : TEXCOORD1;
#endif
};
ps_input vert(vs_input v)
{
ps_input o;
o.pos = UnityObjectToClipPos(v.vertex);
float2 uv = UnityStereoScreenSpaceUVAdjust(v.texcoord, _MainTex_ST);
float2 offs = _HighlightingBlurOffset * _MainTex_TexelSize.xy;
#if defined(ALL_DIRECTIONS)
// Diagonal
o.uv0.x = uv.x - offs.x;
o.uv0.y = uv.y - offs.y;
o.uv0.z = uv.x + offs.x;
o.uv0.w = uv.y - offs.y;
o.uv1.x = uv.x + offs.x;
o.uv1.y = uv.y + offs.y;
o.uv1.z = uv.x - offs.x;
o.uv1.w = uv.y + offs.y;
// Straight
o.uv2.x = uv.x - offs.x;
o.uv2.y = uv.y;
o.uv2.z = uv.x + offs.x;
o.uv2.w = uv.y;
o.uv3.x = uv.x;
o.uv3.y = uv.y - offs.y;
o.uv3.z = uv.x;
o.uv3.w = uv.y + offs.y;
#elif defined(STRAIGHT_DIRECTIONS)
// Straight
o.uv0.x = uv.x - offs.x;
o.uv0.y = uv.y;
o.uv0.z = uv.x + offs.x;
o.uv0.w = uv.y;
o.uv1.x = uv.x;
o.uv1.y = uv.y - offs.y;
o.uv1.z = uv.x;
o.uv1.w = uv.y + offs.y;
#else
// Diagonal
o.uv0.x = uv.x - offs.x;
o.uv0.y = uv.y - offs.y;
o.uv0.z = uv.x + offs.x;
o.uv0.w = uv.y - offs.y;
o.uv1.x = uv.x + offs.x;
o.uv1.y = uv.y + offs.y;
o.uv1.z = uv.x - offs.x;
o.uv1.w = uv.y + offs.y;
#endif
return o;
}
half4 frag(ps_input i) : SV_Target
{
half4 color1 = tex2D(_MainTex, i.uv0.xy);
fixed4 color2;
// For straight or diagonal directions
color2 = tex2D(_MainTex, i.uv0.zw);
color1.rgb = max(color1.rgb, color2.rgb);
color1.a += color2.a;
color2 = tex2D(_MainTex, i.uv1.xy);
color1.rgb = max(color1.rgb, color2.rgb);
color1.a += color2.a;
color2 = tex2D(_MainTex, i.uv1.zw);
color1.rgb = max(color1.rgb, color2.rgb);
color1.a += color2.a;
// For all directions
#if defined(ALL_DIRECTIONS)
color2 = tex2D(_MainTex, i.uv2.xy);
color1.rgb = max(color1.rgb, color2.rgb);
color1.a += color2.a;
color2 = tex2D(_MainTex, i.uv2.zw);
color1.rgb = max(color1.rgb, color2.rgb);
color1.a += color2.a;
color2 = tex2D(_MainTex, i.uv3.xy);
color1.rgb = max(color1.rgb, color2.rgb);
color1.a += color2.a;
color2 = tex2D(_MainTex, i.uv3.zw);
color1.rgb = max(color1.rgb, color2.rgb);
color1.a += color2.a;
#endif
color1.a *= _HighlightingIntensity;
return color1;
}
ENDCG
}
}
Fallback off
}
HighlightingCut.shader
通过模板测试,将RenderTexture中和场景中重叠的像素A通道设置为0.
Shader "Hidden/Highlighted/Cut"
{
Properties
{
[HideInInspector] _MainTex ("", 2D) = "" {}
[HideInInspector] _HighlightingFillAlpha ("", Range(0.0, 1.0)) = 1.0
}
SubShader
{
Lighting Off
Fog { Mode off }
ZWrite Off
ZTest Always
Cull Back
Pass
{
Stencil
{
Ref 1
Comp NotEqual
Pass Keep
ZFail Keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#pragma target 2.0
#include "UnityCG.cginc"
struct vs_input
{
float4 vertex : POSITION;
half2 texcoord : TEXCOORD0;
};
struct ps_input
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
ps_input vert(vs_input v)
{
ps_input o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = UnityStereoScreenSpaceUVAdjust(v.texcoord, _MainTex_ST);
return o;
}
fixed4 frag(ps_input i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
}
Pass
{
Stencil
{
Ref 1
Comp Equal
Pass Keep
ZFail Keep
}
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#pragma target 2.0
#include "UnityCG.cginc"
struct vs_input
{
float4 vertex : POSITION;
};
struct ps_input
{
float4 pos : SV_POSITION;
};
uniform float _HighlightingFillAlpha;
ps_input vert(vs_input v)
{
ps_input o;
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag() : SV_Target
{
return fixed4(0, 0, 0, _HighlightingFillAlpha);
}
ENDCG
}
}
FallBack Off
}
HighlightingComposite.shader
根据RenderTexture中A通道的值,将RenderTexture的RGB值和屏幕纹理插值叠加。得出最后的描边效果。
Shader "Hidden/Highlighted/Composite"
{
Properties
{
[HideInInspector] _MainTex ("", 2D) = "" {}
[HideInInspector] _HighlightingBuffer ("", 2D) = "" {}
}
SubShader
{
Pass
{
Lighting Off
Fog { Mode off }
ZWrite Off
ZTest Always
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#pragma target 2.0
#include "UnityCG.cginc"
struct vs_input
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};
struct ps_input
{
float4 pos : SV_POSITION;
half2 uv0 : TEXCOORD0;
half2 uv1 : TEXCOORD1;
};
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform float4 _MainTex_TexelSize;
uniform sampler2D _HighlightingBuffer;
ps_input vert(vs_input v)
{
ps_input o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv0 = UnityStereoScreenSpaceUVAdjust(v.texcoord, _MainTex_ST);
o.uv1 = o.uv0;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
{
o.uv1.y = 1-o.uv1.y;
}
#endif
return o;
}
fixed4 frag(ps_input i) : SV_Target
{
fixed4 c1 = tex2D(_MainTex, i.uv0);
fixed4 c2 = tex2D(_HighlightingBuffer, i.uv1);
c1.rgb = lerp(c1.rgb, c2.rgb, c2.a);
return c1;
}
ENDCG
}
}
FallBack Off
}