GLSL のデータ型とコンストラクタの挙動まとめ

WebGL 2.0 で使える GLSL ES 3.0 における各種データ型やコンストラクタの挙動を自分用にまとめます[1]

データ型

全て precision が highp であるときのビット数を表します。

  • float: 32ビット浮動小数点数型
  • int: 32ビット符号付き整数型
  • uint: 32ビット符号なし整数型
  • bool: ブール型
  • vec2, vec3, vec4: float の 2, 3, 4 次元ベクトル版
  • ivec2, ivec3, ivec4: int の 2, 3, 4 次元ベクトル版
  • uvec2, uvec3, uvec4: uint の 2, 3, 4 次元ベクトル版
  • bvec2, bvec3, bvec4: bool の 2, 3, 4 次元ベクトル版
  • mat2, mat2x2: 2 次正方行列
  • mat3, mat3x3: 3 次正方行列
  • mat4, mat4x4: 4 次正方行列
  • mat2x3: 2 列 3 行の行列
  • mat2x4: 2 列 4 行の行列
  • mat3x2: 3 列 2 行の行列
  • mat3x4: 3 列 4 行の行列
  • mat4x2: 4 列 2 行の行列
  • mat4x3: 4 列 3 行の行列

行列の要素の型は全て float です。
サンプラーや構造体もありますが省略します。

行列の型がやばい

普通 m\times n 行列と書くと mn 列の行列を表しますが、GLSL の matmxnmnになっています。したがって、乗算可能な行列の型の組合せは

matAxB * matBxC = matAxC

ではなく

matAxB * matCxA = matCxB

となります。知らないと絶対に間違えますし知ってても面倒臭いことこの上ないです。

Column-major order

行列の型がやばいのは内部で column-major order を使っていることに起因します。row-major order では 1 行をユニットにして列数だけ繰り返すことでデータを保持しますが、column-major order では 1 列をユニットにして行数だけ繰り返すことでデータを保持します。2\times3 行列では、row-major order と column-major order で次のような順番で要素が格納されます。

\text{Row-major order}&\colon \begin{pmatrix}
0 & 1 & 2 \\
3 & 4 & 5
\end{pmatrix} \\
\text{Colomn-major order}&\colon \begin{pmatrix}
0 & 2 & 4 \\
1 & 3 & 5
\end{pmatrix} \\

GLSL は後者です。この要素の並びにしておくと、行列から列ベクトルを抜き出しやすくなるのが分かるかと思います(添え字が連続しているので)。GLSL では次のようにして 2\times3 行列から列ベクトルを抜き出せます。

mat3x2 a = mat3x2(0, 1, 2, 3, 4, 5);
a[0]; // vec2(0, 1)
a[1]; // vec2(2, 3)
a[2]; // vec2(4, 5)

ここまでくると GLSL が matmxnn\times m 行列を表している理由が推測できます。

float a[3][2] = {{0, 1}, {2, 3}, {4, 5}};
a[0]; // {0, 1}
a[1]; // {2, 3}
a[2]; // {4, 5}

配列版と比較してみると、名前と要素のアクセス方法に一貫性があります。したがって、行列をただの 2 次元配列と見た場合は特段気にすることはなく、代数的な処理を行う際だけ column-major であることに注意すればいいことになります。まあ行列でやることってほとんどが代数的処理な気がしますが。

ベクトルと行列の積

ベクトルと行列の積を計算する際にも注意が必要です。ベクトルは行列の左右から掛けることができ、それぞれ行ベクトル、列ベクトルとして扱われます[2]

正しい乗算の型は次の通りです。

matAxB * vecA = vecB

vecB * matAxB = vecA

行列の場合と合わせて、「外側が一致している必要があり、抜き出した内側の順序を反転させたものが演算結果になる」と覚えておくと多少はマシになります。

コンストラクタの挙動

「引数の要素をつなぎ合わせてデータを作ってくれる」程度の印象しかありませんでしたが、実際調べてみるとかなり非自明な挙動をします。

用語の定義

  • スカラー型: float, int, uint, bool
  • ベクトル型: vecn, ivecn, uvecn, bvecn
  • 行列型: matn, matmxn
  • 数値型: スカラー型、ベクトル型、行列型のすべて

共通する制約

引数に行列型が含まれる場合、引数はその行列ただ一つでなければならない。

例:

  • 引数に行列型を 2 つ以上渡してはならない
  • 引数に行列型とスカラー型を混ぜて渡してはならない
  • 引数に行列型とベクトル型を混ぜて渡してはならない

スカラー型を作る場合

