第1部 WebGL2 の基本 — 第5章

GLSL ES 3.00 言語ガイド

第3章から、GLSL ES 3.00 のシェーダーを「登場した要素だけ」の説明で書いてきました。 この章でいったん立ち止まり、言語そのものを一通り整理します。読み物として通読できる構成にしてありますが、 性格としてはリファレンスです。以降のすべての章で「あの関数なんだっけ」 「なぜコンパイルが通らない?」となったら、ここに戻ってきてください。

three.js で言えば、ShaderMaterialvertexShader/ fragmentShaderに文字列で書いていた、あの言語そのものの話です。

第3章と同じ三角形ですが、フラグメントシェーダーが補間された頂点色をスウィズル(.bgr)と組み込み関数(fractabs)で加工しています。 このシェーダーがこの章の実験台です。

デモのフラグメントシェーダーは次のとおりです。本文で紹介する機能を、 このファイルを書き換えながら試していきます。

src/lessons/05-glsl-guide/playground.frag
#version 300 es

// フラグメントシェーダーの float には既定精度がないため、この宣言が必須(本文 7 節)
precision highp float;

// 頂点シェーダーの out と同じ名前・型で受け取る。補間済みの頂点色が入る
in vec3 v_color;

out vec4 fragColor;

void main() {
  // 補間された頂点色を素材に、スウィズルと組み込み関数で加工する実験台。
  // 各行をコメントアウトしたり、数値や関数を書き換えたりして試すのがこの章の演習
  vec3 color = v_color;

  // スウィズル: 成分の並べ替え(R と B を入れ替える)
  color = color.bgr;

  // fract: 小数部だけを残す。値を増幅してから取ると縞模様になる
  color = fract(color * 3.0);

  // abs を使った折り返し: 0〜1 の値を「0 → 1 → 0」の山型に変える
  color = 1.0 - abs(color * 2.0 - 1.0);

  // step: しきい値でくっきり 2 値化(試すときはコメントを外す)
  // color = step(0.5, color);

  fragColor = vec4(color, 1.0);
}

この章で学ぶこと:

1. バージョン宣言 — #version 300 es は 1 行目

第3章の復習です。GLSL ES 3.00 のシェーダーは、ファイルの先頭 1 行目#version 300 esで始まります。前にコメントや空行が 1 つあるだけで GLSL ES 1.00 として解釈され、in/ outが軒並み構文エラーになるのでした。この章の差分表(11 節)を見ると、この 1 行が「どちらの言語か」を切り替えるスイッチであることがよくわかります。

2. 型 — スカラー・ベクトル・行列

GLSL の基本の型は、スカラー 4 種と、それを束ねたベクトル・行列です。

内容用途の例
float浮動小数点数座標・色・時間など、ほぼすべて
int / uint符号付き / 符号なし整数ループカウンタ、配列の添字、頂点番号
bool真偽値条件分岐
vec2 / vec3 / vec4float の 2 / 3 / 4 成分ベクトルUV 座標、RGB 色、クリップ空間座標 (gl_Position)
ivec2〜4 / uvec2〜4 / bvec2〜4int / uint / bool のベクトルピクセル座標、ベクトル比較の結果
mat2 / mat3 / mat4float の 2×2 / 3×3 / 4×4 行列回転・座標変換(演算と意味は第11章でじっくり)
型の宣言(イメージ)
float f = 1.0; // 小数点があれば float
int i = -3; // なければ int
uint u = 7u; // 符号なし整数はサフィックス u
bool b = true;

vec3 color = vec3(1.0, 0.5, 0.0); // float × 3
ivec2 cell = ivec2(3, 7); // int × 2
bvec3 mask = bvec3(true, false, true); // bool × 3
mat3 m = mat3(1.0); // 3×3 行列(この書き方は単位行列。演算の意味は第11章)

このほかに、配列と構造体(struct)、テクスチャの読み出し口であるsampler2Dなどの型があります(テクスチャは第16章、uniform の配列・構造体は第18章で使います)。

型そのものより重要なのが、次のルールです。GLSL ES 3.00 には暗黙の型変換が一切ありません。JavaScript や C のように「int を書けば float に昇格してくれる」ことはなく、11.0は完全に別物です。

暗黙変換がないことによるエラーの典型(イメージ)
float a = 1; // コンパイルエラー: int から float への暗黙変換はない
float b = 1.0; // OK

