【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 } } }
【Unity Shader】BoatAttackの崖シェーダー【1】 #121
はじめに
今回はUnity公式が出しているGithubのBoatAttackの中の崖シェーダーについて触れていきます。
static_lsland
シーン内のこちらになります。
環境
Unity 2021.3.6f1 Universal RP 12.1.7
CliffShader
CliffShader
という名前のシェーダーグラフで崖を表現しています。
全貌は以下になります。
- Albedo
- Normal
- Smoothness
- Occlusion
の4つの要素からなっているので、それぞれを分解していけば理解できそうです。
Albedo
ここから具体的な解説に移ります。
AlbedoはMultiply
ノードとLerp
ノードの計算結果からなっています。
まずはLerpのAに繋がっている値を見ます。
草のテクスチャのサンプリング
ここは、草のテクスチャをサンプリングしています。
ですので、コードにすると以下になります。
// 草の密度 float2 glassUv = IN.uv * 4; half4 grassColor = SAMPLE_TEXTURE2D(_GrassBaseMap, sampler_GrassBaseMap, glassUv);
崖のテクスチャのサンプリング
ここも崖のテクスチャをサンプリングしています。
コードにすると以下になります。
half4 cliffColor = SAMPLE_TEXTURE2D(_CliffTex, sampler_CliffTex, IN.uv);
草のマスク
崖にどのくらい草が生えるようにするかを計算している箇所になります。
まずは、シェーダーグラフ上に記載されているコメントを日本語に翻訳します。
// このグループでは、草と岩が互いに混ざり合っている場所のマスクを作成しています。 // これは、ワールド空間の「Y」位置と法線に基づいています。 // これにより、草の表面と岩の表面の間のラープで後で使用できる白黒のマスクが生成されます。
草と岩のブレンド具合のマスクを生成しているようです。
草のマスクの補完
まずは、SmoothStepノードのIn
に入ってくる箇所以外を行います。
まずは、後で解説するIn
の部分が0~1の値が入ってくるので、倍率をあわせるために0.01を乗算しています。
草の生える量であるGrassAngle
が大きい方がたくさん生えてほしいのでOneMinus
ノードを使用します。
その値を滑らかに補完するために各値を加算、減算しSmoothStep
ノードを使用します。
草のマスクの補完される値
SmoothStepノードのIn
に入ってくる値の部分の計算をします。
崖テクスチャ
ここもコメントを翻訳します。
// 崖のテクスチャでは、法線とAO(アンビエントオクルージョン)を同じアセットにパックしています。 // これは、2つのアセットを持つことを節約するためですが、Unityがテクスチャを法線マップとしてエンコードすることに依存できないことを意味し、そうでなければAOのカスタムパッキングが台無しになります。 // この場合、法線マップデータを入力テクスチャの0-1の範囲から、法線ベクトルがある-1-1に再マップする必要があります。 // また、AOは法線よりも圧縮することができるので、RGBではなくGBAチャンネルを使用します。
このテクスチャでは、AGBチャンネルが法線情報でRチャンネルがAO情報が入っているようです。
実際に見てみると、RチャンネルとGBチャンネルが若干違います。
また、Aチャンネルにも透明度以外のものが入っているように見えます。
このテクスチャは、法線とAOが入っている関係上、TextureTypeDefault
に設定しています。
また、Default
なのでテクスチャが0~1になっています。
これを法線として扱えるように-1~1にリマップする必要があります。
これらをコードにすると以下になります。
// 崖のテクスチャでは、法線とAO(アンビエントオクルージョン)を同じアセットにパックしている // この場合、法線マップデータを入力テクスチャの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;
接空間をワールド空間に変換
草のマスクのコメントに
>// これは、ワールド空間の「Y」位置と法線に基づいています。
とあるので、この法線の値の空間を合わせるために接空間からワールド空間へと変換します。
接空間にある法線をワールド空間の法線に変換するには、
接線、従法線、法線の転置行列があれば変換できます。
このあたりは以下のサイト様がわかりやすいです。
これをコードにすると以下になります。
// 接空間をワールド空間へと変換する 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; }
Inを計算する
これでSmoothStepのInの元となる値が計算できました。
まずは、ワールド空間のY座標を元に、どのくらいブレンドするかを決めます。
これらをSaturate
で0~1の範囲に変換します。
その後、崖の上の方に草を生やしたいので、OneMinus
で生える位置を反転させます。
ここまでのTを計算する箇所をコードにすると以下になります。
// 草と岩が互いに混ざり合っている場所のマスクを作成する // ワールド空間の「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)); }
水のマスク
ここまでで、Lerp
に繋がっているABTと、Multiply
に繋がっているBまでが終了しました。
Multiply
に繋がっているAを求めてAlbedoは終了になります。
ここがAに繋がっている箇所になります。
ここも翻訳してみます。
// ここでは、水が濡れている場所(0)と乾いている場所(1)を定義するマスクを作成します。 // ワールド スペースの位置 Y に基づいて、サーフェスがウォーター ライン上にある場合は、フェード オフしてドライになります。 // また、完全に水没している場合は、水面が濡れているようには見えないので、フェードバックして乾燥状態にします。 // また、「空洞」マップを使用して、ひび割れや隙間の濡れた状態をより鮮明に表現しています。
ワールド空間のY座標によって水に濡れている表現を行っています。
ここの翻訳にある空洞マップは、崖のテクスチャで使用したRチャンネルのAOになります。
要素としては2つで、AOをどの程度反映させるか計算したものと、
ワールド空間のY座標でのしきい値を計算している箇所になります。
これをコードにすると以下になります。
// リマップ 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); }
Albedoの出力
これでMultiply
ノードも取得できたので、これまで出した値を計算します。
half4 frag (Varyings IN) : SV_Target { // 草の密度 float2 glassUv = IN.uv * 4; half4 grassColor = SAMPLE_TEXTURE2D(_GrassBaseMap, sampler_GrassBaseMap, 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); float wetnessMask = WetnessMask(cliffColor.a, IN.positionWS.y); half4 blendGrassBase = lerp(grassColor, cliffColor, grassMask) * wetnessMask; return blendGrassBase; }
結果
Albedoまでを適応した結果になります。
また、各種パラメータを変更したときの変化になります。
BoatAttackの崖シェーダーと比較してみます。
左が今回の対応のもので、右が今回の崖シェーダーになります。
Albedoまでしか行っていないからか、全体的にのっぺりとした印象を受けます。
今回は以上になります。
ご視聴ありがとうございました。
【Unity】使用しているTagsAndLayersを検索 #120
はじめに
TagsAndLayersで使用しているものをリストアップするエディタ拡張を制作しました。
TagsAndLayersはEdit/Project Settings...
にある、以下の部分になります。
環境
Unity 2021.3.6f1
ソースコード
TagとLayer
using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEditor; namespace Utility { public class SearchUsingTagLayer : MonoBehaviour { [MenuItem("SearchUsing/TagLayer")] private static void FindAssets() { var myStructs = AssetDatabase .FindAssets("t:Prefab", new[] { "Assets" }) .Select(AssetDatabase.GUIDToAssetPath) .SelectMany(path => { var objs = AssetDatabase.LoadAssetAtPath<GameObject>(path) .GetComponentsInChildren<Transform>(true) .Select(x => x.gameObject); return objs.Select(x => new MyStruct { Path = path, Obj = x }); }).ToArray(); var tagDic = new Dictionary<string, List<string>>(); var layerDic = new Dictionary<string, List<string>>(); foreach (var myStruct in myStructs) { if (tagDic.ContainsKey(myStruct.Obj.tag)) { tagDic[myStruct.Obj.tag].Add(myStruct.Path); } else { tagDic[myStruct.Obj.tag] = new List<string> { myStruct.Path }; } var layerName = LayerMask.LayerToName(myStruct.Obj.layer); if (layerDic.ContainsKey(layerName)) { layerDic[layerName].Add(myStruct.Path); } else { layerDic[layerName] = new List<string> { myStruct.Path }; } } foreach (var d in tagDic) { var str = string.Join("\n", d.Value); Debug.Log($"tag: {d.Key}\n {str}"); } foreach (var d in layerDic) { var str = string.Join("\n", d.Value); Debug.Log($"layer: {d.Key}\n {str}"); } } private struct MyStruct { public string Path; public GameObject Obj; } } }
SortingLayer
using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEditor; namespace Utility { public class SearchUsingSortingLayer : MonoBehaviour { [MenuItem("SearchUsing/SortingLayer")] private static void FindAssets() { var myStructs = AssetDatabase .FindAssets("t:Prefab", new[] { "Assets" }) .Select(AssetDatabase.GUIDToAssetPath) .SelectMany(path => { var objs = AssetDatabase.LoadAssetAtPath<GameObject>(path) .GetComponentsInChildren<Transform>(true) .Select(x => x.gameObject) .ToArray(); return objs.Select(x => new MyStruct { Path = path, Obj = x }); }).ToArray(); var sprites = myStructs .Where(x => x.Obj.GetComponent<SpriteRenderer>() != null) .Select(x => new SpriteRendererParam { Path = x.Path, Obj = x.Obj.GetComponent<SpriteRenderer>() }); var canvases = myStructs .Where(x => x.Obj.GetComponent<Canvas>() != null) .Select(x => new CanvasParam { Path = x.Path, Obj = x.Obj.GetComponent<Canvas>() }); var particles = myStructs .Where(x => x.Obj.GetComponent<ParticleSystemRenderer>() != null) .Select(x => new ParticleSystemRendererParam { Path = x.Path, Obj = x.Obj.GetComponent<ParticleSystemRenderer>() }); var sortingLayerDic = new Dictionary<string, List<string>>(); foreach (var sprite in sprites) { if (sortingLayerDic.ContainsKey(sprite.Obj.sortingLayerName)) { sortingLayerDic[sprite.Obj.sortingLayerName].Add(sprite.Path); } else { sortingLayerDic[sprite.Obj.sortingLayerName] = new List<string> { sprite.Path }; } } foreach (var canvas in canvases) { if (sortingLayerDic.ContainsKey(canvas.Obj.sortingLayerName)) { sortingLayerDic[canvas.Obj.sortingLayerName].Add(canvas.Path); } else { sortingLayerDic[canvas.Obj.sortingLayerName] = new List<string> { canvas.Path }; } } foreach (var particle in particles) { if (sortingLayerDic.ContainsKey(particle.Obj.sortingLayerName)) { sortingLayerDic[particle.Obj.sortingLayerName].Add(particle.Path); } else { sortingLayerDic[particle.Obj.sortingLayerName] = new List<string> { particle.Path }; } } foreach (var d in sortingLayerDic) { var str = string.Join("\n", d.Value); Debug.Log($"layer: {d.Key}\n {str}"); } } private struct MyStruct { public string Path; public GameObject Obj; } private struct SpriteRendererParam { public string Path; public SpriteRenderer Obj; } private struct CanvasParam { public string Path; public Canvas Obj; } private struct ParticleSystemRendererParam { public string Path; public ParticleSystemRenderer Obj; } } }
使い方
Unityのメインメニュー上にSearchUsing
が追加されるので、該当するものを押します。
現在使用しているものがDebug.Logとして出てきます。
【Unity Shader】SwapBuffer #119
はじめに
今回はURP12で追加されたSwapBufferを解説します。
Unity 2021.3.6f1 Universal RP 12.1.7
ソースコード
ScriptableRendererFeature
using UnityEngine; using UnityEngine.Rendering.Universal; namespace SwapBuffer { public class GrayscaleRendererFeatureBySwapBuffer : ScriptableRendererFeature { [SerializeField] private Shader shader; private GrayscalePassBySwapBuffer grayscalePass; // 初期化 public override void Create() { grayscalePass = new GrayscalePassBySwapBuffer(shader); } // 1つ、または複数のパスを追加する public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(grayscalePass); } } }
ScriptableRenderPass
using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; namespace SwapBuffer { public class GrayscalePassBySwapBuffer : ScriptableRenderPass { private const string ProfilerTag = nameof(GrayscalePassBySwapBuffer); private readonly Material material; public GrayscalePassBySwapBuffer(Shader shader) { material = CoreUtils.CreateEngineMaterial(shader); renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd = CommandBufferPool.Get(ProfilerTag); // Blit1回で良くなった Blit(cmd, ref renderingData, material); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } } }
シェーダー
Shader "SwapBuffer/GrayScale" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" "Renderpipeline"="UniversalPipeline" } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float2 uv : TEXCOORD0; float4 positionHCS : SV_POSITION; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); CBUFFER_START(UnityPerMaterial) float4 _MainTex_ST; CBUFFER_END Varyings vert (Attributes IN) { Varyings OUT; OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz); OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex); return OUT; } half4 frag (Varyings IN) : SV_Target { half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv); half gray = dot(col.rgb, half3(0.299, 0.587, 0.114)); return half4(gray, gray, gray, col.a); } ENDHLSL } } }
SwapBufferとは
一言でいうと、ポストエフェクトを楽に書ける機能のことになります。
以前のバージョンのURPですと、ポストエフェクトを適応する際に以下の手順を踏む必要がありました。
- 一時的なレンダーテクスチャを取得
- 元のテクスチャから一時的なレンダーテクスチャにポストエフェクトを適応して描画
- 一時的なレンダーテクスチャから元のテクスチャに描画
- 一時的なレンダーテクスチャの解放
URP12からはScriptableRenderPass.Blit
メソッドを使用することで、一時的なレンダーテクスチャを用意せずとも、カラーバッファにポストエフェクトを適応させることができるようになりました。
今回のコード上の箇所ですと、以下になります。
var cmd = CommandBufferPool.Get(ProfilerTag); // Blit1回で良くなった Blit(cmd, ref renderingData, material); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd);
以前との比較
右がURP10でのポストエフェクト、左がUPR12でのSwapBufferを用いたポストエフェクトになります。
コード量の差が一目瞭然かと思います。
描画結果
参考サイト様
【Unity Shader】Shader Graphでレイマーチング #118
はじめに
今回はShaderGraphを用いて、レイマーチングを行います。
以下記事にレイマーチングについてまとめていますので、こちらを参照しながら見ていただけると理解がより深まるかと思います。
コード
ShaderGraph
このような形でレイマーチングが可能になります。
Raymerching.hlsl
float RecursiveTetrahedron(float3 p, half3 offset, half scale) { float4 z = float4(p, 1.0); for (int i = 0; i < 8; i++) { if (z.x + z.y < 0.0) { z.xy = -z.yx; } if (z.x + z.z < 0.0) { z.xz = -z.zx; } if (z.y + z.z < 0.0) { z.zy = -z.yz; } z *= scale; z.xyz -= offset * (scale - 1.0); // 適当に動かす z.xyz += sin(_Time.y) * 0.5; } return (length(z.xyz) - 1.5) / z.w; } float Dest(float3 p) { return RecursiveTetrahedron(p, 1.0, 2.0); } float3 CalcNormal(float3 p) { // 勾配 float2 ep = float2(0, 0.001); return normalize( float3( Dest(p + ep.yxx) - Dest(p), Dest(p + ep.xyx) - Dest(p), Dest(p + ep.xxy) - Dest(p) )); } // 精度のサフィックスを指定する必要がある void RayMarching_float(float3 rayPosition, float3 rayDirection, out bool hit, out float3 hitPosition, out float3 hitNormal) { float3 pos = rayPosition; for (int i = 0; i < 64; i++) { float d = Dest(pos); pos += d * rayDirection; if (d < 0.001) { hit = true; hitPosition = pos; hitNormal = CalcNormal(pos); return; } } }
GetLight.hlsl
void GetLight_float(out float3 direction, out half3 color) { // shader graphのpreviewからライトを取得できないので適当な値を渡す #ifdef SHADERGRAPH_PREVIEW direction = half3(0.5, 0.5, 0); color = 1; #else Light light = GetMainLight(); direction = light.direction; color = light.color; #endif }
円の描画
カメラの位置
レイを飛ばす前にカメラの位置を決めます。 カメラはCameraノードから取得できます。 また、Transformノードでワールド空間からオブジェクト空間へと変換します。
レイの向き
次にレイの向きを決めます。 シェーダーでいう
normalize(pos.xyz - _WorldSpaceCameraPos);
になります。
レイをすすめる
カメラの位置とレイの向きを決めることができたので、レイを進める処理を書きます。
ただし、ShaderGraphではループ処理を書くことができません。
なので、CustomFunctionノードを使用します。
RayMarching.hlsl
float Dest(float3 p) { return length(p) - 0.5; } // 精度のサフィックスを指定する必要がある void RayMarching_float(float3 rayPosition, float3 rayDirection, out bool hit, out float3 hitPosition) { float3 pos = rayPosition; for (int i = 0; i < 64; i++) { float d = Dest(pos); pos += d * rayDirection; if (d < 0.001) { hit = true; hitPosition = pos; return; } } }
CustomFunctionのinspector
コメントにも記載していますが、CustomFunctionノードで呼び出す関数には精度のサフィックスを指定する必要があるので気をつけてください。
レイにあたった部分を描画
レイにあたった部分を描画するには、Branchノードを使用します。
今回は距離関数に円を使用しているので、これで円が表示されるはずです。
円を表示するレイマーチングの全体
ここまでの各ノードを繋げたものが以下のようなものになります。
GraphSettingsで描画の設定を忘れないように指定してください。
今回は半透明なものを描画するので、TransparentとAlphaを指定します。
また、後述するレイマーチングにとって都合が良いのでTwoSidedのトグルを入れてください。
これは両面描画する命令を出すものになります。
描画結果
以下の画像のようになっていれば成功です。
ライティング
シンプルにランバート拡散反射でライティングを実装します。
シェーダー上ですと、以下のように記述します。
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz)); float3 diffuse = saturate(dot(normal, lightDir)) * _LightColor0;
ライトの向き
シェーダーグラフにDirectionalLightを取得するノードがないのでCustomFunctionで対応します。
GetLight.hlsl
void GetLight_float(out float3 direction, out half3 color) { // shader graphのpreviewからライトを取得できないので適当な値を渡す #ifdef SHADERGRAPH_PREVIEW direction = half3(0.5, 0.5, 0); color = 1; #else Light light = GetMainLight(); direction = light.direction; color = light.color; #endif }
SHADERGRAPH_PREVIEW
これはShaderGraphのプレビュー上を判断するものになります。
プレビュー上ですと、後術するGetMainLight()
が取得できずコンパイルエラーになってしまいます。
ですので、ifdefで区切ってあげてプレビューには適当な値を返してあげています。
SHADERGRAPH_PREVIEWはShaderGraph10.x以前では、"#if
以降では#ifdef
となっています。
余談ですが、ideによってはエラーを返しますが無視してもUnity上では影響ありません。
GetMainLight
その名の通り、メインのライトを取得するものになります。
CustomFunctionのinspector
法線の追加
法線を取得するためにRayMarching.hlsl
を以下のように修正します。
float Dest(float3 p) { return length(p) - 0.5; } // 追加 float3 CalcNormal(float3 p) { // 勾配 float2 ep = float2(0, 0.001); return normalize( float3( Dest(p + ep.yxx) - Dest(p), Dest(p + ep.xyx) - Dest(p), Dest(p + ep.xxy) - Dest(p) )); } // 精度のサフィックスを指定する必要がある // out hit Normalを追加 void RayMarching_float(float3 rayPosition, float3 rayDirection, out bool hit, out float3 hitPosition, out float3 hitNormal) { float3 pos = rayPosition; for (int i = 0; i < 64; i++) { float d = Dest(pos); pos += d * rayDirection; if (d < 0.001) { hit = true; hitPosition = pos; // 追加 hitNormal = CalcNormal(pos); return; } } }
これにより勾配で法線が取得できます。
CustomFunctionのinspectorに法線の引数を追加します。
ライティングのShaderGraph
ランバート拡散反射を反映したノード
ランバート拡散反射を元に、ノードを繋げていきます。
全体ノード
また、全体は以下のようになります。
描画結果
ライティングが反映されました。
距離関数の変更
以下のサイト様のRecursive Tetrahedron
を参考に距離関数を変更します。
RayMarching.hlsl
// 追加 float RecursiveTetrahedron(float3 p, half3 offset, half scale) { float4 z = float4(p, 1.0); for (int i = 0; i < 8; i++) { if (z.x + z.y < 0.0) { z.xy = -z.yx; } if (z.x + z.z < 0.0) { z.xz = -z.zx; } if (z.y + z.z < 0.0) { z.zy = -z.yz; } z *= scale; z.xyz -= offset * (scale - 1.0); // 適当に動かす z.xyz += sin(_Time.y) * 0.5; } return (length(z.xyz) - 1.5) / z.w; } float Dest(float3 p) { // 追加 return RecursiveTetrahedron(p, 1.0, 2.0); } float3 CalcNormal(float3 p) { // 勾配 float2 ep = float2(0, 0.001); return normalize( float3( Dest(p + ep.yxx) - Dest(p), Dest(p + ep.xyx) - Dest(p), Dest(p + ep.xxy) - Dest(p) )); } // 精度のサフィックスを指定する必要がある void RayMarching_float(float3 rayPosition, float3 rayDirection, out bool hit, out float3 hitPosition, out float3 hitNormal) { float3 pos = rayPosition; for (int i = 0; i < 64; i++) { float d = Dest(pos); pos += d * rayDirection; if (d < 0.001) { hit = true; hitPosition = pos; hitNormal = CalcNormal(pos); return; } } }
描画結果
参考サイト様
【Unity Shader】Built-inのポストエフェクトをURPに変更 #117
はじめに
Bilt-inで記載されているポストエフェクトをURPに置換してみます。
今回対応させて頂くものは、以下サイト様のものになります。
ソースコード
ScriptableRendererFeature
using UnityEngine; using UnityEngine.Rendering.Universal; namespace Day5.Practice2 { public class GlareRendererFeature : ScriptableRendererFeature { [SerializeField] private Shader shader; [SerializeField, Range(0.0f, 1.0f)] private float threshold = 0.5f; [SerializeField, Range(0.5f, 0.95f)] private float attenuation = 0.9f; [SerializeField, Range(0.0f, 10.0f)] private float intensity = 1.0f; private GlarePass glarePass; public override void Create() { glarePass = new GlarePass(shader); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { glarePass.SetRenderTarget(renderer.cameraColorTarget); glarePass.SetShaderProperty(threshold, attenuation, intensity); renderer.EnqueuePass(glarePass); } } }
ScriptableRenderPass
using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; namespace Day5.Practice2 { public class GlarePass : ScriptableRenderPass { private const string ProfilerTag = nameof(GlarePass); private readonly Material material; private readonly int paramsPropertyId = Shader.PropertyToID("_Params"); private readonly int thresholdPropertyId = Shader.PropertyToID("_Threshold"); private readonly int attenuationPropertyId = Shader.PropertyToID("_Attenuation"); private readonly int intensityPropertyId = Shader.PropertyToID("_Intensity"); private RenderTargetHandle destRenderTargetHandle; private RenderTargetHandle tmpRenderTargetHandle1; private RenderTargetHandle tmpRenderTargetHandle2; private RenderTargetIdentifier cameraColorTarget; private float threshold; private float attenuation; private float intensity; public GlarePass(Shader shader) { material = CoreUtils.CreateEngineMaterial(shader); renderPassEvent = RenderPassEvent.AfterRenderingTransparents; destRenderTargetHandle.Init("_destRT"); tmpRenderTargetHandle1.Init("_TempRT1"); tmpRenderTargetHandle2.Init("_TempRT2"); } public void SetRenderTarget(RenderTargetIdentifier target) { cameraColorTarget = target; } public void SetShaderProperty(float threshold, float attenuation, float intensity) { this.threshold = threshold; this.attenuation = attenuation; this.intensity = intensity; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (renderingData.cameraData.isSceneViewCamera) { return; } var cmd = CommandBufferPool.Get(ProfilerTag); var descriptor = renderingData.cameraData.cameraTargetDescriptor; cmd.GetTemporaryRT(destRenderTargetHandle.id, descriptor); cmd.GetTemporaryRT(tmpRenderTargetHandle1.id, descriptor); cmd.GetTemporaryRT(tmpRenderTargetHandle2.id, descriptor); material.SetFloat(thresholdPropertyId, threshold); material.SetFloat(attenuationPropertyId, attenuation); material.SetFloat(intensityPropertyId, intensity); cmd.Blit(cameraColorTarget, destRenderTargetHandle.Identifier()); for (var i = 0; i < 4; i++) { cmd.Blit(cameraColorTarget, tmpRenderTargetHandle1.Identifier(), material, 0); var currentSrc = tmpRenderTargetHandle1.Identifier(); var currentTarget = tmpRenderTargetHandle2.Identifier(); var parameters = Vector3.zero; parameters.x = i is 0 or 1 ? -1 : 1; parameters.y = i is 0 or 2 ? -1 : 1; for (var j = 0; j < 4; j++) { parameters.z = j; cmd.SetGlobalVector(paramsPropertyId, parameters); cmd.Blit( currentSrc, currentTarget, material, 1); (currentSrc, currentTarget) = (currentTarget, currentSrc); } cmd.Blit(currentSrc, destRenderTargetHandle.Identifier(), material, 2); } cmd.Blit(destRenderTargetHandle.Identifier(), cameraColorTarget); cmd.ReleaseTemporaryRT(destRenderTargetHandle.id); cmd.ReleaseTemporaryRT(tmpRenderTargetHandle1.id); cmd.ReleaseTemporaryRT(tmpRenderTargetHandle2.id); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } } }
シェーダー
Shader "Day5/URPGlare" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Cull Off ZTest Always ZWrite Off Tags { "RenderType"="Opaque" // レンダリングパイプラインをURPにする "Renderpipeline" = "UniversalPipeline" } Pass { // HLSLを記述する HLSLPROGRAM #pragma vertex vert #pragma fragment frag // hlslでよく使用されるマクロをインクルード // #include "UnityCG.cginc"に近い #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" // appdeta -> Attributes struct Attributes { // vertex -> positionOS // OSはObject Spaceの略 float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; // v2f -> Varyings struct Varyings { float2 uv : TEXCOORD0; // vertex -> positionHCS // HSCはHomogeneous Clip Space(等質クリップ座標)の略 float4 positionHCS : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float _Threshold; // Texture2Dを宣言 TEXTURE2D(_CameraDepthTexture); // SamplerStateを宣言 SAMPLER(sampler_CameraDepthTexture); Varyings vert (Attributes IN) { Varyings OUT; // UnityObjectToClipPos -> TransformObjectToHClip OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz); OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex); return OUT; } // hlslではfixedが使えないのでhalfにする half4 frag (Varyings IN) : SV_Target { // SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); // textureとsamplerが必要になった half depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, IN.uv); // Linear01Depth(depth); // zBufferParamが必要になった half linear01Depth = Linear01Depth(depth, _ZBufferParams); half4 col = tex2D(_MainTex, IN.uv); half brightness = max(col.r, max(col.g, col.b)); half contribution = max(0, brightness - _Threshold); contribution /= max(brightness, 0.00001); return col * contribution * (1 - linear01Depth); } // HLSLの記述を終える ENDHLSL } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float2 uv : TEXCOORD0; float4 positionHCS : SV_POSITION; half2 uvOffset : TEXCOORD1; half pathFactor : TEXCOORD2; }; sampler2D _MainTex; float4 _MainTex_ST; half4 _MainTex_TexelSize; half3 _Params; float _Attenuation; Varyings vert (Attributes IN) { Varyings OUT; OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz); OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex); OUT.pathFactor = pow(4, _Params.z); OUT.uvOffset = half2(_Params.x, _Params.y) * _MainTex_TexelSize.xy * OUT.pathFactor; return OUT; } half4 frag (Varyings IN) : SV_Target { half4 col = half4(0, 0, 0, 1); half2 uv = IN.uv; [unroll] for (int j = 0; j < 4; j++) { col.rgb += tex2D(_MainTex, uv).rgb * pow(saturate(_Attenuation), j * IN.pathFactor); uv += IN.uvOffset; } return col; } ENDHLSL } Pass { Blend One One ColorMask RGB HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float2 uv : TEXCOORD0; float4 positionHCS : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float _Intensity; Varyings vert (Attributes IN) { Varyings OUT; OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz); OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex); return OUT; } half4 frag (Varyings IN) : SV_Target { return tex2D(_MainTex, IN.uv) * _Intensity; } ENDHLSL } } }
スクリプト側について
ポストエフェクトの解説については元ブログ様を参考にしてください。
また、RendererFeatureについては以下記事で行っています。
URP Assetの設定
Depth Textureの設定をオンにします。
そうすることでDepthTextureである_CameraDepthTexture
が取得できます。
シェーダーをURPに対応
シェーダー
にコメントを記載しているので、そちらを参考にしてください。
以下に自分が気になったマクロの中身を記載します。
TransformObjectToHClip
// Transforms position from object space to homogenous space float4 TransformObjectToHClip(float3 positionOS) { // More efficient than computing M*VP matrix product return mul(GetWorldToHClipMatrix(), mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0))); } // Transform to homogenous clip space float4x4 GetWorldToHClipMatrix() { return UNITY_MATRIX_VP; } // Return the PreTranslated ObjectToWorld Matrix (i.e matrix with _WorldSpaceCameraPos apply to it if we use camera relative rendering) float4x4 GetObjectToWorldMatrix() { return UNITY_MATRIX_M; }
SAMPLE_DEPTH_TEXTURE
#define SAMPLE_DEPTH_TEXTURE(textureName, samplerName, coord2) SAMPLE_TEXTURE2D(textureName, samplerName, coord2).r
Linear01Depth
// Z buffer to linear 0..1 depth (0 at camera position, 1 at far plane). // Does NOT work with orthographic projections. // Does NOT correctly handle oblique view frustums. // zBufferParam = { (f-n)/n, 1, (f-n)/n*f, 1/f } float Linear01Depth(float depth, float4 zBufferParam) { return 1.0 / (zBufferParam.x * depth + zBufferParam.y); }
結果
Threshold
Attenuation
Intensity
不明な点
for文内でのmaterial.SetHoge()
for (var j = 0; j < 4; j++) { parameters.z = j; cmd.SetGlobalVector(paramsPropertyId, parameters); cmd.Blit( currentSrc, currentTarget, material, 1); (currentSrc, currentTarget) = (currentTarget, currentSrc); }
のcmd.SetGlobalVector()
を初めはmaterial.SetVector()
で行っていたが、エフェクトが適応されないでいた。
原因としては、シェーダーにparameters
が常に(1, 1, 3)で渡っていたため。
SetGlobalVector()
にすることで解決したが、詳しい原因は不明。
【Unity Shader】Renderer Featureによるポストプロセス #116
はじめに
Renderer Featureとシェーダーグラフを使用して、ポストプロセスを実装します。
ソースコード
ScriptableRendererFeature
using UnityEngine; using UnityEngine.Rendering.Universal; namespace Day4.Practice1.PostEffect { public class GrayscaleRendererFeature : ScriptableRendererFeature { [SerializeField] private Shader shader; private GrayscalePass grayscalePass; // 初期化 public override void Create() { grayscalePass = new GrayscalePass(shader); } // 1つ、または複数のパスを追加する public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { grayscalePass.SetRenderTarget(renderer.cameraColorTarget); renderer.EnqueuePass(grayscalePass); } } }
ScriptableRenderPass
using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; namespace Day4.Practice1.PostEffect { public class GrayscalePass: ScriptableRenderPass { private const string ProfilerTag = nameof(GrayscalePass); private readonly Material material; // 描画対象をハンドリングする private RenderTargetHandle tmpRenderTargetHandle; private RenderTargetIdentifier cameraColorTarget; public GrayscalePass(Shader shader) { material = CoreUtils.CreateEngineMaterial(shader); renderPassEvent = RenderPassEvent.AfterRenderingTransparents; tmpRenderTargetHandle.Init("_TempRT"); } public void SetRenderTarget(RenderTargetIdentifier target) { cameraColorTarget = target; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (renderingData.cameraData.isSceneViewCamera) { return; } // コマンドバッファの生成 var cmd = CommandBufferPool.Get(ProfilerTag); // RenderTextureDescriptorの取得 var descriptor = renderingData.cameraData.cameraTargetDescriptor; // 今回深度は不要なので0に descriptor.depthBufferBits = 0; cmd.GetTemporaryRT(tmpRenderTargetHandle.id, descriptor); cmd.Blit(cameraColorTarget, tmpRenderTargetHandle.Identifier(), material); cmd.Blit(tmpRenderTargetHandle.Identifier(), cameraColorTarget); cmd.ReleaseTemporaryRT(tmpRenderTargetHandle.id); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } } }
シェーダー
RendererFeature
ユーザーが独自にカスタマイズした描画パスを追加できるURPで提供されている機能になります。
ポストプロセスや、ブラーによるダウンサンプリング等に使用されます。
独自に定義したパスを追加するScriptableRendererFeature
描画の具体的な内容を記載するScriptableRenderPass
の2つを使用することにより、描画パスを追加できます。
ScriptableRendererFeature
// 初期化 public override void Create() { grayscalePass = new GrayscalePass(shader); }
Createは初期化をする箇所で、monobehaviourでいうStartに近いものになります。
今回はパスの初期化を行っています。
// 1つ、または複数のパスを追加する public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { grayscalePass.SetRenderTarget(renderer.cameraColorTarget); renderer.EnqueuePass(grayscalePass); }
AddRenderPassesでパスを追加します。
renderer.cameraColorTarget
はカメラに写っている対象の色になります。
これをScriptableRenderPass側に渡すことによりポストプロセスを可能にしています。
renderer.EnqueuePass
はその名の通り、パスをEnqueueしているものになります。
ScriptableRenderPass
コンストラクタ
public GrayscalePass(Shader shader) { material = CoreUtils.CreateEngineMaterial(shader); renderPassEvent = RenderPassEvent.AfterRenderingTransparents; tmpRenderTargetHandle.Init("_TempRT"); }
CoreUtils.CreateEngineMaterial
/// <summary> /// Creates a Material with the provided shader. /// hideFlags will be set to HideFlags.HideAndDontSave. /// </summary> /// <param name="shader">Shader used for the material.</param> /// <returns>A new Material instance using the provided shader.</returns> public static Material CreateEngineMaterial(Shader shader) { if (shader == null) { Debug.LogError("Cannot create required material because shader is null"); return null; } var mat = new Material(shader) { hideFlags = HideFlags.HideAndDontSave }; return mat; }
シェーダーを元に、Hierarchyに表示せずシーンに保存しない、オブジェクトによりアンロードしないマテリアルを制作してくれるものになります。
RenderPassEvent
パスを実行するタイミングを制御するものになります。
一覧は公式のドキュメントに記載しています。
RenderTargetHandle
描画対象をハンドリングするものになります。
FrameDebugからもレンダーテクスチャが確認できます。
Execute
実際にパスを実行する関数になります。
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (renderingData.cameraData.isSceneViewCamera) { return; } // コマンドバッファの生成 var cmd = CommandBufferPool.Get(ProfilerTag); // RenderTextureDescriptorの取得 var descriptor = renderingData.cameraData.cameraTargetDescriptor; // 今回深度は不要なので0に descriptor.depthBufferBits = 0; cmd.GetTemporaryRT(tmpRenderTargetHandle.id, descriptor); cmd.Blit(cameraColorTarget, tmpRenderTargetHandle.Identifier(), material); cmd.Blit(tmpRenderTargetHandle.Identifier(), cameraColorTarget); cmd.ReleaseTemporaryRT(tmpRenderTargetHandle.id); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); }
CommandBufferPool.Get
var cmd = CommandBufferPool.Get(ProfilerTag);
コマンドバッファを生成する関数になります。
引数にstring型を渡すことで名前をつけてくれます。
Descriptor
var descriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor.depthBufferBits = 0;
カメラからRenderTextureDescriptor
を取得し、今回は不要ですので深度情報を0にしています。
RenderTextureDescriptorはRenderTextureを作成するための全ての情報が含まれた構造体になっています。
RenderTextureに書き込む
cmd.GetTemporaryRT(tmpRenderTargetHandle.id, descriptor); cmd.Blit(cameraColorTarget, tmpRenderTargetHandle.Identifier(), material); cmd.Blit(tmpRenderTargetHandle.Identifier(), cameraColorTarget); cmd.ReleaseTemporaryRT(tmpRenderTargetHandle.id);
以下のことを行っています。
- 一時的なレンダーテクスチャを取得
- 元のテクスチャから一時的なレンダーテクスチャにエフェクトを適応して描画
- 一時的なレンダーテクスチャから元のテクスチャに描画
- 一時的なレンダーテクスチャの解放
GetTemporaryRT
とReleaseTemporaryRT
はセットなので解放を忘れないようにしてください。
Identifier
はハンドルに登録されたレンダーテクスチャを識別するもので、今回はidが割り振られているのでそのまま返ってきます。
public RenderTargetIdentifier Identifier() { if (id == -1) { return BuiltinRenderTextureType.CameraTarget; } if (id == -2) { return rtid; } return new RenderTargetIdentifier(id, 0, CubemapFace.Unknown, -1); }
コマンドバッファの実行
context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd);
ExecuteCommandBuffer
は引数のコマンドバッファを実行するものになります。
最後にRelease
でコマンドバッファを解放してあげるのを忘れないでください。
結果
制作したRenderFeatureを、Addして今回制作したシェーダーグラフをアタッチしてください。
カメラのRendererに今回のPipelineを選択して準備完了になります。
対応前
対応後
詰まった箇所
画面が青く描画される
シェーダーをUnlitで使用していて、画面一面が青くなってしまっていた。
litに修正することで治ったが、Unlitだと何故起きるのかまでは突き止められなかった。
原因は、DepthNormalsパスとSceneSelectionPassパスで青く描画されていることだとは思うがなんで青いかもいまいち掴めていない。
シェーダーが適応されない
intermediate textureをAlwaysにして強制しないと適応されない時があった。