Unity3D Shader系列之描边


1 引言

总结下描边效果的实现方式,主要有以下几种:
①法线外拓+ZTest Always
②法线外拓+Cull Front
③法线外拓+ZWrite Off
④法线外拓+模板测试
⑤基于屏幕后处理

2 顶点沿法线外拓方式

法线外拓的原理如下:
基本原理还是很简单的:模型渲染两次,第一次渲染时将模型的顶点沿法线方向外拓,然后绘制描边颜色,第二次渲染按正常的渲染即可。也就是用第二次渲染去覆盖掉第一次的渲染,由于第二次没有法线外拓,所以只会覆盖掉中间的部分,从而实现描边。
这个过程就像画家绘画样,对于同一个位置,用后画的颜色去覆盖掉先画的颜色。
法线外拓实现描边原理
所以这里最主要的问题在于,如何保证第二个Pass一定能覆盖第一个Pass。以下几种方法都可实现,一般方法②和方法③用得多一点:
方法①第二个Pass开启ZTest Always
方法②第一个Pass使用Cull Front
方法③第一个Pass使用ZWrite Off
方法④使用模板测试

2.1 法线外拓+ZTest Always

2.1.1 代码

要保证第二个Pass一定能覆盖掉第一个Pass,最简单的方法就是让深度测试一直通过,即使用ZTest Always。但是使用ZTest Always问题是非常多的,我们下一节再详说。

伪代码如下:

// 先用描边颜色渲染
Pass
{
    
    
	...
	// 顶点着色器:顶点沿着法线外拓
	v2f vert (appdata v)
    {
    
    
        v2f o;
		v.vertex.xy += normalize(v.normal) * _OutlineWidth;
		o.vertex = UnityObjectToClipPos(v.vertex);
        return o;
    }
	
	// 片元着色器:直接绘制描边颜色
	fixed4 frag (v2f i) : SV_Target
	{
    
    
		return _OutlineColor;
	}
}

// 再正常渲染
Pass
{
    
    
	// 保证此Pass一定会渲染
	ZTest Always
	// ...
}

完整代码如下:

Shader "LaoWang/Outline_Example01"
{
    
    
    Properties
    {
    
    
        _MainTex ("Texture", 2D) = "white" {
    
    }
		_OutlineWidth ("Outline width", Range(0.01, 4)) = 0.01
		_OutlineColor ("Outline Color", color) = (1.0, 1.0, 1.0, 1.0)
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" "Queue"="Geometry"}
        LOD 100

		Pass
		{
    
    
			CGPROGRAM

			#pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
				float3 normal : NORMAL;
            };

            struct v2f
            {
    
    
                float4 vertex : SV_POSITION;
            };

			float _OutlineWidth;
			fixed4 _OutlineColor;

            v2f vert (appdata v)
            {
    
    
                v2f o;
				v.vertex.xy += normalize(v.normal) * _OutlineWidth;
				o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                return _OutlineColor;
            }

			ENDCG
		}

        Pass
        {
    
    
        	ZTest Always
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#include "Lighting.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
    
    
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
    
    
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                fixed4 color = tex2D(_MainTex, i.uv);
                return fixed4(color.rgb, 1.0);
            }
            ENDCG
        }
    }
}

2.1.2 问题点

由于第二个Pass使用了ZTest Always,会导致两个问题。
①模型自身会穿透自身
但是我们会发现只有部分网格会穿透自身。为什么只有部分网格会穿透呢?我也没弄清楚。
GPU绘制某一模型时,同一模型中的各个三角面的渲染顺序是如何控制的呢?希望知道的同学解答一下,在此谢过。
ZTest Always的问题01
②物体将会永远再最前面
ZTest Always的问题02
所以这种方式基本没人使用。

2.2 法线外拓+Cull Front

2.2.1 代码

原理和上一节类似,只不过不是用ZTest Always来保证第二个Pass覆盖第一个Pass,而是在第一个Pass中使用Cull Front,即第一个Pass只渲染模型的背面,然后让背面向外拓展一下,因为一般背面都在正面的后面(即背面的深度值比正面的深度值大),所以第二个Pass就会覆盖掉中间部分。