int i = 3;
float c = i * 0.5; // コンパイルエラー: int と float は混ぜられない
float d = float(i) * 0.5; // OK: コンストラクタ構文で明示的に変換する

int ratio = 1 / 2; // エラーではないが 0 になる(整数同士の除算は切り捨て)
float e = 1.0 / 2.0; // 0.5

3. コンストラクタとスウィズル

ベクトルはコンストラクタで組み立てます。型名をそのまま関数のように呼ぶ構文で、 スカラーとベクトルを自由に混ぜられます。成分数が足りていればOK で、余った成分は先頭から使われて残りは捨てられます(ただし、1 成分も使われない引数を渡すとエラーです)。

コンストラクタ(イメージ)
vec3 v = vec3(1.0, 2.0, 3.0);

// ベクトルやスカラーを混ぜて組み立てられる
vec4 a = vec4(v, 1.0); // (1.0, 2.0, 3.0, 1.0) — 色 + アルファの定番
vec4 b = vec4(v.rg, 0.0, 1.0); // スウィズルとの合わせ技
vec3 c = vec3(0.5); // 引数 1 つなら全成分に同じ値 (0.5, 0.5, 0.5)
vec2 d = vec2(a); // 大きいベクトルは先頭から切り詰め (1.0, 2.0)

逆にベクトルから成分を取り出す・並べ替えるのがスウィズル (swizzle)です。 成分名には 3 系統の別名があり、どれも同じ成分を指します。 意味に合わせて読みやすいものを選ぶだけで、機能の差はありません(ただし 1 つのスウィズル内で系統を混ぜること — .xg など — はできません)。

系統成分名主な用途
位置x y z w座標・ベクトル
r g b a
テクスチャ座標s t p qテクスチャ座標(慣習としてはあまり使われない)
スウィズル(イメージ)
vec4 v = vec4(1.0, 2.0, 3.0, 4.0);

// 読み出しは並べ替え・重複が自由
vec3 a = v.xyz; // (1.0, 2.0, 3.0)
vec3 b = v.bgr; // (3.0, 2.0, 1.0) — rgba 系でも同じ成分を指す
vec4 c = v.xxyy; // (1.0, 1.0, 2.0, 2.0) — 重複もできる
float x = v.x; // 1 成分だけならスカラー

// 左辺(書き込み先)にも使えるが、同じ成分を繰り返すことはできない
v.xy = vec2(9.0, 8.0); // OK: x と y だけ書き換える
// v.xx = vec2(9.0, 8.0); // コンパイルエラー: 書き込み先で x が重複

読み出しでは並べ替えも重複も自由ですが、左辺(書き込み先)に使う場合だけは、 同じ成分を 2 回以上書けません。「1 回の代入で同じ場所に 2 つの値を書く」ことに なってしまうためです。

vec4(v_color.bgr, 1.0)のように、コンストラクタとスウィズルの組み合わせはシェーダーで最も頻繁に書くイディオムです。 デモのplayground.fragの最終行もこの形をしています。

4. 演算子 — ベクトル演算は成分ごと

+ - * /はすべて成分ごと (component-wise)に働きます。特に注意したいのが乗算で、vec3 * vec3は各成分を掛け合わせるだけです。数学の内積や外積ではありません。 色同士の乗算(乗算合成)がまさにこの動きなので、シェーダーではこの仕様がむしろ便利に働きます。

ベクトルの演算(イメージ)
vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(10.0, 20.0, 30.0);

vec3 sum = a + b; // (11.0, 22.0, 33.0) — 成分ごと
vec3 prod = a * b; // (10.0, 40.0, 90.0) — これも成分ごと。内積ではない!
vec3 scaled = a * 2.0; // (2.0, 4.0, 6.0) — スカラーは全成分に効く

float d = dot(a, b); // 内積が欲しいときは関数を使う (= 140.0)

5. 関数

関数は C と同じ見た目で定義します。エントリポイントはvoid main()で、引数も戻り値もありません。自分で定義する関数には、引数に修飾子を付けられます。

修飾子意味
in(既定)値渡し。関数内で書き換えても呼び出し元には影響しない
out出力専用。関数内で代入した値が呼び出し元の変数に書き戻される
inout入出力。値を受け取り、書き換えた結果が書き戻される
関数定義(イメージ)
// 戻り値と引数の型を明示する。既定の修飾子は in(値渡し)
float invert(float v) {
  return 1.0 - v;
}

