【ShaderGraph】ベジェ曲線を波打たせてみる

2021/08/20に公開

以下のような表現を作ってみました。

https://www.youtube.com/watch?v=qLMJUGbwlo4

実装

以下のような実装になっています。

  1. 多角形(水色)に内接するベジェ曲線を作成 (緑色)
  2. ベジェ曲線からメッシュを生成
  3. メッシュ頂点を法線方向に波打たせる

メッシュに持たせる情報

今回の表現でメッシュに以下のような情報を持たせています。

情報 詳細
頂点カラー 波や着色に利用するグラデーション
法線情報 頂点が動く方向のグラデーション

頂点カラー

メッシュには以下のような頂点カラーを持たせています。

チャンネル 情報
Rチャンネル 波打たせたい方向のグラデーション (0~1)
Gチャンネル 着色に利用するグラデーション(0~1)

法線情報

メッシュは内側から外側へ波打たせる際、法線を利用しています。
法線は以下のようになっています。

法線ベクトル(水色)

実装01 : ベジェ曲線の生成

多角形に内接する曲線をベジェ曲線を使って作ります。

多角形に内接するベジェ曲線の作り方

多角形の1つの頂点と、両隣の辺の中点を制御点としてベジェ曲線を生成します。

上記の処理をすべての頂点に関して実行すると、多角形に内接するベジェ曲線になります。

ベジェ曲線の座標計算

制御点をP1, P2, P3としてベジェ曲線上の座標を取得するコードです。

/// <summary>
/// P1, P2 ,P3を制御点とするベジエ曲線上の点を取得 (tは0~1の値)
/// </summary>
public static Vector3 Bezier3(Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
    return t * t * (p1 - 2f * p2 + p3)
           + t * 2f * (- p1 + p2)
           + p1;
}

法線の計算

メッシュを波打たせる際、法線ベクトルも必要になるので実装します。

法線ベクトル(水色)

ベジエ曲線の接線方向のベクトルと(0,0,1)との外積を計算することで法線ベクトルが求まります。

/// <summary>
/// P1, P2 ,P3を制御点とするベジエ曲線上の点を取得 (tは0~1の値)
/// </summary>
public static Vector3 Bezier3Normal(Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
    float dt = 1e-8f;
    
    // ベジエ曲線の近い2点をとる
    Vector3 b1, b2;
    if (t < dt / 2f)
    {
        b1 = Bezier3(p1, p2, p3, 0f);
        b2 = Bezier3(p1, p2, p3, dt);
    }
    else if (t > 1f - dt / 2f)
    {
        b1 = Bezier3(p1, p2, p3, 1f - dt);
        b2 = Bezier3(p1, p2, p3, 1f);
    }
    else
    {
        b1 = Bezier3(p1, p2, p3, t - dt / 2f);
        b2 = Bezier3(p1, p2, p3, t + dt / 2f);
    }

    // 接線方向ベクトル
    var v = Vector3.Normalize(b2 - b1);

    // 法線方向ベクトル
    var n = Vector3.Cross(v, new Vector3(0, 0, 1)).normalized;

    return n;
}

結果

ベジェ曲線を使うことで、多角形から曲線を作ることができます。

実装02 : 曲線のメッシュ化

ベジェ曲線からメッシュを生成します。

ベジエ曲線のメッシュ化

曲線をメッシュ化するためには、曲線を三角形の組み合わせで表現する必要があります。

考え方

曲線の中心点と、曲線上の2点を利用して三角形を作ります。

これを繰り返すことで、曲線を三角形の組み合わせで表現することができます。

メッシュ化の実装

CenterPolygonMeshGeneratorというクラスを作成し、その中でメッシュ化を行うようにしました。

Mesh mesh = new Mesh();
CenterPolygonMeshGenerator.CreateMesh(mesh, vertices, normals);

CenterPolygonMeshGeneratorの実装

CenterPolygonMeshGenerator.cs
using UnityEngine;

public static class CenterPolygonMeshGenerator
{
    public static void CreateMesh(Mesh mesh, Vector3[] vertices, Vector3[] normals)
    {
        int vertexCount = vertices.Length;
        var center = GetCenter(vertices);
        var meshVertices = new Vector3[1 + vertexCount];
        var meshTriangles = new int[3 * vertexCount];
        var meshColors = new Color[meshVertices.Length];
        var meshNormals = new Vector3[meshVertices.Length];
        var meshUV = new Vector2[meshVertices.Length];

        // 頂点座標
        meshVertices[vertexCount] = center;
        for (int i = 0; i < vertexCount; i++)
        {
            meshVertices[i] = vertices[i];
        }

        // 法線
        meshNormals[vertexCount] = Vector3.zero; // 中心は波打たせないので法線ゼロ
        for (int i = 0; i < vertexCount; i++)
        {
            meshNormals[i] = normals[i];
        }

        // 頂点カラー
        meshColors[vertexCount] = Color.black;
        for (int i = 0; i < vertexCount; i++)
        {
            float t = (float)i / (vertexCount);
            meshColors[i] = new Color(t, 1f, 0f, 1f);
        }

        // UV
        meshUV[vertexCount] = Vector2.zero;
        for (int i = 0; i < vertexCount; i++)
        {
            float t = (float)i / (vertexCount - 1);
            meshUV[i] = new Vector2(t, 1);
        }

        // 頂点インデックス
        int ti = 0;
        for (int i = 0; i < vertexCount; i++)
        {
            meshTriangles[ti] = vertexCount;
            meshTriangles[ti + 1] = (i + 1) % vertexCount;
            meshTriangles[ti + 2] = i;
            ti += 3;
        }

        // メッシュ更新
        mesh.triangles = null;
        mesh.vertices = meshVertices;
        mesh.colors = meshColors;
        mesh.uv = meshUV;
        mesh.normals = meshNormals;
        mesh.triangles = meshTriangles;
    }

