2. ベクトル

第1章では、(3,2) や (2,3,4) の様な座標で位置を表しました。
此の章では、其の数の並びを計算出来る物として扱い始めます。其れがベクトルです。
ベクトルはグラフィックスプログラミングで最もよく使われる物です。
位置、移動、方向、色、テクスチャ座標、面の法線は全てベクトルで、皆さんが書くシェーダーコードの殆ど全ての行がベクトルを扱います。

2.1 点とベクトル

数式に入る前に、紙の上では同じに見えるけれど意味が違う2つの考え方を区別しておきましょう。

点は場所です。「宝物は (5,4) にある」。
ベクトルは変位、つまり移動です。「東に4歩、北に3歩歩く」。
点は「何処?」に答え、ベクトルは「どっちに、どれだけ?」に答えます。

点Aから点Bへのベクトル4, 3

点Aから点Bへの移動はベクトル (4,3)、つまり右に4、上に3です。
ベクトル其の物はAからBへの真っ直ぐな矢印で、図の「右に4」と「上に3」は其の2つの部分です。

ここが大事な所です。ベクトルには決まった出発点がありません。
「東に4歩、北に3歩歩く」は、家から出発しても駅から出発しても同じ指示です。
従って、同じベクトルは何処に描いても良く、どのコピーも同じ様に正しいです:

2つの違う出発点から描いた同じベクトルv

紛らわしい事に、点もベクトルも同じ様に数の並びで書きます。
種明かしをすると、点の数は実は原点から其の点へのベクトルです。
点 (5,4) は「原点から出発してベクトル (5,4) だけ移動した所」という意味です。
コードでは、どちらも普通同じ Vec3 型に入れるので、どちらがどちらかを覚えておくのは自分の役目です。
此の区別は第9章で再び重要になります。其処では点と方向が平行移動に対して違う振る舞いをします。

2.2 数の並びとしてのベクトル

ベクトルは 𝐯 の様に太字の小文字で書きます。
ベクトルの数を成分と呼び、添字で名前を付けます。vx、vy、vz は 𝐯 のx成分、y成分、z成分です。
此のシリーズでは、ベクトルを縦に書きます:

𝐯=[vxvyvz]=[312]

縦に書くと文章の中で場所を取るので、文中では同じベクトルを 𝐯=(3,1,2) と書きます。
意味は縦の列のままです。
何故横ではなく縦なのかは、第6章で行列が出てきた時に重要になります。今は単にそう書くと覚えておいて下さい。

成分の個数を、其のベクトルの次元と呼びます:

  • 2Dベクトル (x,y):画面上の位置、テクスチャ座標、平らな地図上の移動。
  • 3Dベクトル (x,y,z):3Dワールドの位置と方向、又はRGBの色。
  • 4Dベクトル (x,y,z,w):RGBAの色(赤、緑、青と、不透明度であるアルファ)、又は第9章で出てくる特別な4成分の位置。

ベクトル一般について話す時は、成分に番号を付けます。n 個の成分を持つベクトルは 𝐯=(v1,v2,…,vn) です。
此の章の内容は成分の個数に関係ありません。全ての演算は2Dでも3Dでも4Dでも100Dでも同じ様に動きます。

特別な名前を持つベクトルが1つあります。全ての成分が0のゼロベクトルです:

𝟎=(0,0,0)

「全く動かない」ベクトルです。
ゼロベクトルには方向が無く、其の事が第3章で少し問題になります。

2.3 公式

此の章の演算を一度に全部並べます。
2つの3Dベクトル 𝐚=(ax,ay,az) と 𝐛=(bx,by,bz)、及び1つの数 s を考えます。
s の様な普通の1つの数をスカラーと呼びます。後で分かる通り、ベクトルを拡大縮小(スケール)するからです。

足し算:対応する成分同士を足します。

𝐚+𝐛=[ax+bxay+byaz+bz]

引き算:対応する成分同士を引きます。

𝐚−𝐛=[ax−bxay−byaz−bz]

符号反転:全ての成分の符号を反転します。

−𝐚=[−ax−ay−az]

スカラー倍:全ての成分に同じ数を掛けます。

s𝐚=[saxsaysaz]

線型補間(lerp):0から1の間の量 t だけ、𝐚 から 𝐛 へ混ぜます。

