11. 座標空間とカメラ

此処数章で、かなり実用的な道具箱が揃いました。
物を拡大縮小・回転・移動させる行列(第8章と第9章)、そして其れを元に戻す逆行列(第10章)です。
此の章では、其の道具箱を3Dプログラムで最も重要な物、つまりカメラに使います。
1つの頂点がモデルファイルからピクセルになる迄の旅を追い掛け、「座標空間を変える」とは本当は何なのかを学び、有名なlookAtビュー行列を一から作ります。
最後には、一人称カメラとオービットカメラを自分で書ける様になります。

11.1 頂点の旅

友達に自分のマグカップの場所を教える場面を想像して下さい。
「キーボードの10cm左」とも言えますし、「3階の302号室」とも言えますし、GPS座標を伝える事も出来ます。
どれも同じマグカップです。
変わったのは基準座標系だけです。
つまり、原点を何処に置くかと、どの方向を軸と呼ぶかです。

3Dプログラムは、全ての頂点に対して毎フレーム何度も此れと同じ事をしています。
其々の基準座標系を空間と呼び、行列が点を或る空間から次の空間へ運びます:

オブジェクト空間からスクリーン空間迄の空間の流れと、其の間の行列及び処理

空間 原点は何処? 何に使う?
オブジェクト空間(ローカル空間) モデルのピボット(アーティストが決める) ファイルに保存された頂点データ
ワールド空間 シーンの或る固定点 物体の配置、物理、ライト
ビュー空間(カメラ空間) カメラの目の位置 此処ではカメラは−Z方向を向く
クリップ空間 単純な原点は無い(4Dの同次座標) GPUが画面外の部分を切り取る
NDC(正規化デバイス座標) 画面の中心 xとyが−1〜1に収まる
スクリーン空間 ウィンドウの角 実際のピクセル

最初の3つの矢印は、頂点シェーダーで自分で書きます。
最後の2つはGPUがやってくれます。
モデル行列は既に知っていますね。
第9章の𝐓𝐑𝐒行列です。
投影行列は次の章で丸ごと扱います。
此の章のテーマはビュー行列と、上の図の全ての矢印の裏に隠れている1つの考え方、基底変換です。

11.2 基底変換:公式

まずは図です。
下のグリッドには普通のx軸とy軸がありますが、別の人は傾いた軸𝐛1と𝐛2を使っているとします。
2人とも同じ点Pを表せますが、書き出す数字が違うだけです。

傾いた軸 b1 と b2、及び 2 b1 + b2 と歩いて辿り着く点P

新しい基底ベクトルを古い座標で書き、其れを列として行列に並べます:

𝐁=[𝐛1𝐛2𝐛3]

新しい軸に沿って測った座標が𝐩newである点の、古い座標は次の通りです:

𝐩old=𝐁𝐩new

逆方向に行くには、逆行列(第10章)で𝐁を元に戻します:

𝐩new=𝐁−1𝐩old

更に、新しい軸が正規直交(どの軸も長さが1で、互いに全て垂直)なら、逆行列は単なる転置になります:

𝐁−1=𝐁⊤従って𝐩new=𝐁⊤𝐩old

新しい座標系が自分の原点𝐨(古い座標で表した物)も持っている場合は、第9章の4×4の技を使い、𝐨を4列目に入れます:

𝐩old=[𝐛1𝐛2𝐛3𝐨0001]𝐩new

11.3 基底変換の仕組み

𝐩new=(2,1)とは何を意味するのでしょうか。
「𝐛1に沿って2歩、其の後𝐛2に沿って1歩進む」という意味です。
古い座標では、其の歩みは2𝐛1+1𝐛2です。
第6章を思い出して下さい。
行列とベクトルの積は、正に行列の列の線型結合でした。
従って2𝐛1+1𝐛2=𝐁𝐩newとなり、此れが公式の全てです。

