原神甘雨渲染复刻

日式卡通角色渲染

Posted by Pavel on January 2, 2025

1735750159114-Movie_001.gif

最近面试也是经常被问到关于卡渲的知识,所以将一年前做的面向知乎的甘雨角色逆向复习了一下。

贴图解析

Snipaste_2025-01-01_19-15-42.png LightMap.r

Snipaste_2025-01-01_19-16-23.png LightMap.g Snipaste_2025-01-01_19-17-24.png LightMap.bSnipaste_2025-01-01_19-14-40.png LightMap.a

  • LightMap.r: 存储这区分材质的信息主要是高光类型:BlinPhong或者裁边视角光
  • LightMap.g: 存储着AO信息
  • LightMap.b: 存储BlinPhong高光强度
  • LightMap.a:存储 Ramp类型的LayerMask Snipaste_2025-01-01_19-28-55.png 0-0.45 Snipaste_2025-01-01_19-29-18.png 0.45-0.55Snipaste_2025-01-01_19-29-35.png 0.6-1

甘雨的RampMask可分为三种丝绸,布料,皮肤。这里我有个疑问为什么没有将金属单独分开?

直接光漫反射

Snipaste_2025-01-01_19-56-15.png

                //ParamLine : X:RampAreaMask, Y:RampSmooth1, Z:RampSmooth2, W:RampSmooth3
                float rampValue1 = smoothstep(_ParamLine1.y, _ParamLine1.z, halfLambert);
                float rampValue2 = smoothstep(_ParamLine2.y, _ParamLine2.z, halfLambert);
                float rampValue3 = smoothstep(_ParamLine3.y, _ParamLine3.z, halfLambert);   

对halfLambert结果重新映射范围。

                #if _DAYNIGHT_SWITCH_DAY
                    rampUV1 = float2(rampValue1, 0.95);    
                    rampUV2 = float2(rampValue2, 0.85);   
                    rampUV3 = float2(rampValue3, 0.75);  
                #else
                    rampUV1 = float2(rampValue1, 0.45);   
                    rampUV2 = float2(rampValue2, 0.35);  
                    rampUV3 = float2(rampValue3, 0.25);  
                #endif

准备采样Ramp图的uv分为白天黑夜两种,半兰伯特作为采样ramp的u值,y值指定采样Ramp的哪一条。

Snipaste_2025-01-01_20-49-39.png

                float3 var_RampMap1 = SAMPLE_TEXTURE2D(_RampMap,sampler_RampMap,rampUV1);
                float3 var_RampMap2 = SAMPLE_TEXTURE2D(_RampMap,sampler_RampMap,rampUV2);
                float3 var_RampMap3 = SAMPLE_TEXTURE2D(_RampMap,sampler_RampMap,rampUV3);

采样ramp图的结果。 Snipaste_2025-01-01_20-13-37.png

            float AOMask = min(LightMap.g+0.8,1);
                //RampColor
                half3 rampColor1 = var_RampMap1 * _ShadColLine1.rgb;
                rampColor1 = lerp(rampColor1, _BaseColLine1.rgb, smoothstep(0.65, _ParamLine1.w, halfLambert * AOMask));
                rampColor1 *= AllRampAreaMask[floor(_ParamLine1.x)];
                half3 rampColor2 = var_RampMap2 * _ShadColLine2.rgb;
                rampColor2 = lerp(rampColor2, _BaseColLine2.rgb, smoothstep(0.65, _ParamLine2.w, halfLambert * AOMask));               
                rampColor2 *= AllRampAreaMask[floor(_ParamLine2.x)];          
                half3 rampColor3 = var_RampMap3 * _ShadColLine3.rgb;
                rampColor3 = lerp(rampColor3, _BaseColLine3.rgb, smoothstep(0.65, _ParamLine3.w, halfLambert * AOMask));
                rampColor3 *= AllRampAreaMask[floor(_ParamLine3.x)];
                float3 RampColor = rampColor1 + rampColor2 + rampColor3;

