GLSL入門|Three.jsのShaderMaterialで自作シェーダーを書く
みなさんこんにちは。フロントエンドエンジニアのしゅん(@shun_webdesign)です。
僕はXでThree.jsのシェーダー作品をちょこちょこ投稿しているんですが、その多くの土台になっているのが「GLSL」です。
Three.jsで用意されているマテリアル(MeshStandardMaterial など)だけでも十分きれいな表現は作れます。でも触っていくうちに、「もっと独自の質感が出したい」「ライブラリの範囲を超えた表現がしたい」 という壁にぶつかります。その壁を越える鍵がGLSLシェーダーです。
この記事では、そもそもGLSLって何? というところから、Three.jsの ShaderMaterial で実際に自作シェーダーを動かすところまでを、できるだけやさしく解説していきます。
Table of Contents
GLSL(シェーダー)とは何か
GLSL は、GPUに「どう描くか」を指示するためのプログラミング言語です。そのプログラムのことをシェーダーと呼びます。
ポイントは、シェーダーが 1つのピクセル(あるいは1つの頂点)だけを担当する小さなプログラム だということ。JavaScriptで画面全体を塗るなら「全ピクセルをfor文で回す」コードを書きますが、シェーダーにループは書きません。「1ピクセル分の処理」だけを書けば、GPUがそれを何百万ピクセル分もまとめて並列実行してくれます。
この「自分は1ピクセルのことだけ考えればいい」という発想の転換が、最初の山場であり、慣れれば最大の武器になります。
僕たちが書くシェーダーは、大きく2種類です。
- 頂点シェーダー … 頂点の位置を決める(=形を決める)
- フラグメントシェーダー … 各ピクセルの色を決める(=色を決める)
この2つだけ書けば、あとの面倒な描画処理はGPUが自動でやってくれます。
ShaderMaterialで自作シェーダーを使う
Three.jsで自分のGLSLを動かすには、ShaderMaterial を使います。vertexShader(頂点シェーダー)と fragmentShader(フラグメントシェーダー)にGLSLのコードを文字列で渡すだけです。
const material = new THREE.ShaderMaterial({
vertexShader: `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
void main() {
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0);
}
`,
});
const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 64), material);
scene.add(mesh);
これで球体がオレンジ色で塗りつぶされます。たったこれだけがシェーダーの最小構成です。
変わるのはマテリアルの作り方だけで、Scene・Camera・Renderer を用意して描画ループを回す、というThree.jsの基本構造はそのままです。基礎に不安がある方は、先に Three.js入門ガイド を読んでから戻ってくると理解が早いはずです。
頂点シェーダーの gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); は呪文っぽく見えますが、「Three.jsが用意した行列を掛けて3D座標を2D座標に変換する」という定型文です。しばらくはこのままコピーして使えばOKです。
フラグメントシェーダーで色を塗る
フラグメントシェーダーの仕事は「このピクセルの色を決める」こと。答えは gl_FragColor という特別な変数に vec4 で代入します。
void main() {
gl_FragColor = vec4(1.0, 0.5, 0.0, 1.0);
}
vec4 の4つの値は (R, G, B, A) を表し、それぞれ 0.0〜1.0 の範囲です。CSSの rgb(255, 0, 0) が vec4(1.0, 0.0, 0.0, 1.0) に対応します。255で割った値がGLSLでの色だと覚えておくと換算しやすいです。
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 赤
gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0); // 青
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // 白
ここで一点だけ、GLSL特有の落とし穴を。GLSLは float に必ず小数点を書きます。1 は整数、1.0 は小数として区別され、型が合わないとエラーになります。
float x = 1.0; // OK
float y = 1; // エラー! int と float の不一致
「なぜか動かない」と思ったら、まずこの .0 の付け忘れを疑ってみてください。
varyingとUV座標でグラデーションを作る
全ピクセルを同じ色で塗るだけだと面白くありません。ピクセルの位置によって色を変えると、一気にシェーダーらしくなります。
その「位置」を教えてくれるのがUV座標です。UV座標は、オブジェクト表面を平面に展開した 0.0〜1.0 の座標で、Three.jsが uv という名前で各頂点に用意してくれています。
ただし uv は頂点シェーダーが持っている情報なので、フラグメントシェーダーに渡すには varying という仕組みを使います。両方のシェーダーで同じ名前・型のvaryingを宣言すると、値が自動的に受け渡されます。
// 頂点シェーダー
varying vec2 vUv;
void main() {
vUv = uv; // UV座標をvaryingに入れる
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// フラグメントシェーダー
varying vec2 vUv;
void main() {
// 2色を左から右へブレンドする
vec3 colorA = vec3(0.1, 0.2, 0.8); // 青
vec3 colorB = vec3(1.0, 0.4, 0.1); // オレンジ
vec3 col = mix(colorA, colorB, vUv.x);
gl_FragColor = vec4(col, 1.0);
}
mix(a, b, t) は2つの色を t の割合でブレンドする関数です。vUv.x は左端が 0.0、右端が 1.0 なので、そのままブレンド係数として渡すだけで左から右へのグラデーションになります。CSSの linear-gradient に近い感覚ですね。
vUv.x を vUv.y に変えれば縦方向のグラデーションになります。数式のひとつの値が見た目に直結する——この感覚こそがシェーダーの醍醐味です。
頂点シェーダーで形を動かす
色だけでなく、形もシェーダーで動かせます。頂点シェーダーでは position に各頂点の座標が入っていて、これを加工すると形そのものが変わります。
void main() {
vec3 pos = position; // positionは読み取り専用なのでコピー
pos.y += sin(pos.x * 3.0) * 0.3; // X座標に応じてY方向に波を作る
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
position は読み取り専用なので、vec3 pos = position; とコピーしてから操作するのがお作法です(ここはよく忘れます)。これを PlaneGeometry に適用すると、平面が波打つように変形します。
ひとつ注意点。頂点シェーダーは「頂点」を動かすので、頂点が少ないとカクカクします。なめらかに波打たせたいときは new THREE.PlaneGeometry(2, 2, 64, 64) のように分割数を増やして頂点を細かくしておきましょう。「コードは正しいのに変形がカクつく」ときは、たいていこれが原因です。
GLSLの次に待っている「TSL」
ここまでが、Three.jsでGLSLを書くときの入り口です。ShaderMaterial にGLSL文字列を渡す——この書き方はThree.jsの定番で、今も現役です。
ただ、正直に言うとGLSLには地味なつらみもあります。ただの文字列なので補完も型チェックも効かず、タイポに気づきにくい。処理を部品化して使い回すのも少し面倒です。
そこで登場したのが、WebGPU時代の新しいシェーダーの書き方 TSL(Three.js Shading Language) です。JavaScriptでシェーダーを書くと、Three.jsがそれをWebGPU用にもWebGL用にも自動変換してくれる、という仕組みです。
とはいえ、TSLを使いこなすうえでもGLSLの考え方が土台になります。「1ピクセルずつ処理する」「UV座標で位置を知る」「mixで色を混ぜる」——この記事で触れた発想は、TSLでもそのまま活きます。まずはGLSLで感覚をつかむのがおすすめです。
(TSLが気になる方は [TSLとは?WebGPU時代の新しいシェーダーの書き方] もどうぞ。WebGPUへの移行そのものは [Three.jsでWebGPUを使う(WebGPURenderer移行ガイド)] にまとめています)
まとめ
- GLSL = GPUに「どう描くか」を指示するシェーダー言語。1ピクセル/1頂点ぶんの処理だけを書けばいい
- Three.jsでは
ShaderMaterialのvertexShader・fragmentShaderにGLSLを文字列で渡す - フラグメントシェーダーは
gl_FragColor(vec4のRGBA・各成分0.0〜1.0)で色を決める - varying × UV座標で、位置によって色を変える(
mixでグラデーション) - 頂点シェーダーで
positionを動かせば形も変えられる(positionはコピーしてから) floatの.0忘れは定番のつまずき。画面が真っ黒なときはコンソールでエラーを確認
GLSLはまだ日本語の情報が少ない領域なので、これから触る人の入口になればうれしいです。僕もXでシェーダー作品を出していくので、よかったら覗いてみてください。
次は、その土台の先にある [TSLとは?WebGPU時代の新しいシェーダーの書き方] も読んでみてください。全体像は Three.js入門ガイド にまとめています。
それでは、よいThree.jsライフを🌊