第2部 シェーダーで学ぶ数学 — 第10章

極座標と 2D 回転

第2部の最終章です。ここまでの章では、ピクセルの位置をずっと (x, y) の組で扱ってきました。この章の前半では、同じ位置を「中心からの距離」と「角度」で表し直す極座標 (polar coordinates)という見方を手に入れます。直交座標では書きにくかった放射模様・花・渦・万華鏡が、極座標では数行になります。

後半は 2D 回転です。第4章の頂点シェーダーで「回転の定型文としていまは写して構いません」と書いた sin / cos の直書きの式を、2×2 の行列 mat2に整理します。行列と聞くと大げさですが、やることは「4 つの数を 1 つの変数にまとめる」だけです。そしてこのまとめ方にこそ、「行列とは座標変換である」という第3部の核心が 最小サイズで入っています。three.js でmesh.rotation.zに値を入れたとき、内部で頂点に掛かっていたのは、まさにこの章で組む形の回転行列です。

この章のデモ 4 本。ボタンで切り替えられます。「極座標の可視化」は r と θ をそのまま色にしたデバッグ表示、「放射模様と花」は sin(θ×N) だけで作る模様、「万華鏡」はポインタの左右で分割数が変わり、「mat2 回転」は市松模様が 回転行列で回ります(ポインタの左右で角度が加わります)。

この章で学ぶこと:

1. 極座標 — 距離 r と角度 θ

第7章の標準座標 p(中心が原点、短辺が -1〜+1)の上の 1 点は、「原点からどの方向に、どれだけ離れているか」でも一意に指せます。この (r, θ) の組が極座標です。中心から放射状に広がるもの・中心の周りを回るものを扱うときは、(x, y) より圧倒的に書きやすくなります。GLSL での変換は 2 行です。

src/lessons/10-polar-and-rotation/01-polar-debug.frag(抜粋)
// 標準座標(第7章): 中心が原点、短辺が -1〜+1
vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);

// 極座標: 同じ点を「距離 r と角度 θ」で見直す
float r = length(p); // 中心からの距離。短辺の端でちょうど 1.0
float theta = atan(p.y, p.x); // 角度。右が 0、反時計回りが正、範囲は -PI〜+PI

θ の約束事は図で覚えるのが早いです。右(+x 方向)が 0反時計回りが正で、返り値の範囲は-π〜+π。上半分をたどると左端で +π に達し、下半分をたどると左端で -π に達する — つまり左端(-x 方向)で +π と -π が接しています。

+x+yp = (x, y)rθ反時計回りが正θ = 0θ = +π/2θ = -π/2上から近づくと +π下から近づくと -π±π が接する継ぎ目
極座標の約束事。角度 θ は +x 方向から反時計回りに測り、範囲は -π〜+π。左端では +π と -π という「値としては 2π 離れた同じ場所」が隣り合います。

新しい座標を手に入れたら、まず色にして眺める — 第7章で uv に対してやったデバッグを、(r, θ) でもやります。それがデモ 1 本目の01-polar-debug.fragです。

src/lessons/10-polar-and-rotation/01-polar-debug.frag
#version 300 es

// 第10章 デモ1: 極座標のデバッグ表示。
// 第7章の「uv をそのまま色にして眺める」の極座標版で、
// r(中心からの距離)を赤、θ(角度)を緑に入れて構造を目で確かめる。

precision highp float;

uniform vec2 u_resolution;

out vec4 fragColor;

const float PI = 3.141592653589793;

void main() {
  // 標準座標(第7章): 中心が原点、短辺が -1〜+1
  vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);

  // 極座標: 同じ点を「距離 r と角度 θ」で見直す
  float r = length(p); // 中心からの距離。短辺の端でちょうど 1.0
  float theta = atan(p.y, p.x); // 角度。右が 0、反時計回りが正、範囲は -PI〜+PI

  // -PI〜+PI を 0〜1 に写す常套句。色や mix の係数に使うときはほぼ毎回これ
  float theta01 = theta / (2.0 * PI) + 0.5;

  // r を赤、θ を緑へ。中心ほど暗く、反時計回りに緑が増えていき、
  // 左端で緑が 1 → 0 に急に切り替わる(±PI の継ぎ目がそのまま見える)
  vec3 color = vec3(r, theta01, 0.35);

  // r の等高線(0.25 刻み)を重ねると「同心円」の構造が見える。
  // 一番近い等高線までの距離を出し、しきい値 0.006 と比べて細い線にする(第8章の常套句)
  float dist = abs(fract(r * 4.0 + 0.5) - 0.5) / 4.0;
  float w = fwidth(dist);
  float ring = 1.0 - smoothstep(0.006 - w, 0.006 + w, dist);
  color += ring * 0.3;

  fragColor = vec4(color, 1.0);
}