lerp(𝐚,𝐛,t)=𝐚+t(𝐛−𝐚)

全てが成分毎に計算される事に注目して下さい。
x成分はx成分としか関わらず、y成分はy成分としか関わりません。
其の御蔭で、ベクトルの計算はとても簡単です。

2.4 仕組み

足し算:矢印を繋げる

2つのベクトルを足すとは、1つの移動の後にもう1つの移動をする事です。
𝐚 に沿って歩き、辿り着いた所から 𝐛 に沿って歩きます。
出発点から到着点への近道が 𝐚+𝐛 です:

矢印を繋げる:aの後にbでa + b

此れを頭と尻尾を繋ぐ規則と呼びます。𝐚 の矢印の先に 𝐛 の根元を置きます。
(図は下の計算例のベクトルで描いていますが、矢印がどの向きでも規則は同じです。)
横の移動は横の移動同士で、縦の移動は縦の移動同士で足されます。足し算が成分毎になるのは正に其の為です。

𝐚=(3,1) と 𝐛=(1,2) の計算例:

𝐚+𝐛=[31]+[12]=[3+11+2]=[43]

右に3、上に1歩いてから、右に1、上に2歩くと、出発点から右に4、上に3の所にいます。
3Dでも同じで、𝐚=(3,1,2) と 𝐛=(1,2,−1) なら:

𝐚+𝐛=[3+11+22+(−1)]=[431]

点にベクトルを足す事も出来ます。点に移動を足すと新しい点になります。
点 (1,1) から (4,3) だけ移動すると (1+4,1+3)=(5,4) に着きます。2.1節の図の通りです。

引き算:AからBへの矢印

引き算は「ここから其処へはどう行けば良い?」という質問に答えます。
𝐚 と 𝐛 が2つの点AとBの位置なら、𝐛−𝐚 はAからBへのベクトルです:

b − aはAからBへの矢印

何故でしょうか?
AからBへ行くには、Aから原点Oへ戻り(其れが −𝐚)、OからBへ行けば良い(其れが 𝐛)からです。
合わせると −𝐚+𝐛=𝐛−𝐚 です。
順番の覚え方:行き先引く出発点。

計算例:Aが (1,1)、Bが (5,4) にあります。

𝐛−𝐚=[5−14−1]=[43]

2.1節で見た (4,3) と同じです。
逆向きに辿って確かめましょう。𝐚+(𝐛−𝐚)=(1+4,1+3)=(5,4) で、Bになります。
逆の順番で引くと 𝐚−𝐛=(−4,−3) で、BからAに戻る矢印になります。
此の順番を間違えるのは定番のバグです。敵がプレイヤーに向かってくるのではなく、プレイヤーから逃げていきます。

3Dの例:プレイヤーが 𝐩=(2,0,5) に、敵が 𝐞=(6,0,2) に立っています。
プレイヤーから敵へのベクトルは次の通りです。

𝐞−𝐩=[6−20−02−5]=[40−3]

つまり、敵は右に4、見ている人から見て3だけ奥にいます(+Zは見ている人の方を向くので、−3は奥という意味です)。
第3章では其の距離を求め、プレイヤーが狙いを定められる方向に変えます。

符号反転:同じ矢印を逆向きに

ベクトルの符号を反転すると全ての成分の符号が変わり、長さを変えずに矢印の向きが逆になります:

−[40−3]=[−403]

(4,0,−3) がプレイヤーから敵を指すなら、(−4,0,3) は敵からプレイヤーを指します。
引き算は、実は符号を反転した物を足しているだけです:𝐚−𝐛=𝐚+(−𝐛)。

スカラー倍:伸ばす、縮める、反転する

ベクトルにスカラー s を掛けると、全ての成分に s が掛かります。
矢印への効果は次の通りです:

ベクトルv、2v、0.5v、0v、−v、−2v

  • s>1 なら、矢印は長くなります。
  • 0<s<1 なら、短くなります。
  • s=0 なら、ゼロベクトルに潰れます。
  • s<0 なら、反転して逆向きになります。

反転以外に向きが変わる事は無く、変わるのは長さだけです。
スカラーという名前は、ベクトルをスケール(拡大縮小)する事から来ています。

𝐚=(3,1,2) の計算例:

