1. はじめに:線型代数と座標系

線型代数シリーズへようこそ。
此のシリーズでは、線型代数の予備知識は一切必要ありません。
足し算、引き算、掛け算、割り算が出来れば始めるには十分で、記号は全て初めて出てきた時に説明します。

主な対象はゲーム及びグラフィックスのプログラマーですが、他の分野(ウェブ、組み込み、AI等)の開発者も勿論歓迎します。
ゲーム開発への転職を考えている方も、趣味でゲームを作りたい方も、自分の分野でスキルアップしたい方も、どなたでも大歓迎です。

又、此のシリーズは今後公開予定のSlang、GLSL、Vulkan、OpenGLシリーズの土台になります。
其れらのシリーズでは「法線を正規化する」「ビュー行列を掛ける」「投影でYが反転する」の様な言葉が当たり前の様に出てきます。
此のシリーズを終えれば、其れらはもう魔法ではなくなります。

1.1 線型代数とは?

簡単に言うと、線型代数とは足し合わせる事と拡大縮小する事が出来る物の数学です。
単純過ぎる様に聞こえますが、此の説明に当てはまる物は驚く程沢山あります:

  • 数の並び。例えば位置 (3,2,5) や色 (1.0,0.5,0.0)。
  • 空間の中の矢印。例えば「東に3メートル、北に2メートル歩く」。
  • 其れらの間の変換。例えば「全てを30°回転する」や「全てを2倍の大きさにする」。

数の並びはベクトル(第2章)と呼ばれ、変換は行列(第6章)と呼ばれる数の格子で表します。
「線型」という言葉は、此れらの変換が直線を直線のまま保つ事に由来します。
回転した四角形は相変わらず真っ直ぐな辺で出来ていて、拡大した三角形は相変わらず三角形です。

グラフィックスが線型代数の上に成り立っている理由

3Dモデルは、点(頂点と呼びます)の大きなリストと、どの点同士が三角形を作るかという情報に過ぎません。
GPUは毎フレーム、全てのモデルの全ての頂点に幾つもの行列を掛けます。
モデルをワールドに配置する行列、カメラからワールドを見る為の行列、そして3Dの景色を平らな2D画面に押し潰す行列です。

頂点が20,000個あるキャラクター1体を、毎秒60フレームで描画する場合を計算してみましょう:

20000×60=1200000 頂点/秒

キャラクター1体だけで毎秒100万回以上の頂点変換で、ゲームのシーンには何百ものオブジェクトがあります。
ライティング、影、カメラ、物理、アニメーションも全て、ここで学ぶ僅かな演算から作られています。
其の演算を理解すれば、GPUが一日の殆どを何に費やしているかが理解出来ます。

1.2 どんな分野で役に立つ?