将RampColor跟BaseColor混合,混合参数是半兰伯特乘以AO再重映射值。 Snipaste_2025-01-01_20-55-54.png 乘以BaseColor

直接光高光

非金属部分裁边高光

Snipaste_2025-01-02_00-30-53.png LightMap.r(0.3~0.5)非金属裁边高光遮罩

Snipaste_2025-01-01_21-07-33.png 使用dot(N, V)高光

                float3 normalVS=normalize(mul((float3x3)UNITY_MATRIX_V,N));
                half stepLightLayer = LightMap.r * 255;
                half StepMask = step(85, stepLightLayer) - step(135, stepLightLayer);
                StepSpecular = step(1 - _StepSpecularGloss, saturate(dot(N, V))) * _StepSpecularIntensity * StepMask;
                StepSpecular = min(StepSpecular,1);

金属高光

Snipaste_2025-01-02_00-31-22.png LightMap.r(0.7~1)金属高光遮罩 Snipaste_2025-01-01_21-39-55.png

                float2 MetalMapUV = mul((float3x3) UNITY_MATRIX_V,N).xy * 0.5 + 0.5;

准备采样金属的matcapUV:屏幕空间法线xy值并映射到0~1范围内。 Snipaste_2025-01-01_21-36-31.pngAvatar_Tex_MetalMap.png 采样金属MatCap。

Snipaste_2025-01-01_21-02-36.png blinPhong高光Snipaste_2025-01-01_21-03-18.png GGX_D高光

                // GGX_D 金属高光
                half MetalMask = step(0.7, LightMap.r);
                float2 MetalMapUV = mul((float3x3) UNITY_MATRIX_V,N).xy * 0.5 + 0.5;
                float metallic = pow(lerp(0,SAMPLE_TEXTURE2D(_MetalCap, sampler_MetalCap,MetalMapUV).r, MetalMask), 2);
                float GGXSpecular = min(D_GGX(dot(N, H), _GGX_DIntensity), 1) * 10;
                MetalSpecular = GGXSpecular * metallic * diffuseColor;

Snipaste_2025-01-01_21-48-34.png

                FinalSpecular = StepSpecular + MetalSpecular.rgbb;
                FinalSpecular = lerp(0, FinalSpecular * _DirectMainLightColor, LightMap.r);
                FinalSpecular *= NL * LightMap.g;

最后将高光混合。

基于深度的等宽边缘光

Snipaste_2025-01-01_22-17-35.png

                float3 normalVS = normalize(mul((float3x3)UNITY_MATRIX_V,N));
                float3 normalWS = input.normalWS;
                float3 positionVS = input.positionVS;
                
                float3 samplePositionVS = float3(positionVS.xy + normalVS.xy * _RimOffset, positionVS.z); // 保持z不变(CS.w = -VS.z)
                float4 samplePositionCS = TransformWViewToHClip(samplePositionVS); // input.positionCS不是真正的CS 而是SV_Position屏幕坐标
                float4 samplePositionVP = TransformHClipToViewPortPos(samplePositionCS);
               
                float depth = input.positionNDC.z / input.positionNDC.w;
                float linearEyeDepth = LinearEyeDepth(depth, _ZBufferParams); // 离相机越近越小
                float offsetDepth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, samplePositionVP).r; // _CameraDepthTexture.r = input.positionNDC.z / input.positionNDC.w
                float linearEyeOffsetDepth = LinearEyeDepth(offsetDepth, _ZBufferParams);
                float depthDiff = linearEyeOffsetDepth - linearEyeDepth;
                
                float rimIntensity = step(_RimThreshold, depthDiff);

原理:将相机空间的顶点沿法线XY方向偏移,采样偏移后的深度度图深度,与原来的深度作比较,大于某个阈值就是边缘。 Snipaste_2025-01-01_23-10-48.png

边缘光混合菲涅尔和BaseColor。

                float3 viewDirectionWS = SafeNormalize(GetCameraPositionWS() - input.positionWS);
                float rimRatio = 1 - saturate(dot(viewDirectionWS, normalWS));             
                rimIntensity = lerp(0, rimIntensity, rimRatio);
                half4 RimColor = float4(_RimColor.rgb,1) * rimIntensity * BaseMap;