2𝐚=[2×32×12×2]=[624]0.5𝐚=[0.5×30.5×10.5×2]=[1.50.51]

ベクトルをスカラーで割るのは、其の逆数を掛けるのと同じです:𝐚2=12𝐚=0.5𝐚。
符号反転は−1のスカラー倍です:−𝐚=(−1)𝐚。

此の章に無い物にも注意して下さい。標準的な「ベクトル掛けるベクトル」はここにはありません。
2つのベクトルを掛けるという事には幾つかの意味があり、有名な2つ、内積と外積にはそれぞれ専用の章(第4章と第5章)があります。

性質

全ての演算は各成分に対する普通の算数なので、ベクトルは数で既に知っている規則に従います。
任意のベクトル 𝐚,𝐛,𝐜 とスカラー s,t について:

  • 𝐚+𝐛=𝐛+𝐚(足す順番は関係無い)
  • (𝐚+𝐛)+𝐜=𝐚+(𝐛+𝐜)(括弧の付け方も関係無い)
  • 𝐚+𝟎=𝐚(ゼロベクトルを足しても何も変わらない)
  • 𝐚+(−𝐚)=𝟎(ベクトルと其の符号反転は打ち消し合う)
  • s(𝐚+𝐛)=s𝐚+s𝐛(和をスケールするのは、スケールした物の和と同じ)
  • (s+t)𝐚=s𝐚+t𝐚
  • s(t𝐚)=(st)𝐚
  • 1𝐚=𝐚

𝐚=(3,1)、𝐛=(1,2)、s=2 で1つ確かめてみましょう:

2(𝐚+𝐛)=2[43]=[86]2𝐚+2𝐛=[62]+[24]=[86]

同じ答えです。
此れらの規則の御蔭で、普通の代数と同じ様にベクトルの式を自由に変形出来ます。

2.5 線型結合と標準基底

ベクトルのスカラー倍と足し算が出来れば、其れらを組み合わせられます。幾つかのベクトルをスケールして、結果を足すのです。
此れを線型結合と呼びます:

s𝐚+t𝐛

𝐚=(1,2)、𝐛=(3,−1)、s=2、t=3 の計算例:

2[12]+3[3−1]=[24]+[9−3]=[111]

では、組み合わせるベクトルとして一番単純な物を選びましょう。各軸の方向を丁度向いた、長さ1の矢印です。
3Dでは3つあり、それぞれに名前があります:

𝐢=[100]𝐣=[010]𝐤=[001]

此れらを標準基底ベクトルと呼びます。𝐢 は+X、𝐣 は+Y、𝐤 は+Zを向きます。
(𝐞1,𝐞2,𝐞3 と書く本もありますが、同じ物です。)
2Dでは 𝐢=(1,0) と 𝐣=(0,1) だけで足ります。

ここが肝心です。全てのベクトルは基底ベクトルの線型結合で、其の成分が係数になります:

[xyz]=x𝐢+y𝐣+z𝐤

2Dのベクトル (3,2) は、文字通り「𝐢 を3歩、其の後 𝐣 を2歩」です:

iを3歩とjを2歩でv = 3i + 2j

3Dの (2,−1,3) で、全ての手順を書き出して確かめましょう:

2𝐢+(−1)𝐣+3𝐤=[200]+[0−10]+[003]=[2+0+00−1+00+0+3]=[2−13]

同じ事を2回言っているだけの、意味の無い書き方に見えるかも知れません。
そうではありません。此れはシリーズ全体で最も重要な考え方の種です。
𝐢、𝐣、𝐤 を別のベクトルに置き換えると、同じ手順 x𝐢+y𝐣+z𝐤 で全てのベクトルが新しい場所に移ります。空間全体が回転したり、伸びたり、歪んだりするのです。
其の置き換えたベクトルを横に並べて書いた物が行列で、第6章では正に此の方法で行列を導入します。

2.6 線型補間

グラフィックスでは、2つの値の間の何かが欲しい事がとても多いです。経路の途中の位置、赤から青へ30%進んだ色、目標に向かって滑らかに動くカメラ等です。
其の為の道具が線型補間で、殆どの場合lerpと略されます:

lerp(𝐚,𝐛,t)=𝐚+t(𝐛−𝐚)