1 つだけ加工が要ります。θ は -π〜+π なので、そのまま色に入れると負の半分が黒く潰れてしまいます。 そこで 0〜1 へ写してから使います。この 1 行は今後何度も出てくる常套句です。

θ を 0〜1 に直す常套句
// -PI〜+PI を 0〜1 に写す常套句。色や mix の係数に使うときはほぼ毎回これ
float theta01 = theta / (2.0 * PI) + 0.5;

デモを眺めると、極座標の性質がそのまま見えます。赤(r)は中心から外へ向かって明るくなる同心円状、 緑(θ)は右(θ=0 → 0.5)から反時計回りに増えていき、左端に緑の急な切り替わりの線が 1 本立ちます。これが ±π の継ぎ目です。継ぎ目は バグではなく「角度という量が一周で元に戻る」 ことの現れで、次節で付き合い方を覚えます。

2. 極座標で模様を作る — 放射・花・渦

第9章ではsinを x 座標に沿って走らせて縦縞を作りました。同じ sin を θ に沿って走らせると、縞が中心の周りに巻き付いて放射状になります。一周は 2π なので、sin(theta * N)は一周でちょうど N 回振動します。N = 6 なら山が 6 つ — つまり 6 枚の花びらの下地です。

「花」にするには、第8章の道具をそのまま使います。半径一定の円の代わりに、目標半径を θ で波打たせた円を考え、r との差を符号付き距離としてsmoothstep+fwidthでマスクにするだけです。

src/lessons/10-polar-and-rotation/02-radial.frag(抜粋)
// 花: 「θ に応じて半径が波打つ円」。r と目標半径の差 = 符号付き距離(第8章)
float petalRadius = BASE_RADIUS + AMPLITUDE * sin(theta * PETALS);
float d = r - petalRadius;
float w = fwidth(d);
float mask = 1.0 - smoothstep(-w, w, d); // 第8章の塗りの常套句

もう 1 つの定番がです。sin の位相に θ だけでなく r も混ぜてsin(theta * N + r * M - u_time)とすると、「同じ位相になる場所」が、角度を進めるぶん距離を下げる…というトレードオフの曲線上に 並ぶため、縞が渦を巻きます。u_timeを引いているので、渦は外へ流れ続けます。デモ 2 本目にはこの行がコメントで入れてあるので、 差し替えて試してください。

src/lessons/10-polar-and-rotation/02-radial.frag
#version 300 es

// 第10章 デモ2: 極座標で作る模様 — 放射模様と花。
// 冒頭の定数は書き換え前提。まず PETALS を変えて、枚数と ±PI の継ぎ目を観察する。

precision highp float;

uniform float u_time;
uniform vec2 u_resolution;

out vec4 fragColor;

const float PI = 3.141592653589793;

// 花びらの枚数。整数なら ±PI の継ぎ目は見えない。5.5 などにすると左に切れ目が出る
const float PETALS = 6.0;
const float BASE_RADIUS = 0.5; // 花の基準半径
const float AMPLITUDE = 0.13; // 花びらの出っ張り(半径の振幅)

void main() {
  vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);

  float r = length(p);
  // θ に一定角を足すだけで模様全体が回る(回転を行列で書くのはデモ4)
  float theta = atan(p.y, p.x) + u_time * 0.15;

  // 放射模様: sin(θ * N) は一周でちょうど N 回振動する
  float stripes = sin(theta * PETALS);
  // 渦にするなら: 位相に r も混ぜる。上の行とこの行を差し替えて試す
  // float stripes = sin(theta * PETALS + r * 12.0 - u_time);

  vec3 color = mix(
    vec3(0.07, 0.09, 0.16),
    vec3(0.16, 0.22, 0.38),
    stripes * 0.5 + 0.5 // -1〜+1 → 0〜1(第9章)
  );

  // 花: 「θ に応じて半径が波打つ円」。r と目標半径の差 = 符号付き距離(第8章)
  float petalRadius = BASE_RADIUS + AMPLITUDE * sin(theta * PETALS);
  float d = r - petalRadius;
  float w = fwidth(d);
  float mask = 1.0 - smoothstep(-w, w, d); // 第8章の塗りの常套句

  vec3 flowerColor = mix(
    vec3(0.95, 0.75, 0.35),
    vec3(0.90, 0.40, 0.25),
    r / (BASE_RADIUS + AMPLITUDE) // 中心から外へ向かうグラデーション
  );
  color = mix(color, flowerColor, mask);

  fragColor = vec4(color, 1.0);
}

