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

Table of Contents

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

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

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

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

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

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

Shader "Unlit/半透明"
{
    Properties
    {
        _Diffuse("Diffuse",Color) = (1,1,1,1)  //  漫反射
        _MainTex("MainTex",2D) = "white"{}  //  2D纹理贴图
        _AlphaScale("Alpha Scale",Range(0,1)) = 1  //  控制Alpha参数
    }
    
    SubShader
    {
        Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" }   //渲染顺序设置Transparent
        
        LOD 100
        // 用两个pass通道来处理,防止出现渲染错误,第一个pass通道
        // 每个pass通道都会渲染一次
        Pass  
        {
            ZWrite On   //  注意这里:写入深度 为了确认渲染顺序
            ColorMask 0  //  掩码遮罩代表这个pass通道不写入任何颜色值
        }

        Pass  //  第二个pass通道
        {
            Tags{"LightMode" = "ForwardBase"}

            ZWrite Off  // 注意这里:关闭ZWrite(深度写入)
            Blend SrcAlpha OneMinusSrcAlpha  //  源颜色因子  正常透明混合

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct v2f 
            {
                float4 vertex :SV_POSITION;  //  输出顶点信息
                fixed3 worldNormal : TEXCOORD0;
                float3 worldPos:TEXCOORD1;
                float2 uv:TEXCOORD2;
            };

            fixed4 _Diffuse;
            float _AlphaScale;
            sampler2D _MainTex;
            float4 _MainTex_ST; 
            
            v2f vert(appdata_base v)  //  顶点着色器
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); 
                o.worldNormal = worldNormal;
                o.worldPos = mul(unity_ObjectToWorld, v.vertex); 
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
                fixed4 texColor = tex2D(_MainTex, i.uv);  
                fixed3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);

                fixed3 diffuse = 
                texColor.rgb * _LightColor0.rgb * _Diffuse.rgb * 
                max(0, dot(worldLightDir,i.worldNormal) * 0.5 + 0.5); 
                fixed3 color = ambient + diffuse;
                return fixed4(color, texColor.a * _AlphaScale); 
            }
            ENDCG
        }
    }
    FallBack "Transparent/VertexLit"    
}

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

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

 public class DrawObjectsPass : ScriptableRenderPass
 {
    FilteringSettings m_FilteringSettings;
    RenderStateBlock m_RenderStateBlock;
    List<ShaderTagId> m_ShaderTagIdList = new List<ShaderTagId>();
    string m_ProfilerTag;
    ProfilingSampler m_ProfilingSampler;
    bool m_IsOpaque;

    static readonly int s_DrawObjectPassDataPropID = Shader.PropertyToID("_DrawObjectPassData");

    // 注意观察参数ShaderTagId[] shaderTagIds
    public DrawObjectsPass(string profilerTag, ShaderTagId[] shaderTagIds,
    bool opaque, RenderPassEvent evt, RenderQueueRange renderQueueRange,
    LayerMask layerMask, StencilState stencilState, int stencilReference)
    {
        base.profilingSampler = new ProfilingSampler(nameof(DrawObjectsPass));
        m_ProfilerTag = profilerTag;
        m_ProfilingSampler = new ProfilingSampler(profilerTag);
        
        foreach (ShaderTagId sid in shaderTagIds)
            m_ShaderTagIdList.Add(sid);
            
        renderPassEvent = evt;
        m_FilteringSettings = new FilteringSettings(
                                          renderQueueRange, layerMask);
        m_RenderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
        m_IsOpaque = opaque;

        if (stencilState.enabled)
        {
            m_RenderStateBlock.stencilReference = stencilReference;
            m_RenderStateBlock.mask = RenderStateMask.Stencil;
            m_RenderStateBlock.stencilState = stencilState;
        }
    }

    // 注意观察参数new ShaderTagId
    public DrawObjectsPass(string profilerTag, bool opaque, 
    RenderPassEvent evt, RenderQueueRange renderQueueRange, 
    LayerMask layerMask, StencilState stencilState, int stencilReference)
        : this(profilerTag,
            new ShaderTagId[] { 
            new ShaderTagId("SRPDefaultUnlit"), 
            new ShaderTagId("UniversalForward"), 
            new ShaderTagId("UniversalForwardOnly"), 
            new ShaderTagId("LightweightForward")},
            opaque, evt, renderQueueRange, layerMask, stencilState, stencilReference)
    {}
}

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

参考网页

kumakoko avatar
kumakoko
pure coder
comments powered by Disqus