此の章の道具で読んでみましょう。𝐛−𝐚 は 𝐚 から 𝐛 への矢印です。
其れを t でスケールして途中までだけ進み、𝐚 に足して 𝐚 から出発させます。

t = 0、0.25、0.5、0.75、1でのaとbの間の点

  • t=0 では 𝐚+0=𝐚 になります。
  • t=1 では 𝐚+(𝐛−𝐚)=𝐛 になります。
  • t=0.5 では中点、つまり2つの平均になります。

計算例:𝐚=(2,4,0)、𝐛=(10,0,4)、t=0.25。
先ず 𝐚 から 𝐛 への矢印:

𝐛−𝐚=[10−20−44−0]=[8−44]

次に其の4分の1:

0.25[8−44]=[2−11]

最後に出発点に足します:

[240]+[2−11]=[431]

t=0.5 なら同じ手順で (2,4,0)+(4,−2,2)=(6,2,2) となり、中点になります。

式を展開すると、同じ位よく使われる2つ目の形が出てきます:

𝐚+t(𝐛−𝐚)=𝐚+t𝐛−t𝐚=(1−t)𝐚+t𝐛

此方は「𝐚 を 1−t、𝐛 を t だけ取る」という加重平均として読めます。
例で確かめましょう:0.75(2,4,0)+0.25(10,0,4)=(1.5,3,0)+(2.5,0,1)=(4,3,1)。
同じ答えです。
2つの形は厳密な数学では等しいですが、浮動小数点数では、2つ目の形は t=1 で必ず丁度 𝐛 になるのに対し、1つ目の形は僅かな丸め誤差でずれる事があります。
グラフィックスでは、どちらでも問題ありません。

0から1の外ではどうなるでしょうか?
t=1.5 なら (2,4,0)+1.5(8,−4,4)=(2,4,0)+(12,−6,6)=(14,−2,6) で、同じ直線上の 𝐛 を通り過ぎた点になります。
此れを外挿と呼びます。
役に立つ事もありますが(動いている物体が何処に行くかの予測等)、普通はバグなので、コードでは先に t を0から1の範囲にクランプする事がよくあります。

lerpの「線型」は、t によって一定の速さで直線上を動くという意味です。
ゆっくり始まって段々速くなる動きが欲しいなら、lerpは其のままで、t の時間による増え方を変えます。アニメーションのイージングカーブがしているのは其れです。

2.7 グラフィックスプログラミングでの使い道

位置と速度

動く物体には位置 𝐩(点)と速度 𝐯(ベクトル:1秒でどの方向にどれだけ動くか)があります。
ゲームは毎フレーム、速度に前のフレームからの経過時間 Δt を掛けた分だけ物体を動かします(「デルタt」と読みます。Δはギリシャ文字の大文字デルタで、「変化量」を表すのに使います):

𝐩←𝐩+𝐯Δt

矢印 ← は「古い値を新しい値で置き換える」という意味で、コードの = と同じです。
スカラー倍の後に足し算をするだけで、其れ以上の事はありません。

計算例:物体が 𝐩=(1,2,0) から、毎秒 𝐯=(4,0,−2) の速度で出発し、Δt=0.5 秒ずつ進めます。

𝐩+𝐯Δt=[120]+0.5[40−2]=[120]+[20−1]=[32−1]

次の2ステップで (5,2,−2)、(7,2,−3) になります。

v Δtずつ動く3回の位置の更新

何故毎フレーム 𝐯 を足すのではなく、Δt を掛けるのでしょうか?
フレームレートが変わるからです。
𝐯=(6,0,0) とします。
毎秒60フレームなら Δt=160 で、1フレーム毎に (0.1,0,0) 動き、60フレーム後には6動いています。
毎秒30フレームなら Δt=130 で、1フレーム毎に (0.2,0,0) 動き、30フレーム後にはやはり6動いています。
コンピューターの速さに関係無く、ゲーム内の1秒は同じ距離になります。

加速度も、1段上で全く同じ様に動きます。
重力はベクトル 𝐠=(0,−9.8,0)(メートル毎秒毎秒)で、毎フレーム先に速度を、次に位置を更新します:

𝐯←𝐯+𝐠Δt𝐩←𝐩+𝐯Δt