Shader "LaoWang/Outline_CullFront"
{
    
    
    Properties
    {
    
    
        _MainTex ("Texture", 2D) = "white" {
    
    }
		_OutlineWidth ("Outline width", Range(0.01, 4)) = 0.01
		_OutlineColor ("Outline Color", color) = (1.0, 1.0, 1.0, 1.0)
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" "Queue"="Geometry"}
        LOD 100

		Pass
		{
    
    
			Cull Front
			CGPROGRAM

			#pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
				float3 normal : NORMAL;
            };

            struct v2f
            {
    
    
                float4 vertex : SV_POSITION;
            };

			float _OutlineWidth;
			fixed4 _OutlineColor;

            v2f vert (appdata v)
            {
    
    
                v2f o;
				v.vertex.xy += normalize(v.normal) * _OutlineWidth;
				o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                return _OutlineColor;
            }

			ENDCG
		}

        Pass
        {
    
    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#include "Lighting.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
    
    
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
    
    
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                fixed4 color = tex2D(_MainTex, i.uv);
                return fixed4(color.rgb, 1.0);
            }
            ENDCG
        }
    }
}

效果是这样的。
描边效果
虽然Robot Kyle这个模型使用这种方式效果不好,但是对于其他绝大部分模型还是够用了。
一般的描边就是使用这种方式。

2.2.2 改进点

①无论相机距离物体多远或者观察视角的变化,都让描边的宽度保持等比例。
如图。
各处描边厚度不一样的问题
出现这样问题的原因在于我们是在模型空间对顶点进行外拓的,外拓的距离是一样的。但是由于是透视相机,模型上离相机近的地方描边效果较粗,而远的地方描边效果较细。
解决这个问题的方法是,我们不在模型空间外拓,而在齐次裁剪空间将顶点沿法线方向进行外拓。
而这个方法最大的问题在于,如何才能求到齐次裁剪空间中的法线方向?
顶点从模型空间变换到齐次裁剪空间的变换矩阵是MVP,那法线的变换能否直接使用MVP矩阵呢?答案是不行。法线的变换应该是变换矩阵的逆转置矩阵,即我们这里将使用(MVP)-1T来进行法线变换。
为什么法线变换不能直接使用变换矩阵,而要使用逆转置矩阵呢?主要是为了保证存在非等比缩放时,变换后的法线方向依然是垂直与表面的。如果不存在非等比缩放,即只存在旋转,那么法线的变换是可以直接使用变换矩阵的。(具体描述详见《Unity Shader入门精要》4.7节 法线变换)
法线变换问题
那MVP矩阵是只有旋转吗?不是的。P矩阵即从观察空间到齐次裁剪空间的变换矩阵一定是存在非等比缩放的。所以,我们这里需要用到MVP的逆转置矩阵。
两种相机的P矩阵
回到最初的问题,MVP的逆转置矩阵该怎么求?很遗憾Unity的Shader中并没有直接提供相应的变量,要真正得到这个逆转置矩阵需要从C#端计算然后传递到shader。但其实我们并不需要那么高的精度,近似即可。有两种近似方式。
一是直接使用MVP矩阵来近似。

v2f vert (appdata v)
{
    
    
	o.vertex = UnityObjectToClipPos(v.vertex);
	float3 clipNormal = mul((float3x3) UNITY_MATRIX_VP, mul((float3x3) UNITY_MATRIX_M, v.normal));
	o.vertex.xy += normalize(clipNormal).xy * _OutlineWidth;
}

二是使用(MV)的逆转置矩阵* P来近似。为什么使用MV的逆转置矩阵呢,是因为Unity刚好提供了这个变量,UNITY_MATRIX_IT_MV。

v2f vert (appdata v)
{
    
    
	o.vertex = UnityObjectToClipPos(v.vertex);
	float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
	float2 clipNormal = mul((float2x2)UNITY_MATRIX_P, viewNormal.xy);
	o.vertex.xy += normalize(clipNormal) * _OutlineWidth;
}

效果对比如下,可以看到上面两种方式的效果其实差不多。
三种外拓方式效果对比