線型代数はゲームだけの物ではありません。
日常的に使われている分野の一部を紹介します:

  • ゲーム開発者:行列変換(平行移動、回転、拡大縮小、せん断)、3D投影とカメラシステム、物理積分の為の時間差分、衝突判定、物理エンジン(剛体、拘束)、グラフィックスのレンダリングパイプライン、スケルタルアニメーション、インバースキネマティクス。
  • コンピュータービジョン:画像変換、特徴検出、カメラキャリブレーション、姿勢推定、ホモグラフィー行列。
  • 物理学及び科学計算:力学、電磁気学、流体力学、シミュレーションに於ける連立方程式の解法。
  • 信号処理及び音声処理:フィルター、フーリエ解析、オーディオエフェクト、圧縮アルゴリズム、ノイズ除去。
  • データサイエンス:高次元データセットの扱い、特徴量同士の関係を理解する為の共分散行列、特徴量の変換と正規化、次元削減の為の主成分分析(PCA)、データ圧縮やレコメンドエンジンの為の特異値分解(SVD)。
  • 建設及び土木工学:測量やBIM/CIMの為の座標系と座標変換、面積及び体積の計算、行列による構造荷重解析、安全率の計算、応力とひずみの有限要素モデリング。
  • 組み込みシステム:リアルタイムのセンサーフュージョンの為の効率的な行列演算、状態推定(カルマンフィルター)、制御システム、ベクトル化計算の為のメモリ最適化、固定小数点演算の実装。
  • サイバーセキュリティ:暗号(特に格子暗号と楕円曲線暗号)、暗号化及び復号のアルゴリズム、線型性を持つハッシュ関数、サイドチャネル攻撃の解析、秘密計算(セキュアマルチパーティ計算)。
  • 建築及びCAD/3Dデザイン:構造解析(有限要素法)、BIM(ビルディング・インフォメーション・モデリング)、複雑な幾何変換。
  • 金融及びクオンツ分析:ポートフォリオ最適化、リスクモデリング、線型計画法、資産価格評価やモンテカルロシミュレーションの為の大規模な連立方程式の解法。
  • バイオインフォマティクス及び計算生物学:配列アラインメント、タンパク質構造予測、個体群モデリング、ゲノムデータ行列の解析。
  • 機械学習及びAI:ニューラルネットワークの層(重み行列と順伝播・逆伝播)、線型回帰とロジスティック回帰、PCA/SVDによる次元削減、レコメンドシステム、クラスタリング(距離行列を使ったk平均法)、勾配やヘッセ行列による最適化。
  • ロボティクス及びオートメーション:順運動学と逆運動学、ロボットアームの制御、経路計画、センサーフュージョン、座標系の為の変換行列。

ここに自分の分野が無くても、普段使っているツールやライブラリの裏側で線型代数が活躍している可能性は高いです。

1.3 此のシリーズの構成

此のシリーズは全13章です。
各章は前の章の上に積み上げていくので、順番に読む事をお勧めします:

  1. はじめに:線型代数と座標系 — 此の章。数学のおさらいと、位置を数で表す方法。
  2. ベクトル — 矢印と数の並び。足し算、拡大縮小、そして2つのベクトルの間の補間。
  3. 長さ・距離・正規化 — ベクトルの長さ、2点間の距離、単位ベクトル。
  4. 内積 — 2つの方向がどれだけ揃っているかを測る。ライティングの心臓部。
  5. 外積 — 2つの方向に垂直な方向を求める。三角形の法線。
  6. 行列と行列・ベクトル積 — ベクトルを変換する数の格子。
  7. 行列の積と転置 — 変換を繋げる。
  8. 線型変換:拡大縮小・回転・せん断・鏡映 — 動きの基本的な部品。
  9. 同次座標と平行移動 — 行列で物を移動させる為の4つ目の座標。
  10. 行列式と逆行列 — 行列が空間をどれだけ引き伸ばすか、そして其れを元に戻す方法。
  11. 座標空間とカメラ — オブジェクト空間、ワールド空間、ビュー空間、そしてlookAt行列。
  12. 投影変換 — 3Dのシーンを2Dの画像にする。OpenGLとVulkanの両方。
  13. 回転とクォータニオン — ジンバルロック無しで滑らかに回転する。

各章の構成

第2章以降、全ての章は同じ流れで進みます:

  • 公式:考え方を数式で簡潔に示します。
  • 仕組み:何故其の公式になるのかを説明し、全ての計算過程を省略せずに数値例を解きます。
  • グラフィックスプログラミングでの使い道:ゲームやレンダラーの何処で実際に出てくるか。シェーダーコードも多く載せます。
  • 其の他の使い道:グラフィックス以外で同じ考え方が出てくる場所。
  • コード:数学ライブラリに追加するC++の関数と、小さなテストプログラム及び其の期待される出力。
  • まとめと課題:短い振り返りと、理解を確かめる為の練習問題。

課題には難易度が付いています。
★は紙とペンでの計算、★★はコードを書く問題、★★★はより深く考えたり発展させたりする問題です。
解答は載せませんが、コードの課題には期待される出力を書いているので、自分で答え合わせが出来ます。

シェーダーコードの読み方

シェーダーの例は、Slang、GLSL、HLSLの3つの言語で載せます。
最初はSlangが表示され、GLSL又はHLSLのタブをクリックすると同じコードの其の言語版が表示されます。
例えば、此れは角度と半径から円周上の点を求める小さな関数です:

float2 pointOnCircle(float angleDeg, float radius) {
  float a = radians(angleDeg);
  return float2(cos(a), sin(a)) * radius;
}
vec2 pointOnCircle(float angleDeg, float radius) {
  float a = radians(angleDeg);
  return vec2(cos(a), sin(a)) * radius;
}
float2 pointOnCircle(float angleDeg, float radius) {
  float a = radians(angleDeg);
  return float2(cos(a), sin(a)) * radius;
}

今は理解出来なくても大丈夫です。
此の章を読み終わる頃には radians、cos、sin が何をするのか分かる様になり、float2 と vec2(2つの数の組)は第2章で扱います。
3つの言語全てに radians、sin、cos が組み込まれているので、GPU上で其れらを自分で書く必要は無いという事に注目して下さい。

作るC++ライブラリ:linalg.hh

シリーズを通して、linalg.hh という1つのヘッダーファイルに小さなC++の数学ライブラリを作っていきます。
各章で学んだ関数を追加していくので、第13章を終える頃にはベクトル、行列、カメラ、投影、クォータニオンを備えた完全なライブラリが出来上がります。
標準ライブラリ以外に依存しない素のC++20で書くので、最近のコンパイラーなら何でも使えます。
各章で自分で書いていく事をお勧めしますが、自分の版と比べたい場合や先に進みたい場合は、完成したヘッダーを此処からダウンロード出来ます:linalg.hh。

テストプログラムは、常に linalg.hh と同じ場所に置いた1つの main.cc です。
GCC又はClangの場合、次の様にコンパイルして実行します:

g++ -std=c++20 -Wall -O2 main.cc -o main
./main

clang++ -std=c++20 -Wall -O2 main.cc -o main
./main

WindowsのVisual Studioの場合、空のC++コンソールプロジェクトを作成して両方のファイルを追加し、プロジェクト → プロパティ → 構成プロパティ → C/C++ → 言語 → C++言語標準をISO C++20標準 (/std:c++20)に設定して下さい。
其の後、Ctrl + F5でビルドして実行します。
コマンドラインの方が好みなら、Developer Command Promptを開いて cl /std:c++20 /EHsc main.cc を実行して下さい。

プログラミングが初めての方は、最初はコードの部分を飛ばして数学に集中しても全く問題ありません。
数学はコードに依存しません。

1.4 数学のおさらい

ベクトルに入る前に、後で使う学校の数学を少しだけ思い出しておきましょう。
既に全部知っている方は、ざっと目を通して座標系の節に進んで下さい。

負の数と変数

数は0より小さくもなります。
温度計を思い浮かべて下さい。2 °Cより5度寒いのは 2−5=−3 °Cです。
次の規則は何度も出てきます:

−(−4)=43−(−2)=3+2=5(−3)×4=−12(−3)×(−4)=12

言葉で言うと、負の数を引くのは足すのと同じで、負の数同士を掛けると正の数になります。

変数とは、数の代わりに置く文字の事です。
x=3 なら、2x+1=2×3+1=7 です。
2x は「2掛ける x」という意味である事に注意して下さい。数学では、物を並べて書くと掛け算を意味します。
掛け算を明示的に点 2⋅x や十字 2×x で書く事もあります。
普通の数に対しては3つとも同じ意味ですが、注意して下さい。ベクトルに対しては、点と十字は全く別の演算になります(第4章と第5章)。

変数の右下に小さく書かれた数字や文字、添字も使います。
添字は単なるラベルです。x1 と x2 は2つの別々の変数で、vx は「vのx成分」という意味です。

二乗と平方根

ある数の二乗とは其の数を其の数自身で掛けた物で、右上に小さな2を書いて表します:

32=3×3=9(−3)2=(−3)×(−3)=90.52=0.25

負の数同士を掛けると正になるので、二乗は決して負になりません。