このデモはu_timeでゆっくり回転していますが、回し方はatanの結果に角度を足しているだけです。極座標の中にいる限り、回転は足し算 1 つで済みます。行列が要るのは、直交座標のまま回したいとき — それが 4 節です。

3. 角度を繰り返す — mod と万華鏡

第9章ではmodで x 座標を折り畳んで、同じ模様を横に繰り返しました。同じことを θ に対してやると、扇形が繰り返します。一周 2π を n 等分した角度をsectorとすると、mod(theta, sector)はどの扇形にいても「扇形の中での角度」だけを返します(GLSL のmodは負の入力でも 0〜sector に収まる定義なので、θ の負の半分もそのまま畳めます)。

src/lessons/10-polar-and-rotation/03-kaleido.frag(抜粋)
// ここがドメイン変形: θ を mod(第9章)で扇形 1 枚分に畳み込み、
// さらに中央で折り返す(abs)と、隣どうしが鏡映しになって継ぎ目がつながる
float a = mod(theta, sector);
a = abs(a - sector * 0.5);

// 畳んだ極座標 (r, a) を直交座標に戻す。以降は普通に絵を描くだけ
vec2 q = r * vec2(cos(a), sin(a));

mod だけだと、扇形の境界で絵がぶつ切りになります(片端は角度 0、もう片端は角度 sector の絵が接するため)。そこでabs(a - sector * 0.5)で扇形の中央で折り返すと、隣どうしが鏡映しになって境界がぴったりつながります。 本物の万華鏡が鏡の反射で絵を増やしているのと同じ理屈です。折り畳んだ (r, a) をr * vec2(cos(a), sin(a))で直交座標に戻せば、あとはいつもどおり形を描くだけで、描いたものが全扇形に増殖します。

src/lessons/10-polar-and-rotation/03-kaleido.frag
#version 300 es

// 第10章 デモ3: θ の mod による万華鏡。
// 「座標を加工してから、いつもどおり形を描く」= ドメイン変形の最初の例。
// canvas 上でポインタを左右に動かすと、分割数が 3〜12 で切り替わる。

precision highp float;

uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;

out vec4 fragColor;

const float PI = 3.141592653589793;

void main() {
  vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);

  float r = length(p);
  float theta = atan(p.y, p.x);

  // マウス x(0〜1)→ 分割数 3〜12。floor(x + 0.5) で四捨五入して整数にする
  float mouse01 = u_mouse.x / u_resolution.x;
  float n = floor(mix(3.0, 12.0, mouse01) + 0.5);
  float sector = 2.0 * PI / n; // 扇形 1 枚分の角度

  // ここがドメイン変形: θ を mod(第9章)で扇形 1 枚分に畳み込み、
  // さらに中央で折り返す(abs)と、隣どうしが鏡映しになって継ぎ目がつながる
  float a = mod(theta, sector);
  a = abs(a - sector * 0.5);

  // 畳んだ極座標 (r, a) を直交座標に戻す。以降は普通に絵を描くだけ
  vec2 q = r * vec2(cos(a), sin(a));

  // 「1 枚分の絵」: 動き回る円 1 個と縞。ここを描き替えると全扇形に反映される
  vec2 center = vec2(0.5 + 0.10 * sin(u_time * 0.7), 0.22 + 0.08 * cos(u_time * 0.9));
  float d = length(q - center) - 0.16;
  float w = fwidth(d);
  float circle = 1.0 - smoothstep(-w, w, d); // 第8章の塗りの常套句
  // なお分割数を増やすと円の中心が扇形の外に出て、鏡映で花びら状に切り取られる。
  // これは万華鏡の「筒の外の景色が映り込む」のと同じで、意図した見え方

  float waves = sin(q.x * 18.0 - u_time * 1.5) * 0.5 + 0.5;
  float vignette = 1.0 - smoothstep(0.8, 1.15, r);

  vec3 color = mix(vec3(0.06, 0.08, 0.14), vec3(0.18, 0.30, 0.50), waves * vignette);
  color = mix(color, vec3(0.95, 0.60, 0.30), circle);

  fragColor = vec4(color, 1.0);
}

