知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【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