平方根 x は其の逆で、二乗すると x になる0以上の数です。
従って 9=3、25=5、2≈1.414 です(記号 ≈ は「ほぼ等しい」という意味です)。
負の数の平方根は普通の数の範囲には存在しません。此れは後で、プログラムがうっかり負の数の sqrt を取ってしまった時に問題になります。

よくある落とし穴が1つあります。和の平方根は、平方根の和ではありません。

9+16=25=5但し9+16=3+4=7

ピタゴラスの定理(三平方の定理)

1つの角が90°の三角形(直角三角形)を考えます。
90°の角に接する2辺を a と b、其の角の向かい側にある一番長い辺を斜辺 c と呼びます:

直角三角形の2辺aとb及び斜辺c

ピタゴラスの定理は次の通りです:

a2+b2=c2従ってc=a2+b2

例:a=3、b=4 の場合:

c=32+42=9+16=25=5

此れが直線距離の測り方です。
点 (1,2) から点 (4,6) まで歩くと、右に 4−1=3、上に 6−2=4 移動します。
此の2つの移動が直角三角形の2辺になるので、直線距離は 32+42=5 です。
第3章では、正に此の考え方をベクトルの長さにします。

度とラジアン

角度は度で測ると習ったと思います。1回転が360°、半回転が180°、直角が90°です。
然し、数学ライブラリやシェーダーは角度をラジアンで測ります。

1ラジアンとは、円周に沿って半径と同じ長さだけ歩いた時の角度です。
円周全体の長さは 2πr で、π≈3.14159、r は半径です。
従って、1回転は 2π ラジアンです:

1ラジアン:弧の長さが丁度半径rと同じ

360°=2π rad180°=π rad

其処から2つの変換公式が出てきます:

ラジアン=度×π180度=ラジアン×180π

例:45°をラジアンに変換します。

45×π180=45π180=π4≈0.7854

例:2ラジアンを度に変換します。

2×180π=360π≈114.59°

何故わざわざラジアンを使うのでしょうか?
公式が簡単になるからです。
例えば、θ(ギリシャ文字のシータ。角度によく使う名前です)がラジアンなら、弧の長さは単に s=rθ です。
半径0.5 mの車輪が4ラジアン回ると、0.5×4=2 メートル進みます。
もっと実用的な理由もあります。std::sin、std::cos、そしてシェーダーの sin と cos は全てラジアンを受け取ります。
うっかり度を渡してしまうのはグラフィックスのコードで最もよくあるバグの1つなので、此のシリーズでは全ての公式及び全てのコードの角度はラジアンです。

サイン・コサイン・タンジェント

原点を中心とした半径1の円を描きます。此れを単位円と呼びます。
右端の点から出発し、反時計回りに角度 θ だけ円周上を進みます。
辿り着いた点の座標が (cosθ,sinθ) です。
此れが定義です。コサインは其の点の横の位置、サインは縦の位置です。

単位円上の点P = cos θ, sin θ

Pからx軸に真っ直ぐ線を下ろすと、斜辺が1、横の辺が cosθ、縦の辺が sinθ の直角三角形が出来ます。
するとピタゴラスの定理から、とても便利な事実が得られます:

cos2θ+sin2θ=1

半径が1ではなく r の円なら、全てが其の分拡大されるだけで、点は (rcosθ,rsinθ) です。
例:原点から10離れていて、角度が30°の点は次の位置にあります。

(10cos30°,10sin30°)≈(10×0.866,10×0.5)=(8.66,5)

タンジェントは其の2つの比です:

tanθ=sinθcosθ

原点からPへの直線の傾き、つまり右に1進む毎にどれだけ上がるかを表します。
90°では線が垂直になり、cosθ=0 なので、タンジェントは定義されません(0で割る事になる為)。

一目で分かる様にしておきたい値は次の通りです:

度 ラジアン sinθ cosθ tanθ
0° 0 0 1 0
30° π6 12 32≈0.866 33≈0.577
45° π4 22≈0.707 22≈0.707 1
60° π3 32≈0.866 12 3≈1.732
90° π2 1 0 定義されない
180° π 0 −1 0
270° 3π2 −1 0 定義されない
360° 2π 0 1 0

