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の実装
今回はこちらの記事を参考に実装をしてみました
この中でも今回はハーフランバートを用いた方法を採用しました。
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関数を用いました。
これを用いて非線形に色味を渡すことが可能です
結果
肌の感じはそれっぽくなりましたが、影の付き方とかいろいろおかしくなってる…