逆方向は「此の点は𝐛1に沿って何歩の所にある?」という質問です。
正規直交基底なら、第4章が既に答えてくれています。
単位ベクトルとの内積は、ベクトルが其の方向にどれだけ伸びているかを測ります。
従って新しい座標は単に𝐛1⋅𝐩と𝐛2⋅𝐩です。
複数の内積を一度に計算するのは、𝐛1と𝐛2を行に持つ行列で、其の行列こそが𝐁⊤です。
此れが、正規直交行列を転置で元に戻せる理由です。
第10章で回転に使ったのと同じ事実ですね。

2Dの計算例

新しい軸を次の様にします:

𝐛1=[0.60.8],𝐛2=[−0.80.6]

まず正規直交である事を確認しましょう:

‖𝐛1‖=0.36+0.64=1,‖𝐛2‖=0.64+0.36=1,𝐛1⋅𝐛2=−0.48+0.48=0

良いですね。
どちらも長さ1で、互いに垂直です。
(普通のx軸とy軸を約53°回転させただけの物です。)
では、新しい座標が𝐩new=(2,1)の点を考えます。
新から旧へ:

𝐩old=2[0.60.8]+1[−0.80.6]=[1.2−0.81.6+0.6]=[0.42.2]

そして転置(行が𝐛1と𝐛2)で旧から新へ戻します:

𝐁⊤𝐩old=[0.60.8−0.80.6][0.42.2]=[0.24+1.76−0.32+1.32]=[21]

(2,1)に戻りました。
往復は成功です。
もし新しい軸が正規直交でなければ(例えば片方の長さが2なら)、転置では間違った答えになり、第10章の本物の逆行列が必要になります。

此の節で覚えて欲しい一文は此れです:

座標系Aの軸(と原点)を座標系Bの座標で書き、其れを列に並べた行列は、点をAからBへ変換し、其の逆行列はBからAへ戻します。

モデル行列は正に此れです。
其の列はワールド座標で書いた物体の軸と位置なので、オブジェクト空間からワールド空間へ変換します。

11.4 物体としてのカメラ

カメラはワールドの中にある物体の1つに過ぎません。
位置、つまり目の位置(eye)𝐞と、3つの単位ベクトルで表す向きを持っています:

  • 𝐫:右(right)
  • 𝐮:上(up)
  • 𝐟:前(forward)、カメラが見ている方向

此の3つは正規直交基底です。
此処で、シリーズ全体の約束を思い出して下さい。
ビュー空間では、カメラは−Z方向を向きます。
+Xが右、+Yが上の右手座標系では、+Zは画面からこちら側、つまりあなたの方を向きます。
従って「画面の奥」は−Zで、カメラ自身の+Z軸は後ろ、つまり−𝐟の方向を向きます。
手系も確認出来ます。
𝐫×𝐮は𝐢×𝐣=𝐤と同じで後ろを向くので、𝐫×𝐮=−𝐟です。

横から見たカメラ:u は上、f は前、カメラの+Zは後ろを向く

前の節のルールを使うと、カメラ座標をワールド座標に変換する行列は、カメラの軸と位置を列に持ちます:

𝐂=[rxux−fxexryuy−fyeyrzuz−fzez0001]

此れがカメラのワールド行列で、他の物体のモデル行列と全く同じ物です。
回転𝐑(最初の3列)の後に目の位置への平行移動が続くので、𝐂=𝐓(𝐞)𝐑です。

11.5 ビュー行列とlookAt:公式

でも欲しいのはカメラからワールドへの変換ではありません。
手元にあるのはワールド空間の頂点で、知りたいのは其れがカメラから見て何処にあるか、つまりワールドからカメラへの変換です。
其れは逆行列です:

𝐕=𝐂−1

此れがビュー行列です。
面白い考え方があります。
GPUはカメラを動かしません。
代わりにビュー行列がワールド全体を動かし、カメラが原点に来て−Zを向く様にします。
カメラを1m右に動かす事は、ワールド全体を1m左にずらす事と同じなのです。

