rn.log

備忘録など

【シェーダーグラフメモ その16】マグマの波紋を作ろう

マグマが広がるような表現を作る方法を解説します。

f:id:r-ngtm:20181202133634g:plain

サンプル

ShaderGraphExample/Assets/ShaderGraphExample/16_MagmaRipple at master · rngtm/ShaderGraphExample · GitHub

ざっと解説

座標(X, Y, Z)のXZ成分を取り出し、座標(0, 0)からの距離をとります。
この距離を使って波を作っています。
f:id:r-ngtm:20181202134840p:plain

シェーダーグラフを作る

STEP0 : 板オブジェクトとUnlitシェーダーグラフの準備

Unity標準のPlaneオブジェクトを作り、これにUnlitシェーダーグラフから作ったマテリアルを適用しておきます。
f:id:r-ngtm:20181202141017p:plain:w200 f:id:r-ngtm:20181202141156p:plain:h200

STEP1 : Distanceノードを使って原点(0,0)からの距離をとる

以下のようにノードをつなぎます。
f:id:r-ngtm:20181202140708p:plain

原点(0,0)からの距離が板の色に反映されるようになりました。
f:id:r-ngtm:20181202140825p:plain:w256

STEP2: 距離にSineノードを適用する

距離をSineノードにつなぎます。
f:id:r-ngtm:20181202141352p:plain

波紋っぽい色になりました。
f:id:r-ngtm:20181202141506p:plain:w256

STEP3: Sineの範囲を(-1,1)から(0,1)に変換する

Sineノードの出力は(-1,1)の範囲になっているため、Remapノードを使って範囲(-1, 1)を(0, 1)へ変換します。
後に頂点座標へ加算する時、負の値が含まれていると板が下へ凹んでしまうためこのような補正を入れています。
f:id:r-ngtm:20181202141958p:plain
f:id:r-ngtm:20181202142020p:plain:w256

STEP4: 波紋を時間で動かす

SubtractノードとTimeノードを使い、距離から時間を減算して波紋が広がるようにします。
f:id:r-ngtm:20181202142744p:plain
f:id:r-ngtm:20181202142814g:plain

STEP5: 波紋を使って頂点を動かす

波紋を頂点座標のY成分へ加算して頂点を動かします。
f:id:r-ngtm:20181202143634p:plain

板がY方向に動くようになりました。
f:id:r-ngtm:20181202143806g:plain

STEP6: smoothstepで波紋をとがらせる

smoothstepノードを使って波紋をとがらせます。
f:id:r-ngtm:20181202152144p:plain
f:id:r-ngtm:20181202152231g:plain

補足 : smoothstepの挙動について

f:id:r-ngtm:20181202154212p:plain:w200

smoothstepが返す値

smoothstepノードは以下のような値を返します。

  • In<Edge1の場合は0.0を返す
  • In>Edge2の場合は1.0を返す
  • Edge1 < In < Edge2の場合は0.0~1.0の間を滑らかに補間するような値を返す
    f:id:r-ngtm:20181202153235p:plain

グラフの描画にはiq氏のGraphToyを使わせていただきました。(http://www.iquilezles.org/apps/graphtoy/)

smoothstepをsinに対して使った場合

smoothstepをsin(x)に対して使用することで、sinカーブをとがらせることができます。
f:id:r-ngtm:20181202153849p:plain

今回はsin(x)が(0,1)の範囲に収まるように補正しているため、実際は以下のようなグラフを描きます。 f:id:r-ngtm:20181202153933p:plain

STEP7: 波の高さを調整する

Multiplyノードを使って波の高さを1.4倍にします。
f:id:r-ngtm:20181202155524p:plain

なんだか良い感じの形の波になってきました。
f:id:r-ngtm:20181202155613g:plain

STEP8: SampleGradientノードで色をつける

波紋(Vector1型の数値)をSampleGradientでカラーグラデーションに変換して色を付けます。
f:id:r-ngtm:20181202160009p:plain

マグマっぽくなりました。
f:id:r-ngtm:20181202160141g:plain

STEP9 : Smoothstepノードで色を絞る

波が立っていないところにも色がついてしまっていて残念な印象なので、SmoothStepノードでくっきりとさせたいと思います。
f:id:r-ngtm:20181202160728p:plain

以下のようにSmoothstepノードをつなぎます。
f:id:r-ngtm:20181202161521p:plain

色がくっきりとしました。
f:id:r-ngtm:20181202161539g:plain

変化が分かりにくいので、変更前と変更後を並べてみました。
f:id:r-ngtm:20181202161915p:plain

Step10 : ノイズで質感を加える

原点(0,0)からの距離を利用して波紋を作っていましたが、この距離にノイズを加えてみます。
今回はNoiseScaleに45.0を指定してみました。
f:id:r-ngtm:20181202162440p:plain f:id:r-ngtm:20181202162526g:plain

Step11 : 板オブジェクトを差し替える(完成)

25x25の頂点からなるサイズ10の板オブジェクトに差し替えると以下のような見た目になります。
f:id:r-ngtm:20181202163145g:plain

今回はC#スクリプトで板オブジェクトを生成してみました。

using UnityEngine;

/// <summary>
/// 板状のメッシュを作成C#スクリプト
/// </summary>
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class GeneratePlane : MonoBehaviour
{
    [SerializeField] bool generateMeshOnUpdate = false; // 毎フレームメッシュ更新
    [SerializeField] int xCount = 25; // x方向の頂点数
    [SerializeField] int zCount = 25; // y方向の頂点数
    [SerializeField] float planeSize = 10f; // 板のサイズ
    Mesh mesh;

    void Start()
    {
        mesh = new Mesh();
        GetComponent<MeshFilter>().mesh = mesh;
        Create();
    }

    private void Update()
    {
        if (generateMeshOnUpdate)
        {
            Create();
        }
    }

    private void Create()
    {
        if (xCount < 2) xCount = 2;
        if (zCount < 2) zCount = 2;

        // 頂点座標(vertices)と法線(normals)作成
        Vector3[] vertices = new Vector3[xCount * zCount];
        Vector3[] normals = new Vector3[vertices.Length];
        int vi = 0;
        for (int z = 0; z < zCount; z++)
        {
            for (int x = 0; x < xCount; x++)
            {
                vertices[vi++] = new Vector3(
                    (float)x / (xCount - 1) - 0.5f, 
                    0f, 
                    (float)z / (zCount - 1) - 0.5f
                    ) * planeSize;
            }
        }
        for (int i = 0; i < vertices.Length; i++)
        {
            normals[i] = new Vector3(0f, 1f, 0f);
        }

        // 頂点インデックス(triangles)作成
        int[] triangles = new int[(xCount - 1) * (zCount - 1) * 6];
        int ti = 0;
        int triangleOffset = 0;
        for (int z = 0; z < zCount - 1; z++)
        {
            for (int x = 0; x < xCount - 1; x++)
            {
                triangles[ti++] = triangleOffset + xCount; // 2
                triangles[ti++] = triangleOffset + 1; // 1
                triangles[ti++] = triangleOffset + 0; // 0
                triangles[ti++] = triangleOffset + xCount + 1; // 3
                triangles[ti++] = triangleOffset + 1; // 1
                triangles[ti++] = triangleOffset + xCount; // 2
                triangleOffset++;
            }
            triangleOffset++;
        }

        // メッシュ更新
        mesh.triangles = null;
        mesh.vertices = vertices;
        mesh.normals = normals;
        mesh.triangles = triangles;
    }
}