    /// <summary>
    /// 中心の計算
    /// </summary>
    private static Vector3 GetCenter(Vector3[] vertices)
    {
        int vertexCount = vertices.Length;

        var sum = Vector3.zero;
        for (int i = 0; i < vertexCount; i++)
        {
            sum += vertices[i];
        }
        return sum / vertexCount;
    }
}

実装03 : 頂点アニメーションの実装(ShaderGraph)

頂点アニメーションの実装

以下のような頂点が波打つアニメーションを実装します。
https://youtu.be/8aJ9aFAks_U

頂点アニメーションの実装の全体は以下のようになります。

頂点Sineアニメーション

メッシュの頂点カラーのRチャンネルには、メッシュの外周に沿って0~1のグラデーションを設定しています。
これを利用して、外周に沿った頂点アニメーションをつけます。

波の周波数を乗算

周波数というパラメータを用意して、メッシュの外周に沿って配置したい波の個数をコントロールします。

ShaderGraph

これを実現するノードは以下のようになります。 Constantノード(TAU)を利用することで、2πという数値が取れます。

数式

数式であらわすと以下のようになります。

sin (2 \pi f x)
\begin{aligned} f & : 波の周波数 \\\ x & : 頂点カラー (0 ≤ x ≤ 1) \\\ π & : 円周率 \end{aligned}
2π乗算する理由

ある値xが0~2πの値をとったときに sin(x)はちょうど一つの波を描きます。
グラフの横軸は入力x, グラフのタテ軸は出力yをあらわしています。

今回、Sineへの入力となる頂点カラーは0~1の範囲をとるため、これを2π倍して0~2πとなるようにします。

シェーダーグラフでは、2πという数値はConstantノード(TAU)で取得できるので、これを利用して以下のようなノードを組みます。

頂点を法線方向に移動させる

Sine波とメッシュ法線を乗算し、これを頂点座標に足すことで頂点が押し出されます。


周波数5のSineで頂点押し出し

Sine波を移動させる

sinの入力に時間を足すことで、波が動くようになります。

数式であらわすと以下のようになります。

sin (2 \pi f x + vt)
\begin{aligned} v & : 波の移動スピード \\\ t & : 時間 \end{aligned}

結果

頂点が波打つようになります。
https://youtu.be/8aJ9aFAks_U

実装04 : 頂点カラーを利用した着色 (ShaderGraph)

頂点カラーのGチャンネルには、内側から外側へ向かうグラデーションが設定されています。

これを利用して、色を付けます。

ShaderGraphの実装は以下のようになっています。

  1. グラデーションとLerpを利用して2色を混ぜて内側のグラデーションを作る
  2. グラデーションとStepを利用して、輪郭部分を白くしたマスクを取り出す
  3. 1.の2色グラデーションと2.のマスクを重ねる

参考 : グラデーションを使った着色は以下のページでも解説しています。
https://zenn.dev/r_ngtm/books/shadergraph-cookbook/viewer/recipe-grayscale-color

1) 2色グラデーションの作成

以下のノードでは、グラデーションを元にして2色をつけています。

2) 輪郭抽出

グラデーションにStepを適用し、輪郭線マスクを作成します。

3) 2色グラデーションと輪郭の合成

1)で作成したグラデーションと、2)で作成した輪郭線マスクをLerpで合成します。

結果

色味などを調整します。

ShaderGraph全体

ShaderGraphの全体の実装は以下のようになっています。

  • 頂点カラー(Rチャンネル)と法線、Sineを利用した頂点アニメーション
  • 頂点カラー(Gチャンネル)を利用した着色

シェーダープロパティ

シェーダーのプロパティは以下のようなものを実装しています。

プロパティ名 詳細
Color ポリゴン全体に乗算する色
Extrusion 波のゆれ幅(頂点押し出し量)
Frequency 波の周波数
Fill Color 01 グラデーションの内側の色
Fill Color 02 グラデーションの外側の色
Edge Color 輪郭線の色
Edge Width 輪郭線の太さ

マテリアル設定

マテリアルは以下のように設定しています。

Discussion