此処では一般的な4×4の逆行列は要りません。
第10章で安い方法を学んだからです。
「靴下と靴」の(𝐀𝐁)−1=𝐁−1𝐀−1、回転の逆行列は転置、平行移動の逆行列は逆向きの平行移動です:

𝐕=(𝐓(𝐞)𝐑)−1=𝐑−1𝐓(𝐞)−1=𝐑⊤𝐓(−𝐞)

𝐑⊤の行は𝐫、𝐮、−𝐟です。
𝐓(−𝐞)を掛けると、まず目の位置を引き、其の結果と各行の内積を取るので、平行移動の列は𝐑⊤(−𝐞)になります:

𝐕=[rxryrz−𝐫⋅𝐞uxuyuz−𝐮⋅𝐞−fx−fy−fz𝐟⋅𝐞0001]

行毎に読むと意味が分かります。
点の新しいxは「目からどれだけ右にあるか」、新しいyは「どれだけ上にあるか」、新しいzは「どれだけ後ろにあるか」(負なら前)です。

勿論、𝐫、𝐮、𝐟を手で打ち込みたい人は居ません。
lookAt関数は、3つの簡単な情報から其れらを作ります。
カメラが何処にあるか(𝐞)、何を見ているか(ターゲット𝐭)、そして大体どちらが上か(𝐮𝐩、普通はワールドの(0,1,0))です:

𝐟=𝐭−𝐞‖𝐭−𝐞‖,𝐫=𝐟×𝐮𝐩‖𝐟×𝐮𝐩‖,𝐮=𝐫×𝐟

11.6 lookAtの仕組みを一歩ずつ

3行を1つずつ説明します。

  1. 前は目からターゲットへの矢印(第2章:ターゲット引く目)を、長さ1に正規化した物です(第3章)。
  2. 右は前とも上とも垂直でなければならず、外積(第5章)は正に両方に垂直なベクトルをくれます。前と上のヒントは普通垂直ではない(斜め下を見ているカメラ等)ので、外積の長さは1より小さいsinθになり、其の為に正規化します。
  3. 本当の上は右とも前とも垂直です。𝐫と𝐟は90°を成す単位ベクトルで1⋅1⋅sin90°=1なので、最初から長さ1です。此れはワールドの上ではなく、カメラと一緒に傾いたカメラ本来の上です。

一番単純なカメラ、つまり−Zを向いて上が+Yのカメラで方向を確認しましょう。
すると𝐫=(−𝐤)×𝐣=−(𝐤×𝐣)=−(−𝐢)=𝐢で、+X、つまり右です。
そして𝐮=𝐢×(−𝐤)=−(𝐢×𝐤)=−(−𝐣)=𝐣で、上です。
外積の中の順番は大事です(第5章:入れ替えると符号が反転します)。
此の順番が、あたし達の右手座標系に合う物です。

計算例

カメラは𝐞=(3,0,4)にあり、原点𝐭=(0,0,0)を見ていて、上のヒントは(0,1,0)です。

目が 3, 0, 4 にあり原点を見る lookAt の例を上から見た図

ステップ1、前。

𝐭−𝐞=(−3,0,−4),‖𝐭−𝐞‖=9+0+16=5,𝐟=(−0.6,0,−0.8)

ステップ2、右。 外積の公式(aybz−azby,azbx−axbz,axby−aybx)に𝐛=(0,1,0)を入れます:

𝐟×𝐮𝐩=(0⋅0−(−0.8)⋅1,(−0.8)⋅0−(−0.6)⋅0,(−0.6)⋅1−0⋅0)=(0.8,0,−0.6)

長さは既に0.64+0.36=1です(此のカメラは完全に水平を見ているからです)。
従って𝐫=(0.8,0,−0.6)です。

ステップ3、本当の上。

𝐫×𝐟=(0⋅(−0.8)−(−0.6)⋅0,(−0.6)(−0.6)−0.8⋅(−0.8),0.8⋅0−0⋅(−0.6))=(0,1,0)

