URPでSubSurfaceScattering実装してみる

はじめに

URPでキャラクターシェーダーを実装するにあたり、PBRで実装をしてみましたが、なんか全体的に暗いなと感じてしまいました。
どうしたらいいかなと調べていたところ、SubSurfaceScatteringというものをみつけました。今回は近似的な実装をしてみました。

環境

環境は以下の通りです。

Unity2022.3.7f1
URP14.0.8

Shader

Shader "Custom/SSS" {
	Properties
    {
        _BaseMap("Base Map", 2D) = "white" {}
        _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        [Normal] _NormalMap("Normal Map", 2D) = "bump" {}
        _Metallic("Metallic", Range(0.0, 1.0)) = 0.0
        _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.0
        [HDR] _SSSColor ("SSS Color", Color) = (0, 0, 0)
        
        [HideInInspector] _Surface("_surface", Float) = 0.0
        [HideInInspector] _Blend("_blend", Float) = 0.0
        [HideInInspector] _AlphaClip("_clip", Float) = 0.0
        [HideInInspector] _SrcBlend("_src", Float) = 1.0
        [HideInInspector] _DstBlend("_dst", Float) = 0.0
        [HideInInspector] _ZWrite("_zw", Float) = 1.0
        [HideInInspector] _Cull("_cull", Float) = 2.0
    }
    
    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "RenderPipeline" = "UniversalPipeline"
        }
        LOD 100

        Pass
        {
            Tags{"LightMode" = "UniversalForward"}

            Blend[_SrcBlend][_DstBlend]
            ZWrite[_ZWrite]
            Cull[_Cull]
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
           #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
            #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
            #pragma multi_compile_fragment _ _SHADOWS_SOFT
            #pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
            #pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
            #pragma multi_compile _ SHADOWS_SHADOWMASK

            // Unity keywords
            #pragma multi_compile_DIRLIGHTMAP_COMBINED
            #pragma multi_compile_LIGHTMAP_ON
            #pragma multi_compile_fog

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
           
            struct Attribute
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
                float2 lightmapUV : TEXCOORD1;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                float3 normalWS : TEXCOORD2;
                float3 tangentWS : TEXCOORD3;
                float3 bitangentWS : TEXCOORD4;
                float3 viewDirWS : TEXCOORD5;
                half fogFactor : TEXCOORD6;
                half3 vertexLight   : TEXCOORD7;
                #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
                float4 shadowCoord : TEXCOORD8;
                #endif
                DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 9);
            };

            TEXTURE2D(_BaseMap);  SAMPLER(sampler_BaseMap);
            TEXTURE2D(_NormalMap);  SAMPLER(sampler_NormalMap);

            CBUFFER_START(UnityPerMaterial)
            float4 _BaseMap_ST;
            half4 _BaseColor;
            float4 _NormalMap_ST;
            float _NormalScale;
            float _Metallic;
            float _Smoothness;
            CBUFFER_END
            float3 _SSSColor;
            
            half3 PBRLighting(BRDFData brdfData, Light light, half3 normalWS, half3 viewDirectionWS)
            {
                 half NdotL = saturate(dot(normalWS, light.direction) * 0.5 + 0.5);

                 half src = NdotL * light.shadowAttenuation;
                 half3 factor = 1.0 / _SSSColor;
                 
                 half3 radiance = pow(src, factor);
                 radiance *= light.color;
                
                return DirectBDRF(brdfData, normalWS, light.direction, viewDirectionWS) * grad;
            }

            half4 SubsurfacePBR(InputData inputData, half3 albedo, half metallic, half3 specular,
                half smoothness, half occlusion, half3 emission, half alpha)
            {
                BRDFData brdfData;
                InitializeBRDFData(albedo, metallic, specular, smoothness, alpha, brdfData);
            
                Light mainLight = GetMainLight(inputData.shadowCoord);
            
                MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI, inputData.shadowMask);
                half3 color = GlobalIllumination(brdfData, inputData.bakedGI, occlusion, inputData.normalWS, inputData.viewDirectionWS);
                color += PBRLighting(brdfData, mainLight, inputData.normalWS, inputData.viewDirectionWS);
                 
                color += inputData.vertexLighting * brdfData.diffuse;
                color += emission;
                return half4(color, alpha);
            }

            Varyings vert(Attribute input)
            {
                Varyings output = (Varyings)0;

                 output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                output.positionWS = TransformObjectToWorld(input.positionOS);
                output.positionHCS = TransformWorldToHClip(output.positionWS);
                output.viewDirWS = GetWorldSpaceViewDir(output.positionWS);
                output.normalWS = TransformObjectToWorldNormal(input.normal);
                output.tangentWS = TransformObjectToWorldDir(input.tangent.xyz);
                output.bitangentWS = cross(output.normalWS, output.tangentWS) * input.tangent.w;
                OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV);
                OUTPUT_SH(output.normalWS.xyz, output.vertexSH);
                output.fogFactor = ComputeFogFactor(output.positionHCS.z);
                output.vertexLight = VertexLighting(output.positionWS, output.normalWS);
                // Shadow
                #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
                    output.shadowCoord = TransformWorldToShadowCoord(output.positionWS);
                #endif

                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                // SurfaceDataを作成
                SurfaceData surfaceData;
                surfaceData.normalTS = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv));
                half4 col = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;
                surfaceData.albedo = col.rgb;
                surfaceData.alpha = col.a;
                surfaceData.emission = 0.0;                
                surfaceData.metallic = _Metallic;
                surfaceData.occlusion = 1.0;
                surfaceData.smoothness = _Smoothness;
                surfaceData.specular = 0.0;

                // InputDataを作成
                InputData inputData = (InputData)0;
                inputData.positionWS = input.positionWS;
                inputData.normalWS = TransformTangentToWorld(surfaceData.normalTS, half3x3(input.tangentWS.xyz, input.bitangentWS.xyz, input.normalWS.xyz));
                inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
                inputData.viewDirectionWS = SafeNormalize(input.viewDirWS);
                inputData.fogCoord = input.fogFactor;
                inputData.vertexLighting = input.vertexLight;
                inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS);
                inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionHCS);
                inputData.shadowMask = SAMPLE_SHADOWMASK(input.lightmapUV);
                #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
                    inputData.shadowCoord = input.shadowCoord;
                #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
                    inputData.shadowCoord = TransformWorldToShadowCoord(input.positionWS);
                #else
                    inputData.shadowCoord = float4(0, 0, 0, 0);
                #endif
                
                half4 color = SubsurfacePBR(inputData, surfaceData.albedo, surfaceData.metallic,
                    surfaceData.specular, surfaceData.smoothness,
                    surfaceData.occlusion, surfaceData.emission, surfaceData.alpha);
                
                color.rgb = MixFog(color.rgb, inputData.fogCoord);
                
                return color;
            }
            ENDHLSL
        }
        Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}

            ZWrite On
            ZTest LEqual
            Cull[_Cull]

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature _ALPHATEST_ON

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing
            #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
            ENDHLSL
        }
        Pass
        {
            Name "DepthOnly"
            Tags{"LightMode" = "DepthOnly"}

           ZWrite On
            ColorMask 0
            Cull[_Cull]

            HLSLPROGRAM
            #pragma vertex DepthOnlyVertex
            #pragma fragment DepthOnlyFragment

             #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
            ENDHLSL
        }
    }
    FallBack "Hidden/InternalErrorShader"
}

SubSurfaceScatteringの実装

今回はこちらの記事を参考に実装をしてみました

unityshader.hatenablog.com

この中でも今回はハーフランバートを用いた方法を採用しました。

half NdotL = saturate(dot(normalWS, light.direction) * 0.5 + 0.5);

ハーフランバートは以上のようなコードで表現できます。
ハーフランバートランバート反射をなだらかな変化を出せるようにしたライティング方式です。

half src = NdotL * light.shadowAttenuation;
half3 factor = 1.0 / _SSSColor;
                 
half3 radiance = pow(src, factor);
radiance *= light.color;

次に求めたランバート反射に対して肌の赤みをのせていくのですが、pow関数を用いました。

light11.hatenadiary.com

これを用いて非線形に色味を渡すことが可能です

結果


肌の感じはそれっぽくなりましたが、影の付き方とかいろいろおかしくなってる…

まとめ

はじめてPBRやSubSurfaceScatteringを実装してみました。色味は雰囲気出ていましたが影がうまくいってなかったりなどまだまだ課題はありそうです。目標としては画像のような、リムライトとSSSの含まれた表現ができることを目指したいと考えています。
https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Skin_Subsurface_Scattering.jpg/220px-Skin_Subsurface_Scattering.jpg