基本構文
GLSL は C / C++ に似た静的型の言語。main() から始まり、
各シェーダごとに役割を果たす。型の自動変換は限定的、ポインタや動的メモリは無い。
シェーダの骨格
#version 300 es
precision highp float;
uniform float uTime;
in vec2 vUv; // vertex shader からの varying(補間値)
out vec4 outColor; // fragment の出力
void main() {
outColor = vec4(vUv, sin(uTime) * 0.5 + 0.5, 1.0);
}
構造の要点:
#version 300 es— GLSL ES 3.00 を宣言。1行目に書くprecision highp float;— 浮動小数点の精度(後述)uniform/in/out— 入出力void main()— エントリーポイント。引数なし、戻り値なし
基本データ型
| 型 | 意味 |
|---|---|
void | 戻り値なし(関数のみ) |
bool | 真偽値(true/false) |
int | 整数 |
uint | 符号なし整数(GLSL ES 3.00+) |
float | 32bit 浮動小数 |
vec2 / vec3 / vec4 | float の 2/3/4 要素ベクトル |
ivec2 / ivec3 / ivec4 | int のベクトル |
bvec2 / bvec3 / bvec4 | bool のベクトル |
mat2 / mat3 / mat4 | float の正方行列 |
sampler2D / samplerCube / sampler3D | テクスチャ |
変数の宣言
float a = 1.0; // float リテラルは小数点必須
int b = 5;
vec3 c = vec3(1.0, 0.5, 0.0); // RGB
vec4 d = vec4(c, 1.0); // 既存 vec3 から拡張
mat3 m = mat3(1.0); // 単位行列
const float PI = 3.14159;
1 と 1.0 は別物
GLSL は暗黙の型変換が弱い。float a = 1; はエラーになる場合がある。
浮動小数点リテラルは必ず小数点を書く: 1.0、0.5、2. はOK。
修飾子
ストレージ修飾子
| 修飾子 | 意味 |
|---|---|
const | 定数(コンパイル時定数) |
uniform | JS から渡す。同じ描画バッチ内で不変 |
in | 入力(vertex の attribute、fragment への varying) |
out | 出力(vertex から fragment へ、または fragment の最終色) |
inout | 関数引数で参照渡し(読み書き) |
精度修飾子(precision)
浮動小数点・整数の精度。GLSL ES では必ず 1 つ以上指定する必要がある。
highp— 32bit 相当。座標・行列・大事な計算はこれmediump— 16bit 相当。色や軽い計算なら十分。モバイル GPU で大きく速くなる場合ありlowp— 8bit 程度。色だけならこれでも動くが、精度切れに注意
precision highp float; // 全 float がデフォルト highp
precision highp int;
// 個別に指定もできる
mediump vec3 color = vec3(1.0);
Three.js の ShaderMaterial では自動で precision が挿入されるので、初心者は気にしなくてもよい。
演算子
四則・比較
+ - * /、比較 == != < <= > >=。
ベクトル/行列にも使える。
vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = a + b; // (1.5, 2.5, 3.5)
vec3 d = a * b; // 要素ごと: (0.5, 1.0, 1.5)
vec3 e = a * 2.0; // スカラ倍: (2.0, 4.0, 6.0)
vec * vec は要素ごと、mat * vec は線形変換
ベクトル同士の * はcomponent-wise(要素ごと)。
内積は dot(a, b)。
行列とベクトルの掛け算 m * v は線形変換(数学の通常の意味)。
論理演算
&& / || / !。bool に対して使う。
代入
= / += / -= / *= / /=。
制御フロー
if
if (uv.x > 0.5) {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
} else {
outColor = vec4(0.0, 1.0, 0.0, 1.0);
}
for
vec3 sum = vec3(0.0);
for (int i = 0; i < 10; i++) {
sum += sample(i);
}
ループ回数は定数か、コンパイル時に決まる範囲が無難。WebGL1 (GLSL ES 1.00) では ループ回数を uniform で動的に変えられない制約がある。GLSL ES 3.00 ではより自由。
while / do-while
書けるが、ほぼ使わない。GPU は分岐・ループが苦手なので、可能なら
step / mix / smoothstep といったブランチレスに書く。
break / continue / return
ループの中で使える。fragment shader での discard はそのピクセルの描画を放棄する。
if (alpha < 0.01) discard;
関数定義
float circle(vec2 uv, vec2 center, float radius) {
return step(length(uv - center), radius);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
float c = circle(vUv, vec2(0.5), 0.3);
outColor = vec4(vec3(c), 1.0);
}
ポインタ・参照は無い。引数は値渡しが既定。
out / inout 修飾子で参照渡しっぽく振る舞える:
void getRGB(in vec3 hsv, out vec3 rgb) {
rgb = hsv2rgb(hsv);
}
void clampColor(inout vec3 color) {
color = clamp(color, 0.0, 1.0);
}
構造体
struct Light {
vec3 position;
vec3 color;
float intensity;
};
uniform Light uLight;
void main() {
vec3 toLight = normalize(uLight.position - vWorldPos);
...
}
配列
float weights[5] = float[5](0.1, 0.2, 0.4, 0.2, 0.1);
vec2 offsets[3];
offsets[0] = vec2(-1.0, 0.0);
offsets[1] = vec2( 0.0, 0.0);
offsets[2] = vec2( 1.0, 0.0);
WebGL1 では配列のインデックスは定数でないと不可。WebGL2 (ES 3.00) では動的インデックスが可能。
プリプロセッサ
C と同様の #define / #if / #ifdef / #endif が使える。
#define PI 3.14159265
#define TAU (2.0 * PI)
#ifdef USE_NORMAL_MAP
vec3 normal = sampleNormal(vUv);
#else
vec3 normal = vNormal;
#endif
Three.js のシェーダ(onBeforeCompile 等)は #include <chunk_name> という独自ディレクティブを使って
共通シェーダコードをインライン展開する。これは Three.js のプリプロセッサ機能で、純粋な GLSL ではない。
コメント
// 1行コメント
/* 複数行
コメント */
スコープと寿命
- 関数内の変数はそのフレーム・そのスレッドのみ
uniform/in/outはシェーダ全体で有効- シェーダ間で値を「保存」する仕組みは無い(GPU は状態を持たない)
- フレーム間で値を持ちたいなら、レンダーターゲット(FBO)に書き込んで次フレームで読む
頂点 / fragment の見分け方
どちらも GLSL だが、使えるビルトイン変数が違う。共通もある。
| 変数 | 頂点 | fragment |
|---|---|---|
gl_Position | ○ 出力(必須) | — |
gl_PointSize | ○ 出力(Points のみ) | — |
gl_VertexID | ○ 入力 | — |
gl_InstanceID | ○ 入力 | — |
gl_FragCoord | — | ○ 入力(画面座標) |
gl_PointCoord | — | ○ 入力(Points 描画時の0..1) |
gl_FrontFacing | — | ○ 入力(表面か) |
gl_FragColor | — | ○ 出力(ES 1.00 のみ) |
discard | — | ○ 命令 |
名前付け規約(慣習)
- uniform は
uで始める:uTime,uColor,uTexture - varying は
vで始める:vUv,vNormal,vPosition - attribute はそのまま(Three.js の慣習):
position,normal,uv - 定数は大文字:
const float PI = 3.14159;
頭の中で持つべき2つのスケール
- クリップ空間:
gl_Positionの単位。x/w,y/wが -1..1 の正規化座標 - UV 空間:
vUv等の 0..1 のテクスチャ座標
この2つを混同しないこと。fragment shader で「画面の真ん中を中心とした 0..1 座標」が欲しい時は vUv、
「画面のどこにいるか」のピクセル単位が欲しい時は gl_FragCoord を resolution uniform で割る。