【ゲーム制作】剣の軌跡(ソードトレイル)の描き方|リングバッファで残像を作る

sword trail thumbnail ゲーム制作

「剣を振ったときに残る、あの光る軌跡ってどうやって描いてるの?」「毎フレームの位置を覚えておけばいいのは分かるけど、具体的にどう管理するの?」「古くなった軌跡はどうやって消えていくの?」

こんな疑問はありませんか?

今回は、アクションゲームで武器を振ったときに残るソードトレイル(剣の軌跡)について、実際にコンパイル・実行した結果を交えながら解説していきます。中身は難しい数式ではなく、リングバッファという考え方1つです。

ソードトレイルとは?

ソードトレイルとは、

・武器の根本と先端の位置を毎フレーム記録し続け、直近数フレーム分をつなげて1枚の帯(リボン)として描画することで軌跡を表現するエフェクト

です。1フレームごとの剣の位置(根本と先端の2点)を「区間(セグメント)」として保存していき、新しい区間ほど太く明るく、古い区間ほど薄く消えていくように描くと、いかにも武器が空気を切り裂いたような軌跡になります。

リングバッファで実装してみる

ポイントは、「今の位置を先頭に追加し、一定数を超えたら一番古いものを捨てる」だけのシンプルな管理です。実際にコンパイル・実行してみます。

struct Segment {
    float rootX, rootY; // 剣の根本の位置
    float tipX, tipY;   // 剣の先端の位置
};

class SwordTrail {
public:
    static const int MAX_SEG = 4; // 説明用に短めに設定(実際はもっと大きい値を使う)
    std::vector segments;

    // 毎フレーム呼ぶ: 今の剣の位置を先頭に追加し、古い履歴を切り捨てる
    void Update(float rootX, float rootY, float tipX, float tipY) {
        segments.insert(segments.begin(), { rootX, rootY, tipX, tipY });
        if ((int)segments.size() > MAX_SEG) {
            segments.pop_back(); // 一番古い履歴を捨てる
        }
    }
};

剣が円弧を描くように6フレーム分振り回した結果です(履歴は最大4区間に制限しています)。

frame 0: segments=1  [(0.0,0.0)-(2.0,0.0)]
frame 1: segments=2  [(0.0,0.0)-(1.8,1.0) (0.0,0.0)-(2.0,0.0)]
frame 2: segments=3  [(0.0,0.0)-(1.1,1.7) (0.0,0.0)-(1.8,1.0) (0.0,0.0)-(2.0,0.0)]
frame 3: segments=4  [(0.0,0.0)-(0.1,2.0) (0.0,0.0)-(1.1,1.7) (0.0,0.0)-(1.8,1.0) (0.0,0.0)-(2.0,0.0)]
frame 4: segments=4  [(0.0,0.0)-(-0.8,1.8) (0.0,0.0)-(0.1,2.0) (0.0,0.0)-(1.1,1.7) (0.0,0.0)-(1.8,1.0)]
frame 5: segments=4  [(0.0,0.0)-(-1.6,1.2) (0.0,0.0)-(-0.8,1.8) (0.0,0.0)-(0.1,2.0) (0.0,0.0)-(1.1,1.7)]

frame 0〜3では区間がどんどん増えていき、MAX_SEG(4)に達したframe 3以降は新しい区間が増えるたびに、一番古い区間が消えているのが分かります。例えばframe 4では、frame 0で記録した(2.0,0.0)の区間がリストから消えています。この「先頭に追加、末尾を捨てる」動きこそがリングバッファの正体です。あとはこのsegments配列を先頭から順に、根本と先端をつなぐ四角形(帯)として描画すれば、軌跡のリボンになります。

【重要】実装を調べていて気づいたこと:両面カリングの罠

実際にソードトレイルを描画するコードを調べていて、地味に見落としやすいポイントに気づきました。トレイルは「表」と「裏」がどちらか分からない薄い帯であることが多く、剣を振る向きとカメラの位置関係によっては、ポリゴンの表裏が入れ替わって、途中から急に見えなくなることがあります。

通常の3Dモデルは「裏側は映さなくていい」ように片面カリング(D3D11_CULL_BACKなど)を有効にして負荷を減らしますが、トレイルのような向きが常に変わる薄い帯には、これが逆に牙を剥きます。対処法は、トレイルを描画するときだけ両面カリング無効(cull none)のラスタライザーステートに切り替えることです。

D3D11_RASTERIZER_DESC rsDesc = {};
rsDesc.FillMode = D3D11_FILL_SOLID;
rsDesc.CullMode = D3D11_CULL_NONE; // 表裏どちらも描画する
device->CreateRasterizerState(&rsDesc, &noCullRasterizerState);

// トレイルを描くときだけ適用する
context->RSSetState(noCullRasterizerState);

常時オフにするのではなく、トレイルを描く直前にだけ切り替えて、描き終わったら元に戻すのがポイントです。他のオブジェクトの描画まで両面カリングのままだと無駄な負荷が増えてしまいます。

ソードトレイルのよくある失敗例と対処法

①片面カリングのままで描いてしまう

上で説明した通りです。カメラの角度によって軌跡が消える不具合が出たら、まずカリング設定を疑いましょう。

②古い区間を捨て忘れてバッファが無限に伸びる

今回のデモのように、上限(MAX_SEG)を超えたら必ず末尾を捨てる処理を入れましょう。捨て忘れると、時間が経つほど頂点数が増え続け、パフォーマンスがじわじわ悪化します。

③攻撃していないときも記録し続けてしまう

常にUpdateを呼び続けると、待機中も剣の位置がずっと同じ区間として記録され、無駄な描画コストがかかります。攻撃モーション中だけ記録し、それ以外はsegmentsを空にしておくと綺麗です。

注意点

  • リングバッファは「先頭に追加、末尾を捨てる」だけのシンプルな仕組み
  • トレイル描画時は両面カリング無効にする
  • 上限を超えたら必ず古い区間を捨てる
  • 記録するのは攻撃モーション中だけに絞る

まとめ

  • ソードトレイルは根本と先端の履歴をリングバッファで管理して帯として描く
  • 上限を超えると自動的に一番古い区間が消えることを実際に確認した
  • 両面カリングを無効にしないと、角度によって軌跡が消える罠がある
  • トレイル描画時だけカリング設定を切り替え、終わったら元に戻す

ソードトレイルの本体はリングバッファというシンプルなデータ構造ですが、実際の描画では「薄い帯には裏表を気にしないカリング設定が必要」という、レンダリング特有の落とし穴があることも分かりました。

ここまで読んでくださりありがとうございました。

関連記事