*为了在URP中获取深度图需要加入DepthOnly Pass并且在管线设置中开启深度图

URP多Pass实现外轮廓

Snipaste_2025-01-02_00-35-30.png 外轮廓的实现原理很简单经典的外轮廓Pass,顶点外扩,剔除正面,渲染模型背面。但是受FOV和距离影响,FOV较小的时候外轮廓会变得很粗,距离近的时候也会变很粗,我们需要FOV角度和距离对外轮廓的粗细进行矫正,保证在屏幕上看起来描边粗细不会变化。

        float GetCameraFOV()
        {
            float t = unity_CameraProjection._m11;
            float Rad2Deg = 180 / 3.1415;
            float fov = atan(1.0f / t) * 2.0 * Rad2Deg;
            return fov;
        }

获取FOV角度与屏幕长宽比矫正外轮廓宽度。

unity_CameraProjection._m11是投影矩阵中第一行第一列也就是以下公式内容:

\[m_{11} = \left( \frac{w}{h} \right) \cdot \frac{1}{\tan\left(\frac{\theta}{2}\right)}\]
        float GetOutlineCameraFovAndDistanceFixMultiplier(float positionVS_Z)
        {
            float cameraMulFix;
            if (unity_OrthoParams.w == 0)
            {
                cameraMulFix = abs(positionVS_Z);
                cameraMulFix = ApplyOutlineDistanceFadeOut(cameraMulFix);
                cameraMulFix *= GetCameraFOV();
            }
            else
            {
                float orthoSize = abs(unity_OrthoParams.y);
                orthoSize = ApplyOutlineDistanceFadeOut(orthoSize);
                cameraMulFix = orthoSize * 50;
            }
            return cameraMulFix * 0.0001;
        }

分开处理透视投影和正交投影下的外轮廓宽度修复。

        float3 TransformPositionWSToOutlinePositionWS(half vertexColorAlpha, float3 positionWS, float positionVS_Z, float3 normalWS)
        {
            float outlineExpandAmount = vertexColorAlpha * _OutlineWidth * GetOutlineCameraFovAndDistanceFixMultiplier(positionVS_Z);
            return positionWS + normalWS * outlineExpandAmount;
        }

应用顶点沿法线外扩并且应用距离和FOV矫正。

        Pass
        {
            Name "CHARACTER_OUTLINE"
            Tags {  }
            Cull Front
            HLSLPROGRAM
            #pragma shader_feature_local_fragment ENABLE_ALPHA_CLIPPING
            #pragma vertex OutlinePassVertex
            #pragma fragment OutlinePassFragment

            float4 OutlinePassFragment(Varyings input): COLOR
            {
                half4 ColorMask = SAMPLE_TEXTURE2D(_LightMap, sampler_LightMap,input.uv).a;
                half4 baseColor = SAMPLE_TEXTURE2D(_MainTex, sampler__MainTex, input.uv.xy);
                
                float rampAreaMask0  = 0;
                float rampAreaMask1 = step(0.00, ColorMask) - step(0.45, ColorMask);
                float rampAreaMask2 = step(0.25, ColorMask) - step(0.85, ColorMask);
                float rampAreaMask3 = step(0.60, ColorMask) - step(1.05, ColorMask);
                float3 skinLineColor = rampAreaMask3*_skinLineColor;
                float3 cloths1LineColor = rampAreaMask2 *_Coloth1LineColor;
                float3 cloths2LineColor = rampAreaMask1 *_Coloth2LineColor;
                half4 FinalColor = (_OutlineColor*(skinLineColor+cloths1LineColor+cloths2LineColor)).xyzz;

                return FinalColor;
            }

            ENDHLSL
        }

最后添加一个Pass渲染外轮廓。

参考

Unity URP Shader 与 HLSL 自学笔记六 等宽屏幕空间边缘光

TA技术美术-原神角色还原学习笔记