ここで、この章と第2部を貫く考え方に名前を付けておきます。ドメイン変形 (domain transformation)— 形を描く式は変えず、その手前で座標(定義域)のほうを加工する手法です。 このデモで描いたのは「円 1 個と縞」だけで、万華鏡らしさはすべて座標の畳み込みが作っています。 座標をずらせば繰り返し(第9章の mod)、畳めば対称、回せば回転(次節)。 さらに第26章では、座標をノイズで歪ませるドメインワーピングに発展し、雲や大理石の ような有機的な模様になります。第24章では、この万華鏡の対称性を SDF と組み合わせて扱い直します。

4. 2D 回転を mat2 にまとめる

第4章の頂点シェーダーに、こう書いたのを覚えているでしょうか。

src/lessons/04-animation-and-uniforms/triangle.vert(再掲)
// ① ゆっくり回転する。sin / cos を直接書いた 2D 回転
//    (この計算が mat2 という行列 1 つにまとまることを第10章で学ぶ)
float angle = u_time * 0.5;
float c = cos(angle);
float s = sin(angle);
vec2 pos = vec2(
  a_position.x * c - a_position.y * s,
  a_position.x * s + a_position.y * c
);

点 (x, y) を角度 θ だけ回した位置は (x·cosθ − y·sinθ, x·sinθ + y·cosθ)。第4章では 「回転の定型文としていまは写して構いません。mat2にまとまることを第10章で学びます」と約束しました。ここで回収します。GLSL には 2×2 の行列型mat2があり、行列 × ベクトルの掛け算が*演算子で書けます。上の式は次の 3 行に整理できます。

src/lessons/10-polar-and-rotation/04-rotation.frag(抜粋)
// 第4章で sin / cos を直書きした回転が、この 1 行にまとまる。
// GLSL の mat2(a, b, c, d) は「列」から埋まる:
//   1 列目 (c, s) = x 軸の基底 (1, 0) の行き先
//   2 列目 (-s, c) = y 軸の基底 (0, 1) の行き先
float c = cos(angle);
float s = sin(angle);
mat2 rot = mat2(c, s, -s, c);

// 先に座標を回し、それから模様を描く(順序が大事。入れ替えの実験は下のコメント)
vec2 q = rot * p;

まず、コンストラクタの読み方をはっきりさせます。GLSL の mat2(a, b, c, d) は、引数を「列」の順に詰めます(列優先、column-major)。つまりmat2(c, s, -s, c)は「1 列目が (c, s)、2 列目が (-s, c)」です。紙に書く行列は行ごとに読むので、同じ行列を 紙に書くと次のようになり、コードの並びとは転置の関係に見えます。ここを 曖昧にすると後々ずっと混乱するので、「コンストラクタは列から」を最初に固定してください。

(1, 0)(0, 1)(cosθ, sinθ)= 1 列目(-sinθ, cosθ)= 2 列目θθ紙に書く回転行列cosθ-sinθsinθcosθ列 = 基底ベクトルの行き先コードは列の順: mat2(c, s, -s, c)
回転行列の中身。1 列目(強調色)は (1, 0) の行き先、2 列目は (0, 1) の行き先です。紙の行列は行ごとに読むため、列の順に引数を並べるmat2(c, s, -s, c)とは並びが転置に見えます。

では、なぜこの 4 つの数で回転になるのか。行列 × ベクトルの計算規則は 1 つだけ覚えれば十分です:「各列を、ベクトルの対応する成分で重み付けして足す」

行列 × ベクトルの計算規則
// 行列 × ベクトルは「各列を、ベクトルの成分で重み付けして足す」
// rot * p = p.x * (1 列目) + p.y * (2 列目)
//         = p.x * vec2(c, s) + p.y * vec2(-s, c)
//         = vec2(c * p.x - s * p.y,
//                s * p.x + c * p.y) // 第4章の直書きの式と同じ

