💫

【Unity】AnimationCurveで動的に作る剣の軌跡

2020/10/23に公開

はじめに

UnityにはAnimationCurveというものがあります。
その名の通りオブジェクトのアニメーションに使われるものですが、今回これを利用して剣の軌跡を作ってみたいと思います。

メッシュの作成

最初に剣の軌跡となるメッシュを作成します。
divisionNum は軌跡の最初から最後までを何分割するかのパラメーターです。
divisionNumが0の時は四角形1枚で軌跡を表現し、1のときは四角形2枚、2のときは四角形3枚…といった具体です。
この値が大きいほどきれいな軌跡になりますが負荷も増えます。
メッシュは以下の要素で構成されます。

頂点

ポリゴンの頂点です。
4頂点を四角形1枚として、分割数の分だけメモリを確保します。
分割された隣り合う四角形の2点は共有するので、分割数が1増えるごとに頂点は2増えます。
頂点は軌跡によって変化するのでここでは座標は設定しません。

トライアングル

ポリゴンを形成する三角形のリストです。
何番目の頂点を使ってポリゴンを形成するのかのパラメーターです。
ポリゴンは三角形なので3個1セットで、四角形を表現するには6個1セットとなります。

UV

各頂点のテクスチャ座標です。
頂点と同じ数だけメモリを確保します。
軌跡の先頭がテクスチャの左側、軌跡の最後がテクスチャの右側になるようにUVを設定します。

public int divisionNum = 100;   // 軌跡の分割数
Mesh mesh = null;               // メッシュ
Vector3[] vertices = null;      // メッシュの頂点

// メッシュの作成
void Start()
{
    // 頂点は動的に書き換えるので変数に持っておく
    vertices = new Vector3[4 + (divisionNum * 2)];

    // トライアングルの設定
    int[] triangles = new int[6 + (divisionNum * 6)];
    for (int i = 0; i < divisionNum + 1; i++)
    {
        triangles[i * 6 + 0] = i * 2 + 0;
        triangles[i * 6 + 1] = i * 2 + 1;
        triangles[i * 6 + 2] = i * 2 + 2;

        triangles[i * 6 + 3] = i * 2 + 2;
        triangles[i * 6 + 4] = i * 2 + 1;
        triangles[i * 6 + 5] = i * 2 + 3;
    }

    // UVの設定
    Vector2[] uv = new Vector2[4 + (divisionNum * 2)];
    for (int i = 0; i < divisionNum + 2; i++)
    {
        // 軌跡の位置を0~1の範囲で取得する
        float normalize_pos = i / (float)(divisionNum + 1);

        // テクスチャ座標の設定
        uv[i * 2 + 0] = new Vector2(normalize_pos, 0.0f);
        uv[i * 2 + 1] = new Vector2(normalize_pos, 1.0f);
    }

    // メッシュを作る
    mesh            = new Mesh();
    mesh.vertices   = vertices;
    mesh.triangles  = triangles;
    mesh.uv         = uv;
    GetComponent<MeshFilter>().sharedMesh = mesh;
}

メッシュを作ったら MeshFilter に作ったメッシュをセットしてあげます。
メッシュを描画するには MeshFilterとMeshRendererが必要なのでRequireComponentを使って忘れないようにしておくとよいでしょう。

