从半透明模型的描边到URP Multipass Rendering
请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
卡通渲染技术中,描边的实现的有多种做法,有基于后处理实现,也有基于非后处理,通过多次绘制模型的方法实现。
通过多次绘制模型的实现,方法无非就是:
- 绘制模型本体,同时写入深度值。
- 让模型顶点沿着法线外扩一定的偏移量,即让模型膨胀一些
- 全用描边的颜色绘制膨胀过的模型,且,在绘制时,使用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中负责绘制半透明模型即可。