ステップ4、平行移動の列。

−𝐫⋅𝐞=−(0.8⋅3+0⋅0+(−0.6)⋅4)=−(2.4−2.4)=0
−𝐮⋅𝐞=−(0⋅3+1⋅0+0⋅4)=0
𝐟⋅𝐞=(−0.6)⋅3+0⋅0+(−0.8)⋅4=−1.8−3.2=−5

ステップ5、組み立てる。 行は𝐫、𝐮、−𝐟です:

𝐕=[0.80−0.6001000.600.8−50001]

ステップ6、確認する。
ターゲット(原点)は距離5の真正面にある筈です。
(0,0,0,1)を掛けると最後の列が其の儘出て来るので、(0,0,−5)です。
真正面に5、ぴったりです。
目の位置自体はx=0.8⋅3−0.6⋅4=0、y=0、z=0.6⋅3+0.8⋅4−5=0となり、カメラはビュー空間の原点にあります。
では点𝐩=(4,2,−3)はどうでしょう:

x=0.8⋅4+0⋅2−0.6⋅(−3)+0=3.2+1.8=5
y=0⋅4+1⋅2+0⋅(−3)+0=2
z=0.6⋅4+0⋅2+0.8⋅(−3)−5=2.4−2.4−5=−5

従ってビュー空間で𝐩は(5,2,−5)にあります。
右に5、上に2、前に5です。
此の点を覚えておいて下さい。
第12章で、此の点がどのピクセルに映るかを求めます。

lookAtが壊れる時

カメラが真上又は真下を向くと、前が上のヒントと平行になります。
すると𝐟×𝐮𝐩はゼロベクトルになります(第5章:平行なベクトルの外積はゼロ)。
第3章のnormalizeはゼロ除算の代わりにゼロベクトルを返すので、𝐫も𝐮もゼロになり、行列は全てを平らに潰し(行列式が0、第10章)、画面は真っ暗になるか、ちらつきます。
対策は簡単です。
カメラを上のヒントと完全に同じ方向に向けない(下のピッチ制限を参照)か、其の場合だけ別の上のヒント、例えば真上から見下ろすマップカメラなら(0,0,−1)を渡します。

11.7 一人称カメラとオービットカメラ

ヨーとピッチから作る一人称カメラ

一人称のゲームでは、ターゲットの点は保存しません。
代わりにマウスで変わる2つの角度を保存します:

  • ヨーθ:左右を向く事、ワールドの上軸Y周りの回転
  • ピッチϕ:上下を見る事、カメラの右軸周りの回転

前方向(0,0,−1)から始めて、X周りにピッチ、次にY周りにヨーを掛けます。
第8章のrotateXとrotateYを使うと𝐟=𝐑y(θ)𝐑x(ϕ)(0,0,−1)です。
ピッチの段階で(0,sinϕ,−cosϕ)になり、ヨーの段階で其れをY周りに回します。
掛け算を展開すると:

𝐟=[−sinθcosϕsinϕ−cosθcosϕ]

第1章のsin2+cos2=1を2回使うと、常に長さ1である事が分かります:

‖𝐟‖2=cos2ϕ(sin2θ+cos2θ)+sin2ϕ=cos2ϕ+sin2ϕ=1

計算例:ヨー60°、ピッチ30°。
必要なのはsin60°≈0.866、cos60°=0.5、sin30°=0.5、cos30°≈0.866です。

fx=−0.866⋅0.866=−0.75,fy=0.5,fz=−0.5⋅0.866=−0.433

確認:0.5625+0.25+0.1875=1です。
カメラは−Zより左側(右手の法則により、正のヨーは上から見て反時計回りだから)、そして30°上を向いています。
つまり、マウスを右に動かした時はヨーを減らす必要があります。

