从半透明模型的描边到URP Multipass Rendering

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

卡通渲染技术中,描边的实现的有多种做法,有基于后处理实现,也有基于非后处理,通过多次绘制模型的方法实现。

通过多次绘制模型的实现,方法无非就是:

  1. 绘制模型本体,同时写入深度值
  2. 让模型顶点沿着法线外扩一定的偏移量,即让模型膨胀一些
  3. 全用描边的颜色绘制膨胀过的模型,且,在绘制时,使用cull front,即只绘制模型的背向摄像机面。在第1步中因为绘制模型本体时已经写入深度值。在本步骤中只要不改变深度测试函数的方式,那么除了膨胀部分之外,是无法写入像素的,能写入的部分,便形成了边缘。

上述的方式,一般应用于不透明的材质上。如果是半透明的材质的话就有问题,因为在第1步绘制半透明材质时,一般是不写入深度值的。那么第3步在绘制膨胀模型时,模型背部的地方就会被绘制上去。

解决这个问题有很多,可以利用stencil test的方式。也可以在第一步绘制,不绘制任何颜色,只写入深度,然后再绘制模型,最后再绘制描边。后一种方法,如果用Unity内置管线,大约就是这个样子:

 1Shader "Unlit/半透明"
 2{
 3    Properties
 4    {
 5        _Diffuse("Diffuse",Color) = (1,1,1,1)  //  漫反射
 6        _MainTex("MainTex",2D) = "white"{}  //  2D纹理贴图
 7        _AlphaScale("Alpha Scale",Range(0,1)) = 1  //  控制Alpha参数
 8    }
 9    
10    SubShader
11    {
12        Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }   //渲染顺序设置Transparent
13        
14        LOD 100
15        // 用两个pass通道来处理,防止出现渲染错误,第一个pass通道
16        // 每个pass通道都会渲染一次
17        Pass  
18        {
19            ZWrite On   //  注意这里:写入深度 为了确认渲染顺序
20            ColorMask 0  //  掩码遮罩代表这个pass通道不写入任何颜色值
21        }
22
23        Pass  //  第二个pass通道
24        {
25            Tags{"LightMode" = "ForwardBase"}
26
27            ZWrite Off  // 注意这里:关闭ZWrite(深度写入)
28            Blend SrcAlpha OneMinusSrcAlpha  //  源颜色因子  正常透明混合
29
30            CGPROGRAM
31            #pragma vertex vert
32            #pragma fragment frag
33            #include "UnityCG.cginc"
34            #include "Lighting.cginc"
35
36            struct v2f 
37            {
38                float4 vertex :SV_POSITION;  //  输出顶点信息
39                fixed3 worldNormal : TEXCOORD0;
40                float3 worldPos:TEXCOORD1;
41                float2 uv:TEXCOORD2;
42            };
43
44            fixed4 _Diffuse;
45            float _AlphaScale;
46            sampler2D _MainTex;
47            float4 _MainTex_ST; 
48            
49            v2f vert(appdata_base v)  //  顶点着色器
50            {
51                v2f o;
52                o.vertex = UnityObjectToClipPos(v.vertex);
53                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); 
54                o.worldNormal = worldNormal;
55                o.worldPos = mul(unity_ObjectToWorld, v.vertex); 
56                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
57                return o;
58            }
59
60            fixed4 frag(v2f i) : SV_Target
61            {
62                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
63                fixed4 texColor = tex2D(_MainTex, i.uv);  
64                fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
65
66                fixed3 diffuse = 
67                texColor.rgb * _LightColor0.rgb * _Diffuse.rgb * 
68                max(0, dot(worldLightDir,i.worldNormal) * 0.5 + 0.5); 
69                fixed3 color = ambient + diffuse;
70                return fixed4(color, texColor.a * _AlphaScale); 
71            }
72            ENDCG
73        }
74    }
75    FallBack "Transparent/VertexLit"    
76}

内置管线的方式很直观。但URP到目前为止(版本10.3.2),无法支持太多的PASS,

打开【com.unity.rende-pipelines.unversal@10.3.2/Runtime/Passes/DrawObjects.cs】文件可以看到:

 1 public class DrawObjectsPass : ScriptableRenderPass
 2 {
 3    FilteringSettings m_FilteringSettings;
 4    RenderStateBlock m_RenderStateBlock;
 5    List<ShaderTagId> m_ShaderTagIdList = new List<ShaderTagId>();
 6    string m_ProfilerTag;
 7    ProfilingSampler m_ProfilingSampler;
 8    bool m_IsOpaque;
 9
10    static readonly int s_DrawObjectPassDataPropID = Shader.PropertyToID("_DrawObjectPassData");
11
12    // 注意观察参数ShaderTagId[] shaderTagIds
13    public DrawObjectsPass(string profilerTag, ShaderTagId[] shaderTagIds,
14    bool opaque, RenderPassEvent evt, RenderQueueRange renderQueueRange,
15    LayerMask layerMask, StencilState stencilState, int stencilReference)
16    {
17        base.profilingSampler = new ProfilingSampler(nameof(DrawObjectsPass));
18        m_ProfilerTag = profilerTag;
19        m_ProfilingSampler = new ProfilingSampler(profilerTag);
20        
21        foreach (ShaderTagId sid in shaderTagIds)
22            m_ShaderTagIdList.Add(sid);
23            
24        renderPassEvent = evt;
25        m_FilteringSettings = new FilteringSettings(
26                                          renderQueueRange, layerMask);
27        m_RenderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
28        m_IsOpaque = opaque;
29
30        if (stencilState.enabled)
31        {
32            m_RenderStateBlock.stencilReference = stencilReference;
33            m_RenderStateBlock.mask = RenderStateMask.Stencil;
34            m_RenderStateBlock.stencilState = stencilState;
35        }
36    }
37
38    // 注意观察参数new ShaderTagId
39    public DrawObjectsPass(string profilerTag, bool opaque, 
40    RenderPassEvent evt, RenderQueueRange renderQueueRange, 
41    LayerMask layerMask, StencilState stencilState, int stencilReference)
42        : this(profilerTag,
43            new ShaderTagId[] { 
44            new ShaderTagId("SRPDefaultUnlit"), 
45            new ShaderTagId("UniversalForward"), 
46            new ShaderTagId("UniversalForwardOnly"), 
47            new ShaderTagId("LightweightForward")},
48            opaque, evt, renderQueueRange, layerMask, stencilState, stencilReference)
49    {}
50}

所以为了在URP中解决这个问题,一般采用双材质球的方式,去把这些多个pass拆分到两个或者多个材质球去完成。在第一个材质球中,第一个pass的LightMode为SRPDefaultUnlit。在此pass中只写入深度,第二个pass的LightMode为"UniversalForward"。绘制描边,然后在第二个材质球中,在第一个pass中负责绘制半透明模型即可。

参考网页