// out 引数を使うと、戻り値のほかにも結果を返せる
void polar(in vec2 p, out float radius, out float angle) {
  radius = length(p);
  angle = atan(p.y, p.x);
}

void main() {
  float r;
  float a;
  polar(vec2(1.0, 1.0), r, a);
  // invert(...) を invert の定義の中から呼ぶような再帰は禁止
}

大きな制約が 1 つあります。再帰は禁止です。直接でも間接でも、 自分自身に戻る呼び出しはコンパイルまたはリンクの段階で弾かれます。GPU の実行モデルには関数呼び出しのスタックがないためで、再帰的なアルゴリズムはループに 書き換えることになります。

6. 制御構文

if / elseforwhiledo-whileswitch、三項演算子 — 制御構文もほぼ C のとおりです。

制御構文(イメージ)
vec3 color = v_color;

// if / else は普通に書ける
if (color.r > 0.5) {
  color = color.bgr;
}

// ループ回数が実行時に決まるループも WebGL2 (GLSL ES 3.00) では書ける
// (GLSL ES 1.00 ではループの境界はコンパイル時定数でなければならなかった)
uniform int u_steps;
// ...
for (int i = 0; i < u_steps; i++) {
  color = fract(color * 1.5);
}

歴史的な補足をひとつ。WebGL1 の GLSL ES 1.00 では、ループは「回数がコンパイル時に決まる形の for」しか動作が保証されず、古い記事で不自然に展開されたループを見かけるのはこのためです。 GLSL ES 3.00 ではこの制約が撤廃され、uniform や計算結果に応じて回数が変わるループも 普通に書けます。

7. precision — 精度修飾子

第3章から書き続けてきたprecision highp float;の正体を、ここで正式に回収します。GLSL の数値型にはhighp / mediump / lowpという精度の区分があり、それぞれ「最低限これだけの精度・範囲を保証する」という下限が 仕様で決まっています。

修飾子float の最低保証(仕様上の下限)目安
highp相対精度 2-24、範囲 -2126〜+212732bit 浮動小数点(IEEE 754 単精度)。座標計算はこれ
mediump相対精度 2-10、範囲 ±21416bit 半精度相当。色など許容誤差が大きい値
lowp絶対精度 2-8、範囲 -2〜+20〜1 に収まる値専用。現在はあまり使われない

重要なのはこれが最低保証だという点です。GPU はより高い精度で計算しても構いません。実際、デスクトップ GPU の多くは mediump を指定しても内部では 32bit で計算するため、「PC では動くのにスマートフォンでは(本当に 16bit で計算されて)模様が崩れる」という互換性バグの温床になります。迷ったら highp にしておくのが安全です。

そして、既定値がこの表のとおり非対称です。

シェーダーfloat の既定int の既定
頂点シェーダーhighphighp
フラグメントシェーダー既定なし — 宣言必須mediump

頂点シェーダーにprecision行を書いていなかったのは、float が最初から highp だからです。 フラグメントシェーダーだけ宣言が必須なのは、「フラグメントシェーダーの float に既定精度がない」と仕様で決められているからです。かつてのモバイル GPU ではフラグメント側の highp 対応が任意だった、という事情の名残ですが、GLSL ES 3.00(= WebGL2)ではフラグメントシェーダーでも highp のサポートが必須になりました。 本サイトが迷わずprecision highp float;と書いているのはこのためです。

precision の書き方(イメージ)
#version 300 es

// このシェーダー内の float 系(float / vec* / mat*)の既定精度を宣言する
precision highp float;

// 変数単位で個別に指定することもできる
mediump float roughness;

8. ストレージ修飾子と補間修飾子

ここまでの章で登場したグローバル変数の修飾子を、一覧に整理します。同じin/ outでも、頂点シェーダーとフラグメントシェーダーで意味(つながる先)が違うことに注意してください。

修飾子場所意味
in頂点シェーダー頂点属性。VBO から頂点ごとに渡される(第3章)
out頂点シェーダーフラグメントシェーダーへ渡す値。ラスタライザが補間する(第3章)
inフラグメントシェーダー頂点シェーダーの out を、補間済みの値として受け取る(名前と型を一致させる)
outフラグメントシェーダー出力する色。描画先(フレームバッファ)へ書き込まれる(第3章)
uniform両方1 回のドローコールの間、全頂点・全フラグメントで共通の値。CPU 側から設定(第4章)
const両方コンパイル時定数。円周率など

