アニメーションと uniform
第3章の三角形は、一度描いたきり止まっていました。頂点データもシェーダーも描画の瞬間に固定で、 外から値を差し込む口がなかったからです。この章ではuniformという 2 つ目の入力経路を追加し、時間とマウス座標を毎フレーム流し込んで、三角形を動かします。 あわせて、第2章から持ち越していた宿題 — 毎フレーム描画するループと、リサイズ処理のループへの統合 — を片付けます。
three.js で言えば、ShaderMaterialの uniforms に THREE.Clockの経過時間を毎フレーム書き込み、renderer.setAnimationLoopで回していた、あの仕組みの中身です。
u_timeでゆっくり回転・脈動し、canvas 上でポインタを動かすとu_mouseに反応してポインタの方向へ寄ります。頂点データは 1 度転送したきりで、毎フレーム変わっているのは uniform の値 2 つだけです。 回転中に横へ伸びて見えるのは、canvas が横長 (3:2) のままクリップ空間を引き伸ばしているためです(第3章と同じ理由。補正は第7章)。この章で学ぶこと:
- attribute と uniform の違い — 「頂点ごとの値」と「ドローコール全体で共通の値」
getUniformLocationとuniform1f系 API、その状態機械的な性質requestAnimationFrameによる描画ループと時間の扱い- マウス座標を DOM 座標からクリップ空間へ変換して渡す
- リサイズ処理を「ループの先頭で毎フレームチェック」に統合する(第2章の回収) — 毎フレーム DOM を読むコストも見積もる
1. uniform — ドローコール全体で共通の値
シェーダーへ CPU から値を渡す経路は、大きく 2 つあります。1 つは第3章で使ったattribute (頂点属性)で、VBO から頂点ごとに違う値が届きます。もう 1 つがこの章のuniformで、1 回のドローコールの間、全頂点・全フラグメントで同じ値を取ります。 名前の uniform は「一様な」という意味で、値が実行単位ごとに変わらないことを指しています。
GLSL 側の宣言は 1 行です。attribute と違って頂点ごとのバッファも VAO の配線も要らないので、CPU 側の準備もずっと軽くなります。
uniform float u_time;— 経過時間(秒)。名前のu_は「uniform 由来」を表す本サイトの命名規約です(a_は attribute、v_は頂点→フラグメント間の値)。uniform vec2 u_mouse;— マウス位置。この章ではクリップ空間 (-1〜+1) の座標として渡します。
時間は u_time(秒)、マウスは u_mouse、解像度を渡す場合はu_resolution(描画バッファのピクセル数、vec2)— という名前を、本サイトでは今後も一貫して使います(u_mouseをどの座標系で渡すかだけは用途で変わります。この章はクリップ空間ですが、第6章で全画面シェーダー向けの定義に切り替えます)。第6章でこの 3 つを標準セットとして毎回自動で渡すハーネスを作り、第2部以降の全章がその上で動きます。
2. uniform API — ロケーションの取得と値の設定
uniform へ値を入れる手順は 2 段階です。
- ロケーションを取得する—
gl.getUniformLocation(program, 'u_time')。リンク済みプログラムの中から uniform を名前で探し、書き込み先のハンドル (WebGLUniformLocation) を返します。 - 値を設定する—
gl.uniform1f(location, 値)のような型別の関数で書き込みます。
// uniform のロケーションは初期化時に 1 回だけ取得してキャッシュする
// (getUniformLocation は名前の文字列検索なので、毎フレーム呼ぶものではない)
const timeLocation = gl.getUniformLocation(program, 'u_time');
const mouseLocation = gl.getUniformLocation(program, 'u_mouse');ロケーションの取得は名前の文字列検索なので、初期化時に 1 回だけ行い、変数にキャッシュします。毎フレームgetUniformLocationを呼ぶコードは動きはしますが、無駄な検索を毎秒何十回も繰り返すことになります。attribute のlayout(location = 0)のように番号をシェーダー側で固定する方法は、GLSL ES 3.00 の uniform にはありません(uniform をまとめてブロックごとバインドする UBO という仕組みが第34章で登場します)。
値の設定関数は、シェーダー側の型に合わせて選びます。名前の末尾が「成分数 + 型」になっています。
| GLSL の型 | 設定する関数 | 例 |
|---|---|---|
float | uniform1f(loc, x) | この章の u_time |
vec2 | uniform2f(loc, x, y) | この章の u_mouse |
vec3 / vec4 | uniform3f / uniform4f | 色 (RGB / RGBA) など |
int / bool / sampler2D | uniform1i(loc, i) | テクスチャユニット番号(第16章) |
uint / uvec2 など(WebGL2 で追加) | uniform1ui / uniform2ui など | ビット演算を使う場面(第25章の hash など) |
| 同じ型の uniform 配列 | uniform1fv / uniform2fv など末尾 v の系列 | 複数光源のパラメータ(第18章) |
mat2 / mat3 / mat4 | uniformMatrix2fv / 3fv / 4fv | 変換行列(第12章) |
もう 1 つ、状態機械としての性質を押さえておきます。uniform の値はプログラムオブジェクトごとに保存される状態です。uniform1fを呼んだ瞬間にどこかへ送信されるのではなく、「そのプログラムのu_timeは今この値」という設定が書き換わり、次のドローコールがそれを読みます。一度設定した値は次に上書きするまで残るので、毎フレーム変わらない uniform は初期化時に 1 回設定すれば十分です。また、この設定関数はuseProgramで現在使用中のプログラムに対して働くため、呼ぶ順序は必ず「useProgram→ uniform1f」です(使用中のプログラムがない状態で呼ぶとエラーになります)。 リンク直後の uniform はすべて 0 で初期化されています。
3. 描画ループ — requestAnimationFrame
アニメーションの骨格は WebGL ではなくブラウザの API です。requestAnimationFrame(以下 rAF)にコールバックを渡すと、ブラウザは次の画面更新の直前に 1 回だけそれを呼びます。コールバックの最後で自分自身を再予約すると、画面のリフレッシュレートに 同期したループになります(多くの環境で毎秒 60 回、ディスプレイによっては 120 回以上)。
function frame(timestamp: DOMHighResTimeStamp): void {
resizeIfNeeded();
const time = timestamp / 1000; // rAF の引数はミリ秒。秒に直して使う
gl.clear(gl.COLOR_BUFFER_BIT);
// uniform の設定は「いま使っているプログラム」に対して行われる
gl.useProgram(program);
gl.uniform1f(timeLocation, time);
gl.uniform2f(mouseLocation, mouseX, mouseY);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame); // 次のフレームを予約する
}
requestAnimationFrame(frame); // ループ開始ポイントは 3 つあります。
- 時間はコールバックの引数でもらう— rAF はコールバックに timestamp(ページを開いてからの経過時間、ミリ秒)を渡してきます。 秒に直してから使うと、シェーダー側の
sin(u_time)などの係数が人間に扱いやすい大きさになります。Date.now()を使う必要はありません。 - タブが見えていないときは止まる— 非表示のタブでは rAF のコールバックは呼ばれません。無駄な GPU 負荷がかからない、ありがたい仕様です。タブに戻ると timestamp は経過した分だけ一気に進んだ値で再開します。
- 1 フレームの仕事が「クリア → uniform 更新 → ドロー」に整理された— プログラムの作成、VBO の転送、VAO の配線、クリア色の設定といった初期化は、すべてループの外(1 回だけ)です。GPU に毎フレーム送り直しているのは float が 3 個だけ、という点が uniform の効率の良さです。
4. 時間で動かす — u_time を頂点シェーダーへ
受け取った時間をどう動きに変えるかは、シェーダー側の仕事です。この章の頂点シェーダーは、第3章のものに 3 ステップの計算を足しています。
#version 300 es
// 頂点属性: 頂点ごとに違う値。VBO 経由で渡される(第3章と同じ)
layout(location = 0) in vec2 a_position;
layout(location = 1) in vec3 a_color;
// uniform: ドローコール全体で共通の値。CPU 側から設定する
uniform float u_time; // 経過時間(秒)
uniform vec2 u_mouse; // マウス位置(クリップ空間 -1〜+1)
out vec3 v_color;
void main() {
v_color = a_color;
// ① ゆっくり回転する。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
);
// ② sin で拡大率を揺らして脈動させる(0.55〜0.75 倍)
pos *= 0.65 + 0.1 * sin(u_time * 2.0);
// ③ マウスの方向へ寄せる(u_mouse はクリップ空間なのでそのまま足せる)
pos += u_mouse * 0.3;
gl_Position = vec4(pos, 0.0, 1.0);
}- ① 回転— 点 (x, y) を角度 θ だけ回した位置は (x·cosθ − y·sinθ, x·sinθ + y·cosθ) になります。三角関数の加法定理から出てくる式ですが、いまは「回転の定型文」として 写して構いません。この式が
mat2という 2×2 行列 1 つにまとまり、「行列 = 変換」という見方につながることを第10章で学びます。 - ② 脈動—
sin(u_time * 2.0)は -1〜+1 を往復するので、0.65 + 0.1 * sin(...)で拡大率が 0.55〜0.75 を往復します。* 2.0が速さ(周波数)、0.1が振れ幅(振幅)です。sin の使いこなしは第9章でまとめて扱います。 - ③ マウスへの追従—
u_mouseをクリップ空間の座標として渡してあるので、変換なしでそのまま頂点座標に足せます。 このデモでは動きを控えめにするためu_mouse * 0.3と 0.3 倍しており、三角形はポインタの方向へ「少し寄る」動きになります(0.3を1.0にするとポインタに完全に追従します — 座標系が一致している証拠です)。
フラグメントシェーダー側でも u_time を使って、色をゆっくり明滅させています。
#version 300 es
precision highp float;
// uniform は頂点・フラグメントのどちらのシェーダーでも宣言できる。
// 同じ名前・同じ型で両方に宣言すると、リンク後は 1 つの uniform として共有される
uniform float u_time;
in vec3 v_color;
out vec4 fragColor;
void main() {
// 頂点色をゆっくり明滅させる(0.75〜1.0 倍)
float pulse = 0.875 + 0.125 * sin(u_time * 3.0);
fragColor = vec4(v_color * pulse, 1.0);
}5. マウスを渡す — DOM 座標からクリップ空間へ
マウスの扱いで本質的なのは、イベント処理よりも座標系の変換です。ブラウザのポインタイベントが くれる座標は「左上が原点、y は下向き、単位は CSS ピクセル」。一方、頂点シェーダーが足し算に使いたいのは 「中央が原点、y は上向き、-1〜+1」のクリップ空間です。第2章で「viewport や scissor の原点は左下で DOM と逆」という注意がありましたが、あれと同じ食い違いがここにも現れます。WebGL の座標系はすべて y が上向き、DOM は下向き — なので、DOM から WebGL へ座標を持ち込むときは必ずどこかで y を反転することになります。
let mouseX = 0; // クリップ空間 (-1〜+1) で保持する。初期値は中央
let mouseY = 0;
canvas.addEventListener('pointermove', (event) => {
const rect = canvas.getBoundingClientRect();
// ① canvas の描画領域内の位置 → 0〜1 に正規化。
// rect はボーダーを含むので、clientLeft/Top(ボーダー幅)を引き、
// ボーダーを除いた表示サイズ clientWidth/Height で割る
const x = (event.clientX - rect.left - canvas.clientLeft) / canvas.clientWidth;
const y = (event.clientY - rect.top - canvas.clientTop) / canvas.clientHeight;
// ② 0〜1 → クリップ空間 (-1〜+1)。y は向きが逆なので反転する
mouseX = x * 2 - 1; // -1(左)〜+1(右)
mouseY = 1 - y * 2; // -1(下)〜+1(上)
});pointermoveはマウス・ペン・タッチを 1 系統で扱えるイベントです(mousemove の上位互換と考えて構いません)。- イベントの
clientX / clientYはビューポート基準なので、getBoundingClientRect()で canvas の位置を取り、canvas ローカルへ直します。rect はボーダーを含む外形なので、割る大きさにはボーダーを除いたclientWidth / clientHeightを使います(このデモの canvas には CSS で 1px のボーダーが付いています)。CSS ピクセルどうしの計算なので、devicePixelRatio はここでは登場しません。これらの呼び出しコストは 6 節の補足で扱います。 - イベントハンドラでは変換した値を変数に保存するだけにして、uniform の設定と描画はループに任せます。イベントの発生頻度とフレームレートは無関係なので、 ハンドラ内で描画まで行うと二重描画や描き漏れのもとになります。
マウス座標を「クリップ空間の vec2」として渡すのは、この章の定義です。頂点座標と同じ座標系なので 足し算 1 つで反応が作れる、いま一番便利な形だからです。何をどの座標系で渡すかは設計次第で、 たとえばピクセル座標のまま渡して シェーダー側で変換する流儀もあります。
6. リサイズをループに統合する
第2章では ResizeObserver でサイズ変化を検知し、そのたびに描き直す方式を取りました。さらに 「dpr だけが変わるケースは ResizeObserver では拾えない」ため、matchMedia の監視まで併用していたことを思い出してください。毎フレーム描くようになった今は、もっと単純にできます。ループの先頭で毎回サイズを計算し、変わっていたら作り直すだけです。
function resizeIfNeeded(): void {
const dpr = Math.min(window.devicePixelRatio, 2);
const width = Math.floor(canvas.clientWidth * dpr);
const height = Math.floor(canvas.clientHeight * dpr);
// width / height への代入は描画バッファの作り直し(中身も消える)なので、変化時のみ
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
// 描画バッファを作り直したら viewport も合わせ直す(第2章)
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
}
}比較すると要点がはっきりします。「サイズ変化 → 描き直し」というイベント駆動の設計では、 変化を漏れなく検知する仕掛け(ResizeObserver + matchMedia)が必要でした。毎フレーム描くループ駆動の設計では、検知は不要です — どうせ次のフレームで気づくからです。表示サイズの変化・ウィンドウリサイズ・ ブラウザのズーム・dpr の異なるディスプレイへの移動、どこから来た変化でも同じ 1 箇所で拾えます。本サイトのループを持つデモは、以降すべてこの形です(静止画を 1 枚描くだけのデモでは、引き続き ResizeObserver が適役です)。チェックの中身はclientWidth / clientHeightの読み取りと数値の比較だけです。これらはレイアウトの結果を読むプロパティなので無条件に安いわけではありませんが、同じコールバックの中で、読むより先に DOM を書き換えていないかぎり、毎フレーム呼んでも負荷は問題になりません(条件と実測値は次の補足)。
コード全文
シェーダー 2 本は上に掲載したとおりです。main.ts の全文を載せます。コンパイル・リンクの関数と頂点まわりは第3章のままで、変わったのは後半 — 「初期化(1 回)」と「ループ(毎フレーム)」の 2 部構成になったことです。この構造は、この先どんなに 複雑な作品になっても変わりません。
// 第4章: アニメーションと uniform
// 第3章の静止した三角形に uniform で時間とマウス座標を渡し、
// requestAnimationFrame のループで毎フレーム描画する。
// リサイズ処理は「ループの先頭でサイズが変わっていれば作り直す」形に統合する(第2章の続き)。
import fragmentSource from './triangle.frag?raw';
import vertexSource from './triangle.vert?raw';
// ---------------------------------------------------------------------------
// シェーダーのコンパイルとプログラムのリンク(第3章と同じ)
// ---------------------------------------------------------------------------
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;
}
// ---------------------------------------------------------------------------
// デモ本体
// ---------------------------------------------------------------------------
function setup(gl: WebGL2RenderingContext, canvas: HTMLCanvasElement): void {
// --- 初期化(1 回だけ実行する部分) ---------------------------------------
const program = linkProgram(
gl,
compileShader(gl, gl.VERTEX_SHADER, vertexSource),
compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource),
);
// 頂点データと VAO の配線は第3章とまったく同じ
// biome-ignore format: 1 頂点 = 1 行の並びを保つ
const positions = new Float32Array([
0.0, 0.8, // 上
-0.8, -0.8, // 左下
0.8, -0.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);
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);
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);
// uniform のロケーションは初期化時に 1 回だけ取得してキャッシュする
// (getUniformLocation は名前の文字列検索なので、毎フレーム呼ぶものではない)
const timeLocation = gl.getUniformLocation(program, 'u_time');
const mouseLocation = gl.getUniformLocation(program, 'u_mouse');
// クリア色も「状態」なので、毎フレーム設定し直す必要はない
gl.clearColor(0.06, 0.07, 0.09, 1.0);
// --- マウス: DOM 座標をクリップ空間へ変換して保持する --------------------
let mouseX = 0; // クリップ空間 (-1〜+1) で保持する。初期値は中央
let mouseY = 0;
canvas.addEventListener('pointermove', (event) => {
const rect = canvas.getBoundingClientRect();
// ① canvas の描画領域内の位置 → 0〜1 に正規化。
// rect はボーダーを含むので、clientLeft/Top(ボーダー幅)を引き、
// ボーダーを除いた表示サイズ clientWidth/Height で割る
const x = (event.clientX - rect.left - canvas.clientLeft) / canvas.clientWidth;
const y = (event.clientY - rect.top - canvas.clientTop) / canvas.clientHeight;
// ② 0〜1 → クリップ空間 (-1〜+1)。y は向きが逆なので反転する
mouseX = x * 2 - 1; // -1(左)〜+1(右)
mouseY = 1 - y * 2; // -1(下)〜+1(上)
});
// --- リサイズ: ループの先頭で「変わっていれば作り直す」(第2章の統合形) ----
function resizeIfNeeded(): void {
const dpr = Math.min(window.devicePixelRatio, 2);
const width = Math.floor(canvas.clientWidth * dpr);
const height = Math.floor(canvas.clientHeight * dpr);
// width / height への代入は描画バッファの作り直し(中身も消える)なので、変化時のみ
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
// 描画バッファを作り直したら viewport も合わせ直す(第2章)
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
}
}
// --- 描画ループ: 毎フレーム実行する部分 ----------------------------------
function frame(timestamp: DOMHighResTimeStamp): void {
resizeIfNeeded();
const time = timestamp / 1000; // rAF の引数はミリ秒。秒に直して使う
gl.clear(gl.COLOR_BUFFER_BIT);
// uniform の設定は「いま使っているプログラム」に対して行われる
gl.useProgram(program);
gl.uniform1f(timeLocation, time);
gl.uniform2f(mouseLocation, mouseX, mouseY);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame); // 次のフレームを予約する
}
requestAnimationFrame(frame); // ループ開始
}
// ---------------------------------------------------------------------------
// 要素とコンテキストの取得
// ---------------------------------------------------------------------------
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 で ShaderMaterialに uniforms を渡した経験があれば、この章はその配管を直接触った、と言えます。
| three.js | この章の生 WebGL2 |
|---|---|
renderer.setAnimationLoop(animate)(または自前の rAF ループ) | requestAnimationFrame で自分自身を再予約するループ |
THREE.Clock の getElapsedTime() | rAF の timestamp 引数を 1000 で割って秒にする |
ShaderMaterialのuniforms: { u_time: { value: 0 } } | getUniformLocation でロケーションを取得してキャッシュ |
毎フレーム material.uniforms.u_time.value = t | 毎フレーム useProgram → uniform1f(timeLocation, t) |
three が自動で更新する uniform(modelViewMatrix、projectionMatrixなど) | まだ登場しない — 第11〜12章で行列として自作し、自分で渡す |
手元で動かして、壊してみる
この章のコードはsrc/lessons/04-animation-and-uniforms/にあります。動くものは、止め方・壊れ方を見るのが一番勉強になります。
main.tsの'u_time'を'u_tim'に変えて、console.log(timeLocation)を足してみる —nullが出力され、エラーは 1 つも出ないまま、回転と明滅だけが止まります(uniform は 0 のまま。null への設定は黙って無視、の実地確認です)gl.uniform1f(timeLocation, time)の行をコメントアウトする — 同じく止まります。uniform が「上書きするまで残る状態」であることと、リンク直後の値が 0 であることの確認ですtriangle.vertの周波数と振幅で遊ぶ — 回転の0.5、脈動の2.0と0.1をそれぞれ 10 倍にすると何が起きるか、変える前に予想してから試すmain.tsのmouseY = 1 - y * 2をmouseY = y * 2 - 1に変える — 三角形が上下逆に反応するようになります。y 反転を「おまじない」でなく 理解できているかのチェックですtriangle.vertのu_mouse * 0.3をu_mouse * 1.0にする — 三角形の中心がポインタに完全に一致して追従します。DOM 座標からクリップ空間への変換が正しいことの実証ですframeの先頭のresizeIfNeeded()をコメントアウトしてウィンドウ幅を変える — 描画バッファが初期サイズのまま引き伸ばされて ぼやける、第2章の「事故」が再現します
まとめ
- attribute は「頂点ごとに違う値」、uniform は「ドローコール全体で共通の値」。時間・マウス・解像度・行列など、シーン単位の入力はすべて uniform で渡す
- ロケーションは初期化時に
getUniformLocationで取得してキャッシュし、値はuseProgram後にuniform1f系で設定する。値はプログラムごとの状態として上書きまで残る - 未使用の uniform は最適化で消えてロケーションが
nullになる(正常)。null への設定は黙って無視される - 描画ループは rAF で自分自身を再予約する。時間は timestamp 引数(ミリ秒)を秒に直して
u_timeへ - DOM のマウス座標は「左上原点・y 下向き」。クリップ空間へは範囲の写し替え + y 反転
- ループがあるなら、リサイズは「フレーム先頭で変わっていれば作り直す」が最も単純で確実。 レイアウトを読むプロパティを毎フレーム読んでも、同じコールバックの中で読むより先に DOM を書き換えていないかぎりコストは問題にならない
シェーダーに外から値が届くようになったので、表現の主役は GLSL のコードそのものに移っていきます。次章はいったん手を止めて、GLSL ES 3.00 という言語を一度きちんと整理します — 型とベクトル、スウィズル、組み込み関数。第2部からシェーダーを本格的に書き始めるための足場です。