此れだけで、投げたボールの(単純ですが)完全な物理シミュレーションです。此の章の演算しか使っていません。

オフセット

ゲームの多くの位置は「何かから見て何処か」です。銃口、キャラクターの頭上の名札の位置、プレイヤーの後ろを付いていくカメラ等です。
此れらは点にオフセットベクトルを足した物です。
プレイヤーが (10,0,4) に立っていて、名札をプレイヤーの2つ上に浮かべたい場合:

[1004]+[020]=[1024]

名札に必要なのは「上」だけで、プレイヤーが向きを変えても変わりません。
「0.5だけ前」の様な銃口のオフセットは、プレイヤーが振り向くと変わります。オフセットを回転させる方法は第8章で扱います。

ベクトルとしての色

画面上の色は3つの数で、赤、緑、青の光をどれだけ出すかを、普通0から1で表します。
其れは3Dベクトルで、此の章の演算には其のまま見た目の意味があります:

  • 色を足すと光が足されます。暗いオレンジ (0.2,0.1,0.0) に灰色の環境光 (0.3,0.3,0.3) を足すと (0.5,0.4,0.3) になります。複数のライトの寄与は此の様に合成されます。
  • 色をスケールすると明るさが変わります。0.5×(0.8,0.6,0.4)=(0.4,0.3,0.2) は同じ色合いで半分の明るさです。
  • 色をlerpするとフェードします。赤 (1,0,0) から青 (0,0,1) へ t=0.25 で:(1,0,0)+0.25(−1,0,1)=(0.75,0,0.25)、赤みがかった紫です。

シェーダーでは、標準的なベクトルの数学には無い演算をもう1つ使います。2つのベクトルを成分毎に掛ける演算です。
色の場合はティント(色付け)や変調と呼ばれ、テクスチャの色をライトの色やティントで濾す時に使います:

[0.80.60.4] を [1.00.50.5] でティント=[0.8×1.00.6×0.50.4×0.5]=[0.80.30.2]

(1.0,0.5,0.5) のティントは赤を全部残して緑と青を半分にするので、結果はより赤く見えます。
次は、スプライトにティントを掛けて、ダメージを受けた時にある色へ向かってフラッシュさせる小さなシェーダー関数です:

float3 shadeSprite(float3 texColor, float3 tint, float3 flashColor, float flash) {
  float3 tinted = texColor * tint; // 成分毎の掛け算 / component-wise multiplication
  return lerp(tinted, flashColor, saturate(flash));
}
vec3 shadeSprite(vec3 texColor, vec3 tint, vec3 flashColor, float flash) {
  vec3 tinted = texColor * tint; // 成分毎の掛け算 / component-wise multiplication
  return mix(tinted, flashColor, clamp(flash, 0.0, 1.0));
}
float3 shadeSprite(float3 texColor, float3 tint, float3 flashColor, float flash) {
  float3 tinted = texColor * tint; // 成分毎の掛け算 / component-wise multiplication
  return lerp(tinted, flashColor, saturate(flash));
}

線型補間を、SlangとHLSLでは lerp、GLSLでは mix と呼びますが、何れも正確に 𝐚+t(𝐛−𝐚) を計算します。
SlangとHLSLの saturate(x) は値を0から1の範囲にクランプし、GLSLの clamp(x, 0.0, 1.0) と同じ事をします。
上のティントした色、白いフラッシュ (1,1,1)、flash = 0.5 なら:

[0.80.30.2]+0.5([111]−[0.80.30.2])=[0.80.30.2]+0.5[0.20.70.8]=[0.90.650.6]

flash を数フレームかけて1から0に動かせば、定番の「被ダメージ点滅」になります。

テクスチャ座標

UV座標(第1章)は2Dベクトルなので、此れらは全てUVにも当てはまります。
時間と共に大きくなるベクトルを足すとテクスチャがスクロールします。水、溶岩、ベルトコンベアの多くのエフェクトは此の方法で作られています:

float2 scrollUv(float2 uv, float2 speed, float time) {
  return frac(uv + speed * time);
}
vec2 scrollUv(vec2 uv, vec2 speed, float time) {
  return fract(uv + speed * time);
}
float2 scrollUv(float2 uv, float2 speed, float time) {
  return frac(uv + speed * time);
}

