関連ブログ
- [UE4][UE5]開発環境の容量を少しでも減らす 2024.08.14UE
- [UE5] PushModel型のReplicationを使い、ネットワーク最適化を図る 2024.05.29UE
- [UE5]マテリアルでメッシュをスケールする方法 2024.01.17UE
CATEGORY
2018.12.05UE4
執筆バージョン: Unreal Engine 4.20 |
よくあるカメラの操作方法の一つにドラッグで背景を掴むように動かすというものがあります。
360度動画と相性がよいようで、YouTubeや360Channelで採用されています。
ところが、これらに実装されているドラッグ操作は個人的にモヤモヤするものです(この記事を執筆している時点では)。
あろうことか、ドラッグ中に掴んでいる位置がずれるのです。
別に操作の正確性を要求されるものではないので閲覧に支障は無いのですが、
美しくはありません(個人の意見です)。
今回はUE4のちからでこの解決を試みます。
前提条件として、カメラのロール回転は行わないものとします。
まずは、何も考えずにやってみます。
マウスドラッグに合わせて適当にカメラRotationのYaw/Pitchを足し込めばこうなりますね。
水平に近い位置ではまだマシですが、
鉛直に近づくにしたがってひどくズレるようになります。
視界をドラッグするという操作の意味するところを分析することから始めて、
しっかりつかめるようにしてみます。
視界のドラッグは以下の手順により行われます。
①ドラッグ開始(ドラッグするポイント(アンカー)を決定)
②マウスポインタの移動
ドラッグするポイントとありますが、3Dのシーンにおいてはポイントというよりレイ(始点の座標+方向)になります。
雰囲気でこれをアンカーと呼んでおきます。
アンカーはドラッグ操作の最中変化しないことになります。
②でマウスポインタを移動した際、そのポインタとアンカーとが重なるように制御するのが要件ということになります。
雰囲気で可視化すると以下のような感じです。
①ドラッグ開始(「?」マークをドラッグするイメージ) | ②マウスポインタを左に移動させた |
アンカー(赤い矢印)は空間上で固定されており、ドラッグ操作によって変化しません。
マウスポインタ(白い円)が常にアンカーに重なるようカメラを回転させます。 |
ドラッグによってカメラの回転を制御する際、未知の値と既知の値は下記のように想定できます。
カメラの位置とアンカーレイの視点は一致し、計算の過程でキャンセルされるので不要です。
未知の値(求めるべき値) |
既知の値 |
カメラの姿勢 | ポインタのビューポート上の位置(0~1に正規化)
ドラッグ開始時に決定されたアンカーベクトル(WorldSpace) カメラの画角 ビューポートのアスペクト比 |
ターゲットとなるカメラの姿勢以外の状況はだいたい判明している状態です。
このへんから方程式を立てて解析すればできあがりです。
整理するために、カメラの正面、距離1にある平面(VirtualScreen(VS) / 仮想スクリーン)上にアンカーを投影し正規化します。
この平面上では以下のような値が計算できます。
項目 | 記号 | 関係式 | 備考 |
視界の幅 | VSWidth | 2*tan(FovX/2) | FovX : カメラ視野角(水平) |
視界の高さ | VSHeight | VSWidth / Aspect | Aspect : アスペクト比(幅/高さ) |
ポインタ位置
(仮想スクリーン上の位置、中心(つまりカメラの正面)を原点とする) |
VSPointer | (ScreenPosition / ScreenSize – (0.5, 0.5)) * (VSWitdh, VSSize) | ScreenPosition は左下原点を想定
ここでベクトルの乗算・除算は成分ごとの計算を表す (a,b)*(c,d) = (a*c, b*d) (a,b)/(c,d) = (a/c, b/d) |
アンカーと平面との交点を考えることにより以下の関係が得られます。
(Anchor dot CameraForward) = (OP dot CameraForward) * |Anchor|/|OP|
Anchor : アンカー方向
CameraForward : カメラ前方向ベクトル(未知数)
O : カメラ位置
P : 交点
平面とカメラの位置関係から
OP dot CameraForward = 1.0
|OP| = sqrt(1+VSPointer.Length^2)
これより(Ray dot CameraForward)を計算できます。
次に
OP = Ray * (OP dot CameraForward) / (Ray dot CameraForward)
によりOPが計算できます。
以降、OPとカメラに固定された各軸ベクトルとのドット積を仮想スクリーン上のポインタ位置と比較することで
カメラの姿勢を特定することができます。
以下を連立して解きます。下記(3)の条件が使えるためにこの軸が最も容易に定まります。
1) OP dot CameraRight = VSPointer.X
2) |CameraRight| = 1
3) CameraRight.Z = 0 (カメラのロール回転を扱わないという制約より)
これを適当にいじくれば何とかなります。
解を短く書くために以下のエイリアスを用います。
P <= OP
S <= VSPointer
A <= P.X*P.X+P.Y*P.Y
解:
Right.X = (S.X*P.X ± P.Y*sqrt(A-S.X*S.X)) / A
Right.Y = (S.X*P.Y∓P.X*sqrt(A-S.X*S.X)) / A
Right.Z = 0
±があり解が二通り生じますが、これはPがカメラの前方にあるという条件でいずれかに特定できます。
以下を連立して解きます。
1) CameraUp dot CameraRight = 0 (軸同士は直交)
2) OP dot CameraUp = VSPointer.Y
3) |CameraUp| = 1
なんだかんだとこねくり回すと何とかなります。
解を短く書くために以下のエイリアスを用います。
P <= OP
S <= VSPointer
R <= CameraRight
C <= P.X*R.Y – P.Y*R.X
D <= square(C*S.Y / (C*C+P.Z*P.Z)) – (S.Y*S.Y-P.Z*P.Z)/(C*C+P.Z*P.Z)
F <= -(C*S.Y) / (C*C+P.Z*P.Z) ± sqrt(D)
解:
Up.X = -R.Y*F
Up.Y = R.X*F
Up.Z = sqrt(1-F*F)
Fに±が含まれやはり解が二通りありますが、Pの位置を条件として特定が可能です。
CameraForward = CameraRight cross CameraUp
これはそのままですね。
以上でカメラの姿勢が特定可能です。
尚、条件に当てはまる姿勢を作れない場合があります。
例えばロール回転が無効であることから鉛直方向に近い位置を画面の左右端に置くことはできず、
その場合は解なし(NaN)になります(sqrtの中身が負の値になる)。
アプリケーションで行うべき処理としては単に姿勢の変更をスキップするくらいでしょうか。
こんな感じですね。
コメント控えめです。
PointerUVがビューポート左下を(0,0)と仮定していることには注意してください。
なるべく本記事と変数名を揃えたので合わせて参考にしてみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
bool UDragSkyUtils::CalcCameraRotationFromAnchor(FRotator &Result, const FVector &AnchorVector, const FVector2D &PointerUV, float FovXInDegree, float AspectRatio) { const float FovX = FMath::DegreesToRadians(FovXInDegree); // // VirtualScreen(VS) : viewport projected toward the plane in front of camera in distance of 1.0 // const FVector2D VSPointerNormalized = (PointerUV - FVector2D(0.5f, 0.5f)).ClampAxes(-0.5f, 0.5f); const float VSWidth = 2.0f*FMath::Tan(FovX*0.5f); const float VSHeight = VSWidth / AspectRatio; const FVector2D VSPointer = VSPointerNormalized * FVector2D(VSWidth, VSHeight); const float AnchorDepth = AnchorVector.Size() / FMath::Sqrt(1.0f + FMath::Square(VSWidth*VSPointerNormalized.X) + FMath::Square(VSHeight*VSPointerNormalized.Y)); // Anchor vector projected onto VirtualScreen const FVector ProjectedAnchorVector = AnchorVector / AnchorDepth; // Alias for simple expression const FVector &P = ProjectedAnchorVector; const FVector2D &S = VSPointer; FVector CameraRight; { // ProjectedAnchor dot Right = VSPointer.X // -> W.X*Right.X + W.Y*Right.Y = S.X // Right.Size() = 1 // -> Right.X*Right.X + Right.Y*Right.Y = 1 // Right.Z = 0 // const float A = P.X*P.X + P.Y*P.Y; const float RightX1 = (S.X * P.X + P.Y*FMath::Sqrt(A - S.X * S.X))/A; const float RightX2 = (S.X * P.X - P.Y*FMath::Sqrt(A - S.X * S.X))/A; const float RightY1 = (S.X * P.Y - P.X*FMath::Sqrt(A - S.X * S.X))/A; const float RightY2 = (S.X * P.Y + P.X*FMath::Sqrt(A - S.X * S.X))/A; const FVector Right1(RightX1, RightY1, 0.0f); const FVector Right2(RightX2, RightY2, 0.0f); const FVector TemporaryForward = AnchorVector; if (FVector::CrossProduct(TemporaryForward, Right1).Z >= 0) { CameraRight = Right1; } else { CameraRight = Right2; } if (CameraRight.ContainsNaN()) { UE_LOG(LogTemp, Log, TEXT("Right vector contains nan")); return false; } } FVector CameraForward, CameraUp; { // ProjectedAnchor dot Up = VSPointer.Y // -> W.X*Right.X + W.Y*Right.Y = S.Y // Up.Size() = 1 // -> Up.X*Up.X + Up.Y*Up.Y + Up.Z*Up.Z = 1 // Up dot Right = 0 // const FVector &R = CameraRight; const float C = (P.X*R.Y - P.Y*R.X); const float Determinant = FMath::Square(C*S.Y / (C*C + P.Z*P.Z)) - (S.Y*S.Y - P.Z*P.Z) / (C*C + P.Z*P.Z); if (Determinant<0.0f) { UE_LOG(LogTemp, Log, TEXT("Determinant : %f"), Determinant); return false; } const float F1 = -(C*S.Y) / (C*C + P.Z*P.Z) + FMath::Sqrt(Determinant); const float F2 = -(C*S.Y) / (C*C + P.Z*P.Z) - FMath::Sqrt(Determinant); const FVector Up1(-R.Y*F1, R.X*F1, FMath::Sqrt(1.0 - F1 * F1)); const FVector Up2(-R.Y*F2, R.X*F2, FMath::Sqrt(1.0 - F2 * F2)); const FVector Forward1 = FVector::CrossProduct(R, Up1); const FVector Forward2 = FVector::CrossProduct(R, Up2); const float Dot1 = FVector::DotProduct(Forward1, P); const float Dot2 = FVector::DotProduct(Forward2, P); const float Diff1 = FMath::Abs(Dot1 - 1.0f); const float Diff2 = FMath::Abs(Dot2 - 1.0f); const float MinDiff = FMath::Min(Diff1, Diff2); if (MinDiff > 0.001f) { UE_LOG(LogTemp, Log, TEXT("No solution found")); return false; } if (Diff1 < Diff2) { CameraUp = Up1; CameraForward = Forward1; } else { CameraUp = Up2; CameraForward = Forward2; } if (CameraUp.ContainsNaN()) { UE_LOG(LogTemp, Log, TEXT("NaN detected")); return false; } if (CameraForward.ContainsNaN()) { UE_LOG(LogTemp, Log, TEXT("NaN detected")); return false; } } FMatrix RotMatrix(CameraForward, CameraRight, CameraUp, FVector::ZeroVector); Result = RotMatrix.Rotator(); if (Result.ContainsNaN()) { UE_LOG(LogTemp, Log, TEXT("rotator contains nan")); return false; } return true; } |