【Unity】3Dの会話シーンでも2D的な表現をしたい - カメラに対して同じ角度を保つ

拙作の3D ADVにて発生したちょっとした修正のお話。

# この記事の対象
- Unity、C#初心者
- Unityの角度制御について理解したい方

# 発生した問題と修正結果

初めはこのようにモーションを作っていました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">アニメーションの連続再生もできた！！！<br>セリフに合わせて表示もされるようになった！！！<br>勝った！！！！！！！<a href="https://twitter.com/hashtag/gamedev?src=hash&amp;ref_src=twsrc%5Etfw">#gamedev</a> <a href="https://t.co/KTLN6AiIMJ">pic.twitter.com/KTLN6AiIMJ</a></p>&mdash; 殻栗ポルタ🌰⚙️3D ADV 制作中 (@Karakuri_Polta) <a href="https://twitter.com/Karakuri_Polta/status/1832069720511545752?ref_src=twsrc%5Etfw">September 6, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

いい感じのアニメーションになったのですが、複数のキャラクターを左右に並べて表示したところ……

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">モデルの位置の変更、入退場、複数モデルの管理ができるようになった！！！！<br>もう3D ADVの基礎はできたと言っても過言じゃない<a href="https://twitter.com/hashtag/gamedev?src=hash&amp;ref_src=twsrc%5Etfw">#gamedev</a> <a href="https://t.co/66php8Qe3g">pic.twitter.com/66php8Qe3g</a></p>&mdash; 殻栗ポルタ🌰⚙️3D ADV 制作中 (@Karakuri_Polta) <a href="https://twitter.com/Karakuri_Polta/status/1835357494153245056?ref_src=twsrc%5Etfw">September 15, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

**君たち、なんか角度違くない？**

原因は単純。左右に並べたことでカメラとの相対的な角度が変わってしまったからです。

![POWERPNT_q0Pv0J1bv5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1102419/2f5f7263-93fa-06bd-ae26-2238108c92b6.png)
（カメラの中央に立っているAと左にずれたA'。同じ向きを向いていてもカメラに対する角度はA'のほうが浅い）

これを解決すると次のようになります。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">角度制御、完璧です<br>やはり数学はすべてを解決する<a href="https://twitter.com/hashtag/gamedev?src=hash&amp;ref_src=twsrc%5Etfw">#gamedev</a> <a href="https://t.co/eMnsdsnWfn">pic.twitter.com/eMnsdsnWfn</a></p>&mdash; 殻栗ポルタ🌰⚙️3D ADV 制作中 (@Karakuri_Polta) <a href="https://twitter.com/Karakuri_Polta/status/1835823866524160266?ref_src=twsrc%5Etfw">September 16, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

どの位置に立っていても中央にいるときと同じような角度になりました。

# 方針
先程述べた通り、カメラに対するモデルの相対角度が変わってしまうことが原因です。
これを完璧に直すには、埋もれてしまった分の角度を足してあげればいいだけです。

図解すると次のようになります。
![POWERPNT_vLREtBj7UF.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1102419/bf930fc0-d77f-5088-5c7f-b10b25f4338b.png)
全く同じ角度を向いている青矢印に比べて、太いオレンジ矢印の方向に向けることでカメラに対して同じ角度*θ*を保つことができます。
これを実現するためには、埋もれてしまった分の角度*φ*を足してあげればよいのです。
そしてその*φ*は、平行線の錯角によりカメラからA'を見たときの角度と等しいです。

それでは、*φ*を求めましょう。と言っても、角度をそのまま測ることは困難です。
なので、視点を変えて、各点の座標から三角比を見ることにしましょう。
![POWERPNT_DeKSSgf6qI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1102419/a9817033-d9f2-8ff7-a5ab-844ca40c3309.png)
オブジェクトA、A'はx座標しか変わりません。したがって、ここに直角三角形が見えてきます。
よって、x,z,φの関係を表すと、**tan *φ = x / z*** となります。
φを求める際は、この逆関数arctanを使います。arctanはtanの値から角度を取り出す関数で、arctan(tan φ) = φです。
これを先程の両辺にかけて、arctan(tan *φ*) = arctan(*x / z*)
よって ***φ* = arctan(*x / z*)** です。

以上から、
1. 座標から角度φを計算する
1. モデルA'の角度をφだけ回転する

この順序で方針を定めます。

