GLSL ES 3.00 言語ガイド
第3章から、GLSL ES 3.00 のシェーダーを「登場した要素だけ」の説明で書いてきました。 この章でいったん立ち止まり、言語そのものを一通り整理します。読み物として通読できる構成にしてありますが、 性格としてはリファレンスです。以降のすべての章で「あの関数なんだっけ」 「なぜコンパイルが通らない?」となったら、ここに戻ってきてください。
three.js で言えば、ShaderMaterialのvertexShader/ fragmentShaderに文字列で書いていた、あの言語そのものの話です。
.bgr)と組み込み関数(fract、abs)で加工しています。 このシェーダーがこの章の実験台です。デモのフラグメントシェーダーは次のとおりです。本文で紹介する機能を、 このファイルを書き換えながら試していきます。
#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);
}この章で学ぶこと:
- スカラー・ベクトル・行列の型と、暗黙の型変換がないことによる落とし穴
- コンストラクタとスウィズルによるベクトルの組み立て・分解
- 関数・制御構文・precision・補間修飾子(flat / smooth)
gl_FragCoordやgl_VertexIDなどの組み込み変数- 組み込み関数カタログ — 以降の章で使う道具の一覧
- WebGL1 時代の GLSL を読み替えるための差分表(第1章の宿題)
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 / vec4 | float の 2 / 3 / 4 成分ベクトル | UV 座標、RGB 色、クリップ空間座標 (gl_Position) |
ivec2〜4 / uvec2〜4 / bvec2〜4 | int / uint / bool のベクトル | ピクセル座標、ベクトル比較の結果 |
mat2 / mat3 / mat4 | float の 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 に昇格してくれる」ことはなく、1と1.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.53. コンストラクタとスウィズル
ベクトルはコンストラクタで組み立てます。型名をそのまま関数のように呼ぶ構文で、 スカラーとベクトルを自由に混ぜられます。成分数が足りていれば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)- 内積は
dot()、外積はcross()— 関数として呼びます(10 節)。 - 例外は行列が絡む乗算で、
mat4 * vec4やmat4 * mat4は数学どおりの行列積になります。この意味と使い方は第11章の主役です。 %は整数専用です。float の剰余には関数mod()を使います。- ベクトル同士の比較に
<や>は使えません(==/!=は全成分の一致判定として使えます)。 成分ごとの比較にはlessThan()などの関数があり、bvecを返します。
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 / else、for、while、do-while、switch、三項演算子 — 制御構文もほぼ 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〜+2127 | 32bit 浮動小数点(IEEE 754 単精度)。座標計算はこれ |
mediump | 相対精度 2-10、範囲 ±214 | 16bit 半精度相当。色など許容誤差が大きい値 |
lowp | 絶対精度 2-8、範囲 -2〜+2 | 0〜1 に収まる値専用。現在はあまり使われない |
重要なのはこれが最低保証だという点です。GPU はより高い精度で計算しても構いません。実際、デスクトップ GPU の多くは mediump を指定しても内部では 32bit で計算するため、「PC では動くのにスマートフォンでは(本当に 16bit で計算されて)模様が崩れる」という互換性バグの温床になります。迷ったら highp にしておくのが安全です。
そして、既定値がこの表のとおり非対称です。
| シェーダー | float の既定 | int の既定 |
|---|---|---|
| 頂点シェーダー | highp | highp |
| フラグメントシェーダー | 既定なし — 宣言必須 | mediump |
頂点シェーダーにprecision行を書いていなかったのは、float が最初から highp だからです。 フラグメントシェーダーだけ宣言が必須なのは、「フラグメントシェーダーの float に既定精度がない」と仕様で決められているからです。かつてのモバイル GPU ではフラグメント側の highp 対応が任意だった、という事情の名残ですが、GLSL ES 3.00(= WebGL2)ではフラグメントシェーダーでも highp のサポートが必須になりました。 本サイトが迷わずprecision highp float;と書いているのはこのためです。
#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 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 | 頂点 / 出力 | float | POINTS 描画時の点の大きさ(ピクセル)。パーティクルで使う(第30章) |
gl_PointCoord | フラグメント / 入力 | vec2 | POINTS 描画時の、点の中での 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)時代のもので、シェーダーの見た目がかなり違います。attributeやgl_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 章連続で書いたので、次章でいよいよ共通モジュールに抽出します。
#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);
}#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);
}// 第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 未指定の ShaderMaterial | three が #define による互換層を差し込み、varyingやgl_FragColorなど GLSL 1.00 風の書き方を 3.00 に読み替えている |
position/ uv / normal、projectionMatrix/ modelViewMatrix など | three が自動で宣言・供給していた attribute / uniform。生 WebGL には存在しないので、第11章以降で自作する |
#include <common> などのチャンク | three 独自のプリプロセッサ的な仕組み (ShaderChunk)。GLSL ES 自体に#includeはない |
RawShaderMaterial | 宣言の自動注入なし。この章の「全部自分で書く」スタイルに最も近い |
手元で動かして、壊してみる
この章のコードはsrc/lessons/05-glsl-guide/にあります。編集するのはほぼplayground.fragだけです。リファレンス章なので、実験の自由度は過去最大です。
playground.fragの加工 3 行(.bgr/fract/abs)を 1 行ずつコメントアウトして、それぞれの効果を切り分ける。fract(color * 3.0)の 3.0 を 8.0 や 1.5 に変えてみる- コメントアウトされている
step(0.5, color)を有効にする。次にsmoothstep(0.3, 0.7, color)に変えて、境界の質の違いを見る(第7〜8章の予告編です) - スウィズルを
color.rrr(モノクロ風)やcolor.gbrに変える。color.xgのように系統を混ぜて、コンパイルエラーになることも確かめる - わざと
fract(color * 3)(小数点なしの 3)と書いて、コンソールに出るエラーメッセージの行番号が実際の行と合っているかを読む(1 節の「ERROR: 0:12」の練習) playground.vertのout vec3 v_color;とplayground.fragのin vec3 v_color;の両方にflatを付けて、補間が消えてプロボーキング頂点(最後の頂点 = 右下)の色一色になることを確かめる。片方にだけ付けるとリンクエラーになることも見ておくprecision highp float;をmediumpに変えてみる。このデモの計算では(多くの環境で)見た目が変わらないことを確認する — 「最低保証」と実際の計算精度の話(7 節)を体感する実験です
まとめ
- GLSL に暗黙の型変換はない。
1と1.0は別物で、int 同士の除算は切り捨てられる — 最頻出のバグ - ベクトルはコンストラクタで組み立て、スウィズル(x y z w / r g b a / s t p q)で分解・並べ替えする。書き込み側では成分の重複不可
- 演算子は成分ごとに働く。
vec * vecは成分積で、内積はdot()。行列が絡む乗算だけが本当の行列積(第11章) - フラグメントシェーダーの float に既定精度はないので
precision highp float;を必ず書く(GLSL ES 3.00 では highp 対応が必須なので遠慮はいらない) - 頂点 → フラグメントの補間は smooth が既定。
flatで止められ、整数型を渡すときは必須 - WebGL1 時代のコードは 11 節の差分表で読み替える
次章では、第3章から 3 回書き写してきたコンパイル・リンクなどの定型コードを共通モジュールへ抽出し、gl_VertexIDを使って頂点バッファなしで画面全体を覆う「全画面シェーダー環境」を作ります。第2部からは、 その環境の上でフラグメントシェーダーだけを書きながら、この章の関数カタログを実際に使い込んでいきます。