スカラー型を作る場合、コンストラクタは任意の数値型を一つのみ受け取ります。受け取った引数がベクトル型であれば 1 つ目の要素、行列であれば 1 列 1 行目の要素を抜き取り、スカラー同士の型変換を行いスカラー型を生成します。

スカラー同士の型変換において、bool に変換する場合は x\neq0 を評価します。bool から変換する場合は、false0, true1 に変換します。

ベクトル型を作る場合

ベクトル型を作る場合、挙動が複雑になります。

スカラー型を一つ受け取った場合

スカラー型を適切に変換後、全ての要素にコピーしベクトル型を作ります。

vec3(2); // vec3(2.0, 2.0, 2.0)
vec4(true); // vec4(1.0, 1.0, 1.0, 1.0)

ベクトル型を一つ受け取った場合

サイズが足りなければエラーを起こし、サイズが大きすぎる場合は目的のサイズまでデータを切り詰めてベクトル型を作成します。

vec3(vec2(1)); // エラー
vec3(vec4(1, 2, 3, 4)); // vec3(1.0, 2.0, 3.0)

行列型を一つ受け取った場合

行列を column-major order に従い 1 次元に並べ直してベクトルを作り、必要であればデータを切り詰めてベクトル型を生成します。行列は少なくとも 4 つの要素を持ち、ベクトル型の最大次元数は 4 なのでデータは常に足ります。

vec4(mat2(1, 2, 3, 4)); // vec4(1.0, 2.0, 3.0, 4.0)
ivec3(mat2(1, 2, 3, 4)); // ivec3(1, 2, 3)

スカラー型またはベクトル型の 2 つ以上の引数を受け取った場合

全ての引数に含まれる要素を順番に連結し、ベクトル型を生成します。要素数が足りなかったり多かったりした場合はエラーとなります。

vec4(1, bvec2(false, true), 2.0); // vec4(1.0, 0.0, 1.0, 2.0)
vec4(vec2(1), vec2(2)); // vec4(1.0, 1.0, 2.0, 2.0)
vec4(1, 2, 3); // エラー
vec3(1, vec2(2, 3), 4); // エラー

その他の場合

前述の制約により、ありえません(全てエラー)。

行列型を作る場合

スカラー型を一つ受け取った場合

対角成分が与えられたスカラー、非対角成分がゼロとなる行列を生成します。

mat3x2(2); // mat3x2(2.0, 0.0, 0.0, 2.0, 0.0, 0.0)
           //   = 2.0  0.0  0.0
           //     0.0  2.0  0.0
mat2(1); // mat2(1.0, 0.0, 0.0, 1.0)

ベクトル型を一つ受け取った場合

行列を column-major order の下でベクトルとみなすと、ベクトル型の生成と同じ挙動になります。

行列型を一つ受け取った場合

行列の左上を揃えて矩形をくり抜いてコピーし、行列型を生成します。ただし、足りなかった部分は単位行列が割り当てられます。

次のように考えると分かりやすいです。

  1. 引数の行列が 4\times4 行列になるよう右下方向に拡大します。広がった部分には単位行列が割り当てられます(対角成分は 1、非対角成分は 0)。
  2. 拡大した行列の左上をくり抜いて目的の行列型を生成します。
mat3(mat2(1, 2, 3, 4)); // mat3(1.0, 2.0, 0.0, 3.0, 4.0, 0.0, 0.0, 0.0, 1.0)
mat2(mat3(1, 2, 3, 4, 5, 6, 7, 8, 9)); // mat2(1.0, 2.0, 4.0, 5.0)

スカラー型またはベクトル型の 2 つ以上の引数を受け取った場合

こちらも、行列を column-major order の下でベクトルとみなすと、ベクトル型の生成と同じ挙動になります。

その他の場合

前述の制約により、ありえません(全てエラー)。

数値型以外のコンストラクタの場合

構造体や配列の場合、数値型のコンストラクタにあったような型変換は一切してくれません。完全に一致する値を順番通り渡してやる必要があります。

おわりに

ついに最後の枷であった iOS 14 が終焉を迎え、WebGL 2.0 の時代が幕を開けようとしています

これから単精度浮動小数点テクスチャへの読み書きや MRT、フラグメントシェーダからの書き込み深度の指定などが当然のようにできると思うとワクワクしますね!


  1. 仕様書はこちら。どこかミスっていたら Twitter かなんかで教えてください ↩︎
  2. 個人的には、こうして行ベクトルと列ベクトルの立場をコロコロ変えるくらいなら、一貫して「ベクトルとはただの 1 階のテンソルである」ということにして行列を row-major order で扱ってほしかったです。 ↩︎

このエントリーをはてなブックマークに追加