## 追記 状況設定について
コメントにて、角度θ傾いている状態からさらにφを動かすのは二度手間ではないかというご意見をいただきました。
今回の例では角度θは**アニメーションに記録**されており、アニメーション再生時点で横を向いていることを想定しています。
例として挙げていた映像と揃えるためこのような状況設定になっております。
説明が足りず申し訳ございません。

# 実装

## Mathf.Atanを使って角度φを計算する
実装にあたりまず問題なのはarctanの計算方法です。しかし、Mathfの中にAtanという関数が存在し、値をいれるだけでarctanを計算し角度をラジアンで返してくれます。便利ですね。
というわけで、次のようになります。
```C#
// オブジェクトA'につけるスクリプト
// cameraはカメラのゲームオブジェクトを指す
float x = transform.position.x - camera.transform.position.x; // カメラからの相対x座標の取得
float z = transform.position.z - camera.transform.position.z; // カメラからの相対z座標の取得
float phi = Mathf.Atan( x / z ); // 角度φ(単位はラジアン)
```
これにて手順1.は終わりました。

## transform.localEulerAnglesを使って角度を制御する
今回は3D ADVの会話シーンでのキャラクターの表示なので、複雑な動きはしません。
せいぜいシナリオのデータに沿ってx座標を動かし、アニメーションを再生するだけです。
そして今回の修正ではy軸周りに回転させるだけです。

このように複雑な回転をしない場合、手っ取り早く角度を指定する方法に`transform.localEulerAngles`があります。
これはオイラー角という角度の表現手法で、要するにUnityのインスペクター上でX軸,Y軸,Z軸周りの回転を指定するあの表現方法をスクリプトで使えるのです。
2軸以上を同時に回転させようとすると意図しない回転のしかたをしてしまいますが、今回はy軸のみを動かすため心配は不要です。

::: note
Unity上では本来、四元数Quaternionという形で角度を指定します。2次元上での回転を複素数で表したのと同様に、複素数をさらに拡張した四元数では3次元上の回転を表す事が可能です。
しかし、四元数は正直わかりにくいです。比較的簡単に組めるようにされてはいますが、今回は初心者でもわかりやすいオイラー角を用います。
:::

φも求まっているのであとは角度をオイラー角にいれるだけですが、以下のように指定しても角度はほとんど変わりません。
```C#
// 問題のあるコード
transform.localEulerAngles = new Vector3(transform.localEulerAngles.x, phi, transform.localEulerAngles.z);
```
なぜなら、オイラー角で指定する値は **度数法（-180°～180°）** の値だからです。
float phiに入っているのはMathf.Atanでの計算結果で単位はラジアン、つまり **弧度法（-π～π）** です。 

よってラジアンから度数に変換する必要があります。

## 定数Mathf.Rad2Degをかける

ラジアンから度数への変換はめちゃくちゃ簡単です。
そもそもπで割って180をかけるだけでできるのですが、この180/πの値が **Mathf.Rad2Deg** (Radian to Degreeの略)として定数になっています。

というわけで、φにこの定数を掛け算した次のコードで完全に動作します。

```C#
// 正常に動作するコード
transform.localEulerAngles = new Vector3(transform.localEulerAngles.x, phi * Mathf.Rad2Deg, transform.localEulerAngles.z);
```

ただしカメラとモデルの状況によっては逆回転する場合があります。その場合はy軸の指定を負にしてください。

```C#
// 逆回転してしまう場合の修正コード
transform.localEulerAngles = new Vector3(transform.localEulerAngles.x, - phi * Mathf.Rad2Deg, transform.localEulerAngles.z);
```

# まとめ
```C#
// cameraはカメラのゲームオブジェクトを指す
float x = transform.position.x - camera.transform.position.x; // カメラからの相対x座標の取得
float z = transform.position.z - camera.transform.position.z; // カメラからの相対z座標の取得
float phi = Mathf.Atan( x / z ); // 角度φ(単位はラジアン)
transform.localEulerAngles = new Vector3(transform.localEulerAngles.x, phi * Mathf.Rad2Deg, transform.localEulerAngles.z); // 回転の適用
```
以上のコードでカメラに対して同じ角度を保つことができます。
ちょっとした回転なら`transform.LookAt`などでもできるのですが、細かい仕様により自力で計算する必要がある場合もあります。
そんなときはぜひ図で整理して、逆三角関数を使ってみてください。きっと強力な武器になります。

## 補足
今回始めに見せた映像は、開発中の「おばけのロッソ」の2次創作3Dアドベンチャーゲームの映像です。
原作「おばけのロッソ」のFAQに則って開発・公開しております。

https://sushiwakame518.wixsite.com/rossotheghost/faq