どの行も先程の事実で確かめられます。
30°の場合:(32)2+(12)2=34+14=1。

符号はPが何処にあるかで決まります。
円の左半分ではx座標が負なので、其処ではコサインが負になります。
円の下半分ではy座標が負なので、其処ではサインが負になります。
例えば120°では点は左上にあり、cos120°=−12、sin120°=32 です。

ヒント:円を 2π 回ると同じ点に戻るので、sin(θ+2π)=sinθ 及び cos(θ+2π)=cosθ です。其の為、サインとコサインは回転、上下の揺れ、明滅するライト、波の様な、繰り返す物にぴったりです。

Σ(シグマ)記号

似た項を沢山足す時、全部書き出すのは面倒です。
ギリシャ文字の大文字シグマ Σ は「此れらを全部足す」の略記です:

∑i=14i=1+2+3+4=10

「i を1から4まで動かして、i を足し合わせる」と読みます。
シグマの下の変数(i=1)が開始値、上の数(4)が終了値、右側の式が毎回足す物です。
プログラミングをする方には、単なる for ループです:

float sum = 0.0f;
for (int i = 1; i <= 4; ++i) {
  sum += i;
}
// sum == 10

式の中では添字付きの変数も使えます。
x1=1,x2=2,x3=3 及び y1=4,y2=5,y3=6 の場合:

∑i=13xiyi=1×4+2×5+3×6=4+10+18=32

此れは覚えておいて下さい。対応する数同士を掛けて足し合わせるのは、正に第4章の内積です。

1.5 座標系

座標系とは、位置を数で表す為の取り決めです。
原点と呼ばれる出発点と、測る方向である1本以上の軸があります。
位置を表す数を、其の位置の座標と呼びます。

1次元:数直線

一番単純な座標系は、真ん中に0があり、右に正の数、左に負の数が並ぶ1本の直線です:

−4から4までの数直線と原点及びx = 3

点の位置を表すには数が1つあれば十分です。
グラフィックスでは、アニメーションの時間、スライダー上の位置、ピクセルの深度等が1次元の座標です。

2次元:デカルト平面(直交座標平面)

原点で1本目と直角に交わる2本目の数直線を置くと、デカルト平面(ルネ・デカルトに因んだ名前)になります。
横の軸がx軸、縦の軸がy軸で、全ての点は組 (x,y) で表されます:

デカルト平面上の点3, 2と−3, −1

(3,2) を見つけるには、原点から右に3、上に2進みます。
(−3,−1) を見つけるには、左に3、下に1進みます。
順番は大事です。(3,2) と (2,3) は別の点で、慣例として常にxが先です。

3次元

xとyの両方に直角な3本目の軸zを加えると、空間の中の全ての位置を3つの数 (x,y,z) で表せます。
此のシリーズでは、+Xが右、+Yが上、+Zが画面の外側、つまり見ている人の方を向きます:

3本の軸:+Xが右、+Yが上、+Zが手前

点 (2,3,4) は「原点から右に2、上に3、手前に4進んだ所」という意味です。

右手系と左手系

xを右、yを上に決めると、zの向きには手前か奥かの2通りの選択肢があります。
どちらも正しく、どちらも実際に使われているので、自分がどちらの中にいるのかを知っておく事が大切です。

右手系と左手系

此の名前は右手の法則から来ています。
右手を上げて、親指を+X(右)、人差し指を+Y(上)に向け、中指を両方に垂直になる様に曲げて下さい。
中指は自分の方を向いている筈です。其れが右手系の+Zです。
同じ事を左手ですると中指は奥を向きます。其れが左手系です。

同じ手で、正の回転の向きも分かります。
右手の親指を軸の向きに合わせると、残りの指が曲がる方向が正の回転です。
言い換えると、軸の正の側から原点の方を見た時、正の回転は反時計回りです。
此のシリーズでは全ての回転に此の規約を使います。