2.3 法线外拓+ZWrite Off

2.3.1 代码

逻辑也很简单,第一个Pass由于关闭了深度写入,那么第二个Pass肯定能够通过深度测试,所以第二个Pass会覆盖掉第一个Pass。

Shader "LaoWang/Outline_ZWriteOff"
{
    
    
    Properties
    {
    
    
        _MainTex ("Texture", 2D) = "white" {
    
    }
		_OutlineWidth ("Outline width", Range(0.01, 4)) = 0.01
		_OutlineColor ("Outline Color", color) = (1.0, 1.0, 1.0, 1.0)
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" "Queue"="Geometry"}
        LOD 100

		Pass
		{
    
    
			ZWrite Off

			CGPROGRAM

			#pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
				float3 normal : NORMAL;
            };

            struct v2f
            {
    
    
                float4 vertex : SV_POSITION;
            };

			float _OutlineWidth;
			fixed4 _OutlineColor;

            v2f vert (appdata v)
            {
    
    
                v2f o;
				//v.vertex.xy += normalize(v.normal) * _OutlineWidth;
				//o.vertex = UnityObjectToClipPos(v.vertex);

				o.vertex = UnityObjectToClipPos(v.vertex);
				float3 clipNormal = mul((float3x3) UNITY_MATRIX_VP, mul((float3x3) UNITY_MATRIX_M, v.normal));
				o.vertex.xy += normalize(clipNormal).xy * _OutlineWidth;

				//o.vertex = UnityObjectToClipPos(v.vertex);
				//float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
				//float2 clipNormal = mul((float2x2)UNITY_MATRIX_P, viewNormal.xy);
				//o.vertex.xy += normalize(clipNormal) * _OutlineWidth;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                return _OutlineColor;
            }

			ENDCG
		}

        Pass
        {
    
    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#include "Lighting.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
    
    
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
    
    
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                fixed4 color = tex2D(_MainTex, i.uv);
                return fixed4(color.rgb, 1.0);
            }
            ENDCG
        }
    }
}

效果如下,可以看到Robot Kyle这个模型使用这种方式效果是最好的。
ZWrite Off效果对比

2.3.2 问题点

ZWrite Off关闭后会有两个问题。具体如下。
我们在场景中加上一个Ground,新建一个标准的材质球。
标准材质球
然后我们就会发现有地板的部分描边就消失了。
ZWrite Off的问题
我们从Frame Debugger中可以看出地板是最后绘制的。而绘制描边时没有开启深度写入,这就导致地板的深度测试会通过。所以地板的颜色会覆盖掉描边部分的颜色。
ZWrite Off的问题测试
要解决这个问题,其实也很简单,最后渲染我们的模型就行了。用什么方法控制我们的模型最后渲染呢?当然是控制渲染队列啦,这点我们在《Unity3D Shader系列之透视效果XRay》中讲过,就不再多说了。
我们将渲染队列设置为“Geometry+1”,即在所有不透明物体渲染后再渲染我们的模型。
调整渲染队列之后的效果
调整之后,描边效果就正常了。
但是这样调整之后依然还有问题,比如我们再复制一个描边模型,然后一个在前一个在后。此时,我们将会发现两个模型重叠的部分没有描边了。
重叠部分的描边没有了
同样,我们去Frame Debugger中看看原因。
ZWrite Off导致的问题
从上图我们可以看到,是先绘制的前面的物体,再绘制后面的物体,就导致绘制后面物体时将前面物体的描边给覆盖掉了。
要解决这个问题,我们得有一个储备知识:Unity在渲染不透明物体时,如果这两个物体的渲染队列一样(Render Queue的值一样),则按距离摄像机由近到远的顺序依次渲染。在渲染半透明物体时,如果这两个物体的渲染队列一样(Render Queue的值一样),则按距离摄像机由远到近的顺序依次渲染。
为什么要这样做?对于不透明物体,先渲染近的再渲染远的,由于硬件的Early-Z等技术,可以减少Over Draw。对于半透明物体,由于需要关闭深度写入,所以必须先渲染远的再渲染近的,这样才能保证混合后的颜色是正确的。
所以我们这里要想让两个模型重叠的部分也能绘制出描边效果,就得先渲染后面的再渲染前面的,那把渲染队列改为Transparent就可以了。但是这个办法也不是完全能解决问题的,因为我们上面给出的距离摄像机的远近其实很模糊,这个远近到底是取哪个值?是取物体的世界坐标与相机的距离呢还是物体的某个顶点距离相机的距离呢?Unity官方也没给出说明。
改为Transparent后的效果

