GLSL入門|Three.jsのShaderMaterialで自作シェーダーを書く

約11分
GLSL入門|Three.jsのShaderMaterialで自作シェーダーを書く

みなさんこんにちは。フロントエンドエンジニアのしゅん(@shun_webdesign)です。

僕はXでThree.jsのシェーダー作品をちょこちょこ投稿しているんですが、その多くの土台になっているのが「GLSL」です。

Three.jsで用意されているマテリアル(MeshStandardMaterial など)だけでも十分きれいな表現は作れます。でも触っていくうちに、「もっと独自の質感が出したい」「ライブラリの範囲を超えた表現がしたい」 という壁にぶつかります。その壁を越える鍵がGLSLシェーダーです。

この記事では、そもそもGLSLって何? というところから、Three.jsの ShaderMaterial で実際に自作シェーダーを動かすところまでを、できるだけやさしく解説していきます。

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.xvUv.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では ShaderMaterialvertexShaderfragmentShader にGLSLを文字列で渡す
  • フラグメントシェーダーは gl_FragColor(vec4のRGBA・各成分0.0〜1.0)で色を決める
  • varying × UV座標で、位置によって色を変える(mix でグラデーション)
  • 頂点シェーダーで position を動かせば形も変えられる(position はコピーしてから)
  • float.0 忘れは定番のつまずき。画面が真っ黒なときはコンソールでエラーを確認

GLSLはまだ日本語の情報が少ない領域なので、これから触る人の入口になればうれしいです。僕もXでシェーダー作品を出していくので、よかったら覗いてみてください。

次は、その土台の先にある [TSLとは?WebGPU時代の新しいシェーダーの書き方] も読んでみてください。全体像は Three.js入門ガイド にまとめています。

それでは、よいThree.jsライフを🌊

しゅん

しゅん

フロントエンドエンジニア / Webデザイナー。 アメリカ カリフォルニア州生まれ。 音楽、映画、芸術を中心としたサブカルが大好き。 お仕事のご相談は下記リンクのポートフォリオの連絡先からお願いします。

← 記事一覧に戻る