展開すると第4章の直書きの式にぴったり一致します。そしてこの規則から、行列の正体が読み取れます。p = (1, 0)を入れると結果は 1 列目そのもの、p = (0, 1)を入れると 2 列目そのものです。つまり行列とは「基底ベクトル(各軸の単位ベクトル)の変換先を、列として並べたもの」です。回転行列の 1 列目 (cosθ, sinθ) は「x 軸の向きの棒を θ 回した先」、2 列目 (-sinθ, cosθ) は「y 軸の向きの棒を θ 回した先」— 上の図の 2 本の矢印が、そのまま行列の中身なのです。 どんな点もこの 2 本の棒の組み合わせ(x 倍と y 倍)なので、棒の行き先さえ決めれば全平面の行き先が 決まる — これが「行列 = 座標変換」ということの意味です。

5. 回転を使う — 座標を変形してから、形を描く

デモ 4 本目は総仕上げです。第9章のfract/floorで作る市松模様を、mat2の回転で回します。手順は万華鏡と同じで、先に座標を変形し(q = rot * p)、そのあとで いつもどおり模様を描く— ドメイン変形の回転版です。

src/lessons/10-polar-and-rotation/04-rotation.frag
#version 300 es

// 第10章 デモ4: mat2 による 2D 回転の総仕上げ。
// 市松模様の格子(第9章の fract / mod)を、mat2 で組んだ回転行列で回す。
// u_time でゆっくり回り、ポインタを左右に動かすと角度が -PI〜+PI ぶん加わる。

precision highp float;

uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;

out vec4 fragColor;

const float PI = 3.141592653589793;

void main() {
  vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);

  // 回転角: 時間でゆっくり進み、マウス x で手動のオフセットを足す
  float mouse01 = u_mouse.x / u_resolution.x;
  float angle = u_time * 0.2 + (mouse01 - 0.5) * 2.0 * PI;

  // 第4章で sin / cos を直書きした回転が、この 1 行にまとまる。
  // GLSL の mat2(a, b, c, d) は「列」から埋まる:
  //   1 列目 (c, s) = x 軸の基底 (1, 0) の行き先
  //   2 列目 (-s, c) = y 軸の基底 (0, 1) の行き先
  float c = cos(angle);
  float s = sin(angle);
  mat2 rot = mat2(c, s, -s, c);

  // 先に座標を回し、それから模様を描く(順序が大事。入れ替えの実験は下のコメント)
  vec2 q = rot * p;

  // 市松模様: fract でタイル内の座標(第9章)、floor でタイルの番地(こちらはこの章が初出)
  vec2 cell = fract(q * 2.0) - 0.5; // 各タイル内の -0.5〜+0.5
  vec2 tile = floor(q * 2.0); // タイルの番地(整数)
  float checker = mod(tile.x + tile.y, 2.0); // 0 と 1 が交互に

  // タイルより少し小さい正方形を描く。max(|x|, |y|) は正方形の簡易距離で、
  // 内側と輪郭付近では十分正確(外側の遠方は正確な距離ではない。厳密な式は第8章の sdBox)
  float d = max(abs(cell.x), abs(cell.y)) - 0.42;
  float w = fwidth(d);
  float mask = 1.0 - smoothstep(-w, w, d); // 第8章の塗りの常套句

  vec3 tileColor = mix(vec3(0.90, 0.55, 0.25), vec3(0.30, 0.50, 0.85), checker);
  vec3 color = mix(vec3(0.06, 0.07, 0.11), tileColor, mask);

  fragColor = vec4(color, 1.0);

  // 実験: 「模様を作ってから回す」と何が変わるか。
  // vec2 q = p; にした上で、cell の行を
  //   vec2 cell = rot * (fract(q * 2.0) - 0.5);
  // に変えると、格子の並びは止まったまま、各タイルの中の正方形だけが回る。
  // 「座標の変形は、形を描く前に済ませる」— 順序で結果が変わることの確認
}

角度はu_timeでゆっくり進み、ポインタの左右で ±π のオフセットが加わります。ファイル末尾のコメントに 「模様を作ってから回す」実験を用意してあります。fractで回すと、格子の並びは止まったまま各タイルの中身だけがその場で回る — 同じ 2 つの操作でも、順序で結果がまったく変わります。第3部で行列を重ねて掛けるようになると、 この「変換の順序」が主役級のテーマになるので、ここで感覚を作っておいてください。