頂点シェーダーの out(とフラグメントシェーダーの対応する in)には、さらに補間修飾子 (interpolation qualifier)を前置できます。第3章のレビューで持ち越していた話題です。

修飾子意味
smooth(省略時の既定)頂点間を滑らかに補間する(遠近補正付き)。第3章のグラデーションはこれ
flat補間しない。プロボーキング頂点 (provoking vertex)— WebGL2 ではプリミティブの最後の頂点— の値を、そのプリミティブの全フラグメントでそのまま使う

補間修飾子はこの 2 つだけです。似た場所に書くcentroid(centroid in/centroid out)は、仕様上は補間修飾子ではなくストレージ修飾子の一種で、 マルチサンプリング時のサンプル位置を調整するものです。名前だけ知っていれば当面十分です。

flat 補間の指定(イメージ)
// 頂点シェーダー側
flat out vec3 v_color;

// フラグメントシェーダー側(同じ修飾子を付ける)
flat in vec3 v_color;

デモの三角形で言えば、v_colorを flat にすると補間が消え、最後に指定した頂点(右下・青)の色だけで塗られます。 ポリゴンごとに 1 つの値(面の ID や単色)を運びたいときに使い、さらに重要なルールとして、int / uint 系の out を頂点からフラグメントへ渡すときは flat が必須です。整数は「滑らかに補間」できないためで、付け忘れるとコンパイルエラーになります。

9. 組み込み変数

宣言なしで使える特別な変数です。gl_Positionはすでに毎章書いていますが、全体を一覧しておきます。

変数場所意味
gl_Position頂点 / 出力vec4クリップ空間での頂点位置。頂点シェーダーの必須出力(第3章・第12章)
gl_PointSize頂点 / 出力floatPOINTS 描画時の点の大きさ(ピクセル)。パーティクルで使う(第30章)
gl_PointCoordフラグメント / 入力vec2POINTS 描画時の、点の中での 0〜1 の位置。gl_PointSize と対で使う(第30章)
gl_VertexID頂点 / 入力int処理中の頂点の番号。頂点バッファなしで形を作るトリックの鍵で、 次章の全画面三角形で早速使う(第6章)
gl_InstanceID頂点 / 入力intインスタンス描画での「何体目か」(第31章)
gl_FragCoordフラグメント / 入力vec4処理中のフラグメントの描画バッファ上の座標。xy は左下原点のピクセル座標で、ピクセル中心は 0.5(第2章の viewport と同じ向き)。第6章から多用する
gl_FrontFacingフラグメント / 入力boolいま塗っている面がポリゴンの表側かどうか(第13章のカリングとセット)
gl_FragDepthフラグメント / 出力floatフラグメントの深度値を手動で上書きする。名前だけ知っていれば当面十分

10. 組み込み関数カタログ

GLSL には数学・幾何の関数が最初から揃っています。ここでは以降の章で使うものを グループごとに一覧します。ほとんどの関数は float にもベクトルにも使え、ベクトルには成分ごとに働きます(例: fract(vec3) は各成分の小数部を取る)。 個々の関数の詳しい使い方は、それぞれが主役になる章で改めて扱うので、 いまは「こういう道具がある」という地図として眺めてください。

角度・三角関数— 波・回転・極座標の材料(第9〜10章)。角度の単位はラジアンです。

関数一言主な出番
radians(d) / degrees(r)度 ⇄ ラジアンの変換
sin(x) / cos(x) / tan(x)三角関数第9章(波・円運動)
asin(x) / acos(x)逆三角関数
atan(y, x) / atan(x)逆正接。2 引数版は点 (x, y) の方位角を全象限で返す(いわゆる atan2)。1 引数版より圧倒的によく使う第10章(極座標)

指数・累乗 — 減衰カーブやコントラスト調整に。

関数一言主な出番
pow(x, y)x の y 乗(x < 0、または x = 0 かつ y ≤ 0 のときは未定義)第17章(鏡面反射)・第27章(ガンマ)
exp(x) / log(x)e を底とする指数・対数減衰・フォグなど
exp2(x) / log2(x)2 を底とする指数・対数
sqrt(x) / inversesqrt(x)平方根と、その逆数 (1/√x)距離・正規化の内部

共通関数— シェーダー表現の主役たち。特にmix/ step / smoothstepは第7〜8章の中心的な道具です。

