💫

【Unity】AnimationEventsで付ける剣の軌跡

2020/10/30に公開

はじめに

【Unity】AnimationCurveで動的に作る剣の軌跡で作った軌跡を実際に付けてみます。
今回のコードは上記ページで紹介したコードからの差分となります。

軌跡をON/OFFできるようにする

前回紹介したコードでは軌跡をON/OFFできる機能が無いので追加しましょう。

追加するパラメーター

軌跡をON/OFFするために現在ONの状態なのかどうか、ONになってからの経過時間のパラメーターを追加します。
OFFにした後パッと消えてしまったら見栄えが悪いのでOFFにしてから完全に消えるまでの余韻のパラメーターも用意しておきましょう。

public float fadeTime = 0.2f;   // 軌跡がフェードアウトするまでの時間
float enabledTime = 0;          // 有効になってからの経過時間
bool trailEnabled = false;      // 軌跡を描くかどうか
float stopFadeTime = 0;         // 軌跡を止めてからの経過時間

軌跡をONにするメソッド

軌跡を有効にするとき、AnimationCurveのキーをクリアしてレンダラーを有効にします。
AnimationCurveにClearメソッドがあればよかったのですが無いようなので作り直します。

public void TrailOn()
{
    trailEnabled = true;
    curveX1 = new AnimationCurve();
    curveY1 = new AnimationCurve();
    curveZ1 = new AnimationCurve();
    curveX2 = new AnimationCurve();
    curveY2 = new AnimationCurve();
    curveZ2 = new AnimationCurve();
    meshRenderer.enabled = true;
}

軌跡をOFFにするメソッド

軌跡の余韻を出すためここでは時間を保存してフラグを折るだけにします。

public void TrailOff()
{
    trailEnabled = false;
    stopFadeTime = enabledTime;
}

軌跡の余韻を作る

軌跡の更新時、TrailOffが呼ばれた時間から余韻の時間が過ぎたら描画を停止します。

void UpdateCurve()
if (!meshRenderer.enabled) return;

if (!trailEnabled)
{   // OFFにした後の余韻の時間が過ぎたらレンダリングを止める
    if(enabledTime >= stopFadeTime + fadeTime)
    {
        meshRenderer.enabled = false;
    }
    return;
}

軌跡の描画ではTrailOffが呼ばれた時点からの経過時間で軌跡のフェード時間を短くすることで軌跡を収束させていきます。

void LateUpdate()
// アニメーションの時間がマイナスにならないフェード時間を取得する
float fade_time = enabledTime - Mathf.Max(enabledTime - fadeTime, 0);
if (!trailEnabled)
{   // 軌跡をOFFにした後の余韻の時間
    fade_time = fade_time - (enabledTime - stopFadeTime);
}

モーションに合わせて軌跡を作る

文章だけだと分かりづらいので動画を用意しました。
剣を持たせるキャラクターにはVRoidのキャラクターを使用しています。
VRoidキャラクターをUnityで使う方法はこちらをご覧ください。

キャラクターに剣を持たせる

キャラクターの右手のオブジェクトに剣のオブジェクトを置きます。
(動画の20秒目くらい)

軌跡の始点と終点を設定する

空のGameObjectを2つ作成して剣の根本と先端に合わせます。
(動画の50秒目くらい)

軌跡コンポーネントをセットする

AnimatorControllerがアタッチされているオブジェクトに軌跡をアタッチし、先ほど作成したGameObjectをアタッチします。
MeshRendererが一緒にアタッチされるので、両面描画されるシェーダーを割り当てたマテリアルをセットします。
(動画の1分10秒目くらい)

アニメーションにイベントを埋め込む

アニメーションのインポートセッティングのEventsに軌跡の開始タイミングにTrailOnのキーを停止タイミングにTrailOffのキーを追加します。
タイミングはゲームの実行中でも調整できるので最初は適当に当てて、ゲームをプレイしながら微調整するとやりやすいです。
(動画の2分目から)

弱点

それなりに見える軌跡ですが、動的に作る剣の軌跡には弱点があります。
弱点を把握しつつ使いどころを見つけていきましょう。

ポリゴンがねじれる

ねじれが発生することで描画がおかしくなることがあります。
分割数を多く持つことで軽減できますが根本の解決は難しいです。

処理落ちすると形状が安定しない

処理落ちでモーションのフレームがスキップされることでアニメーションカーブのキーも飛ばされるし、アニメーションイベントのタイミングもズレるので軌跡の形状が安定しません。

最後に

コードの差分だけだと分かりづらいのでコードの全文を添付します。

TrailMesh.cs
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class TrailMesh : MonoBehaviour
{
    public GameObject point1;       // 剣の根本
    public GameObject point2;       // 剣の先
    public float fadeTime = 0.2f;   // 軌跡がフェードアウトするまでの時間
    public int divisionNum = 200;   // メッシュの分割数

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

    Mesh mesh = null;           // メッシュ
    float enabledTime = 0;      // 有効になってからの経過時間
    Vector3[] vertices = null;  // メッシュの頂点

    bool trailEnabled = false;  // 軌跡を描くかどうか
    float stopFadeTime = 0;
    MeshRenderer meshRenderer = null;

    public void TrailOn()
    {
        trailEnabled = true;
        curveX1 = new AnimationCurve();
        curveY1 = new AnimationCurve();
        curveZ1 = new AnimationCurve();
        curveX2 = new AnimationCurve();
        curveY2 = new AnimationCurve();
        curveZ2 = new AnimationCurve();
        meshRenderer.enabled = true;
    }

    public void TrailOff()
    {
        trailEnabled = false;
        stopFadeTime = enabledTime;
    }

    void Start()
    {
        CreateMesh();
        GetComponent<MeshFilter>().sharedMesh = mesh;

        meshRenderer = GetComponent<MeshRenderer>();
        meshRenderer.enabled = false;
    }

    void CreateMesh()
    {
        // 頂点は動的に書き換えるので変数に持っておく
        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)];
        Color[] colors = new Color[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;
    }

    void UpdateCurve()
    {
        if (!meshRenderer.enabled) return;

        // 有効になってからの時間を更新する
        enabledTime += Time.deltaTime;
        if (!trailEnabled)
        {   // OFFにした後の余韻の時間が過ぎたらレンダリングを止める
            if(enabledTime >= stopFadeTime + fadeTime)
            {
                meshRenderer.enabled = false;
            }
            return;
        }

        // 新しい時間でアニメーションキーを追加する
        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;
            }
        }
    }

    // メッシュを更新する
    private void LateUpdate()
    {
        UpdateCurve();

        if (!meshRenderer.enabled) return;

        // アニメーションの時間がマイナスにならないフェード時間を取得する
        float fade_time = enabledTime - Mathf.Max(enabledTime - fadeTime, 0);
        if (!trailEnabled)
        {   // 軌跡をOFFにした後の余韻の時間
            fade_time = fade_time - (enabledTime - stopFadeTime);
        }

        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();
    }
}

Discussion