ピッチの制限。 ピッチが±90°になると、前は(0,±1,0)になり、ワールドの上と平行になります。
正に前の節の壊れるケースです。
90°を超えると視界が上下逆さまになります。
其の為、一人称カメラは必ずピッチを90°より少し小さい値に制限します。
小さなカメラ更新関数は次の通りです(forwardFromYawPitchは上の公式をコードにした物で、11.10節にあります):

struct FpsCamera {
  Vec3 eye{0, 2, 0};
  float yaw = 0.0f, pitch = 0.0f;
};

// mouseDx, mouseDy: how far the mouse moved this frame, in pixels
inline Mat4 updateFpsCamera(FpsCamera &cam, float mouseDx, float mouseDy, float sensitivity) {
  cam.yaw -= mouseDx * sensitivity;   // mouse right -> turn right
  cam.pitch -= mouseDy * sensitivity; // screen y grows downwards
  const float limit = radians(89.0f);
  if (cam.pitch > limit) cam.pitch = limit;
  if (cam.pitch < -limit) cam.pitch = -limit;

  Vec3 forward = forwardFromYawPitch(cam.yaw, cam.pitch);
  return lookAt(cam.eye, cam.eye + forward, {0, 1, 0});
}

歩く時のちょっとしたコツです。
ピッチを含まない前方向、forwardFromYawPitch(yaw, 0.0f)に沿って移動して下さい。
そうしないと床を見ると歩くのが遅くなります。
第3章の斜め移動のバグと同じ種類のバグです。
横移動の方向はnormalize(cross(forward, up))です。

オービットカメラ

モデルビューアーやストラテジーゲームでは、ターゲットの周りを回るカメラが良く使われます。
同じ2つの角度に距離dを加え、目の位置をターゲットの後ろ、前方向と逆側に置きます:

𝐞=𝐭−d𝐟

其の後lookAt(eye, target, up)でビュー行列を作ります。
マウスのドラッグでヨーとピッチを、ホイールでdを変えます。
負のピッチは下を見るので、目の位置はターゲットより上になります。
例えばターゲット(0,1,0)、d=4、ヨー0、ピッチ−30°なら、𝐟=(0,−0.5,−0.866)で𝐞=(0,1,0)−4(0,−0.5,−0.866)=(0,3,3.464)となり、ターゲットの上かつ後ろです。

11.8 グラフィックスプログラミングでの使い方:P V M

全ての頂点が連鎖全体を通ります。
何時も通り右から左へ読みます(第7章):

𝐩clip=𝐏𝐕𝐌𝐩object

3つの行列は変わる頻度が違います:

  • 𝐌は物体毎に違います。
  • 𝐕はカメラが動く度、つまり1フレームに1回変わります。
  • 𝐏はウィンドウのサイズか視野角が変わった時だけ変わります。

行列の積は結合法則を満たす(第7章)ので、一部をCPUで掛けておき、送る行列を減らせます。
良くある構成は、𝐏𝐕を1フレームに1回、𝐌を物体毎に送る物です。
シェーダーはライティングの為にワールド座標が必要な事が殆どだからです。
3つ全部を使う素直な版は次の通りです:

struct Matrices {
  float4x4 model;
  float4x4 view;
  float4x4 proj;
};
ConstantBuffer<Matrices> matrices;

struct VertexOut {
  float4 position : SV_Position;
  float3 worldPos : TEXCOORD0;
};

[shader("vertex")]
VertexOut vertexMain(float3 position : POSITION) {
  VertexOut o;
  float4 world = mul(matrices.model, float4(position, 1.0));
  float4 viewPos = mul(matrices.view, world);
  o.position = mul(matrices.proj, viewPos);
  o.worldPos = world.xyz;
  return o;
}
#version 450
layout(set = 0, binding = 0) uniform Matrices {
  mat4 model;
  mat4 view;
  mat4 proj;
};

layout(location = 0) in vec3 inPosition;
layout(location = 0) out vec3 outWorldPos;

