SplineComponent は3次関数でベクトルで微分で
- はじめに
- SplineComponent と『時間』について
- セグメントについて
- セグメントの数式
- その式は一体どこから来るのか
- 制御点との関係およびセグメントの連結
- 接ベクトル(Tangent)について
- 媒介変数 t と SplineComponent における時間との関係
- SplineComponent から SplineMesh へ
- セグメントの分割
- おわりに
はじめに
とりあえず公式ドキュメント
お手軽に曲線を得る手段としてみんな大好き SplineComponent について少し掘り下げる記事です。
既に使用している方はご存じかと思いますが、おおざっぱな性質として
「制御点を通して滑らかな曲線が作れる」
あるいはもう少し踏み込んで
「『時間』に対して滑らかに変化する座標やら姿勢やらが出力される」
というものであって、通常の操作においてはこのくらいの認識でも大丈夫だったりはします。
ただ、少し凝った操作をしようとして「予想した挙動と違う」「期待する結果が得られない」といった事態に直面した時、中身についてある程度把握していると問題の解決に役立つこともあるでしょう。
内容について:
- SplineComponent についての基本的な説明は含みません。
- ある程度数学的な話を含みます。
ベクトルと微分が分かるとフルに消化できるかと思います。 - SplineComponent からは座標の他に 回転/スケール を得ることができますが、
これらは設定と計算の内容が異なるのでここでは扱いません。 - なるべく基本的なところから順に書きましたがわりと徒然としています。
ご容赦いただければ幸いです。
よければ冒頭の目次を活用してください。
SplineComponent と『時間』について
先ほど「『時間』に対して~」と書きましたが、曲線は時間と特別な関係にはありません。
図形としての曲線は全体が同時に存在していますし、時間以外のパラメータをもって曲線上の点をサンプリングしても良いわけです。
ただ、時間経過によって点が移動するという状況が人間にとって比較的イメージしやすいので便宜上時間として言い表しているのかもしれません(関数・変数の名前としても Time とか Duration とかの語が使用されています)。
仮に時間としているけれど別のものと見なしても良い、ということを知っておけばここはOKでしょう。
SplineComponent にはそんな時間を扱う変数・関数が幾つかあります。
曲線に沿って何かを動かすような処理においてよく使用することになるので、触れたことのある方も多いでしょう。
- Duration : 曲線全体の始点から終点までの時間を表す変数(始点の時刻は0なので終点の時刻を表すとも言える)。デフォルトでは 1.0
- GetLocationAtTime() : 時間を入力して曲線上の位置を得る関数。0を入れれば始点、上記 Duration と同じ値を入れれば終点を得る。
他にもあります。詳しくは公式ドキュメントをご覧ください。
セグメントについて
SplineComponent の与える曲線は見かけ上は滑らかな一本の曲線のように振る舞い、前述したように 0~Duration の連続した時間に対して滑らかな軌跡を描きます。
しかし内部的には制御点で区切られた曲線を繋ぎ合わせたものになっています。
ClosedLoop 設定でなければ [制御点数-1] の曲線からなり、一つ一つの曲線をセグメント(Segment)と呼びます。
セグメントの数式
各セグメントは、0.0 から 1.0 を値にとる媒介変数 t についての3次関数で表現されています。
具体的には以下のような式です。
色々と記号が出てきますが、
- p(t) : カーブの値。例えば座標。
- t : 媒介変数(0~1)
- p0 : 始点(t=0)での値
- p1 : 終点(t=1)での値
- m0 : 始点(t=0)での p の変化率、あるいは接ベクトル(Tangent)
- m1 : 終点(t=1)での p の変化率、あるいは接ベクトル(Tangent)
のような意味です。
つまり p0, p1, m0, m1 がカーブを決定するパラメータであり、セグメント毎にこの組み合わせが異なるということになります。
なお、ここで t 以外はベクトルです。
その式は一体どこから来るのか
p(t) = a*t^3+b*t^2+c*t+d
のように一般的な3次式を作り、
p(0) = p0
p(1) = p1
p'(0) = m0
p'(1) = m1
を連立して a, b, c, d を求め (p0, p1, m0, m1 で表す) まとめ直すと出来上がります。
もっとスマートな導出や説明の方法があるのかもしれませんが私は知りません。
制御点との関係およびセグメントの連結
各制御点を編集するということは、それにつながるセグメント端の座標および接ベクトル(p0, m0 あるいは p1, m1)を設定するということです。

(このエディタでは中央のハンドルで座標を、両端のハンドルで接ベクトルを編集できます)
制御点は隣り合うセグメント間で共有されています。
したがって制御点から伸びる両セグメントの座標と接ベクトルが一致(※)しますので見かけ上はセグメント同士が滑らかに接続されるということになります。
※)
Segments[n].p1 = Segments[n+1].p0
Segments[n].m1 = Segments[n+1].m0
のような関係になります。
接ベクトル(Tangent)について
SplineComponent にも接ベクトル(Tangent)を取得する関数が備わっていますが、これについて少し注意が必要です。
この文脈での接ベクトルとはいわゆる接線の方向ベクトルではなく、媒介変数に対する値の変化率、ようは微分値です。
つまり
m = dp/dt
という関係です。
ベクトルの長さにも意味がありますので、接ベクトルに対して Normalize を行と必要な情報を失う場合があります。
なお、接ベクトルはやはり接線と平行です。
媒介変数 t と SplineComponent における時間との関係
セグメントの媒介変数 t と SplineComponent の時間(Time)とはわりと単純な関係にあり、互いの変化率は単に比例します。
Time から t を得る計算を疑似コードで書くと
float Key = ( Time / Duration ) * NumSegments;
int SegmentIndex = floor(Key);
float T = Key - SegmentIndex;
のような関係です。
一行目で得た Key は整数部分でセグメント番号、小数部分でセグメント内の位置を指す値ということになります。SplineComponent にはこれを扱う関数もあります(そこでは InputKey というフレーズが使用されています)。
SplineComponent から SplineMesh へ
SplineComponent と似た機能として SplineMeshComponent というものがあり、こちらはカーブに沿ってメッシュを変形させるものです。
SplineMeshComponent はセグメントを一つしか持たないので SplineComponent で作成したスプライン曲線全体を正確にカバーすることは一般にはできません。
1セグメントに対して1メッシュで満足である場合、以下の記事にあるようにセグメント数ぶんだけ SplineMeshComponent を作成することで対応できます。
セグメントの分割
セグメントを(形状を保って)統合することは一般には不可能ですが(異なる曲線であるため)、一つのセグメントをカーブの形を保ったまま分割することは可能です。
同一セグメント上から2点の座標及び接ベクトルをサンプリングし、媒介変数の変化量が1になるよう接ベクトルをスケールして出来上がりです(計算上は、少し不思議に感じるかもしれませんが接ベクトルに対して元の媒介変数の変化量を乗算することになります)。
BPでの簡単な例を載せておきます。
整数部分を同じくする StartKey, EndKey の区間でカーブを切り出して SplineMesh に設定します。
ここで StartKey, EndKey の整数部分の異なる値を設定した場合は切り出す範囲が複数のセグメントにまたがり、元のカーブに一致しない結果を得ます。


上記の例では InputKey(≒ Time)ベースで切り出し範囲を指定しましたが、カーブの長さ(カーブ上の距離)を元にして区間を切りたい場合は GetInputKeyValueAtDistanceAlongSpline 関数が便利です。
おわりに
SplineComponent の計算内容について少し掘り下げました。
このあたりを把握しておくことで Spline の加工で融通を利かせられるようになる、かもしれません。