お勉強記事ということで、数学な内容の投稿をします!
本記事をはじめ数学的な内容を扱う投稿に関しては、「文系卒が数学をやれるとこまで頑張る」という主旨です。実際どこまで知識がつくか暖かく見守っていただきつつ、理解について誤っているところなどあればご指摘ください…。
同時に3Dプログラミングにひーひー言っている非理系の方々にも微力ながら助けになれば幸いです。
本記事では Processing等のクリエイティブコーディングのための言語が持つ、図形を移動させたり拡大縮小したりする便利な関数群が、どのような数学的計算によって実現されているのかを明らかにします。使用するコードは GLSL を選択していますが、それには理由があります。
GLSLでは行列計算をほぼ必ず必要とするからです。ProcessingやopenFrameworksといったラッパーライブラリを使う分には、行列計算をしていることを隠蔽し簡単なインターフェースを提供してくれているので、行列の知識は必要ありません。ありがたい…。しかし、GLSLでは行列を知らないとほぼ何もできなくて震えます。
GLSLをはじめとするシェーダ言語は、3Dプログラミングにおいて欠かせない領域です。GPUを用いて、数万もの並列計算をできるという偉大なるフォースを秘めているからです。
しかし、シェーダ言語はGPUの性質にも大きく依存し、提供される機能はごく限定的です。Processingのように「はい、45度まわってー」といって回ってくれるような従順でかわいげのある環境ではありません。
例えば、頂点シェーダでは、頂点(位置ベクトル)の移動を記述しなければいけないにも関わらず、Processingでいう回転: rotate()、平行移動: translate()、拡大縮小: scale() などのビルトイン関数がないため、3D幾何の知識を駆使して自分で移動を記述する必要があります。ハードコア…。
座標変換をする関数群の中身を GLSLで行列を用いて記述できるようにすることは、若干ハードルを感じますが、GPUパワーを手にできるという大きな意味があります。
行列
残念ながら行列の全てをバスっと説明できるスキルは私にはありません…。行列の定義や個別の演算方法についても、ネット上にわかりやすい記事がたくさんありますので、そちらに譲るとします。(例えばこちらの記事など)
ここでは、正方行列の持つ、とても興味深い幾何学的特徴を紹介するのみとします。
正方行列のかけ算は、掛け合わせる行列に内包されるベクトル同士の内積を集めたものということができます。クロッキー帳の雑なメモですが、行列の成分がベクトルの内積に等しいことを表しています。
しかし、幾何学的にみるとおもしろいことがわかります。
正方行列の持つ列をベクトルと見立てると、ちょうどそのベクトルにで表される軸によって座標系を歪めることができます。つまり、1列目は、x軸の歪めた結果のベクトル、2列目はy軸を歪めた結果、3列目はz軸…ということになります。
このように行列は、x軸、y軸、z軸のそれぞれを指定するベクトルに変形させ座標系を歪められること表しているのです。
上記の行列では、x軸とy軸が変化し、回転とスケーリングが作用していることがわかります。
(OpenGLとGLSLではベクトルを列で表現します。DirectXの場合はベクトルを行で表すので上記説明の列を行と言い換える必要があります。OpenGLでは、ベクトルvに行列Mをかけて座標変換したい場合、vMではなくMvとしないと正しい結果が得られません。DirectXでは逆…。)
GLSLによるコード
回転については前述の通り、「各列がそれぞれ変換後のx軸、y軸、z軸になる」ということを意識できるとかなり理解がしやすいです。sin、cosがわかればガッテンできると思います。
X軸回転: rotateX
1 2 3 4 5 6 7 8 9 10 |
// posをx軸を中心にradだけ回転させる(反時計まわり) vec3 rotateX(vec3 pos, float rad) { mat3 mat = mat3( 1.0, 0.0, 0.0, 0.0, cos(rad), -sin(rad), 0.0, sin(rad), cos(rad) ); return mat * pos; } |
Y軸回転: rotateY
1 2 3 4 5 6 7 8 9 10 |
// posをy軸を中心にradだけ回転させる(反時計まわり) vec3 rotateY(vec3 pos, float rad) { mat3 mat = mat3( cos(rad), 0.0, sin(rad), 0.0, 1.0, 0.0, -sin(rad), 0.0, cos(rad) ); return mat * pos; } |
Z軸回転: rotateZ
1 2 3 4 5 6 7 8 9 10 |
// posをz軸を中心にradだけ回転させる(反時計まわり) vec3 rotateZ(vec3 pos, float rad) { mat3 mat = mat3( cos(rad), -sin(rad), 0.0, sin(rad), cos(rad), 0.0, 0.0, 0.0, 1.0 ); return mat * pos; } |
拡大縮小: scale
回転より簡単…。
1 2 3 4 5 6 7 8 9 10 11 |
// posをx軸方向、y軸方向、z軸方向にスケーリングする // 負の値をとれば、逆方向にスケーリングする vec3 scale(vec3 pos, float xScale, float yScale, float zScale) { mat3 mat = mat3( xScale, 0.0, 0.0, 0.0, yScale, 0.0, 0.0, 0.0, zScale ); return mat * pos; } |
平行移動: Translate
平行移動は、トリッキーですが四次元目のw成分を設定することで表現できます。普通にベクトルの加算と同じ計算結果が得られることがわかります。w成分については、深く考えると哲学的でとてもおもしろいです…。4次元ベクトルが3次元空間に写像として現れている、というそうです。
1 2 3 4 5 6 7 8 9 10 11 12 |
// pos + vec3(x, y, z) と計算結果は同じ vec3 translate(vec3 pos, float x, float y, float z) { mat4 mat = mat4( 1.0, 0.0, 0.0, x, 0.0, 1.0, 0.0, y, 0.0, 0.0, 1.0, z, 0.0, 0.0, 0.0, 1.0 ); vec4 newPos = mat * vec4(pos, 1.0); return newPos.xyz; } |
次回(もしあれば)は、モデル座標変換、ビュー座標変換、投影変換のそれぞれの行列の記述を明らかにしつつ、カメラまわりの関数についてGLSLのコードで記述する試みをしてみたいと思います。
May The Force Be With You!
(SWはやくみたいっすね)