void main() {
  vec4 world = model * vec4(inPosition, 1.0);
  vec4 viewPos = view * world;
  gl_Position = proj * viewPos;
  outWorldPos = world.xyz;
}
struct Matrices {
  float4x4 model;
  float4x4 view;
  float4x4 proj;
};
ConstantBuffer<Matrices> matrices;

struct VertexOut {
  float4 position : SV_Position;
  float3 worldPos : TEXCOORD0;
};

VertexOut vertexMain(float3 position : POSITION) {
  VertexOut o;
  float4 world = mul(matrices.model, float4(position, 1.0));
  float4 viewPos = mul(matrices.view, world);
  o.position = mul(matrices.proj, viewPos);
  o.worldPos = world.xyz;
  return o;
}

今作った物の日常的な使い道を他にも挙げます:

  • ライティング用のカメラ位置。 スペキュラーハイライトにはワールド空間の目の位置が必要です。其れは𝐕−1、つまりカメラのワールド行列𝐂の4列目です。CPUで計算してuniformとして送ります。
  • ビルボード。 パーティクルや名札は常にカメラの方を向くべきです。ビュー行列の最初の2行はワールド空間でのカメラの右と上なので、其れらから四角形の角を作れます。
  • スカイボックス。 空は歩いても近付いてはいけません。ビュー行列をコピーして4列目を(0,0,0,1)に置き換えれば、回転だけが残ります:sky.c[3] = {0, 0, 0, 1};
  • フォグとソート。 ビュー空間では−zがカメラの前方の距離です。フォグや、半透明の物体を奥から手前へ並べ替える時に便利です。

ビルボードの関数は次の通りです。
第6章のインデックスの違いに注意して下さい。
SlangとHLSLでは行をview[0]で読みますが、GLSLは列が先なので、同じ行がview[0][0]、view[1][0]、view[2][0]に散らばっています。

float3 billboardCorner(float4x4 view, float3 center, float2 corner, float size) {
  float3 right = float3(view[0][0], view[0][1], view[0][2]);
  float3 up = float3(view[1][0], view[1][1], view[1][2]);
  return center + (corner.x * right + corner.y * up) * size;
}
vec3 billboardCorner(mat4 view, vec3 center, vec2 corner, float size) {
  vec3 right = vec3(view[0][0], view[1][0], view[2][0]);
  vec3 up = vec3(view[0][1], view[1][1], view[2][1]);
  return center + (corner.x * right + corner.y * up) * size;
}
float3 billboardCorner(float4x4 view, float3 center, float2 corner, float size) {
  float3 right = float3(view[0][0], view[0][1], view[0][2]);
  float3 up = float3(view[1][0], view[1][1], view[1][2]);
  return center + (corner.x * right + corner.y * up) * size;
}

11.9 其の他の使い道

基準座標系の変換は、気を付けて見ると何処にでもあります:

  • ロボット工学。 ロボットの関節、グリッパー、カメラは其々自分の座標系を持っています。「グリッパーから見てカップは何処?」は基底変換です。ROSはtfライブラリで此れらの座標系の木構造を丸ごと管理しています。
  • AR及びVR。 ヘッドセットは頭の位置と向きを追跡し、毎フレーム其処からビュー行列を作ります。左右の目に1つずつ、目の間隔の半分ずらして作ります。
  • GPSとドローン。 GPSは地球中心の座標系で位置を返します。ドローンや自動運転車は其れを基準点周りのローカルな東・北・上(ENU)座標系に変換します。回転と平行移動です。
  • 天文学。 星の位置は赤道座標、黄道座標、銀河座標で表され、決まった回転行列で互いに変換されます。
  • 航空機。 パイロットや飛行ソフトウェアは機体に固定された機体座標系(前、右翼、下)を使います。其の向きはヨー・ピッチ・ロールで表され、其れが第13章に繋がります。

11.10 コード

linalg.hhにあるlookAtは次の通りです。
此れを11.5節と1行ずつ比べてみて下さい。
行は𝐫、𝐮、−𝐟で、平行移動の列は−𝐫⋅𝐞、−𝐮⋅𝐞、𝐟⋅𝐞です。