関数一言主な出番
abs(x) / sign(x)絶対値 / 符号 (-1, 0, +1)折り返し・対称な模様
floor(x) / ceil(x)切り捨て / 切り上げタイル分割
round(x) / roundEven(x)四捨五入 / 偶数丸め(どちらも GLSL ES 3.00 で追加。1.00 には無い)。roundがちょうど 0.5 をどちらへ倒すかは実装依存(§8.3)なので、向きを固定したいときはfloor(x + 0.5)第24章(タイリングのセル番地)
fract(x)小数部 (x - floor(x))。0〜1 の繰り返しを作る第7章〜(繰り返し模様の基本)
mod(x, y)float の剰余第9章(繰り返し)・第24章(タイリング)
min(x, y) / max(x, y)小さい方 / 大きい方第24章(SDF の合成)
clamp(x, lo, hi)x を lo〜hi の範囲に収める色の飽和防止など随所
mix(a, b, t)線形補間 a + (b - a) × t。グラデーションの心臓部第7章
step(edge, x)x が edge 未満なら 0、以上なら 1。くっきりした境界第7〜8章
smoothstep(e0, e1, x)e0〜e1 の間を 0 → 1 へ滑らかに遷移。境界のぼかし第7〜8章(アンチエイリアスの鍵)

幾何関数— 距離・角度・反射。成分ごとではなく「ベクトル 1 本」に対して働きます。

関数一言主な出番
length(v) / distance(a, b)ベクトルの長さ / 2 点間の距離第8章(円を描く)
dot(a, b)内積第11章(意味)・第17章(ライティング)
cross(a, b)外積(vec3 専用)第11章
normalize(v)長さ 1 に正規化第11章・第17章
reflect(i, n) / refract(i, n, eta)反射 / 屈折ベクトル第17章(鏡面反射)

微分関数(フラグメントシェーダー専用) — 第1章で「各要素は互いの結果を読めない」と言ったときに触れた、 「隣接ピクセルとの差分を取る限定的な例外」の正体がこれです。GPU がフラグメントを 2×2 の束で実行していることを利用して、隣との値の差を取得します。

関数一言主な出番
dFdx(v) / dFdy(v)横 / 縦の隣のフラグメントとの値の差(近似偏微分)第8章
fwidth(v)abs(dFdx) + abs(dFdy)。「1 ピクセルでどれだけ値が変わるか」第8章(解像度に依存しないアンチエイリアス)

テクスチャ関数 — 画像データの読み出し。本格的な登場は第16章です。

関数一言主な出番
texture(sampler, uv)UV 座標 (0〜1) でテクスチャの色を読む。WebGL1 の texture2D の後継第16章
texelFetch(sampler, ivec2, lod)整数ピクセル座標で、フィルタリングなしに 1 テクセルを読む第16章・第29章(GPGPU 的な読み出し)

11. WebGL1 (GLSL ES 1.00) との読み替え表

第1章で予告した宿題を回収します。ウェブ上の WebGL 記事・サンプルの多くは WebGL1(GLSL ES 1.00)時代のもので、シェーダーの見た目がかなり違います。attributegl_FragColorを見かけたら、この表で本サイトの書き方に読み替えてください。

GLSL ES 1.00 (WebGL1)GLSL ES 3.00 (WebGL2)
バージョン宣言なし(暗黙に 1.00)#version 300 es を 1 行目に必須
attribute vec2 a_position;(頂点)in vec2 a_position;
varying vec3 v_color;(両方に書く)頂点側 out / フラグメント側 in
gl_FragColor = ...;(組み込みの出力)out vec4 fragColor; を自分で宣言して代入
texture2D(...) / textureCube(...)サンプラーの型で判別される texture(...) に統一
属性番号は JS 側の bindAttribLocation か自動割り当てlayout(location = N) で GLSL 側から固定できる(第3章の書き方)
uint なし、ビット演算なしuint / uvec・ビット演算・整数の % が追加
ループは回数がコンパイル時に決まる形のみ保証制約撤廃。switch も追加(6 節)
補間は制御できない(すべて滑らか)flat / smooth を指定できる(8 節)
フラグメントの highp 対応は任意(非対応の GPU があった)highp のサポートが必須(7 節)

12. プリプロセッサ