2.4 法线外拓+模板测试

先正常渲染物体,将模板缓冲区写为1。然后再法线外拓进行描边,当模板缓冲区值为0时绘制描边。
伪代码。

Pass
{
    
    
	// 将模板缓冲区写为1
	Stencil
	{
    
    
		Ref 1
		Comp Always
		Pass Replace
	}

	// 正常渲染
	...
}

Pass
{
    
    
	Stencil
	{
    
    
		Ref 0
		Comp Equal
	}
	ZWrite Off

	// 渲染描边
	// 顶点着色器法线外拓
	...
}

完整代码。

Shader "LaoWang/Outline_StencilTest"
{
    
    
    Properties
    {
    
    
        _MainTex ("Texture", 2D) = "white" {
    
    }
		_OutlineWidth ("Outline width", Range(0.01, 4)) = 0.01
		_OutlineColor ("Outline Color", color) = (1.0, 1.0, 1.0, 1.0)
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" "Queue"="Geometry+1"}
        LOD 100

		Pass
        {
    
    
			Stencil
			{
    
    
				Ref 1
				Comp Always
				Pass Replace
			}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#include "Lighting.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
    
    
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
    
    
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                fixed4 color = tex2D(_MainTex, i.uv);
                return fixed4(color.rgb, 1.0);
            }
            ENDCG
        }

		Pass
		{
    
    
			Stencil
			{
    
    
				Ref 0
				Comp Equal
			}

			ZWrite Off

			CGPROGRAM

			#pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
    
    
                float4 vertex : POSITION;
				float3 normal : NORMAL;
            };

            struct v2f
            {
    
    
                float4 vertex : SV_POSITION;
            };

			float _OutlineWidth;
			fixed4 _OutlineColor;

            v2f vert (appdata v)
            {
    
    
                v2f o;
				//v.vertex.xy += normalize(v.normal) * _OutlineWidth;
				//o.vertex = UnityObjectToClipPos(v.vertex);

				o.vertex = UnityObjectToClipPos(v.vertex);
				float3 clipNormal = mul((float3x3) UNITY_MATRIX_VP, mul((float3x3) UNITY_MATRIX_M, v.normal));
				o.vertex.xy += normalize(clipNormal).xy * _OutlineWidth;

				//o.vertex = UnityObjectToClipPos(v.vertex);
				//float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
				//float2 clipNormal = mul((float2x2)UNITY_MATRIX_P, viewNormal.xy);
				//o.vertex.xy += normalize(clipNormal) * _OutlineWidth;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
                return _OutlineColor;
            }

			ENDCG
		}
    }
}

效果如下。
模板测试描边效果
这种方式也会有两个模型重叠部分没有描边的问题,但是由于使用的是模板测试,这个问题是无解的了。

2.5 法线外拓实现描边的问题

使用法线外拓实现描边都存在下图这样的问题,即法线不是连续的时候,描边就会中断。
法线外拓实现描边的问题
要解决这个问题需要写一个工具将顶点的法线平滑一下,并将其保存在顶点的颜色数据中,然后在外拓时使用平滑后的法线来外拓。
可以参考这篇文章,里面实现了法线平滑工具。

3 屏幕后处理的方式

使用屏幕后处理实现描边一般有两种方式,一是使用Unity中Camera的着色器替代技术,二是使用Render Command。

3.1 使用Camera的着色器替代技术

这种方式我没有去具体实现,但是去了解了下,这里总结下大概的思路。

3.1.1 着色器替代技术

