このオブジェクトの位置とTargetObject
の位置が極端に近づくとrelativePos
の大きさが非常に小さくなり、向きの誤差が大きくなって異常な動きを誘発しそうですね。
AddTorque
系メソッドを使ってみようという発想も面白いと思いますが、ここは単純に両者の距離が十分近い場合は向き合わせ処理を行わないようにするだけでいいんじゃないでしょうか。
ついでに、再び両者の距離が離れて向き合わせを再開したときに急激に新しい方向へ向いてしまうのを防止するため、「Unity3D LookAt関数でどのベクトルをTargetに向けるか指定したい」の質問文でご提示いただいたQuaternion.Slerpによるスムージングも組み込んでやるといいかもしれません。
C#
1[SerializeField]
2Transform TargetObject; //向きたい方向
3
4// 最新の目標回転を覚えておくためのフィールドを追加する
5Quaternion targetRotation = Quaternion.identity;
6
7float sqrMagnitudeThreshold = 0.1f * 0.1f;
8float speed = 0.1f;
9
10void Update()
11{
12 Vector3 relativePos = TargetObject.position - transform.position;
13
14 if (relativePos.sqrMagnitude > sqrMagnitudeThreshold)
15 {
16 // relativePosがある程度大きいときだけtargetRotationを更新する
17 // relativePos.magnitudeを調べて判定を行ってもいいのですが、この手の距離判定では
18 // 2乗距離を使うことで内部的に行われる平方根計算を1回節約するテクニックがしばしば
19 // 使われるため、せっかくなのでそのようにしてみました
20 // とはいえ、1フレームに一回の頻度なら大して有効性は感じられないと思います
21 targetRotation = Quaternion.FromToRotation(Vector3.up, relativePos);
22 }
23
24 // Slerpを使って現在の回転と目標回転を混合し、混合結果を新しい向きとする
25 transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, speed);
26}
追記
非常にわかりやすい映像で参考になりました。
Vector3.up
を真逆の方向であるVector3.down
に向けて回すとき、その軌道がY軸周りのどの方角を経由するかは曖昧です。たとえ話として、もし地球が海も山もないなめらかな球体で今北極点に立っているとすると、そこから最短の経路で南極点へ行けと言われたときに、どの方角へ歩き始めても距離は同じです。ですが最終的に南極点へ到達して立ち止まったとき、自身のY軸周りの向きは選んだ経路によって変わってくるはずです。
この状況と似たような現象が起こっており、南極点付近だと目標地点がわずかにずれただけでも選択される最短経路が大きく変わり、最終的なY軸周りの向きがころころ変化してしまうのだろうと考えられます。
改善案として「初期の頭の方向(Vector3.up
)ではなく、現在の頭の方向(transform.up
)から目標方向(relativePos
)への回転を求め、それを現在の姿勢(transform.rotation
)に上書きするのではなく追加適用する」というのはどうでしょうか?
言葉で説明するとややこしくなってしまいすみませんが、下記のような回転方法ではどうでしょうか。
C#
1[SerializeField]
2Transform TargetObject; //向きたい方向
3
4// 目標回転に代わって、目標方向を覚えておくようにする
5Vector3 targetRelativePos = Vector3.up;
6
7float sqrMagnitudeThreshold = 0.1f * 0.1f;
8float speed = 0.1f;
9
10void Update()
11{
12 Vector3 relativePos = TargetObject.position - transform.position;
13
14 if (relativePos.sqrMagnitude > sqrMagnitudeThreshold)
15 {
16 targetRelativePos = relativePos;
17 }
18
19 // 現在の頭の向き、つまりtransform.upをtargetRelativePos方向に向ける回転を求め...
20 Quaternion relativeRotation = Quaternion.FromToRotation(transform.up, targetRelativePos);
21
22 // 目標姿勢は現在の姿勢に上記の相対回転を適用した姿勢とする
23 Quaternion targetRotation = relativeRotation * transform.rotation;
24
25 transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, speed);
26}
追記
transform.eulerAngles.y
を毎回0に書き換えるという方法だと、書き換え後のtransform.up
の方向は本来の向きからずれてしまうかと思います。ですが今回の場合では「向きを目標方向に近づける」→「オイラー角のYだけを0に書き換える」→「向きを目標方向に近づける」→「オイラー角のYだけを0に書き換える」...というプロセスをわずかずつ何度も繰り返すことによって、全体の連続性は保たれそうな感じです。ですので、そのやり方で問題がなさそうでしたらOKかと思います(後述しますが、本当にそのやり方でも全方向完璧に自然な回転を実現できるのかどうか不安はあるのですが...)。
私の方でも何かY回転を0にする手がないか考えてみようと思い、下記のようなやり方を試してみました。前回同様にY周り角度の制限なしで向きを設定したあと、transform.up
の向きを維持したままY角度を0にした回転に置き換えています。
C#
1[SerializeField]
2Transform TargetObject; //向きたい方向
3
4Vector3 targetRelativePos = Vector3.up;
5readonly float sqrMagnitudeThreshold = 0.1f * 0.1f;
6readonly float speed = 0.1f;
7
8void Update()
9{
10 Vector3 relativePos = TargetObject.position - transform.position;
11 if (relativePos.sqrMagnitude > sqrMagnitudeThreshold)
12 {
13 targetRelativePos = relativePos;
14 }
15 Quaternion relativeRotation = Quaternion.FromToRotation(transform.up, targetRelativePos);
16 Quaternion targetRotation = relativeRotation * transform.rotation;
17 transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, speed);
18
19 // まず、元のローカル回転を使ってVector3.upを回したときにどの方角を向くかを求める
20 Quaternion oldRotation = transform.localRotation;
21 if (oldRotation.w < 0.0f)
22 {
23 oldRotation = new Quaternion(-oldRotation.x, -oldRotation.y, -oldRotation.z, -oldRotation.w);
24 }
25 Vector3 localUp = oldRotation * Vector3.up;
26
27 // localUpの各成分をもとにZ回転・X回転だけで同じ方向に向けるための角度を算出する
28 // このとき、Z角度を-90°~90°の範囲で設定するパターンと...
29 float phi1 = Mathf.Asin(Mathf.Clamp(-localUp.x, -1.0f, 1.0f)) * Mathf.Rad2Deg;
30 float theta1 = Mathf.Atan2(localUp.z, localUp.y) * Mathf.Rad2Deg;
31 Quaternion newRotation1 = Quaternion.Euler(theta1, 0.0f, phi1);
32 if (newRotation1.w < 0.0f)
33 {
34 newRotation1 = new Quaternion(-newRotation1.x, -newRotation1.y, -newRotation1.z, -newRotation1.w);
35 }
36
37 // Z角度を-180°~-90°、90°~180°の範囲で設定するパターンが考えられるが...
38 float phi2 = -180.0f - phi1;
39 float theta2 = -180.0f + theta1;
40 Quaternion newRotation2 = Quaternion.Euler(theta2, 0.0f, phi2);
41 if (newRotation2.w < 0.0f)
42 {
43 newRotation2 = new Quaternion(-newRotation2.x, -newRotation2.y, -newRotation2.z, -newRotation2.w);
44 }
45
46 // なるべく元の回転に近い方を採用する
47 Quaternion newRotation =
48 Quaternion.Dot(oldRotation, newRotation1) >= Quaternion.Dot(oldRotation, newRotation2)
49 ? newRotation1
50 : newRotation2;
51 transform.localRotation = newRotation;
52}
動きは下図のようになりました。回転が2つの軸の角度だけに支配されていることを視覚的にわかりやすくしようと思い、二軸ジンバルをイメージした台を置いてみました。
補足しますと、動かしている最中にインスペクターを観察するとY回転がちょくちょく180°回転してしまうかと思います。回転生成自体はQuaternion.Euler(theta1, 0.0f, phi1)
といった具合にYが0になっているのですが、クォータニオンとオイラー角の相互変換をする上でのいたしかたない都合でこんなことになってしまいます(以前別の方の「[Unity] オブジェクトのRotationの値を変化させたときの挙動について質問」という件でもちょっと申し上げたことがありました)。
やってみた感想としては、だいたいの場合ではそれらしい見栄えになったものの、たまに姿勢がピョンと飛んでしまうことがあって不満が残りました(図の最後の方で-X方向に向けたときに顕著な飛びが起こっています)。これを解消しようとスムージングをかけたりするとピクピク現象を起こしてしまったりして、一筋縄ではいかなそうです。
今回のように軸の回転を制限したり、あるいはご質問者さんのtransform.eulerAngles.y
書き換えによる回転固定を行うと姿勢の自由度が低下してしまって、どこかしらの方角で姿勢の急激に変化する特異点的な場所が発生するのは避けられないんじゃないかと思います。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/12/16 04:17
2019/12/16 06:48
2019/12/16 20:29
2019/12/16 20:34
2019/12/16 22:19 編集
2019/12/17 14:02
2019/12/19 17:55 編集
2020/03/21 15:15
2020/03/21 23:03