Unity の Transform のパフォーマンス最適化まとめ

（今更ですが）毎フレーム大量の Transform を扱う機会があったので、Unity 上で Transform を更新する際の手法とパフォーマンスの比較・最適化の方法のまとめを。

毎フレーム数百かそれ以上の Transform の .position や .rotation / .eulerAngles へのアクセスや変更がある場合には考慮する意味がある、といった内容です。Transform 沼にハマっているなら。



<!--
https://qiita.com/sator_imaging/items/ff5811885f515a0a4998
-->



#### 備忘録



- [はじめに](#はじめに)
- [【重要】 rotation / eulerAngles の xyzw へのアクセスは必ずキャッシュする](#重要-rotation--eulerangles-の-xyzw-へのアクセスは必ずキャッシュする)
    - [Transform だけキャッシュするのではなく、Quaternion や Vector3 もキャッシュ](#transform-だけキャッシュするのではなくquaternion-や-vector3-もキャッシュ)
- [localRotation が一番高速](#localrotation-が一番高速)
- [オイラー角よりもクオータニオンの方が高速](#オイラー角よりもクオータニオンの方が高速)
- [ワールドスペースよりローカルスペースの方が高速](#ワールドスペースよりローカルスペースの方が高速)
    - [ルートのみワールド空間で扱う](#ルートのみワールド空間で扱う)
- [localEulerAngles += よりも Transform.Rotate()](#localeulerangles--よりも-transformrotate)
- [使えるなら HumanPoseHandler.SetHumanPose() を使う](#使えるなら-humanposehandlersethumanpose-を使う)
- [検証動画](#検証動画)
    - [シーン構成](#シーン構成)
- [まとめ](#まとめ)
    - [クオータニオンは高速](#クオータニオンは高速)
    - [オフセットを加える場合は = Quaternion * Quaternion が一番高速](#オフセットを加える場合は--quaternion--quaternion-が一番高速)
    - [オイラー角は厳禁](#オイラー角は厳禁)
    - [どうしてもオイラー角でオフセットを指定したい](#どうしてもオイラー角でオフセットを指定したい)
    - [Quaternion / Vector3 の xyzw メンバーへのアクセス自体が重い](#quaternion--vector3-の-xyzw-メンバーへのアクセス自体が重い)
    - [HumanPoseHandler の SetHumanPose の場合](#humanposehandler-の-sethumanpose-の場合)
    - [こちらもどうぞ](#こちらもどうぞ)



<!--more-->



## はじめに

面倒くさがってビルドして試さずに、すべてエディター上で確認しています。
また、.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);``

など、メンバーに３回アクセスすると、３回分の Vector3 のコピーが行われる。らしいです。

↓ ↓ ↓

> Transform.position や Transform.rotation、Transform.eulerAngles 等はフィールドではなくプロパティで getter がセットされていて、アクセスのたびに構造体のコピーが行われています。

（というようなことが、ネットのどこかに書いてあった気がしますが失念）



--


キャッシュだけでなく「どうやって変更を加えるか」もパフォーマンスに影響を与えます。

細かなパフォーマンス比較の前にざっくりとまとめると、

↓ ↓ ↓


## localRotation が一番高速

でした。


## オイラー角よりもクオータニオンの方が高速

でした。



## ワールドスペースよりローカルスペースの方が高速

.rotation よりも .localRotation、.eulerAngles よりも .localEulerAngles の方が高速でした。


### ルートのみワールド空間で扱う

場合によってはルートのみワールド空間の値を扱い、それ以下のオブジェクトはすべてローカル空間の値を扱うなど。

テストではローカル値を扱うようにするだけで倍近いスピードに。


## localEulerAngles += よりも Transform.Rotate()

オフセットの調整等はオイラー角の方が直感的なので、`.localEulerAngles += .....` としがちですが、Transform.Rotate() の方が高速です。

数が多い場合は結構なパフォーマンスへのインパクトがあります。

- [Unity - Scripting API: Transform.Rotate](https://docs.unity3d.com/ScriptReference/Transform.Rotate.html)
   <iframe
      class="embed-card embed-webcard"
      scrolling="no"  frameborder="0"
      style="
         display: block;  border: none;
         margin: 10px 0px;
         width: 100%; height: 155px;
         max-width: 500px;
      "
      src="https://hatenablog-parts.com/embed?url=https://docs.unity3d.com/ScriptReference/Transform.Rotate.html">
   </iframe>



## 使えるなら HumanPoseHandler.SetHumanPose() を使う

対象が Humanoid の場合、HumanPoseHandler の SetHumanPose の方が高速な場合があります。

以下がとても参考になります。




- [Bizcast/NoitomHi5InertiaToFingerMuscle](https://github.com/Bizcast/NoitomHi5InertiaToFingerMuscle)
   <iframe
      class="embed-card embed-webcard"
      scrolling="no"  frameborder="0"
      style="
         display: block;  border: none;
         margin: 10px 0px;
         width: 100%; height: 155px;
         max-width: 500px;
      "
      src="https://hatenablog-parts.com/embed?url=https://github.com/Bizcast/NoitomHi5InertiaToFingerMuscle">
   </iframe>




## 検証動画

パフォーマンスの比較を行ったシーンは以下の通り。

フレームレートに大きな変化が出やすい数の Transform を更新してますが、場合によってはキャラ数体の場合でも、秒間 10fps 程度の変化がある可能性も。



### シーン構成

95 個の muscles / Transform を１フレームに 100 回更新 ＝ 秒間に約 10,000～ Transform の向きを更新するテストを行った動画。

※ 揺れもの無しの人型キャラクター 100 体分のボーン数、ポリゴン自体は１体分




<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Unity の Transform のパフォーマンス最適化<br><br>誰かがすでに調べ切っている気がするけども… オイラー角遅すぎる <a href="https://t.co/hvLnTMIGnb">pic.twitter.com/hvLnTMIGnb</a></p>&mdash; サトー (@sator_imaging) <a href="https://twitter.com/sator_imaging/status/1146558650753769472?ref_src=twsrc%5Etfw">July 3, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>




## まとめ


変更の加え方、アクセスの仕方など、各パフォーマンスの比較とまとめ。

- 可能な限りオイラー角は扱わない。
- .eulerAngles / .localEulerAngles に値をセットする・メンバーにアクセスすると極端にパフォーマンスが落ちる。
- Quaternion / Vector3 のメンバーにアクセスするのはコストがかかるので必ずキャッシュする。
    - Transform ではなく、Quaternion / Vector3 をキャッシュ。


↓ 詳細 ↓


### クオータニオンは高速

取得した値を直接放り込むなら、SetHumanPose よりも高速。


- .localRotation を変更した場合のフレームレート
    - pseudo: `.localRotation = Time.time;`
        - 約 800fps

- .rotation を変更した場合のフレームレート
    - pseudo: `.rotation = Time.time;`
        - 約 400fps



### オフセットを加える場合は = Quaternion * Quaternion が一番高速

各キャラクターごとの差分の吸収など、回転のオフセットは可能な限りクオータニオンで扱う。オイラー角を扱わなければ、SetHumanPose よりもパフォーマンスが出る。


- .rotation にクオータニオンのオフセットを加えて変更
    - pseudo: `.rotation = Time.time * Quaternion`
        - 約 360fps


### オイラー角は厳禁


オイラー角を扱うだけで何をしても重いので、可能な限り使わない。


1. .localEulerAngles を変更した場合のフレームレート
    - pseudo: `.localEulerAngles = Time.time;`
        - 約 500fps

1. .eulerAngles を変更した場合のフレームレート
    - pseudo: `.eulerAngles = Time.time;`
        - 約 300fps



### どうしてもオイラー角でオフセットを指定したい


ワールド空間のクオータニオン値をセットしてから、オイラー角でオフセットの数値を入力する必要がある場合は Transform.Rotate() を Space.Self で使う。


- .rotation をセットしてから Transform.Rotate() でオフセットした場合のフレームレート
    - pseudo: `.rotation = Time.time; Transform.Rotate();`
        - 約 210fps
    - pseudo: `.rotation = Time.time; .localEulerAngles += new Vector3()` の場合
        - 約 175fps
        - 意味わからん



### 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` に落とし込むのが一番高速


- .eulerAngles の各メンバーにキャッシュ無しでアクセスした場合（3回のアクセス）
    - pseudo: `.eulerAngles = transform.eulerAngles.xyz + Time.time;`
        - 約 110fps
    - キャッシュすると 約 190fps に
        - pseudo: `.eulerAngles = cachedEulerAngles.xyz + Time.time;`




### HumanPoseHandler の SetHumanPose の場合


Avatar が Humanoid で無いと動かない、Humanoid 依存のソフトウェア・コンポーネントにしたくはないので、あまりちゃんと調べてないですが…。

- HumanPoseHandler.SetHumanPose を使用した場合のフレームレート
    - 約 250fps



--

### こちらもどうぞ

- [#VTuber開発まとめ - Qiita](https://qiita.com/sator_imaging/items/5d3b4466a2a1539184d0)
   <iframe
      class="embed-card embed-webcard"
      scrolling="no"  frameborder="0"
      style="
         display: block;  border: none;
         margin: 10px 0px;
         width: 100%; height: 155px;
         max-width: 500px;
      "
      src="https://hatenablog-parts.com/embed?url=https://qiita.com/sator_imaging/items/5d3b4466a2a1539184d0">
   </iframe>