C と同様、コンパイル前にソースを文字列として加工する#で始まる命令があります。#versionもその 1 つです。当面使うのは定数の#defineと、条件付きコンパイルの#ifdefくらいで十分です。

プリプロセッサ(イメージ)
#define PI 3.141592653589793
#define USE_STRIPES

void main() {
  float angle = PI * 0.5; // コンパイル前に文字列として置換される

#ifdef USE_STRIPES
  // USE_STRIPES が #define されているときだけコンパイルされる
#endif
}

#defineは型チェックのない単純な文字列置換なので、円周率のような定数にはconst float PI = 3.141592653589793;という書き方も使えます。本サイトでは基本的に const を使い、「同じシェーダーから機能違いを 何種類も作る」ような場面(ライティングのバリエーションなど、後半の章)で #ifdef が登場します。

コード全文

この章のデモの全文です。CPU 側 (main.ts) は第3章の三角形と同じ処理をsetup関数にまとめ、静止画デモの定石として第2章の ResizeObserver でリサイズに追従させただけで、新しい API はありません。コンパイル・リンクの関数を第3章からそのまま書き写している点に注目してください — 同じ定型を 3 章連続で書いたので、次章でいよいよ共通モジュールに抽出します。

src/lessons/05-glsl-guide/playground.vert
#version 300 es

// 第3章と同じ: クリップ空間の位置と頂点色を受け取る
layout(location = 0) in vec2 a_position;
layout(location = 1) in vec3 a_color;

// 補間修飾子を省略すると smooth(滑らかに補間)。
// 先頭に flat を付けると補間されなくなる(フラグメント側の in にも同じ修飾子を付けること)
out vec3 v_color;

void main() {
  v_color = a_color;
  gl_Position = vec4(a_position, 0.0, 1.0);
}
src/lessons/05-glsl-guide/playground.frag
#version 300 es

// フラグメントシェーダーの float には既定精度がないため、この宣言が必須(本文 7 節)
precision highp float;

// 頂点シェーダーの out と同じ名前・型で受け取る。補間済みの頂点色が入る
in vec3 v_color;

out vec4 fragColor;

void main() {
  // 補間された頂点色を素材に、スウィズルと組み込み関数で加工する実験台。
  // 各行をコメントアウトしたり、数値や関数を書き換えたりして試すのがこの章の演習
  vec3 color = v_color;

  // スウィズル: 成分の並べ替え(R と B を入れ替える)
  color = color.bgr;

  // fract: 小数部だけを残す。値を増幅してから取ると縞模様になる
  color = fract(color * 3.0);

  // abs を使った折り返し: 0〜1 の値を「0 → 1 → 0」の山型に変える
  color = 1.0 - abs(color * 2.0 - 1.0);

  // step: しきい値でくっきり 2 値化(試すときはコメントを外す)
  // color = step(0.5, color);

  fragColor = vec4(color, 1.0);
}
src/lessons/05-glsl-guide/main.ts
// 第5章: GLSL ES 3.00 言語ガイド
// CPU 側は第3章の三角形とほぼ同じで、新しい API は登場しない。
// この章の主役はフラグメントシェーダー playground.frag —
// スウィズルや組み込み関数を書き換えて試すための実験台になっている。

import fragmentSource from './playground.frag?raw';
import vertexSource from './playground.vert?raw';

// ---------------------------------------------------------------------------
// シェーダーのコンパイルとリンク(第3章と同じ。第6章で共通モジュールへ抽出する)
// ---------------------------------------------------------------------------

function compileShader(gl: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader {
  const shader = gl.createShader(type);
  if (!shader) {
    throw new Error('シェーダーオブジェクトを作成できませんでした');
  }
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const log = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error(`シェーダーのコンパイルに失敗しました:\n${log ?? '(ログなし)'}`);
  }
  return shader;
}

function linkProgram(
  gl: WebGL2RenderingContext,
  vertexShader: WebGLShader,
  fragmentShader: WebGLShader,
): WebGLProgram {
  const program = gl.createProgram();
  if (!program) {
    throw new Error('プログラムオブジェクトを作成できませんでした');
  }
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const log = gl.getProgramInfoLog(program);
    gl.deleteProgram(program);
    throw new Error(`プログラムのリンクに失敗しました:\n${log ?? '(ログなし)'}`);
  }
  gl.deleteShader(vertexShader);
  gl.deleteShader(fragmentShader);
  return program;
}