什么是Camera的着色器替代技术?
说起来高大上,其实本质的东西并不复杂,就是用相机重新渲染一遍场景,但是本次渲染的过程中,场景中的物体(不一定是全部物体,我们可以用代码控制只渲染某一部分物体)不再使用它自身的Shader进行着色,而是使用特定的Shader(所有要渲染的物体都是用同一个Shader)来着色。
从代码上来说,就是下面Camera类中的两个方法。

public void RenderWithShader(Shader shader, string replacementTag);
public void SetReplacementShader(Shader shader, string replacementTag);

RenderWithShader只有调用时的那一帧有效。
SetReplacementShader是调用之后Camera渲染都一直使用指定的Shader,直到代码主动调用ResetReplacementShader方法。

public void ResetReplacementShader();

说一下两个方法的参数。
第一个参数为相机渲染时将使用的Shader。
第二参数为相机查找的标签,一般指定为RenderType。
举个例子,相信大家一看就明白了。
我们这样调用。

Camera.main.SetReplacementShader(Shader.Find("LaoWang/CameraReplace"), "RenderType");

对应的Shader代码如下:

Shader "LaoWang/CameraReplace"
{
    
    
	SubShader
    {
    
    
		// 渲染不透明物体时用此Pass替代
        Tags {
    
     "RenderType" = "Opaque" }

		Pass
		{
    
    
			// ...
		}
    }

	SubShader
    {
    
    
		// 渲染半透明物体时用此Pass替代
        Tags {
    
     "RenderType" = "Transparent" }

		Pass
		{
    
    
			// ...
		}
    }
}

那么我们的主摄像机在渲染时,将会去遍历场景中的所有物体,如果物体使用的Shader的RenderType标签为Opaque,那么相机将使用“LaoWang/CameraReplace”中的第一个Pass渲染该物体;如果物体使用的Shader的RenderType标签为Transparent,那么相机将使用“LaoWang/CameraReplace”中的第二个Pass渲染该物体;如果物体使用的Shader的RenderType标签既不为Opaque也不为Transparent,那该物体将不会渲染。
查找的标签是否一定要指定为RenderType呢?不是的。只是因为Unity中内置的着色器都有这个标签,所以我们一般就指定为它。
Unity中相机输出法线纹理、深度纹理其实就是使用了这种技术,如果某天你发现用相机输出深度纹理时发现不包含某个物体,很有可能就是那个物体的Shader的RenderType标签没有设置或者没有设置正确。

3.1.2 描边思路

①创建一个额外的相机,使用Camera.CopyFrom拷贝主摄像机的参数,并将位置、旋转设置成一样
②设置额外相机的Culling Mask
③创建一个Renderer Texture,假设名为rt,其长宽可以设置为屏幕大小,但是屏幕大小这么大的纹理内存占用比较大,没必要的话建议使用较小的分辨率
④额外相机的Target Texture设置为步骤③创建的Renderer Texture
⑤创建一个脚本,继承自MonoBehaviour,实现OnRenderImage(RenderTexture source, RenderTexture destination)方法,并挂载到主摄像机上
⑥新建一个额外相机渲染的Shader,RenderType标签设置为Opaque,片元着色中只输出纯色;该Shader用于额外相机的着色器替换
⑦额外相机调用SetReplacementShader,第一个参数为步骤⑥创建的Shader,第二个参数为RenderType

SetReplacementShader(Shader.Find(""), "RenderType");

⑧在OnRenerImage方法中,先对rt进行高斯模糊,高斯模糊会让额外相机看到的物体的轮廓往外扩,高斯模糊后的图像再与rt做差得到描边,然后再与主摄像机看到的画面叠加即可

3.1.3 问题点

这种做法有几个问题,首先是需要额外创建出一个摄像机并对其进行管理,Camera本身属于Unity3D场景管理中比较重的对象,他的背后应该还涉及视锥切割,排序等一系列复杂的操作,对于仅需要绘制几个简单物体的操作来说太浪费计算资源了。另外需要绘制的对象需要有单独的层,如果本身已经由其他需要跟其他同类物体指定一个layer的话就不太方便操作了。最后渲染的第一步与后几步分开了,由于最终需要将结果输出到主摄像机上,这意味着两个摄像机上都有一些需要维护的脚本。

