知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【Unity Shader】BoatAttackの崖シェーダー【2】 #122

はじめに

前回に引き続き、Unity公式が出しているGithubのBoatAttackの中の崖シェーダーについて触れていきます。

soramamenatan.hatenablog.com

環境

Unity 2021.3.6f1
Universal RP 12.1.7

Normal

前回まででAlbedoの出力を理解したので、次はNormalになります。

Lerpノードの計算結果が出力されています。
Normalizeノードは消し忘れかと思います。

草のノーマルマップのサンプリング

ここではAlbedoで行った草テクスチャとUVを合わせるために同様に4倍にしたものをサンプリングしています。

コードにすると以下になります。
SampleTexture2DノードのTypeNormalになっているので、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つのノーマルマップをブレンドします。

前半部分は前回解説したので割愛します。
草のマスクの補完される値 の目次で解説しています。

soramamenatan.hatenablog.com

後半部分では、ノーマルマップをブレンドしています。

ここでは、ホワイトアウトブレンディングという手法を使います。
以下のコードでノーマルマップをブレンドします。

float3 r = normalize(float3(n1.xy + n2.xy, n1.z*n2.z));

blog.selfshadow.com

ですので、この部分は以下のようなコードでになります。

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のノードは前回の記事で行った、水のマスクの部分になります。

soramamenatan.hatenablog.com

水マスクとブレンドすることにより、濡れている箇所の滑らかさを抑えています。
これをコードにすると以下になります。

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を使います。

github.com

以下サイト様を参考にコードを記載します。

light11.hatenadiary.com

// 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のもので、右が今回制作したものになります。
見た目は若干異なりますが、やりたい表現はできています。

youtu.be

ソースコード

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の中の崖シェーダーについて触れていきます。

github.com

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」位置と法線に基づいています。

とあるので、この法線の値の空間を合わせるために接空間からワールド空間へと変換します。

接空間にある法線をワールド空間の法線に変換するには、
接線、従法線、法線の転置行列があれば変換できます。

このあたりは以下のサイト様がわかりやすいです。

coposuke.hateblo.jp

これをコードにすると以下になります。

// 接空間をワールド空間へと変換する
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までを適応した結果になります。

また、各種パラメータを変更したときの変化になります。

www.youtube.com

www.youtube.com

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ですと、ポストエフェクトを適応する際に以下の手順を踏む必要がありました。

  1. 一時的なレンダーテクスチャを取得
  2. 元のテクスチャから一時的なレンダーテクスチャにポストエフェクトを適応して描画
  3. 一時的なレンダーテクスチャから元のテクスチャに描画
  4. 一時的なレンダーテクスチャの解放

URP12からはScriptableRenderPass.Blitメソッドを使用することで、一時的なレンダーテクスチャを用意せずとも、カラーバッファにポストエフェクトを適応させることができるようになりました。

今回のコード上の箇所ですと、以下になります。

var cmd = CommandBufferPool.Get(ProfilerTag);
// Blit1回で良くなった
Blit(cmd, ref renderingData, material);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);

以前との比較

右がURP10でのポストエフェクト、左がUPR12でのSwapBufferを用いたポストエフェクトになります。
コード量の差が一目瞭然かと思います。

描画結果

参考サイト様

light11.hatenadiary.com

youtu.be

【Unity Shader】Shader Graphでレイマーチング #118

はじめに

今回はShaderGraphを用いて、レイマーチングを行います。
以下記事にレイマーチングについてまとめていますので、こちらを参照しながら見ていただけると理解がより深まるかと思います。

soramamenatan.hatenablog.com

コード

ShaderGraph

このような形でレイマーチングが可能になります。

f:id:soramamenatan:20220227182336p:plain

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ノードでワールド空間からオブジェクト空間へと変換します。

f:id:soramamenatan:20220227182349p:plain

レイの向き

次にレイの向きを決めます。 シェーダーでいう

normalize(pos.xyz - _WorldSpaceCameraPos);

になります。

f:id:soramamenatan:20220227182358p:plain

レイをすすめる

カメラの位置とレイの向きを決めることができたので、レイを進める処理を書きます。
ただし、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

f:id:soramamenatan:20220227182420p:plain

コメントにも記載していますが、CustomFunctionノードで呼び出す関数には精度のサフィックスを指定する必要があるので気をつけてください。

レイにあたった部分を描画

レイにあたった部分を描画するには、Branchノードを使用します。
今回は距離関数に円を使用しているので、これで円が表示されるはずです。

f:id:soramamenatan:20220227183017p:plain

円を表示するレイマーチングの全体

ここまでの各ノードを繋げたものが以下のようなものになります。

f:id:soramamenatan:20220227183115p:plain

