【转载】NPR卡通渲染 - Rim Light 边缘光 | 深度边缘光

1,430 阅读3分钟

原文链接: NPR卡通渲染 - Rim Light 边缘光 | 深度边缘光

原文写得不错,但是主题格式我不太喜欢,所以我对文章的格式以及原文讲解得不太对的地方进行了补充和修正,方便自己日后阅读

正文

两种 Rim Light 效果对比


Fresnel Rim

  • 使用 NdotV 计算
  • 世界 法向量 与世界 视角向量 点乘

==关键代码==

float NdotV = saturate(dot(wNor, vDir));
float fresnel = pow((1 - NdotV), max(0.01, _Pow));

Fresnel 边缘光在法线比较一致的大平面(例如 Cube 面)表现效果欠佳


Depth Offset Rim

  • 得到相机渲染的深度图
  • 采样深度图时, 将 uv 朝 view 空间法线 xy 方向进行偏移
  • 偏移后采样的深度当前模型深度 做差, 找出边缘

深度做差

这里是先用 屏幕空间 坐标 (scrPos) 采样深度图 _CameraDepthTexture, 然后用采样出的(观察空间)深度(变换到 线性深度)减去 裁剪空间 坐标(i.scrPos)的 w 分量。

之所以可以这么做,是因为:从下图公式 或以下 Unity 源码中可以看出, 裁剪空间 坐标 (i.scrPos)的 w 分量其实还是 观察空间z 分量。

当前模型深度,模型 裁剪空间 坐标的 w 分量, 仍在 观察空间, 即顶点离相机的距离

图片来自 Unity Shader 入门精要

inline float4 ComputeScreenPos(float4 pos) {
    float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    return o;
}

// 用到的 ComputeNonStereoScreenPos
inline float4 ComputeNonStereoScreenPos(float4 pos) {
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}

// 用到的 TransformStereoScreenSpaceTex
float2 TransformStereoScreenSpaceTex(float2 uv, float w)
{
    float4 scaleOffset = unity_StereoScaleOffset[unity_StereoEyeIndex];
    return uv.xy * scaleOffset.xy + scaleOffset.zw * w;
}

==关键代码==

// world normal 
float3 wNor = normalize(i.wNor);
// view normal 
float3 vNor = mul(UNITY_MATRIX_V, float4(wNor, 0.0)).xyz;
// screen pos 
float2 scrPos = i.scrPos.xy / i.scrPos.w; ///< @ShaderJoy 的注释:透视除法得到是 NDC 空间下的 xy,所以下一步 “沿着观察空间下的法线偏移” 的操作正确性存疑 
scrPos += vNor.xy * _Width * 0.001; // uv offset 

float depthTex = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, scrPos);
float depth = LinearEyeDepth(depthTex); ///< @ShaderJoy 的注释:观察空间的线性深度
float rim = saturate((depth - i.scrPos.w) / max(0.001, _Spread * 0.01));/// @ShaderJoy 的注释:起到缩放作用, 防止除零
rim = smoothstep(min(_MinRange, 0.99), _MaxRange, rim);

适合等宽硬边缘, 相比于 Fresnel Rim, 在法线比较一致的部分表现更好


完整代码

Fresnel Rim

Shader "Rim/FresRim"
{
    Properties
    {
        [NoScaleOffset]_MatcapTex ("Matcap Tex", 2D) = "White"{}
        _MinRange ("MinRange", Range(0, 1)) = 0 
        _MaxRange ("MaxRange", Range(0, 1)) = 1
        _Pow ("Power", Float) = 5
        [HDR]_RimCol ("Rim Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags 
        { 
            "RenderType" = "Opaque" 
        }
        LOD 100

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

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
                float3 wPos : TEXCOORD1;
                float3 wNor : TEXCOORD2;
            };

            sampler2D _CameraDepthTexture;
            float _Width;
            float _MinRange, _MaxRange, _Pow;
            sampler2D _MatcapTex;
            float4 _RimCol;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;//world pos 
                o.wNor = UnityObjectToWorldNormal(v.normal);//world norml
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // world normal 
                float3 wNor = normalize(i.wNor);
                // view dir 
                float3 vDir = normalize(_WorldSpaceCameraPos.xyz - i.wPos);
                // fresnel (rim)
                float NdotV = saturate(dot(wNor, vDir));
                float fresnel = pow((1 - NdotV), max(0.01, _Pow));
                fresnel = smoothstep(min(0.99, _MinRange), _MaxRange, fresnel);
                
                // view normal 
                float3 vNor = mul(UNITY_MATRIX_V, float4(wNor, 0.0)).xyz;
                
                // matcap tex 
                float2 matcapUV = (vNor.xy + 1) / 2;
                float4 matcapTex = tex2D(_MatcapTex, matcapUV);
                fixed4 col = 1;
                col.xyz = matcapTex.xyz + fresnel * _RimCol.xyz;
                return col;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

Depth Offset Rim

Shader "Rim/DepthRim"
{
    Properties
    {
        [NoScaleOffset]_MatcapTex ("Matcap Tex", 2D) = "White"{}
        _Width ("Width", Float) = 0
        _MinRange ("MinRange", Range(0, 1)) = 0 
        _MaxRange ("MaxRange", Range(0, 1)) = 1
        _Spread ("Spread", Float) = 1
        [HDR]_RimCol ("Rim Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags 
        { 
            "RenderType" = "Opaque" 
        }
        LOD 100

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

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
                float4 scrPos : TEXCOORD1;
                float3 wNor : TEXCOORD2;
            };

            sampler2D _CameraDepthTexture;
            float _Width;
            float _MinRange, _MaxRange, _Spread;
            sampler2D _MatcapTex;
            float4 _RimCol;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                o.scrPos = ComputeScreenPos(o.pos);         // screen pos 
                o.wNor = UnityObjectToWorldNormal(v.normal);// world norml
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // world normal 
                float3 wNor = normalize(i.wNor);
                //view normal 
                float3 vNor = mul(UNITY_MATRIX_V, float4(wNor, 0.0)).xyz;
                
                /// @note 关键代码 
                // screen pos 
                float2 scrPos = i.scrPos.xy / i.scrPos.w;
                scrPos += vNor.xy * _Width * 0.001;    // uv offset 
                float depthTex = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, scrPos);
                float depth = LinearEyeDepth(depthTex);// view space depth 
                float rim = saturate((depth - i.scrPos.w) / max(0.001, _Spread * 0.01));//稍作缩放, 可以不要
                rim = smoothstep(min(_MinRange, 0.99), _MaxRange, rim);
                
                // matcap tex 
                float2 matcapUV = (vNor.xy + 1) / 2;
                float4 matcapTex = tex2D(_MatcapTex, matcapUV);
                fixed4 col = 1;
                col.xyz = matcapTex.xyz + rim * _RimCol.xyz;
                return col;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

GitHub

Unity-Shader/Toon Shading/Rim Light at main · VAherggoooooo/Unity-Shader (github.com)​