// ビュー行列:カメラがeyeからtargetを見る。カメラは自分の-Z方向を向く。
// View matrix: camera at eye looking at target. The camera looks down its own -Z axis.
inline Mat4 lookAt(Vec3 eye, Vec3 target, Vec3 up) {
  Vec3 f = normalize(target - eye); // 前 / forward
  Vec3 r = normalize(cross(f, up)); // 右 / right
  Vec3 u = cross(r, f);             // 上 / up

  Mat4 m = Mat4::identity();
  m.at(0, 0) = r.x;  m.at(0, 1) = r.y;  m.at(0, 2) = r.z;  m.at(0, 3) = -dot(r, eye);
  m.at(1, 0) = u.x;  m.at(1, 1) = u.y;  m.at(1, 2) = u.z;  m.at(1, 3) = -dot(u, eye);
  m.at(2, 0) = -f.x; m.at(2, 1) = -f.y; m.at(2, 2) = -f.z; m.at(2, 3) = dot(f, eye);
  return m;
}

そして11.7節のforwardFromYawPitchヘルパーを使ったテストプログラムです。
ビュー行列の逆行列(第10章)が本当にカメラのワールド行列なのかも、4列目から目の位置を読んで確認しています。

#include <cstdio>
#include "linalg.hh"

// yaw: turn around +Y, pitch: look up (+) or down (-). yaw = pitch = 0 looks down -Z.
inline Vec3 forwardFromYawPitch(float yaw, float pitch) {
  float cp = std::cos(pitch);
  return {-std::sin(yaw) * cp, std::sin(pitch), -std::cos(yaw) * cp};
}

static void printVec(const char *name, Vec4 v) {
  printf("%-8s (%6.3f, %6.3f, %6.3f, %6.3f)\n", name, v.x, v.y, v.z, v.w);
}

static void printMat(const Mat4 &m) {
  for (int row = 0; row < 4; ++row)
    printf("  [%6.3f %6.3f %6.3f %6.3f]\n", m.at(row, 0), m.at(row, 1), m.at(row, 2), m.at(row, 3));
}

int main() {
  // a camera at (3, 0, 4) looking at the origin
  Mat4 view = lookAt({3, 0, 4}, {0, 0, 0}, {0, 1, 0});
  printf("view matrix:\n");
  printMat(view);

  printVec("origin", view * Vec4{0, 0, 0, 1});
  printVec("eye", view * Vec4{3, 0, 4, 1});
  printVec("p", view * Vec4{4, 2, -3, 1});

  // the camera's world matrix is the inverse of the view matrix
  Mat4 camWorld = inverse(view);
  printVec("camPos", camWorld.c[3]);

  // first-person camera: yaw 60 degrees, pitch 30 degrees
  Vec3 f = forwardFromYawPitch(radians(60.0f), radians(30.0f));
  printf("forward  (%6.3f, %6.3f, %6.3f) length %.3f\n", f.x, f.y, f.z, length(f));

  Vec3 eye{0, 2, 0};
  Mat4 fps = lookAt(eye, eye + f, {0, 1, 0});
  printVec("ahead", fps * Vec4{eye.x + 2 * f.x, eye.y + 2 * f.y, eye.z + 2 * f.z, 1});
  return 0;
}

何時も通りg++ -std=c++20 main.cc -o mainでコンパイルして実行します:

view matrix:
  [ 0.800  0.000 -0.600 -0.000]
  [ 0.000  1.000  0.000 -0.000]
  [ 0.600 -0.000  0.800 -5.000]
  [ 0.000  0.000  0.000  1.000]
origin   ( 0.000,  0.000, -5.000,  1.000)
eye      ( 0.000,  0.000,  0.000,  1.000)
p        ( 5.000,  2.000, -5.000,  1.000)
camPos   ( 3.000,  0.000,  4.000,  1.000)
forward  (-0.750,  0.500, -0.433) length 1.000
ahead    ( 0.000,  0.000, -2.000,  1.000)