此のシリーズは、伝統的なOpenGLの規約やBlender、Mayaの様なツールと同じく右手系を使います。
DirectX(伝統的に)、Unity、Unreal Engineは左手系を使います。
又、どの軸を「上」とするかもツールによって違います。BlenderとUnrealはZが上、Maya、Unity及びOpenGL流のコードは普通Yが上です。
他のツールからモデルを読み込んだら鏡に映った様に反転していたり、仰向けに寝ていたりした場合、殆どの原因は手系か上方向の軸の食い違いです。

画面とピクセルの座標

画面は更に別の規約を使います。
ピクセル座標は普通ウィンドウの左上の角から始まり、xは右に、yは下に増えます:

1920 × 1080のウィンドウのピクセル座標。原点は左上でyは下向き

画像ファイル、OS、殆どのUIライブラリは此の様にピクセルを数えます。0行目が一番上の行です。
此れはyが上に増える数学の規約とは逆なので、2つの間を変換するにはyを反転させる必要があります。
高さ H ピクセルのウィンドウでは、上から測ったy座標は、下から測ると H−y になります。

グラフィックスAPIは更に独自のひねりを加えます。
OpenGLの正規化デバイス座標はyが上向きで、ウィンドウ座標は左下から始まります。
一方、Vulkanのクリップ空間はyが下向きです。
此れは第12章でちゃんと扱います。今は「yはどっち向き?」という質問を常にしなければならない事だけ覚えておいて下さい。

1.6 此のシリーズの記法

シリーズに出てくる記号を一ヶ所にまとめました。
今覚えようとしなくて大丈夫です。見慣れない記号が出てきたら、此の表に戻って来て下さい。

概念 記法 例 章
スカラー(1つの数) 斜体の文字 s=2.5、角度 θ 1
ベクトル 太字の小文字 𝐯=(3,4) 2
ベクトルの成分 添字 vx=3、vy=4 2
ベクトルの長さ 二重縦線 ‖𝐯‖=5 3
単位ベクトル(長さ1) ハット 𝐯^ 3
内積 中央の点 𝐚⋅𝐛 4
外積 バツ印 𝐚×𝐛 5
行列 太字の大文字 𝐌 6
行列の要素 行・列の添字 mij は i 行 j 列 6
転置 右上のT 𝐌⊤ 7
逆行列 右上の−1 𝐌−1 10

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

レンダラーが扱う物は、文字通り全て座標で表されます。

頂点。
モデルは頂点の位置のリストです。
どのグラフィックスのチュートリアルも最初に描く定番の「ハロー・トライアングル」は、たった3つの点です:

ハロー・トライアングルの3つの頂点

キャラクターのモデルは、同じ物が何万個もの点になっただけです。
モデルを移動、回転、アニメーションさせる時、其れらの点の新しい座標を計算しています。

ピクセル。
画面上の全てのピクセルには整数の座標があります。例えば1920×1080のウィンドウの中心は (960,540) です。
フラグメント(ピクセル)シェーダーはピクセル毎に1回実行され、自分の座標を読む事が出来ます。

テクスチャ座標(UV)。
画像をモデルに貼り付ける為に、各頂点は画像の中の2D座標も持っています。此れをUVと呼びます。
Uが横方向、Vが縦方向で、画像のピクセル数に関係無く、どちらも0から1までです:

左上の0, 0から右下の1, 1までのテクスチャ座標

UVは画像に対する割合で測るので、同じモデルが256×256のテクスチャでも4096×4096のテクスチャでも使えます。
サイズ W×H の画像で実際のピクセル(テクセルと呼びます)を求めるには、掛け算します:(u×W,v×H)。
例えば、512×256のテクスチャで u=0.25,v=0.5 はテクセル (0.25×512,0.5×256)=(128,128) です。

注:VulkanとDirect3Dでは、上の図の通りUV (0,0) は画像の左上です。OpenGLでは、(0,0) はアップロードしたテクセルの最初の行を指し、OpenGLは其の行を一番下として扱います。其の為、多くのOpenGLプログラムは画像を読み込む時に上下を反転させています。