GraphSettingsで描画の設定を忘れないように指定してください。
今回は半透明なものを描画するので、TransparentとAlphaを指定します。

また、後述するレイマーチングにとって都合が良いのでTwoSidedのトグルを入れてください。
これは両面描画する命令を出すものになります。

f:id:soramamenatan:20220227183230p:plain

描画結果

以下の画像のようになっていれば成功です。

f:id:soramamenatan:20220227183526p:plain

ライティング

シンプルにランバート拡散反射でライティングを実装します。
シェーダー上ですと、以下のように記述します。

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のプレビュー上を判断するものになります。

f:id:soramamenatan:20220402110952p:plain

プレビュー上ですと、後術するGetMainLight()が取得できずコンパイルエラーになってしまいます。
ですので、ifdefで区切ってあげてプレビューには適当な値を返してあげています。

SHADERGRAPH_PREVIEWはShaderGraph10.x以前では、"#if以降では#ifdefとなっています。

docs.unity3d.com

余談ですが、ideによってはエラーを返しますが無視してもUnity上では影響ありません。

f:id:soramamenatan:20220402111656p:plain

GetMainLight

その名の通り、メインのライトを取得するものになります。

CustomFunctionのinspector

f:id:soramamenatan:20220402105954p:plain

法線の追加

法線を取得するために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に法線の引数を追加します。

f:id:soramamenatan:20220402120713p:plain

ライティングのShaderGraph

ランバート拡散反射を反映したノード

ランバート拡散反射を元に、ノードを繋げていきます。

f:id:soramamenatan:20220402121006p:plain

全体ノード

また、全体は以下のようになります。

f:id:soramamenatan:20220402121104p:plain

描画結果

ライティングが反映されました。

f:id:soramamenatan:20220402121215p:plain

距離関数の変更

以下のサイト様のRecursive Tetrahedronを参考に距離関数を変更します。

qiita.com

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;
        }
    }
}

描画結果

f:id:soramamenatan:20220402122808g:plain

参考サイト様

virtualcast.jp

【Unity Shader】Built-inのポストエフェクトをURPに変更 #117

はじめに

Bilt-inで記載されているポストエフェクトをURPに置換してみます。

今回対応させて頂くものは、以下サイト様のものになります。

light11.hatenadiary.com

ソースコード

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については以下記事で行っています。

soramamenatan.hatenablog.com

URP Assetの設定

Depth Textureの設定をオンにします。 そうすることでDepthTextureである_CameraDepthTextureが取得できます。

f:id:soramamenatan:20220402021507p:plain

シェーダーを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

f:id:soramamenatan:20220402030830g:plain

Attenuation

f:id:soramamenatan:20220402030853g:plain

Intensity

f:id:soramamenatan:20220402030920g:plain

不明な点

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)で渡っていたため。

f:id:soramamenatan:20220402032509p:plainf:id:soramamenatan:20220402032515p:plainf:id:soramamenatan:20220402032521p:plainf:id:soramamenatan:20220402032526p:plain

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);
        }
    }
}

シェーダー

f:id:soramamenatan:20220327131917p:plain

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

パスを実行するタイミングを制御するものになります。
一覧は公式のドキュメントに記載しています。

docs.unity3d.com

RenderTargetHandle

描画対象をハンドリングするものになります。
FrameDebugからもレンダーテクスチャが確認できます。

f:id:soramamenatan:20220327122009p:plain

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);

以下のことを行っています。

  1. 一時的なレンダーテクスチャを取得
  2. 元のテクスチャから一時的なレンダーテクスチャにエフェクトを適応して描画
  3. 一時的なレンダーテクスチャから元のテクスチャに描画
  4. 一時的なレンダーテクスチャの解放

GetTemporaryRTReleaseTemporaryRTはセットなので解放を忘れないようにしてください。

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して今回制作したシェーダーグラフをアタッチしてください。

f:id:soramamenatan:20220327131541p:plain

カメラのRendererに今回のPipelineを選択して準備完了になります。

f:id:soramamenatan:20220327131617p:plain

対応前

f:id:soramamenatan:20220327130238p:plain

対応後

f:id:soramamenatan:20220327130250p:plain

詰まった箇所

画面が青く描画される

シェーダーをUnlitで使用していて、画面一面が青くなってしまっていた。
litに修正することで治ったが、Unlitだと何故起きるのかまでは突き止められなかった。

原因は、DepthNormalsパスとSceneSelectionPassパスで青く描画されていることだとは思うがなんで青いかもいまいち掴めていない。

f:id:soramamenatan:20220327130658p:plain

シェーダーが適応されない

intermediate textureをAlwaysにして強制しないと適応されない時があった。

f:id:soramamenatan:20220327131036p:plain

参考サイト様

light11.hatenadiary.com

edom18.hateblo.jp