-0.000は気にしないで下さい。
浮動小数点数はゼロにも符号ビットがあるので、−(2.4−2.4)は「負のゼロ」になりますが、0と等しいと判定されます。
コンパイラーによっては其処が0.000と表示される事もあります。
最後の行は良い確認です。
カメラの前方向に2進んだ点が(0,0,−2)、つまり−Zの真正面に来ています。

次の章では、ビュー行列の前に投影行列を置き、次の様に1つのビュー投影行列に纏めます:

Mat4 viewProj = perspectiveGl(radians(60.0f), 16.0f / 9.0f, 0.1f, 100.0f) * view;

11.11 まとめ

  • 空間とは、原点と軸の選び方の事です。頂点はM、V、P、wでの除算、ビューポート変換を通って、オブジェクト→ワールド→ビュー→クリップ→NDC→スクリーンと旅します。
  • 座標系Aの軸(と原点)を座標系Bで書いて列に並べた行列は、AからBへ変換します。其の逆行列はBからAへ変換します。
  • 正規直交基底なら逆行列は転置です。新しい座標は軸との内積に過ぎないからです。
  • カメラは目の位置𝐞と、右・上・前の軸を持つ物体です。カメラは自分の−Z方向を向きます。
  • ビュー行列はカメラのワールド行列の逆行列です。行は𝐫、𝐮、−𝐟、平行移動は−𝐫⋅𝐞、−𝐮⋅𝐞、𝐟⋅𝐞です。
  • lookAtは前をターゲット引く目から、右をcross(f, up)から、本当の上をcross(r, f)から作ります。前が上と平行だと壊れます。
  • ヨーとピッチからsinとcosで前ベクトルが作れます。ピッチは90°未満に制限します。
  • 全ての頂点は𝐏𝐕𝐌で右から左へ変換されます。

11.12 課題

  1. ★ 2Dの座標系の軸が𝐛1=(0,1)と𝐛2=(−1,0)です。此の座標系で(3,2)にある点の古い座標は?次に、古い点(1,4)を新しい座標系に変換して下さい。
  2. ★ 𝐛1=(0.6,0,0.8)、𝐛2=(0,1,0)、𝐛3=(−0.8,0,0.6)が正規直交で右手系(𝐛1×𝐛2=𝐛3)である事を確かめ、此の基底での(5,1,5)の座標を求めて下さい。
  3. ★ 目(0,3,4)、ターゲット(0,0,0)、上(0,1,0)のビュー行列を手で作って下さい。正規化する前の𝐟×𝐮𝐩の長さを書き出し、何故1にならないのか説明して下さい。
  4. ★ ヨー90°、ピッチ−45°のforwardFromYawPitchを正確な値(2/2を使う)で計算して下さい。カメラはどちらを向いていますか?
  5. ★★ orbitEye(Vec3 target, float distance, float yaw, float pitch)を書いて下さい。ターゲット(0,1,0)、距離4、ヨー0、ピッチ−30°なら約(0,3,3.464)を返し、lookAt(eye, target, {0, 1, 0}) * Vec4{0, 1, 0, 1}は(0,0,−4,1)を表示する筈です。
  6. ★★ ビュー行列の行から3つの軸を読み出すvoid cameraAxes(const Mat4 &view, Vec3 &right, Vec3 &up, Vec3 &forward)を書いて下さい。11.6節のカメラなら、右(0.8,0,−0.6)、上(0,1,0)、前(−0.6,0,−0.8)と表示される筈です。
  7. ★★★ lookAtは常にワールドの上を使うので、カメラをロール(地平線を傾ける)出来ません。ロール角を追加して下さい。𝐫と𝐮を計算した後、第8章の2D回転で其の2つを其れらの平面の中で回します。代わりに上として(0,−1,0)を渡すとどうなりますか?其れは何故ですか?


コメント(0)

まだコメントはありません。