3.2 Render Command

3.2.1 思路

使用Render Command实现描边的思路与使用相机的着色器替代技术的原理是一样的,只是不再是用一个额外的相机去渲染,而是直接使用Render Command来处理额外渲染的这一步骤了。
Command Buffer实现描边的过程

3.2.2 Command Buffer

Command Render主要使用Command Buffer来实现,其就是对OpenGL\DirectX这些底层渲染接口的API进行了封装,其内部预定义一系列的渲染指令,我们使用起来非常方便。
官方文档详细API
解释下我们描边用到的代码。
先实例化一个CommandBuffer,名字设置为“Render Outline”,这里设置名字主要是方便我们在Frame Debugger中定位到对应的渲染流程。

m_RenderCommand = new CommandBuffer
{
    
    
    name = "Render Outline"
};

CommandBuffer命名的作用
创建一个材质球,用于CommandBuffer的渲染。

m_OutlineMaterial = new Material(Shader.Find(OutlineShader));

然后先将CommandBuffer的背景画面给清空。
使用DrawRenderer将需要额外渲染的物体添加到CommandBuffer的队列中。
下面的示例代码表示在绘制CommandBuffer中的几个物体时,使用的材质为m_OutlineMaterial中的第一个Pass(参数中的第二个0)。

// 顺序将渲染任务加入RenderCommand中
m_RenderCommand.ClearRenderTarget(true, true, Color.clear);
for (int i = 0; i < OutlineObjects.Length; ++i)
{
    
    
    m_RenderCommand.DrawRenderer(OutlineObjects[i], m_OutlineMaterial, 0, 0);
}

然后创建一个渲染纹理,并使用Graphics.SetRenderTarget将渲染目标设置为刚创建的渲染纹理。
再调用Graphics.ExecuteCommandBuffer。
即按照CommandBuffer的设置进行渲染,渲染之后的结果即为刚创建的渲染纹理。

m_OutlineMaterial.SetColor("_OutlineColor", outlineColor);
RenderTexture outlineColorRt = RenderTexture.GetTemporary(Screen.width, Screen.height);
Graphics.SetRenderTarget(outlineColorRt);
Graphics.ExecuteCommandBuffer(m_RenderCommand);

CommandBuffer不再使用时,一定要释放。

m_RenderCommand.Clear();

3.2.3 完整代码

PostEffectOutline.cs

using UnityEngine;
using UnityEngine.Rendering;

[DisallowMultipleComponent]
[RequireComponent(typeof(Camera))]
public class PostEffectOutline : MonoBehaviour
{
    
    
    private const string OutlineShader = "LaoWang/PostEffect/Outline";

    private Material m_OutlineMaterial;
    private CommandBuffer m_RenderCommand;
    public Renderer[] OutlineObjects;

    public Color outlineColor = Color.red;

    [Range(1, 8)]
    public int downSampleScale = 2;                 // 降采样比例
    [Range(0, 4)]
    public int blurIterations = 1;                  // 高斯模糊迭代次数
    [Range(0.2f, 3.0f)]
    public float blurSpread = 0.6f;                 // 高斯模糊

    private void Awake()
    {
    
    
        m_RenderCommand = new CommandBuffer
        {
    
    
            name = "Render Outline"
        };

        m_OutlineMaterial = new Material(Shader.Find(OutlineShader));
    }

    void OnEnable()
    {
    
    
        // 顺序将渲染任务加入RenderCommand中
        m_RenderCommand.ClearRenderTarget(true, true, Color.clear);
        for (int i = 0; i < OutlineObjects.Length; ++i)
        {
    
    
            m_RenderCommand.DrawRenderer(OutlineObjects[i], m_OutlineMaterial, 0, 0);
        }
    }

    void OnDisable()
    {
    
    
        m_RenderCommand.Clear();
    }

