Kumyu ホームLearn
手を動かす教材 · Three.js / GLSL / WebGL2

シェーダーは、
分解して確かめられる。

高さを変える。光を動かす。画面のどこが応答したか見る。そのあと、変化を生んだ小さな計算を自分で実装します。

このガイドは、三段階で海を作るローカル教材の説明です。二つのsin波、独自の照明、光沢、波頭の白い色を組み合わせます。水の流れ、砕波、岸の泡、映り込み、屈折は計算していません。物理ベースの水材質でもありません。まずは形と見え方の関係を確かめます。

操作する前に、一行だけ予想する

Node.js 24以上とnpmを使います。ダウンロードしたフォルダーで次を実行し、127.0.0.1:4173/?lang=jaを開きます。ソースのcheckoutから始める場合は、先にnode setup.jsを実行します。Three.js 0.186.1はローカルに導入され、画面でCDNや外部素材を読み込みません。

npm ci --ignore-scripts
npm start
  1. Reset everything → 平面 → Wireframe。初期化すると全設定とカメラが戻り、時刻ゼロで停止して記録欄を空にします。三角形の格子を探します。この頂点が、形を曲げられる場所です。
  2. 頂点で波を作る → Capture baseline(基準を記録)→ Amplitude(振幅)。基準は時刻2秒で停止します。「輪郭は高くなる。山の間隔は同じ」と書き、振幅だけ変え、Compare one changed control(一項目の変更を比較)を押します。色だけでなく輪郭を見ます。
  3. Reset → 頂点で波を作る → 基準を記録 → Frequency。同じ幅に山が増えるか減るか予想します。同じ停止時刻で比較します。
  4. 光と色 → Light angle。明るい場所がどこへ動くか予想します。光の水平な方角が変わります。仰角と水面の形は固定です。
  5. Specular(光沢)。ハイライトの強さを上げます。PBR材質のroughnessではありません。波頭の白さも色の処理なので、頂点を動かしません。
  6. Play(再生)→ Speed。位相が進む速さを変えます。停止中のStepなら1/30秒ずつ進められます。もう一つ変える前に、観察を記録します。

Tabで操作部へ移り、スライダーは矢印キーで調整できます。基準との比較はスライダーまたはWireframeを一項目だけ変えます。複数が違う場合は変更を減らすよう案内します。記録はページ内のメモリーに残ります。停止状態から始まり、OSの「動きを減らす」を有効にすると停止します。WebGL2が使えない場合は理由と静的な学習用の代替表示を出します。代替表示はGPUで描けた証拠にはなりません。

試行ごとに一行。観察欄は実行してから埋めます。
段階・時刻変更は一つ予想観察・理由
頂点の波、基準は2秒振幅 0.35 → 0.7輪郭は高く、間隔は同じ実行後に記入

画像ができるまで

Geometry頂点を三角形としてつなぐ
Vertex shader頂点を動かし、値を次へ渡す
Fragment三角形が覆う細かな領域に分ける
Fragment shader画面に出す表面の色を計算する

shader(シェーダー)はGPUで実行するプログラムです。本教材ではGLSLのコードをThree.jsのShaderMaterialへ渡し、WebGLRendererで描きます。geometry(形状データ)は頂点の位置などを渡します。vertex shader(頂点シェーダー)は格子の点の高さを求め、fragment shader(フラグメントシェーダー)は色、光、光沢を組み合わせます。Three.jsはこの材質に組込みの頂点属性や行列を用意します。これはGLSL/WebGLの経路で、WebGPUやTSLの例とは別です。ShaderMaterialの公式資料

PlaneGeometry(12, 12, 96, 96)で格子を作り、形状データを回転してローカルのxz平面へ置きます。高さはyです。頂点は97×97個、三角形は96×96×2個です。頂点が少なければ、色に細部があっても輪郭は細かな波に沿って曲がりません。PlaneGeometry・BufferGeometry

波は関数。水の流れを解く計算ではない

h(x,z,t) = A × [sin(kx − st) + 0.5 sin(0.7kz + 0.8st)] / 1.5
A:振幅  k:空間の周波数  s:位相の速さ  t:時間

src/shaders.jsは、GPU上で頂点の高さをこの式に置き換えます。CPUの位置配列を毎フレーム書き直してはいません。src/math.jsには数値を確かめるための対応する式があります。シミュレーションはシステムの状態がどう変わるかを定めます。ここでは状態を積み重ねて水の流れを求めず、選んだ時刻の形を直接計算します。rendering(描画)は、その形と見え方の計算から画像を作る工程です。

uniformは、その描画で共通に使う入力です。たとえばJavaScriptがuniforms.uAmplitude.valueを変えると、各頂点は同じ振幅を使います。再生中は時間が進み、同じ関数を新しい時刻で計算します。頂点ごとに異なる値を持てるattribute(属性)とは役割が違います。uniformの渡し方