speed * time はスカラー倍で、其れを uv に足すとテクスチャがずれます。
frac(SlangとHLSL)と fract(GLSL)は小数点以下の部分だけを残すので、UVは1から0に戻って繰り返します。
例えば、𝐮𝐯=(0.9,0.5)、速度 (0.1,0)、時間2なら (0.9,0.5)+(0.2,0)=(1.1,0.5) で、繰り返して (0.1,0.5) になります。

シェーダーのベクトル:スウィズルと成分毎の計算

シェーディング言語にはベクトルが組み込まれています。SlangとHLSLでは float2、float3、float4、GLSLでは vec2、vec3、vec4 です。
足し算、引き算、スカラー倍は普通の +、-、* 演算子で出来ます。
同じサイズの2つのベクトルに対しては、上のティントの例の様に * と / は成分毎に計算されます。

シェーダーでは、スウィズルという機能で成分を選んだり、並べ替えたり、繰り返したり出来ます。
成分の名前は x y z w で、色の場合は代わりに r g b a も使えます(同じ4つの場所の別名です):

float3 swizzleDemo(float4 v) {
  float3 a = v.xyz; // the first three components
  float3 b = v.zyx; // reversed
  float3 c = v.xxy; // components may repeat
  return a + b + c;
}

float4 makeRedder(float4 color) {
  color.r = 1.0;   // write one component
  color.gb *= 0.5; // write two components at once
  return color;
}
vec3 swizzleDemo(vec4 v) {
  vec3 a = v.xyz; // the first three components
  vec3 b = v.zyx; // reversed
  vec3 c = v.xxy; // components may repeat
  return a + b + c;
}

vec4 makeRedder(vec4 color) {
  color.r = 1.0;   // write one component
  color.gb *= 0.5; // write two components at once
  return color;
}
float3 swizzleDemo(float4 v) {
  float3 a = v.xyz; // the first three components
  float3 b = v.zyx; // reversed
  float3 c = v.xxy; // components may repeat
  return a + b + c;
}

float4 makeRedder(float4 color) {
  color.r = 1.0;   // write one component
  color.gb *= 0.5; // write two components at once
  return color;
}

𝐯=(1,2,3,4) なら、v.xyz は (1,2,3)、v.zyx は (3,2,1)、v.xxy は (1,1,2) です。
其れらを成分毎に足すと (1+3+1,2+2+1,3+1+2)=(5,5,6) になります。

覚えておくべき規則が2つあります。
1つ目に、1つのスウィズルで2つの名前の組を混ぜないで下さい。GLSLでは v.xg はエラーになり、コンパイラーが受け付ける場合でも読み難いです。
2つ目に、スウィズルに書き込む時は同じ成分を2回使えません。v.xx = ... はどの言語でもエラーです。どちらの値が勝つべきか決められないからです。
スウィズルはGPU上では無料なので、好きなだけ使って下さい。

2.8 其の他の使い道

ベクトルは、幾つかの数がひとまとまりになっていて、まとめて足したりスケールしたりする所なら何処にでも出てきます:

  • 物理:力。力はベクトルで、物体に幾つもの力が働く時、合計の力は其れらの和です。(10,0) ニュートンで押され、重力で (0,−5) だけ下に引かれ、ロープで (−4,2) 引っ張られる箱が受ける合力は (10+0−4,0−5+2)=(6,−3) です。
  • 風と航法。空気に対して (200,0) km/hで飛ぶ飛行機は、(−20,15) km/hの風の中では、地面に対して (200−20,0+15)=(180,15) km/hで動きます。パイロットや船乗りは毎日此のベクトルの足し算をしています。
  • 機械学習:特徴ベクトル。家は (120,3,15)、つまり120 m²、3部屋、築15年と表せます。此の様なデータはベクトルとして保存され、成分が何百もある事もよくあります。2軒の家 (120,3,15) と (80,2,25) の平均は t=0.5 のlerpで、(100,2.5,20) です。
  • 買い物。リンゴ3個、パン1個、牛乳2本の買い物かごはベクトル (3,1,2) です。友達の (2,0,1) とまとめるのはベクトルの足し算で (5,1,3)、注文を2倍にするのはスカラー倍で (6,2,4) です。合計金額の計算は第4章の内積です。
  • 音声。ステレオのサンプルは2Dベクトル(左、右)です。2つの音を混ぜるのはベクトルの足し算、音量つまみはスカラー倍、2曲のクロスフェードはlerpです。

