(今更ですが)毎フレーム大量の Transform を扱う機会があったので、Unity 上で Transform を更新する際の手法とパフォーマンスの比較・最適化の方法のまとめを。
毎フレーム数百かそれ以上の Transform の .position や .rotation / .eulerAngles へのアクセスや変更がある場合には考慮する意味がある、といった内容です。Transform 沼にハマっているなら。
備忘録
- はじめに
- 【重要】 rotation / eulerAngles の xyzw へのアクセスは必ずキャッシュする
- localRotation が一番高速
- オイラー角よりもクオータニオンの方が高速
- ワールドスペースよりローカルスペースの方が高速
- localEulerAngles += よりも Transform.Rotate()
- 使えるなら HumanPoseHandler.SetHumanPose() を使う
- 検証動画
- まとめ
はじめに
面倒くさがってビルドして試さずに、すべてエディター上で確認しています。
また、.rotation / .eulerAngles を中心に確認していますが .position / .scale も同様の扱いかと思います。
- Unity 2018.4.0f1(Unity 2018 LTS)
- Windows 10 64bit @ Intel Core i9-9900K 3.6GHz
--
Humanoid キャラ一体の骨が 60~ 以上あったりするので、値の取得含めた Transform の操作は、油断しているとすぐ数百単位になる。そして Transform の値の取得に大きな罠があります。
【重要】 rotation / eulerAngles の xyzw へのアクセスは必ずキャッシュする
これがかなり重要。
場合によっては数十%のパフォーマンスへのインパクトがあります。Transform のキャッシュでは(場合によっては)足りない。
Transform だけキャッシュするのではなく、Quaternion や Vector3 もキャッシュ
vector3.x = 0
は出来るのに、transform.position.x = 0
は出来ない、ということで、こいつらは少し特殊な扱いです。Transform をキャッシュしていたとしても、
..... = new Vector3(cache.eulerAngles.x, cache.eulerAngles.y, cache.eulerAngles.z);
など、メンバーに3回アクセスすると、3回分の Vector3 のコピーが行われる。らしいです。
↓ ↓ ↓
Transform.position や Transform.rotation、Transform.eulerAngles 等はフィールドではなくプロパティで getter がセットされていて、アクセスのたびに構造体のコピーが行われています。
(というようなことが、ネットのどこかに書いてあった気がしますが失念)
--
キャッシュだけでなく「どうやって変更を加えるか」もパフォーマンスに影響を与えます。
細かなパフォーマンス比較の前にざっくりとまとめると、
↓ ↓ ↓
localRotation が一番高速
でした。
オイラー角よりもクオータニオンの方が高速
でした。
ワールドスペースよりローカルスペースの方が高速
.rotation よりも .localRotation、.eulerAngles よりも .localEulerAngles の方が高速でした。
ルートのみワールド空間で扱う
場合によってはルートのみワールド空間の値を扱い、それ以下のオブジェクトはすべてローカル空間の値を扱うなど。
テストではローカル値を扱うようにするだけで倍近いスピードに。
localEulerAngles += よりも Transform.Rotate()
オフセットの調整等はオイラー角の方が直感的なので、.localEulerAngles += .....
としがちですが、Transform.Rotate() の方が高速です。
数が多い場合は結構なパフォーマンスへのインパクトがあります。
使えるなら HumanPoseHandler.SetHumanPose() を使う
対象が Humanoid の場合、HumanPoseHandler の SetHumanPose の方が高速な場合があります。
以下がとても参考になります。
検証動画
パフォーマンスの比較を行ったシーンは以下の通り。
フレームレートに大きな変化が出やすい数の Transform を更新してますが、場合によってはキャラ数体の場合でも、秒間 10fps 程度の変化がある可能性も。
シーン構成
95 個の muscles / Transform を1フレームに 100 回更新 = 秒間に約 10,000~ Transform の向きを更新するテストを行った動画。
※ 揺れもの無しの人型キャラクター 100 体分のボーン数、ポリゴン自体は1体分
Unity の Transform のパフォーマンス最適化
— サトー (@sator_imaging) July 3, 2019
誰かがすでに調べ切っている気がするけども… オイラー角遅すぎる pic.twitter.com/hvLnTMIGnb
まとめ
変更の加え方、アクセスの仕方など、各パフォーマンスの比較とまとめ。
- 可能な限りオイラー角は扱わない。
- .eulerAngles / .localEulerAngles に値をセットする・メンバーにアクセスすると極端にパフォーマンスが落ちる。
- Quaternion / Vector3 のメンバーにアクセスするのはコストがかかるので必ずキャッシュする。
- Transform ではなく、Quaternion / Vector3 をキャッシュ。
↓ 詳細 ↓
クオータニオンは高速
取得した値を直接放り込むなら、SetHumanPose よりも高速。
-
.localRotation を変更した場合のフレームレート
- pseudo:
.localRotation = Time.time;
- 約 800fps
- pseudo:
-
.rotation を変更した場合のフレームレート
- pseudo:
.rotation = Time.time;
- 約 400fps
- pseudo:
オフセットを加える場合は = Quaternion * Quaternion が一番高速
各キャラクターごとの差分の吸収など、回転のオフセットは可能な限りクオータニオンで扱う。オイラー角を扱わなければ、SetHumanPose よりもパフォーマンスが出る。
- .rotation にクオータニオンのオフセットを加えて変更
- pseudo:
.rotation = Time.time * Quaternion
- 約 360fps
- pseudo:
オイラー角は厳禁
オイラー角を扱うだけで何をしても重いので、可能な限り使わない。
-
.localEulerAngles を変更した場合のフレームレート
- pseudo:
.localEulerAngles = Time.time;
- 約 500fps
- pseudo:
-
.eulerAngles を変更した場合のフレームレート
- pseudo:
.eulerAngles = Time.time;
- 約 300fps
- pseudo:
どうしてもオイラー角でオフセットを指定したい
ワールド空間のクオータニオン値をセットしてから、オイラー角でオフセットの数値を入力する必要がある場合は Transform.Rotate() を Space.Self で使う。
- .rotation をセットしてから Transform.Rotate() でオフセットした場合のフレームレート
- pseudo:
.rotation = Time.time; Transform.Rotate();
- 約 210fps
- pseudo:
.rotation = Time.time; .localEulerAngles += new Vector3()
の場合- 約 175fps
- 意味わからん
- pseudo:
Quaternion / Vector3 の xyzw メンバーへのアクセス自体が重い
前述の通り transform.position.x = 0
が出来ない、思っているのと違う奴らです。
数が多い場合、軽い気持ちでメンバーにアクセスするとパフォーマンスへのインパクトが凄いことに。
複数回アクセスする場合は、Transform の .rotation や .eulerAngles は var cached = transform.rotation
等するだけで劇的に高速に。
.... transform.position.x
とか気軽に使いがちだけど、アクセスする数が多い場合は厳禁。必ずキャッシュ。
-
.rotation の各メンバーにキャッシュ無しでアクセスした場合(4回のアクセス)
- pseudo:
.rotation = transform.rotation.xyzw + Time.time;
- 約 200fps
- キャッシュすると 約 320fps に
- pseudo:
.rotation = cachedRotation.xyzw + Time.time;
- クオータニオンのメンバーを直接弄ることはないだろうけど、オフセットを適用するなら
Quaternion * Quaternion
に落とし込むのが一番高速
- pseudo:
- pseudo:
-
.eulerAngles の各メンバーにキャッシュ無しでアクセスした場合(3回のアクセス)
- pseudo:
.eulerAngles = transform.eulerAngles.xyz + Time.time;
- 約 110fps
- キャッシュすると 約 190fps に
- pseudo:
.eulerAngles = cachedEulerAngles.xyz + Time.time;
- pseudo:
- pseudo:
HumanPoseHandler の SetHumanPose の場合
Avatar が Humanoid で無いと動かない、Humanoid 依存のソフトウェア・コンポーネントにしたくはないので、あまりちゃんと調べてないですが…。
- HumanPoseHandler.SetHumanPose を使用した場合のフレームレート
- 約 250fps
--