6. 第2部で揃った道具箱

これで第2部は完結です。手に入れた道具を並べてみます。

第3部では、最後のmat2で開けた扉をくぐります。「列 = 基底の行き先」という読み方はそのままに、ベクトルと行列を 3D と 4×4 へ拡張し(なぜ 3×3 でなく 4×4 が要るのかは第11章のお楽しみです)、gl-matrix を導入して、モデル・ビュー・射影という座標変換の連鎖で立体をスクリーンに映します。

コード全文

シェーダー 4 本は各節に掲載したとおりです。CPU 側のmain.tsは第2部共通の「1 canvas + 切替ボタン」構成で、ボタンが押されるたびに第6章のstop()でループを止め、新しいフラグメントシェーダーでハーネスを開始し直しています。

src/lessons/10-polar-and-rotation/main.ts
// 第10章: 極座標と 2D 回転
// 第2部共通の構成: 1 つの canvas に対して、切替ボタンでフラグメントシェーダーを
// 差し替える。CPU 側のコードはこれだけで、主役は 4 本の .frag ファイル。

import { startFullscreenShader } from '../../lib/fullscreen-shader';
import polarSource from './01-polar-debug.frag?raw';
import radialSource from './02-radial.frag?raw';
import kaleidoSource from './03-kaleido.frag?raw';
import rotationSource from './04-rotation.frag?raw';

const demos = [
  { id: 'polar', label: '極座標の可視化', source: polarSource },
  { id: 'radial', label: '放射模様と花', source: radialSource },
  { id: 'kaleido', label: '万華鏡', source: kaleidoSource },
  { id: 'rotation', label: 'mat2 回転', source: rotationSource },
] as const;

const canvas = document.querySelector<HTMLCanvasElement>('#demo');
const controls = document.querySelector<HTMLParagraphElement>('#demo-buttons');
if (!canvas || !controls) {
  throw new Error('デモに必要な要素が見つかりません');
}

let handle = startFullscreenShader(canvas, demos[0].source);

for (const [index, demo] of demos.entries()) {
  const button = document.createElement('button');
  button.type = 'button';
  button.textContent = demo.label;
  button.setAttribute('aria-pressed', index === 0 ? 'true' : 'false');
  button.addEventListener('click', () => {
    // いまのループを止めてから、新しいシェーダーで開始し直す(第6章の stop() の出番)。
    // 古いプログラムの解放は省略している(後始末は第35章)
    handle.stop();
    handle = startFullscreenShader(canvas, demo.source);
    for (const b of controls.querySelectorAll('button')) {
      b.setAttribute('aria-pressed', 'false');
    }
    button.setAttribute('aria-pressed', 'true');
  });
  controls.append(button);
}

three.js との対応

この章の内容は、three.js では「行列を意識せずに済むように包まれている」部分の中身にあたります。

three.jsこの章
object.rotation.z = angle(オイラー角の z 回転)内部では cosθ / sinθ からこの章と同じ形の回転行列が組まれ、頂点に掛かる(4×4 行列の左上 2×2 に、この章のmat2と同じ 4 つの数が入る)
THREE.Matrix3 / THREE.Matrix4第11章から gl-matrix の mat3 / mat4 で同等物を自作する
Matrix4.elements が列優先の配列mat2(a, b, c, d)が列の順に埋まるのと同じ流儀。gl-matrix も列優先(第11章)
Vector2.angle()atan(p.y, p.x)で自作。ただし範囲の取り方が違う(three.js は 0〜2π、GLSL は -π〜+π)ので、移植時は常套句で 0〜1 に揃えると安全
ShaderMaterialのフラグメントシェーダーに書く極座標・万華鏡の処理GLSL は同じ言語なので、この章のコードがほぼそのまま通用する

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

この章のコードはsrc/lessons/10-polar-and-rotation/にあります。極座標と行列は、値をいじって向きや継ぎ目がどう変わるかを見るのが一番の近道です。

まとめ

次章から第3部「3D ラスタライズパイプライン」です。gl-matrix を導入し、dot / cross / normalize といったベクトル演算を目で確かめながら、この章のmat2を 4×4 へ育てます。平行移動のために次元が 1 つ増える話(同次座標)から始めて、三角形が立体になる までの座標変換の全景を描いていきます。