法線で、形と光をつなぐ

normal(法線)は面が向く方向です。高さをy = h(x,z)とすると、左右の傾きdx = ∂h/∂xと前後の傾きdz = ∂h/∂zから、normalize(vec3(-dx, 1, -dz))を求められます。頂点だけ波にして法線を平面のままにすると、照明が斜面に合いません。今回のシェーダーは変形後の法線を渡し、頂点間の補間後に再び長さを1へそろえます。

光を向いた斜面ほど明るくします。光沢の式は法線を、光と視線から作った方向とも比較します。泡の色は高さ÷振幅が0.65から0.95へ上がる間で白さを増やします。これらは見た目を作る選択で、現実の泡が発生する条件ではありません。

座標は、どの空間での住所か

「上」「光の方」と言う前に、どこから見た空間かを決めます。照明に使う方向は同じ空間へそろえて比較します。

空間今回の意味次への変換
Model / local
モデル・ローカル
水面自身のxz格子。高さはy。modelMatrix
World
ワールド
シーン共通の空間。光の方向の出発点。viewMatrix
View
ビュー
カメラから見た空間。今回はここで照明を比較。projectionMatrix
Clip
クリップ
gl_Positionへ書く同次座標。切り取りと透視除算を経て画面へ。描画パイプライン
vec4 viewPosition = modelViewMatrix * vec4(p, 1.0);
gl_Position = projectionMatrix * viewPosition;
// modelViewMatrix は model → world → view をまとめたもの。

法線はnormalMatrixでビュー空間へ移します。ワールドの光の方向はviewMatrixで変換し、位置ではないので平行移動を足しません。ビュー空間ではカメラが原点になるため、表面からカメラへ向かう方向は-viewPosition.xyzです。法線を位置と同じ行列で扱うと、特に軸ごとに違う拡大縮小がある場合に向きが崩れます。物体の変換・カメラの行列・法線行列

三つの計算を自分で書く

exercises.jsを開きます。三つのTODOは動く本体と別になっているので、課題が未完成でも本体は観察できます。最初の課題は一つの波、本体は二つの波です。簡単な入力の答えを予想してからチェックします。

  1. singleWave(x, time, settings):sin波の高さ。振幅ゼロならどこでも高さゼロ。
  2. normalFromGradient(dx, dz):上向きで長さ1の法線。傾きゼロなら[0, 1, 0]。
  3. crestMask(height, amplitude):高さの比率0.65~0.95で滑らかにつながる0~1の白さ。振幅ゼロは白さゼロ。
npm run exercise -- --list
npm run exercise -- wave-height
npm run exercise -- normal
npm run exercise -- foam

試してからanswers.mdとreference-answers.jsを開きます。描画の流れはsrc/app.js → src/ocean.js → src/shaders.js、数式はsrc/math.jsでたどれます。

GLSLを一か所変える:白い波頭を広げる

  1. 光と色で時刻2秒の基準を記録します。初期設定のまま、白い部分を見ておきます。
  2. src/shaders.jsでsmoothstep(0.65, 0.95, vHeight / uAmplitude)を探します。最初の0.65だけ0.35へ変え、0.95は残します。
  3. 「白い範囲が増え、輪郭は同じ」と予想します。ページを再読み込みして同じ段階を選び、同じ設定で基準を記録し、時刻2秒を表示します。
  4. 白さの範囲と輪郭を比較します。0.65へ戻し、再読み込みしてnpm testとnpm run test:browserを実行します。

変更したのはfragment shaderなので、頂点ではなく色が変わります。一時的な実験ではsrc/math.jsのCPU側の式を変更しません。その数値は元のしきい値を表すので、一時的なGLSLの変更は画面で確認します。最後のチェックの前にシェーダーを戻してください。

実行した結果だけを確認する

npm test
npm run test:browser

数値のチェックは値と上限を扱います。ブラウザーの確認は導入済みのGoogle Chromeと、ヘッドレスのANGLE SwiftShaderというソフトウェアWebGL2を使い、ブラウザーをダウンロードしません。シェーダーのコンパイルと描画を確認します。WebGLRendererは診断を用意し、コンパイルやリンクに失敗した材質は描画できません。ソフトウェアGPUでの成功は、スマートフォンや実機での成功と別に記録します。失敗を明示し、画像が取得できたら見て確かめます。性能は測ったあとに記録してください。このガイドにはFPSの実測結果はありません。WebGLRendererの診断

技術資料はリンク先のThree.js公式資料で、確認日は2026-10-03 UTCです。公開日を示す日付ではありません。Back to English ↑