// ---------------------------------------------------------------------------
// デモ本体: 第3章と同じ三角形を描く(静止画なので、リサイズしたときだけ描き直す)
// ---------------------------------------------------------------------------

function setup(gl: WebGL2RenderingContext, canvas: HTMLCanvasElement): void {
  const program = linkProgram(
    gl,
    compileShader(gl, gl.VERTEX_SHADER, vertexSource),
    compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource),
  );

  // クリップ空間 (-1〜+1) での xy 座標。3 頂点 = 三角形 1 枚
  // biome-ignore format: 1 頂点 = 1 行の並びを保つ
  const positions = new Float32Array([
    0.0, 0.8, // 上
    -0.8, -0.8, // 左下
    0.8, -0.8, // 右下
  ]);

  // 各頂点の RGB 色。flat 補間にすると最後の頂点(右下)の色が全面に使われる(本文 8 節)
  // biome-ignore format: 1 頂点 = 1 行の並びを保つ
  const colors = new Float32Array([
    1.0, 0.25, 0.35, // 上: 赤
    0.25, 0.9, 0.5, // 左下: 緑
    0.3, 0.5, 1.0, // 右下: 青
  ]);

  const vao = gl.createVertexArray();
  gl.bindVertexArray(vao);

  // 位置: シェーダーの layout(location = 0) in vec2 a_position へ
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
  gl.enableVertexAttribArray(0);
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

  // 色: シェーダーの layout(location = 1) in vec3 a_color へ
  const colorBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
  gl.enableVertexAttribArray(1);
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);

  gl.bindVertexArray(null);

  gl.clearColor(0.06, 0.07, 0.09, 1.0);

  function draw(): void {
    gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.useProgram(program);
    gl.bindVertexArray(vao);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
  }

  // 静止画のデモなので、描画ループではなく ResizeObserver で追従する(第2章・第4章)。
  // 表示サイズが変わったときだけ描画バッファを作り直し、描き直す
  function resize(): void {
    const dpr = Math.min(window.devicePixelRatio, 2);
    const width = Math.floor(canvas.clientWidth * dpr);
    const height = Math.floor(canvas.clientHeight * dpr);
    if (canvas.width !== width || canvas.height !== height) {
      canvas.width = width;
      canvas.height = height;
    }
    draw();
  }

  // observe() の直後にも 1 回発火するので、初回の描画もこの経路で行われる(第2章)
  const observer = new ResizeObserver(resize);
  observer.observe(canvas);
}

// ---------------------------------------------------------------------------
// 要素とコンテキストの取得
// ---------------------------------------------------------------------------

const canvas = document.querySelector<HTMLCanvasElement>('#demo');
if (!canvas) {
  throw new Error('canvas 要素 #demo が見つかりません');
}

const gl = canvas.getContext('webgl2');
if (!gl) {
  throw new Error('このブラウザは WebGL2 に対応していません');
}

setup(gl, canvas);

three.js との対応

three.js のシェーダーまわりで見かけるものと、この章の内容の対応です(three.js の挙動は執筆時点のものです)。

three.jsこの章の生 WebGL2 / GLSL
ShaderMaterial + glslVersion: THREE.GLSL3この章の GLSL ES 3.00 をそのまま書ける指定
glslVersion 未指定の ShaderMaterialthree が #define による互換層を差し込み、varyinggl_FragColorなど GLSL 1.00 風の書き方を 3.00 に読み替えている
position/ uv / normalprojectionMatrix/ modelViewMatrix などthree が自動で宣言・供給していた attribute / uniform。生 WebGL には存在しないので、第11章以降で自作する
#include <common> などのチャンクthree 独自のプリプロセッサ的な仕組み (ShaderChunk)。GLSL ES 自体に#includeはない
RawShaderMaterial宣言の自動注入なし。この章の「全部自分で書く」スタイルに最も近い

手元で動かして、壊してみる

この章のコードはsrc/lessons/05-glsl-guide/にあります。編集するのはほぼplayground.fragだけです。リファレンス章なので、実験の自由度は過去最大です。

まとめ

次章では、第3章から 3 回書き写してきたコンパイル・リンクなどの定型コードを共通モジュールへ抽出し、gl_VertexIDを使って頂点バッファなしで画面全体を覆う「全画面シェーダー環境」を作ります。第2部からは、 その環境の上でフラグメントシェーダーだけを書きながら、この章の関数カタログを実際に使い込んでいきます。