仙台の3Dゲームエンジニア、にしきんです。
前回、BatchRendererGroupでSSBOを用いた効果的なインスタンシング及び独自カリングによってパフォーマンスが上がるというような話をしました。記事の最後でAnimationの独自実装について触れていますが、そこに到達するためにいくつかの下準備があります。
その中からまず、便利に独立している内容の記事としてAnimationCurveについて詳解したいと思います。実は、UnityのAnimationCurveはスレッドアンセーフであるためJob/Burstで利用できないのです。そのため、Jobを利用するような独自システムに組み込む場合は自前で実装する必要があります。
最初に断っておきますがこれはとても簡単な話です。解説するつもり自体元々なく、というのも公式がAnimationCurveの評価ロジックコードをどこかに公開していると勘違いしていたのですが、調べてみたらどこにも載ってないようで(どこかにあったらごめんなさい)、そのためにこの記事を書いています。
公式に記載されていない以上私の方の実装は独自で再現したものですが、実際のところ再現のためにUnity側のロジックコードを知る必要はないです。何故ならAnimationCurveは、何かUnityが独自の計算式で不思議なことをやっているようなものでは全くないからです。Keyframe構造体に格納されている情報を見ればそれは明らかで、独自実装のために必要十分な情報があります。
AnimationCurveの基本

UnityのAnimationCurveは横軸と縦軸について値を持ちます。
これはその曲線の図からも明らかなのですが、TangentについてConstantを利用しない限りはCubicなベジェ曲線によって算出される値です。Constantは動きを見ていれば簡単に理解できるので解説を省きます。

また、二つのKeyframe間には制御点が二つ配置されます。よくある手法だという印象ですが、Keyframeに紐づくハンドルで制御します。これにはWeightedモード(ハンドルの長さを変更できるモード)とNoneモード(ハンドルの長さが固定になるモード)があります。
値を実際に利用する際は、Evaluate関数で横軸の値を指定することで、縦軸の値を取り出すことが出来ます。
なぜスレッドアンセーフか
ここまでの話でアンセーフな理由は無いように感じますが、どうやら値をキャッシュしているというらしいという公式スタッフの解答がForumにあります。なんであれ最低限Structであってほしい気がしますが(AnimationCurveはClassでありこの観点でもBurstと合わない)、古いAPIですからね。

値の導出の流れ
行いたいことはスレッドセーフな評価です。AnimationCurve自体を使わずに値を評価する必要がありますが、予めAnimationCurveからKeyframe構造体の中身を取り出して、それに対する評価ロジックを記述することで互換性を保ちながら独自化が出来ます。
Cubicベジェであることさえ把握していれば、Keyframeの構造体の中身を見て評価するためにすべきことは分かるでしょう。Evaluate関数のようなもので指示される横軸の値からCubicベジェ曲線のパラメータを求める処理として機能すればAnimationCurve相当の処理として成立したことになります。実装の注意点を一つ挙げておくと、与えられた横軸の値をそのままベジェのパラメータtに利用しないように注意しましょう。
実装結果

上のGIFのように、パラメータに対応した動きを再現できています。


ということで、再現できているように見えますね。厳密な差分は時間の都合で取っていないですが、計算誤差程度のものとしてはあるはずです。
尚、私の実装ではUnity同様に値をキャッシュできるラッパーも作っています。場合によってはパフォーマンスのためにこのような対応をするべきでしょう。

Unityがどのようなキャッシュ戦略をとってるのかは流石に分からないのですが、ここはユースケースに合わせた実装でよいと思います。特にスレッドアンセーフなところはUnityを模倣する必要はないので、そのような作りは避けたほうがよいでしょう。
おわりに
今回の記事はファンシーさに欠けるテーマだったかもしれません。ただカーブは色々なところで利用したいものなので、Jobから呼び出せると嬉しいことでしょう。嬉しいどころか、普通に必須だと思います。
この件に限らずUnityのソリューションをそのまま使っていい場面って思いのほか少ないですよね。個人的な意見ですが、マルチプラットフォーム用の抽象化API群とレベルエディタだ、くらいの感覚で臨むとUnityの価値を最大化できると思ってます。責任を持つべきところはきちんと認識していきましょう。
ILでは3Dゲーム開発にも取り組んでおります。興味がある方は是非採用情報をご確認ください。