[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class TrailMesh : MonoBehaviour
{
}

軌跡の更新

軌跡を得るためのパラメータです。

public GameObject point1;       // 剣の根本
public GameObject point2;       // 剣の先
public float fadeTime = 0.1f;   // 軌跡がフェードアウトするまでの時間
float enabledTime = 0;          // 軌跡が発生してからの経過時間

// 各座標のアニメーションカーブ
AnimationCurve curveX1 = new AnimationCurve();
AnimationCurve curveY1 = new AnimationCurve();
AnimationCurve curveZ1 = new AnimationCurve();
AnimationCurve curveX2 = new AnimationCurve();
AnimationCurve curveY2 = new AnimationCurve();
AnimationCurve curveZ2 = new AnimationCurve();

軌跡を得るためには始点と終点の座標が必要です。ここではpoint1,point2とします。
軌跡が消えるまでの時間を fadeTime としました。
enabledTimeはAnimationCurveのキーに与える時間になります。
そして、AnimationCurveはpoint1,point2の各軸分用意します。

void UpdateCurve()
{
    // 軌跡が発生してからの経過時間を更新する
    enabledTime += Time.deltaTime;

    // 新しい時間でアニメーションキーを追加する
    curveX1.AddKey(enabledTime, point1.transform.position.x);
    curveY1.AddKey(enabledTime, point1.transform.position.y);
    curveZ1.AddKey(enabledTime, point1.transform.position.z);
    curveX2.AddKey(enabledTime, point2.transform.position.x);
    curveY2.AddKey(enabledTime, point2.transform.position.y);
    curveZ2.AddKey(enabledTime, point2.transform.position.z);

    // 軌跡が消えた後より前の時間のキーは不要なので削除する
    while(curveX1.length > 0)
    {
        if (curveX1.keys[0].time < enabledTime - fadeTime)
        {
            curveX1.RemoveKey(0);
            curveY1.RemoveKey(0);
            curveZ1.RemoveKey(0);
            curveX2.RemoveKey(0);
            curveY2.RemoveKey(0);
            curveZ2.RemoveKey(0);
            continue;
        }
        else
        {
            break;
        }
    }
}

やっていることは単純です。
軌跡が発生してからの経過時間をキーとして始点と終点の各座標を登録します。
そのままだとキーが増え続けてしまうので、軌跡が消える時間よりまえのキーは消してしまいましょう。

メッシュの更新

LateUpdateでメッシュの更新をします。
現在の時間から軌跡が消えるまでの時間を軌跡の分割数で等分し、それぞれの頂点にAnimationCurveで得た座標を設定します。

private void LateUpdate()
{
    UpdateCurve();
    
    // アニメーションの時間がマイナスにならないフェード時間を取得する
    float fade_time = enabledTime - Mathf.Max(enabledTime - fadeTime, 0);

    for (int i = 0; i < divisionNum + 2; i++)
    {
        // 現在の時間から軌跡が消えるまでの時間の間で頂点を形成する
        float vtx_time = enabledTime - (fade_time * (i / (float)(divisionNum + 1)));

        // アニメーションカーブから軌跡の頂点座標を取得する
        Vector3 pos1 = new Vector3(curveX1.Evaluate(vtx_time), curveY1.Evaluate(vtx_time), curveZ1.Evaluate(vtx_time));
        Vector3 pos2 = new Vector3(curveX2.Evaluate(vtx_time), curveY2.Evaluate(vtx_time), curveZ2.Evaluate(vtx_time));

        // ワールド座標からローカル座標に変換して頂点に入れる
        vertices[i * 2 + 0] = transform.InverseTransformPoint(pos1);
        vertices[i * 2 + 1] = transform.InverseTransformPoint(pos2);
    }

    // 頂点の更新
    mesh.vertices = vertices;
    mesh.RecalculateNormals();
    mesh.RecalculateBounds();
}

マテリアルの設定

テクスチャ

テクスチャは左側が軌跡の先頭、右側が軌跡の最後になるようなテクスチャを使用します。
インポート設定の[Wrap Mode]はClampにしておいてください、Repeatのままだと端の描画がおかしくなってしまいます。
今回は下のような単純なフェード画像を使用しました。

シェーダー

シェーダーはポリゴンの両面が表示されるものである必要があります。
Unity標準のものでは Mobile/Particles/Alpha Blended が両面を表示するのでそれを使えばよいでしょう。

最後に

こんな感じになります。
今回紹介したサンプルでは軌跡が出っぱなしになってしまっているので、
次回はAnimationEventsを利用して剣の振りに合わせて軌跡を表示できるように改造していこうと思います。
記事公開しました
【Unity】AnimationEventsで付ける剣の軌跡

Discussion