角度。
回転速度、カメラの視野角、太陽の向きは全て角度です。
アーティストやデザイナーは普通度で考えますが(「視野角60°」)、数学はラジアンを必要とするので、radians() での変換は常に使う事になります。

1.8 其の他の使い道

探してみると、座標系は至る所にあります。

  • 地図及びGPS:地球上の位置は緯度と経度という2つの角度の組で、度で表されます。其れを使って距離や方角を計算するには、先ずラジアンに変換します。
  • 表計算ソフト:C5の様なセルは座標で、C列の5行目です。ピクセルと同じく、行は上から下に数えます。
  • グラフ描画:株価のグラフから科学的なプロットまで、全てのグラフはデータの値を平面上のx座標とy座標に対応させています。
  • CNC工作機械及び3Dプリンター:此れらはGコードで動きます。Gコードはツールヘッドが移動すべき座標のリストで、例えば G1 X10 Y20 Z0.2(x = 10 mm、y = 20 mm、z = 0.2 mmまで直線で移動)の様に書きます。

1.9 コード

では、linalg.hh ヘッダーを作り始めましょう。
今は π の値と、1.4節の2つの角度変換関数だけを入れます。
linalg.hh というファイルを作り、次の内容を書いて下さい:

// linalg.hh — 線型代数チュートリアルシリーズで作る数学ライブラリ
// Math library built throughout the Linear Algebra tutorial series.
//
// 規約 / Conventions:
//   * 右手座標系 / right-handed coordinate system
//   * 列ベクトル、行列は左から掛ける / column vectors, matrices multiply from the left: v' = M * v
//   * 列優先メモリ配置(GLSLと同じ) / column-major storage (same as GLSL)
//   * 角度はラジアン / angles are in radians
#pragma once

#include <cmath>

//------------------------------------------
// 角度 / Angles (chapter 1)
//------------------------------------------

constexpr float PI = 3.14159265358979323846f;

// 度からラジアンへ / degrees to radians
constexpr float radians(float deg) { return deg * PI / 180.0f; }

// ラジアンから度へ / radians to degrees
constexpr float degrees(float rad) { return rad * 180.0f / PI; }

C++について幾つか補足します:

  • #pragma once は、複数のヘッダーが此のヘッダーをインクルードしても、1つのファイルにつき1回だけ読み込まれる様にします。
  • <cmath> で std::sin、std::cos、std::sqrt 等が使える様になります。殆ど全ての章で必要になります。
  • constexpr は、コンパイル時にコンパイラーが値を計算出来るという意味です。定数で radians(90.0f) と書けば、実行時には何の処理も発生しません。
  • 先頭のコメントはシリーズ全体の規約です。一部(列ベクトル、列優先メモリ配置)は第6章まで意味が分からないと思いますが、其れで大丈夫です。

次はテストプログラムです。
ヘッダーと同じ場所に main.cc を作って下さい:

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

int main() {
  float angles[] = {0.0f, 30.0f, 45.0f, 60.0f, 90.0f};

  for (float deg : angles) {
    float rad = radians(deg);
    printf("%4.0f deg = %.4f rad   sin = %.4f   cos = %.4f\n",
           deg, rad, std::sin(rad), std::cos(rad));
  }

  printf("1 rad = %.4f deg\n", degrees(1.0f));
  printf("PI rad = %.1f deg\n", degrees(PI));
  return 0;
}

1.3節のコマンドでコンパイルして実行すると、次の様に表示される筈です:

   0 deg = 0.0000 rad   sin = 0.0000   cos = 1.0000
  30 deg = 0.5236 rad   sin = 0.5000   cos = 0.8660
  45 deg = 0.7854 rad   sin = 0.7071   cos = 0.7071
  60 deg = 1.0472 rad   sin = 0.8660   cos = 0.5000
  90 deg = 1.5708 rad   sin = 1.0000   cos = -0.0000
1 rad = 57.2958 deg
PI rad = 180.0 deg

