知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【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までしか行っていないからか、全体的にのっぺりとした印象を受けます。

今回は以上になります。
ご視聴ありがとうございました。