2.9 コード

では、linalg.hh にベクトルを追加しましょう。
第1章の角度の関数の下に次の内容を書いて下さい:

//------------------------------------------
// ベクトル / Vectors (chapters 2–5)
//------------------------------------------

struct Vec2 {
  float x = 0.0f, y = 0.0f;
};

struct Vec3 {
  float x = 0.0f, y = 0.0f, z = 0.0f;
};

struct Vec4 {
  float x = 0.0f, y = 0.0f, z = 0.0f, w = 0.0f;
};

// 足し算・引き算 / addition and subtraction (chapter 2)
inline Vec2 operator+(Vec2 a, Vec2 b) { return {a.x + b.x, a.y + b.y}; }
inline Vec2 operator-(Vec2 a, Vec2 b) { return {a.x - b.x, a.y - b.y}; }
inline Vec3 operator+(Vec3 a, Vec3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
inline Vec3 operator-(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
inline Vec4 operator+(Vec4 a, Vec4 b) { return {a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w}; }
inline Vec4 operator-(Vec4 a, Vec4 b) { return {a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w}; }
inline Vec3 operator-(Vec3 v) { return {-v.x, -v.y, -v.z}; }

// スカラー倍 / scalar multiplication (chapter 2)
inline Vec2 operator*(float s, Vec2 v) { return {s * v.x, s * v.y}; }
inline Vec3 operator*(float s, Vec3 v) { return {s * v.x, s * v.y, s * v.z}; }
inline Vec4 operator*(float s, Vec4 v) { return {s * v.x, s * v.y, s * v.z, s * v.w}; }
inline Vec3 operator*(Vec3 v, float s) { return s * v; }
inline Vec3 operator/(Vec3 v, float s) { return {v.x / s, v.y / s, v.z / s}; }

// 線型補間 / linear interpolation (chapter 2)
inline Vec3 lerp(Vec3 a, Vec3 b, float t) { return a + t * (b - a); }

幾つか補足します:

  • 各ベクトルはfloatを並べただけの普通の struct です。= 0.0f のデフォルト値の御蔭で Vec3 v; はゼロベクトルになり、Vec3 a{3.0f, 1.0f, 2.0f}; と書けば順番に成分が設定されます。
  • 関数はベクトルを値渡しで受け取ります。Vec3 は僅か12バイトなので、コピーはポインターを渡すのと同じ位安く、コンパイラーもとても上手く最適化します。
  • inline を付けると、複数の .cc ファイルからインクルードされるヘッダーの中で関数を定義しても、リンカーに重複を怒られません。
  • 各演算子は2.3節の公式を成分毎に書き出した物です。return {a.x + b.x, a.y + b.y}; で結果のベクトルを直接作ります。
  • 一番よく使う型なので、Vec3 には一番多くの演算子があります。符号反転、両側からのスカラー倍(2.0f * v と v * 2.0f)、スカラーでの割り算です。Vec2 と Vec4 では、スカラーを左側に書いて下さい。
  • lerp は2.6節の公式で、既にある演算子だけで作っています。C++20の <cmath> にも1つの数の為の std::lerp がありますが、此方は std 名前空間の外にあり、ベクトルを受け取るので、2つがぶつかる事はありません。

此の章の例を一通り実行するテストプログラムです:

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

void print(const char *name, Vec3 v) {
  printf("%-11s = (%g, %g, %g)\n", name, v.x, v.y, v.z);
}

int main() {
  Vec3 a{3.0f, 1.0f, 2.0f};
  Vec3 b{1.0f, 2.0f, -1.0f};

  print("a + b", a + b);
  print("a - b", a - b);
  print("-a", -a);
  print("2 * a", 2.0f * a);
  print("a / 2", a / 2.0f);

  // 線型結合 / linear combination: 2i - j + 3k
  Vec3 i{1, 0, 0}, j{0, 1, 0}, k{0, 0, 1};
  print("2i - j + 3k", 2.0f * i - j + 3.0f * k);

  // 線型補間 / lerp
  Vec3 p0{2, 4, 0}, p1{10, 0, 4};
  print("lerp 0.25", lerp(p0, p1, 0.25f));
  print("lerp 0.5", lerp(p0, p1, 0.5f));

  // 位置の更新 / position update: p = p + v * dt
  Vec3 position{1, 2, 0};
  Vec3 velocity{4, 0, -2};
  float dt = 0.5f;
  for (int frame = 1; frame <= 3; ++frame) {
    position = position + dt * velocity;
    printf("frame %d: ", frame);
    print("position", position);
  }
  return 0;
}

期待される出力:

a + b       = (4, 3, 1)
a - b       = (2, -1, 3)
-a          = (-3, -1, -2)
2 * a       = (6, 2, 4)
a / 2       = (1.5, 0.5, 1)
2i - j + 3k = (2, -1, 3)
lerp 0.25   = (4, 3, 1)
lerp 0.5    = (6, 2, 2)
frame 1: position    = (3, 2, -1)
frame 2: position    = (5, 2, -2)
frame 3: position    = (7, 2, -3)

どの行も此の章の計算例と一致しています。
%g はfloatを一番短い読み易い形で表示するので、4.000000 ではなく 4 と表示されます。

2.10 まとめ

  • 点は場所で、ベクトルは何処に描いても良い変位(方向と距離)です。
  • ベクトルは成分と呼ばれる数の並びで、縦に書きます。𝟎 はゼロベクトルです。
  • 足し算、引き算、符号反転、スカラー倍は全て成分毎に計算します。
  • 𝐛−𝐚 はAからBへの矢印です。行き先引く出発点。
  • スカラーを掛けると、ベクトルは伸びたり、縮んだり、(負なら)反転したりします。
  • 全てのベクトルは基底ベクトルの線型結合です:(x,y,z)=x𝐢+y𝐣+z𝐤。此れが第6章で行列になります。
  • lerp(𝐚,𝐛,t)=𝐚+t(𝐛−𝐚) は2つのベクトルの間を補間します。SlangとHLSLでは lerp、GLSLでは mix です。
  • 移動は 𝐩←𝐩+𝐯Δt で、Δt を掛ける事でフレームレートに依存しなくなります。
  • シェーダーには、スウィズル(.xyz、.rgb、.xxy)と、色のティント等に使う成分毎の掛け算もあります。

2.11 課題

  1. ★ 𝐚=(2,−1,3)、𝐛=(−4,5,1) の時、𝐚+𝐛、𝐚−𝐛、3𝐚、−𝐛、2𝐚−𝐛 を計算して下さい。
  2. ★ 点Aが (1,2,3)、点Bが (4,6,3) にあります。AからBへのベクトルは?BからAへは?Bから出発して、AからBへのベクトルだけもう一度移動すると、何処の点に着きますか?
  3. ★ (5,−2,7) を 𝐢、𝐣、𝐤 の線型結合で書いて下さい。次に、s(1,0)+t(1,1)=(5,3) となる数 s と t を求めて下さい。
  4. ★ 𝐚=(0,10,−4)、𝐛=(8,2,4) の時の lerp(𝐚,𝐛,0.75) を、公式の両方の形で計算して下さい。
  5. ★ ボールが 𝐩=(0,10,0) から速度 𝐯=(2,0,0) で出発し、重力は 𝐠=(0,−10,0) です。Δt=0.5 で、先に速度、次に位置を更新する事を2ステップ行って下さい。各ステップの後、ボールは何処にあり、速度は幾つですか?
  6. ★★ 自分の linalg.hh に足りない演算子を追加して下さい。Vec2 と Vec4 の符号反転、及び Vec2 用の lerp のオーバーロードです。lerp(Vec2{0, 0}, Vec2{10, 20}, 0.3f) が (3, 6) と表示され、-Vec4{1, -2, 3, 4} が (-1, 2, -3, -4) と表示される事を確かめて下さい。
  7. ★★★ (1,2)+(3,4) の様に2つの点を足した答えは座標系の原点を動かすと変わりますが、平均 0.5(1,2)+0.5(3,4) は変わりません。原点を (10,0) だけずらして(両方の点に (10,0) を足して、両方の結果を計算して比べて)此れを確かめて下さい。点の組み合わせが、スカラーの合計が1の時だけ意味を持つのは何故か説明出来ますか?


コメント(0)

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