【Unity Shader】BoatAttackの崖シェーダー【2】 #122
はじめに
前回に引き続き、Unity公式が出しているGithubのBoatAttackの中の崖シェーダーについて触れていきます。
環境
Unity 2021.3.6f1 Universal RP 12.1.7
Normal
前回まででAlbedoの出力を理解したので、次はNormalになります。
Lerp
ノードの計算結果が出力されています。
Normalize
ノードは消し忘れかと思います。
草のノーマルマップのサンプリング
ここではAlbedoで行った草テクスチャとUVを合わせるために同様に4倍にしたものをサンプリングしています。
コードにすると以下になります。
SampleTexture2D
ノードのType
がNormal
になっているので、UnpackNormal
を忘れないようにしてください。
// 草の密度 float2 glassUv = IN.uv * 4; // Albedoで使用したもの half4 grassColor = SAMPLE_TEXTURE2D(_GrassBaseMap, sampler_GrassBaseMap, glassUv); // ここまで half3 grassNormal = UnpackNormal(SAMPLE_TEXTURE2D(_GrassNormal, sampler_GrassNormal, glassUv));
岩と崖のノーマルマップのブレンド
Normalに必要なBの値を計算します。
崖のディテールテクスチャ
崖部分のディティールテクスチャを、スケールに応じてサンプリングしている箇所になります。
コメントを翻訳します。
// オブジェクトのスケールを使用します。 // メッシュのUVを乗算して、ワールドスケールと一致したスケールのUVに近似させることができます。 // つまり、20にスケールアップした崖がある場合、1にスケールアップした同じ崖と同じサイズのディテールテクスチャを持つことになります。
オブジェクトのスケール分を元にUVとしてサンプリングすることによって、
オブジェクトの拡縮時にディティールテクスチャも合わせて拡縮できるようになります。
これをコードにすると以下になります。
// オブジェクトのスケールを使用し、サンプリングするディティールテクスチャのUVもスケールさせる float3 DetailRockNormal(float2 uv) { // Length(Model座標の同じ行の1~3列目)で各スケールが取得できる float scaleX = length(float3(unity_ObjectToWorld[0].x, unity_ObjectToWorld[1].x, unity_ObjectToWorld[2].x)); float scaleY = length(float3(unity_ObjectToWorld[0].y, unity_ObjectToWorld[1].y, unity_ObjectToWorld[2].y)); float scaleZ = length(float3(unity_ObjectToWorld[0].z, unity_ObjectToWorld[1].z, unity_ObjectToWorld[2].z)); float scaleLength = length(float3(scaleX, scaleY, scaleZ)) * _DetailScale * 30; float2 tilingOffset = uv * scaleLength; return UnpackNormal(SAMPLE_TEXTURE2D(_RockDetail, sampler_RockDetail, tilingOffset)); }
ノーマルマップのブレンド
計算した2つのノーマルマップをブレンドします。
前半部分は前回解説したので割愛します。
草のマスクの補完される値 の目次で解説しています。
後半部分では、ノーマルマップをブレンドしています。
ここでは、ホワイトアウトブレンディングという手法を使います。
以下のコードでノーマルマップをブレンドします。
float3 r = normalize(float3(n1.xy + n2.xy, n1.z*n2.z));
ですので、この部分は以下のようなコードでになります。
half4 cliffNormalAO = SAMPLE_TEXTURE2D(_Normal, sampler_Normal, IN.uv); half3 cliffColor = half3(cliffNormalAO.a, cliffNormalAO.g, cliffNormalAO.b) * 2 - 1; // 岩のディティールテクスチャの計算 float3 detailRockNormal = DetailRockNormal(IN.uv); // ホワイトアウトブレンディングをする float3 blendRockNormalCliffNormal = normalize(float3(cliffColor.rg + detailRockNormal.rg, cliffColor.b * detailRockNormal.b));
Normalの出力
計算した値を元に、Normalを出力します。
草のノーマルマップのサンプリングがA
岩と崖のノーマルマップのブレンドがB
前回の記事で解説した、GrassMaskがTとなります。
ですので、コードは以下になります。
float3 normal = lerp(grassNormal, blendRockNormalCliffNormal, grassMask);
Smoothness
次は滑らかさを表現するSmoothnessになります。
岩の滑らかさ
まずはMultiplyノードからです。
Aのノードは 草のマスクの補完される値 で解説したので割愛させて頂きます。
Bのノードは以下キャプチャと繋がっています。
こちらも翻訳すると以下になります。
// 岩の滑らかさについては、アルベドの R チャンネルと Substance で生成された「空洞」マップを混合して作成されます。 // これはアルベドの A チャンネルに格納されます。
崖テクスチャのRAチャンネルにSmoothnessの情報が入っているようです。
Unity上で確認すると、以下のようになっています。
これをコードにすると以下になります。
// 岩のSmoothnessをRチャンネルとAチャンネルに格納されている情報を元に作成する float RockSmoothness(float cliffA, float cliffR) { float cliff = 1 - abs(cliffA - 0.5); return cliff * cliffR * _RockSmoothness; } half4 cliffColor = SAMPLE_TEXTURE2D(_CliffTex, sampler_CliffTex, IN.uv); float rockSmoothness = RockSmoothness(cliffColor.a, cliffColor.r) * grassMask;
水のマスクとブレンドする
最後にLerpの部分になります。
Bのノードは先程出した岩の滑らかさになります。
Tのノードは前回の記事で行った、水のマスクの部分になります。
水マスクとブレンドすることにより、濡れている箇所の滑らかさを抑えています。
これをコードにすると以下になります。
float wetnessMask = WetnessMask(cliffColor.a, IN.positionWS.y); float rockSmoothness = RockSmoothness(cliffColor.a, cliffColor.r) * grassMask; float smoothness = lerp(0.85, rockSmoothness, wetnessMask);
Ambient Occlusion
AOの部分になります。
こちらも翻訳します。
// これは、パックされたノーマル/Aoテクスチャの「R」チャンネルから取得されるだけです。
なので、ノーマルテクスチャのRを見てみます。
こちらをコードにすると以下になります。
half4 cliffNormalAO = SAMPLE_TEXTURE2D(_CliffNormalAO, sampler_CliffNormalAO, IN.uv); float rockOcclusion = cliffNormalAO.r;
PBR
最後に各ノードをFragmentにつなげる箇所になります。
Lit.shaderはPBRベースですので、Lighting.hlsl
にあるUniversalFragmentPBR
を使います。
以下サイト様を参考にコードを記載します。
// PBR half4 CreateSurfaceData(float4 col, float3 normalTS, half occlusion, half smoothness, Varyings IN) { SurfaceData surfaceData; surfaceData.albedo = col.rgb; surfaceData.alpha = col.a; surfaceData.normalTS = normalTS; surfaceData.emission = 0; surfaceData.metallic = 0.1; surfaceData.occlusion = occlusion; surfaceData.smoothness = smoothness; surfaceData.specular = 0; surfaceData.clearCoatMask = 0; surfaceData.clearCoatSmoothness = 0; InputData inputData = (InputData)0; inputData.positionWS = IN.positionWS; inputData.normalWS = NormalizeNormalPerPixel(TransformTangentToWorld(surfaceData.normalTS, float3x3(IN.tangentWS.xyz, IN.binormalWS.xyz, IN.normalWS.xyz))); inputData.viewDirectionWS = SafeNormalize(IN.viewDir); inputData.fogCoord = IN.fogFactor; inputData.vertexLighting = IN.vertexLight; inputData.bakedGI = SAMPLE_GI(IN.lightmapUV, IN.vertexSH, inputData.normalWS); inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(IN.positionHCS); inputData.shadowMask = SAMPLE_SHADOWMASK(IN.lightmapUV); #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) inputData.shadowCoord = IN.shadowCoord; #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS) inputData.shadowCoord = TransformWorldToShadowCoord(IN.positionWS); #else inputData.shadowCoord = float4(0, 0, 0, 0); #endif half4 color = UniversalFragmentPBR(inputData, surfaceData); color.rgb = MixFog(color.rgb, inputData.fogCoord); return color; } half4 frag (Varyings IN) : SV_Target { ・・・ half4 blendGrassBase = lerp(grassColor, cliffColor, grassMask) * wetnessMask; return CreateSurfaceData(blendGrassBase, normal, rockOcclusion, smoothness, IN); }
結果
左がBoatAttackのもので、右が今回制作したものになります。
見た目は若干異なりますが、やりたい表現はできています。
ソースコード
Shader "BoatAttack/Cliff" { Properties { _CliffTex ("Cliff Texture", 2D) = "white" {} _CliffNormalAO ("CliffNormal AO", 2D) = "white" {} _GrassBaseMap ("Grass Base Map", 2D) = "white" {} _GrassNormal ("Grass Normal", 2D) = "bump" {} [Normal]_RockDetail ("Rock Detail", 2D) = "bump" {} _RockSmoothness ("Rock Smoothness", Range(0, 1)) = 0.5 _GrassHeightBlend ("Grass Height Blend", Range(1, 100)) = 1 _GrassAngle ("Grass Angle", Range(0, 90)) = 60 _DetailScale ("DetailScale", float) = 1 } SubShader { Tags { "RenderType"="Opaque" "Renderpipeline"="UniversalPipeline" "UniversalMaterialType" = "Lit" "IgnoreProjector" = "True" "Queue" = "Geometry" } Pass { Tags { "LightMode" = "UniversalForward" } 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 #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 Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 uv : TEXCOORD0; float2 lightmapUV : TEXCOORD1; }; struct Varyings { float2 uv : TEXCOORD0; float4 positionHCS : SV_POSITION; float3 positionWS : TEXCOORD1; float3 normalWS : TEXCOORD2; float3 tangentWS : TEXCOORD3; float3 binormalWS : TEXCOORD4; float3 viewDir : 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(_CliffTex); SAMPLER(sampler_CliffTex); TEXTURE2D(_CliffNormalAO); SAMPLER(sampler_CliffNormalAO); TEXTURE2D(_GrassBaseMap); SAMPLER(sampler_GrassBaseMap); TEXTURE2D(_GrassNormal); SAMPLER(sampler_GrassNormal); TEXTURE2D(_RockDetail); SAMPLER(sampler_RockDetail); CBUFFER_START(UnityPerMaterial) float4 _CliffTex_ST; float4 _CliffNormalAO_ST; float _CliffNormalScale; half _RockSmoothness; half _GrassHeightBlend; half _GrassAngle; half _DetailScale; CBUFFER_END // 草と岩が互いに混ざり合っている場所のマスクを作成する // ワールド空間の「Y」位置と法線に基づき生成する // これにより、草の表面と岩の表面の間の補完で使用できる白黒のマスクが生成される float GrassMask(float worldPositionY, float worldSpaceNormalY) { // ワールド空間のYを元にどのくらいブレンドするか決める half blendHeight = 10; float blendRate = smoothstep(_GrassHeightBlend - blendHeight, _GrassHeightBlend + blendHeight, worldPositionY); float mask = blendRate * worldSpaceNormalY; // _GrassAngleが大きいほうが草を生やしたいので1から減算 // _GrassAngleが0~100で、maskに入ってくる値が0~1なので、合わせるために0.01を乗算 float oneMinusAngle = 1 - (_GrassAngle * 0.01f); // なめらかにするために、軽微な値で保管する return 1 - saturate(smoothstep(oneMinusAngle - 0.05f, oneMinusAngle + 0.05f, mask)); } // 接空間をワールド空間へと変換する float3 ConvertWorldSpaceNormal(half3 cliffNormal, float3 normal, float3 tangent, float3 binormal) { float3x3 transposeTangent = transpose(float3x3(tangent, binormal, normal)); float3 worldNormal = mul(transposeTangent, cliffNormal).xyz; return worldNormal; } // オブジェクトのスケールを使用し、サンプリングするディティールテクスチャのUVもスケールさせる float3 DetailRockNormal(float2 uv) { // Length(Model座標の同じ行の1~3列目)で各スケールが取得できる float scaleX = length(float3(unity_ObjectToWorld[0].x, unity_ObjectToWorld[1].x, unity_ObjectToWorld[2].x)); float scaleY = length(float3(unity_ObjectToWorld[0].y, unity_ObjectToWorld[1].y, unity_ObjectToWorld[2].y)); float scaleZ = length(float3(unity_ObjectToWorld[0].z, unity_ObjectToWorld[1].z, unity_ObjectToWorld[2].z)); float scaleLength = length(float3(scaleX, scaleY, scaleZ)) * _DetailScale * 30; float2 tilingOffset = uv * scaleLength; return UnpackNormal(SAMPLE_TEXTURE2D(_RockDetail, sampler_RockDetail, tilingOffset)); } // リマップ float Remap(half In, half2 InMinMax, half2 OutMinMax) { return OutMinMax.x + (In - InMinMax.x) * (OutMinMax.y - OutMinMax.x) / (InMinMax.y - InMinMax.x); } // ワールド空間のY座標に基づき、水が濡れている場所(0)と乾いている場所(1)を定義するマスクを作成する // また、完全に水没している場合は、水面が濡れているようには見えないので、乾燥状態(1)にします。 // また、AOマップを使用して、ひび割れや隙間の濡れた状態をより鮮明に表現しています。 float WetnessMask(float cliffAO, float worldPositionY) { // 適応させるAOを計算 float cliffAOBase = ((cliffAO - 0.5) * 4 + worldPositionY) * 0.33; // 濡れている場所が0,乾いている場所が1なので反転させる float remapY = Remap(worldPositionY, half2(-1, -0.25), half2(1, 0)); // しきい値以下は濡れているような表現をする必要がないので1にする float mask = max(cliffAOBase, remapY); return clamp(mask, 0.1, 1); } // 岩のSmoothnessをRチャンネルとAチャンネルに格納されている情報を元に作成する float RockSmoothness(float cliffA, float cliffR) { float cliff = 1 - abs(cliffA - 0.5); return cliff * cliffR * _RockSmoothness; } // PBR half4 CreateSurfaceData(float4 col, float3 normalTS, half occlusion, half smoothness, Varyings IN) { SurfaceData surfaceData; surfaceData.albedo = col.rgb; surfaceData.alpha = col.a; surfaceData.normalTS = normalTS; surfaceData.emission = 0; surfaceData.metallic = 0.1; surfaceData.occlusion = occlusion; surfaceData.smoothness = smoothness; surfaceData.specular = 0; surfaceData.clearCoatMask = 0; surfaceData.clearCoatSmoothness = 0; InputData inputData = (InputData)0; inputData.positionWS = IN.positionWS; inputData.normalWS = NormalizeNormalPerPixel(TransformTangentToWorld(surfaceData.normalTS, float3x3(IN.tangentWS.xyz, IN.binormalWS.xyz, IN.normalWS.xyz))); inputData.viewDirectionWS = SafeNormalize(IN.viewDir); inputData.fogCoord = IN.fogFactor; inputData.vertexLighting = IN.vertexLight; inputData.bakedGI = SAMPLE_GI(IN.lightmapUV, IN.vertexSH, inputData.normalWS); inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(IN.positionHCS); inputData.shadowMask = SAMPLE_SHADOWMASK(IN.lightmapUV); #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) inputData.shadowCoord = IN.shadowCoord; #elif defined(MAIN_LIGHT_CALCULATE_SHADOWS) inputData.shadowCoord = TransformWorldToShadowCoord(IN.positionWS); #else inputData.shadowCoord = float4(0, 0, 0, 0); #endif half4 color = UniversalFragmentPBR(inputData, surfaceData); color.rgb = MixFog(color.rgb, inputData.fogCoord); return color; } Varyings vert (Attributes IN) { Varyings OUT; OUT.uv = TRANSFORM_TEX(IN.uv, _CliffTex); OUT.positionWS = TransformObjectToWorld(IN.positionOS.xyz); OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS); OUT.positionHCS = TransformWorldToHClip(OUT.positionWS); OUT.tangentWS = TransformObjectToWorldDir(IN.tangentOS.xyz); OUT.binormalWS = IN.tangentOS.w * cross(OUT.normalWS, OUT.tangentWS); OUT.viewDir = GetWorldSpaceViewDir(OUT.positionWS); OUT.vertexLight = VertexLighting(OUT.positionWS, OUT.normalWS); OUT.fogFactor = ComputeFogFactor(OUT.positionHCS.z); OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, OUT.lightmapUV); OUTPUT_SH(OUT.normalWS.xyz, OUT.vertexSH); #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) OUT.shadowCoord = TransformWorldToShadowCoord(OUT.positionWS); #endif return OUT; } half4 frag (Varyings IN) : SV_Target { // 草の密度 float2 glassUv = IN.uv * 4; half4 grassColor = SAMPLE_TEXTURE2D(_GrassBaseMap, sampler_GrassBaseMap, glassUv); half3 grassNormal = UnpackNormal(SAMPLE_TEXTURE2D(_GrassNormal, sampler_GrassNormal, glassUv)); half4 cliffColor = SAMPLE_TEXTURE2D(_CliffTex, sampler_CliffTex, IN.uv); // 崖のテクスチャでは、法線とAO(アンビエントオクルージョン)を同じアセットにパックしているため。Unpackはしない // この場合、法線マップデータを入力テクスチャの0-1の範囲から、法線ベクトルがある-1-1にリマップする必要がある // また、AOは法線よりも圧縮することができるので、RGBではなくGBAチャンネルを使用している half4 cliffNormalAO = SAMPLE_TEXTURE2D(_CliffNormalAO, sampler_CliffNormalAO, IN.uv); half3 cliffNormal = half3(cliffNormalAO.a, cliffNormalAO.g, cliffNormalAO.b) * 2 - 1; float3 worldSpaceNormal = ConvertWorldSpaceNormal(cliffNormal, IN.normalWS, IN.tangentWS, IN.binormalWS); float grassMask = GrassMask(IN.positionWS.y, worldSpaceNormal.y); float3 detailRockNormal = DetailRockNormal(IN.uv); // ホワイトアウトブレンディングをする float3 blendRockNormalCliffNormal = normalize(float3(cliffNormal.rg + detailRockNormal.rg, cliffNormal.b * detailRockNormal.b)); float3 normal = lerp(grassNormal, blendRockNormalCliffNormal, grassMask); float wetnessMask = WetnessMask(cliffColor.a, IN.positionWS.y); float rockSmoothness = RockSmoothness(cliffColor.a, cliffColor.r) * grassMask; float smoothness = lerp(0.85, rockSmoothness, wetnessMask); float rockOcclusion = cliffNormalAO.r; half4 blendGrassBase = lerp(grassColor, cliffColor, grassMask) * wetnessMask; return CreateSurfaceData(blendGrassBase, normal, rockOcclusion, smoothness, IN); } ENDHLSL } } }