基本構文

GLSL は C / C++ に似た静的型の言語。main() から始まり、 各シェーダごとに役割を果たす。型の自動変換は限定的、ポインタや動的メモリは無い。

シェーダの骨格

最小シェーダ(GLSL ES 3.00)
#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);
}

構造の要点:

基本データ型

意味
void戻り値なし(関数のみ)
bool真偽値(true/false
int整数
uint符号なし整数(GLSL ES 3.00+)
float32bit 浮動小数
vec2 / vec3 / vec4float の 2/3/4 要素ベクトル
ivec2 / ivec3 / ivec4int のベクトル
bvec2 / bvec3 / bvec4bool のベクトル
mat2 / mat3 / mat4float の正方行列
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;
11.0 は別物

GLSL は暗黙の型変換が弱いfloat a = 1;エラーになる場合がある。 浮動小数点リテラルは必ず小数点を書く: 1.00.52. はOK。

修飾子

ストレージ修飾子

修飾子意味
const定数(コンパイル時定数)
uniformJS から渡す。同じ描画バッチ内で不変
in入力(vertex の attribute、fragment への varying)
out出力(vertex から fragment へ、または fragment の最終色)
inout関数引数で参照渡し(読み書き)

精度修飾子(precision)

浮動小数点・整数の精度。GLSL ES では必ず 1 つ以上指定する必要がある。

よくある書き方(fragment shader 冒頭)
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行コメント
/* 複数行
   コメント */

スコープと寿命

頂点 / 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○ 命令

名前付け規約(慣習)

頭の中で持つべき2つのスケール

  1. クリップ空間: gl_Position の単位。x/w, y/w-1..1 の正規化座標
  2. UV 空間: vUv 等の 0..1 のテクスチャ座標

この2つを混同しないこと。fragment shader で「画面の真ん中を中心とした 0..1 座標」が欲しい時は vUv、 「画面のどこにいるか」のピクセル単位が欲しい時は gl_FragCoordresolution uniform で割る。