1.4節の表と比べてみて下さい。一致していますね。
但し、最後の角度をよく見て下さい。cos90° は丁度0になる筈なのに、-0.0000 になっています。
float は π を正確に保存出来ないので、radians(90.0f) は π2 よりほんの少しだけ大きくなり、コサインは約 −0.00000004 になって -0.0000 と表示されます。
此れは浮動小数点数の普通の振る舞いで、コードのバグではありません。
教訓:グラフィックスのコードでは、浮動小数点数の結果を決して == で比較せず、十分に近いかどうかを確かめましょう。

GPU上では此れらは必要ありません。SlangにもGLSLにもHLSLにも radians、degrees、sin、cos、tan が組み込まれているからです:

// 1秒に1回、0と1の間を滑らかに明滅させる / pulse smoothly between 0 and 1, once per second
float pulse(float timeSeconds) {
  float angle = radians(360.0) * timeSeconds;
  return 0.5 + 0.5 * sin(angle);
}
// 1秒に1回、0と1の間を滑らかに明滅させる / pulse smoothly between 0 and 1, once per second
float pulse(float timeSeconds) {
  float angle = radians(360.0) * timeSeconds;
  return 0.5 + 0.5 * sin(angle);
}
// 1秒に1回、0と1の間を滑らかに明滅させる / pulse smoothly between 0 and 1, once per second
float pulse(float timeSeconds) {
  float angle = radians(360.0) * timeSeconds;
  return 0.5 + 0.5 * sin(angle);
}

sin は−1から1の間を動くので、0.5+0.5sin(angle) は0から1の間を動きます。
t=0.25 秒では角度は 2π×0.25=π2 なので、明滅は最大値 0.5+0.5×1=1 に達します。

1.10 まとめ

  • 線型代数は、足し合わせたり拡大縮小したり出来る物の数学です。数の並び、矢印、そして其れらの間の変換を扱います。
  • 全てのモデルの全ての頂点が毎フレーム幾つもの行列を通る為、グラフィックスは線型代数の上に成り立っています。
  • ピタゴラスの定理 c=a2+b2 で直線距離が求まります。
  • 此のシリーズの角度は常にラジアンです。π rad = 180°。radians() と degrees() で変換します。
  • 単位円上で角度 θ の点は (cosθ,sinθ) で、cos2θ+sin2θ=1 です。
  • Σ は「全部足す」という意味で、for ループと同じです。
  • 此のシリーズは右手座標系を使います。+Xが右、+Yが上、+Zが見ている人の方。DirectX、Unity、Unrealは左手系です。
  • ピクセル座標は普通yが下向きです。Vulkanのクリップ空間もyが下向きです(第12章)。
  • テクスチャ座標(UV)は画像全体で0から1までです。

1.11 課題

  1. ★ 120°、270°、15°をラジアン(π の分数)に変換し、π6、3π4、3π を度に変換して下さい。
  2. ★ 直角を挟む2辺が6と8の直角三角形の斜辺の長さは?点 (5,12) は原点からどれだけ離れていますか?(−1,2) と (2,6) の距離は?
  3. ★ 表と符号の規則を使って、単位円上の150°、225°、300°の点の座標を書いて下さい。
  4. ★ ∑i=15i2 と ∑i=14(2i+1) を計算して下さい。
  5. ★ +Xが右、+Yが上、+Zが画面の奥を向くツールは右手系ですか、左手系ですか?+Xが右、+Yが画面の奥、+Zが上の場合は?
  6. ★★ 0°から360°まで15°刻みの全ての角度(25行)について sin と cos を表示するプログラムを書いて下さい。90°の行が sin = 1.0000、180°の行が cos = -1.0000、360°の行が cos = 1.0000 になっている事を確かめて下さい。
  7. ★★★ yが下向きの1920×1080のウィンドウで、ある点がピクセル (480,270) にあります。ウィンドウの中心を原点とし、yを上向きにして、ウィンドウ全体が両方の軸で−1から1になる様に拡大縮小した座標系での座標を求めて下さい(ピクセル座標は連続した値として扱って下さい)。次に、W×H のウィンドウの任意のピクセル (px,py) に対する一般的な公式を書いて下さい。


コメント(0)

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