    void OnDestroy()
    {
    
    
        m_RenderCommand.Clear();
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
    
    
        //1. 绘制颜色
        m_OutlineMaterial.SetColor("_OutlineColor", outlineColor);
        RenderTexture outlineColorRt = RenderTexture.GetTemporary(Screen.width, Screen.height);
        Graphics.SetRenderTarget(outlineColorRt);
        Graphics.ExecuteCommandBuffer(m_RenderCommand);
        // 用于测试
        //Graphics.Blit(outlineColorRt, destination);
        //RenderTexture.ReleaseTemporary(outlineColorRt);

        //2. 降采样
        int rtW = Screen.width >> downSampleScale;
        int rtH = Screen.height >> downSampleScale;
        RenderTexture blurRt = RenderTexture.GetTemporary(rtW, rtH);
        blurRt.filterMode = FilterMode.Bilinear;
        Graphics.Blit(outlineColorRt, blurRt);

        //3. 高斯模糊
        RenderTexture blurTemp = RenderTexture.GetTemporary(rtW, rtH);
        for (int i = 0; i < blurIterations; ++i)
        {
    
    
            m_OutlineMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread);
            // 水平模糊
            Graphics.Blit(blurRt, blurTemp, m_OutlineMaterial, 1);
            // 垂直模糊
            Graphics.Blit(blurTemp, blurRt, m_OutlineMaterial, 2);
        }

        // 用于测试
        //Graphics.Blit(blurRt, destination);

        //4. 叠加
        m_OutlineMaterial.SetTexture("_OutlineColorTex", outlineColorRt);
        m_OutlineMaterial.SetTexture("_BlurTex", blurRt);
        Graphics.Blit(source, destination, m_OutlineMaterial, 3);

        RenderTexture.ReleaseTemporary(outlineColorRt);
        RenderTexture.ReleaseTemporary(blurRt);
        RenderTexture.ReleaseTemporary(blurTemp);
    }
}

PostEffect_Outline.shader

Shader "LaoWang/PostEffect/Outline"
{
    
    
    Properties
    {
    
    
		_OutlineColor ("Outline Color", color) = (1.0, 0, 0, 1.0)
        _MainTex ("Texture", 2D) = "white" {
    
    }
		_BlurSize ("Blur Size", float) = 1.0
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType" = "Opaque" }

		CGINCLUDE

		#include "UnityCG.cginc"

		fixed4 _OutlineColor;
		sampler2D _MainTex;
        half4 _MainTex_TexelSize;
		float _BlurSize;

		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 it = 1; it < 3; it++)
			{
    
    
				sum += tex2D(_MainTex, i.uv[it]).rgb * weight[it];
				sum += tex2D(_MainTex, i.uv[2*it]).rgb * weight[it];
			}

			return fixed4(sum, 1.0);
		}

		ENDCG

		ZTest Always 
		Cull Off 
		ZWrite Off

		pass
		{
    
    
			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag

            v2f_img vert (appdata_img v)
            {
    
    
                v2f_img o;
                o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = v.texcoord;
                return o;
            }

            fixed4 frag (v2f_img i) : SV_Target
            {
    
    
                return _OutlineColor;
            }

			ENDCG
		}

		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
		}

        Pass
        {
    
    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragOutline

			sampler2D _BlurTex, _OutlineColorTex;
			half4 _BlurTex_TexelSize, _OutlineColorTex_TexelSize;

            v2f_img vert (appdata_img v)
            {
    
    
                v2f_img o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                return o;
            }

			fixed4 fragOutline(v2f_img i) : SV_Target
			{
    
    
				fixed4 scene = tex2D(_MainTex, i.uv);
				fixed4 blur = tex2D(_BlurTex, i.uv);
				fixed4 outlieColor = tex2D(_OutlineColorTex, i.uv);
				fixed4 outline = blur - outlieColor;
		
				fixed4 final = scene * (1 - all(outline.rgb)) + _OutlineColor * any(outline.rgb);
				return final;
			}

			ENDCG
		}
    }
	FallBack off
}

4 完整工程

博主个人博客本文链接。
链接:https://pan.baidu.com/s/1AhKWJxMaQI89vAwv8RE-vQ
提取码:xaaz

5 参考文章

猜你喜欢

转载自blog.csdn.net/sinat_25415095/article/details/124053368