BLOGブログ

2018.12.05UE4

[UE4] 視界のドラッグによるカメラ回転

執筆バージョン: Unreal Engine 4.20

0.序文

よくあるカメラの操作方法の一つにドラッグで背景を掴むように動かすというものがあります。

 

360度動画と相性がよいようで、YouTubeや360Channelで採用されています。

ところが、これらに実装されているドラッグ操作は個人的にモヤモヤするものです(この記事を執筆している時点では)。

あろうことか、ドラッグ中に掴んでいる位置がずれるのです。

別に操作の正確性を要求されるものではないので閲覧に支障は無いのですが、

美しくはありません(個人の意見です)。

 

今回はUE4のちからでこの解決を試みます。

 

前提条件として、カメラのロール回転は行わないものとします。

1.初歩的な実装

まずは、何も考えずにやってみます。

 

 

マウスドラッグに合わせて適当にカメラRotationのYaw/Pitchを足し込めばこうなりますね。

水平に近い位置ではまだマシですが、

鉛直に近づくにしたがってひどくズレるようになります。

 

視界をドラッグするという操作の意味するところを分析することから始めて、

しっかりつかめるようにしてみます。

 

2.問題の整理

視界のドラッグは以下の手順により行われます。

①ドラッグ開始(ドラッグするポイント(アンカー)を決定)

②マウスポインタの移動

 

ドラッグするポイントとありますが、3Dのシーンにおいてはポイントというよりレイ(始点の座標+方向)になります。

雰囲気でこれをアンカーと呼んでおきます。

アンカーはドラッグ操作の最中変化しないことになります。

②でマウスポインタを移動した際、そのポインタとアンカーとが重なるように制御するのが要件ということになります。

雰囲気で可視化すると以下のような感じです。

①ドラッグ開始(「?」マークをドラッグするイメージ) ②マウスポインタを左に移動させた
アンカー(赤い矢印)は空間上で固定されており、ドラッグ操作によって変化しません。

マウスポインタ(白い円)が常にアンカーに重なるようカメラを回転させます。

 

 

ドラッグによってカメラの回転を制御する際、未知の値と既知の値は下記のように想定できます。

カメラの位置とアンカーレイの視点は一致し、計算の過程でキャンセルされるので不要です。

未知の値(求めるべき値)

既知の値

カメラの姿勢 ポインタのビューポート上の位置(0~1に正規化)

ドラッグ開始時に決定されたアンカーベクトル(WorldSpace)

カメラの画角

ビューポートのアスペクト比

ターゲットとなるカメラの姿勢以外の状況はだいたい判明している状態です。

このへんから方程式を立てて解析すればできあがりです。

 

3.解析

整理するために、カメラの正面、距離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とカメラに固定された各軸ベクトルとのドット積を仮想スクリーン上のポインタ位置と比較することで

カメラの姿勢を特定することができます。

 

カメラのY軸(右方向)

以下を連立して解きます。下記(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がカメラの前方にあるという条件でいずれかに特定できます。

 

カメラのZ軸(上方向)

以下を連立して解きます。

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の位置を条件として特定が可能です。

 

カメラのX軸(前方向)

CameraForward = CameraRight cross CameraUp

これはそのままですね。

 

以上でカメラの姿勢が特定可能です。

尚、条件に当てはまる姿勢を作れない場合があります。

例えばロール回転が無効であることから鉛直方向に近い位置を画面の左右端に置くことはできず、

その場合は解なし(NaN)になります(sqrtの中身が負の値になる)。

アプリケーションで行うべき処理としては単に姿勢の変更をスキップするくらいでしょうか。

 

4.できたもの

こんな感じですね。

 

5.付録:ソースコード

コメント控えめです。

PointerUVがビューポート左下を(0,0)と仮定していることには注意してください。

なるべく本記事と変数名を揃えたので合わせて参考にしてみてください。