【WebGL】Three.jsのコードをシェーダ言語GLSLに書き換えたサンプルコードをデモ付きで解説

2023年11月25日WebGL & Three.js

Three.jsのコードをシェーダ言語GLSLに書き換えたサンプルコードをデモ付きで解説します。
WebGL(GLSL)の入門として一番最初に知っておくといい内容になっています。

Three.jsをちょっと知ったくらいの段階でも問題ありません。
まったく知らない場合は、【WebGL&Three.js入門】Three.jsの一番シンプルなサンプルコードのチュートリアルの解説を参考にしてみてください。

Three.jsのコードをシェーダ言語に書き換えたデモ



3Dモデルを表示(Three.jsのMeshBasicMaterial)は、普通にThree.jsのみで書いています。
3Dモデルを表示(シェーダ言語でMeshBasicMaterialを再現)では、シェーダ言語を使って同じものを作成しています。
3Dモデルを表示(シェーダ言語でカラフルな色表現を実現)では、Three.jsにはできない色表現を実現しています。

3Dモデルを表示(シェーダ言語でカラフルな色表現を実現)のソースコード

import * as THREE from "three";

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );

  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  // document.body.appendChild(renderer.domElement);
  document.getElementById("canvas-container").appendChild(renderer.domElement);

  const geometry = new THREE.BoxGeometry(3, 3, 3);

  // 3Dモデルを表示(Three.jsのMeshBasicMaterial)
  // const material = new THREE.MeshBasicMaterial({ color: 0x0000ff });

  // 3Dモデルを表示(シェーダ言語でMeshBasicMaterialを再現)
  // const material = new THREE.ShaderMaterial({
  //   vertexShader: `
  //     void main() {
  //       gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  //     }
  //   `,
  //   fragmentShader: `
  //     void main() {
  //       gl_FragColor = vec4(0, 0, 1.0, 1.0);
  //     }
  //   `,
  //   transparent: true, // 透明にしたいとき
  // });

  // 3Dモデルを表示(シェーダ言語でThree.jsにはできない色表現を実現)
  const material = new THREE.ShaderMaterial({
    vertexShader: `
      varying vec2 vUv;

      void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      varying vec2 vUv;

      void main() {
        gl_FragColor = vec4(vUv, 0.5, 0.8);
      }
    `,
    transparent: true, // 透明にしたいとき
  });
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  camera.position.z = 5;

  /* エラー時にシェーダの全体のコードを表示 */
  renderer.debug.onShaderError = (
    gl,
    program,
    vertexShader,
    fragmentShader
  ) => {
    const vertexShaderSource = gl.getShaderSource(vertexShader);
    const fragmentShaderSource = gl.getShaderSource(fragmentShader);

    console.groupCollapsed("vertexShader");
    console.log(vertexShaderSource);
    console.groupEnd();

    console.groupCollapsed("fragmentShader");
    console.log(fragmentShaderSource);
    console.groupEnd();
  };

  function animate() {
    requestAnimationFrame(animate);

    cube.rotation.x = cube.rotation.x + 0.01;
    cube.rotation.y += 0.01;

    renderer.render(scene, camera);
  }

  animate();

Three.jsの基本のコードになるので、Three.js部分がわからない場合は、【WebGL&Three.js入門】Three.jsの一番シンプルなサンプルコードのチュートリアルの解説を参考にしてみてください。

コードのポイント

3つのデモの違いはmaterial(マテリアル)部分だけです。
デモ1は、以下です。

  // 3Dモデルを表示(Three.jsのMeshBasicMaterial)
  const material = new THREE.MeshBasicMaterial({ color: 0x0000ff });

デモ2は、以下です。

  // 3Dモデルを表示(シェーダ言語でMeshBasicMaterialを再現)
  const material = new THREE.ShaderMaterial({
    vertexShader: `
      void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      void main() {
        gl_FragColor = vec4(0, 0, 1.0, 1.0);
      }
    `,
    transparent: true, // 透明にしたいとき
  });

そしてデモ3は以下です。

  // 3Dモデルを表示(シェーダ言語でThree.jsにはできない色表現を実現)
  const material = new THREE.ShaderMaterial({
    vertexShader: `
      varying vec2 vUv;

      void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      varying vec2 vUv;

      void main() {
        gl_FragColor = vec4(vUv, 0.5, 0.8);
      }
    `,
    transparent: true, // 透明にしたいとき
  });

シェーダ言語の中身を詳しく解説していきます。

シェーダ言語を書くときの決まりごと

シェーダ言語を書くときの決まりごとがいくつかあります。

void main() {
gl_Position = ...

gl_FragColor = ...
}

ここの書き方は決まりごとです。
つまり、void main() {}の中にしたい処理を記述します。
gl_Positionの値を変更すると、頂点の位置が変わります。
gl_FragColorの値を変更すると、色が変わります。

vec4について

//                 (R, G,   B,  a)
gl_FragColor = vec4(0, 0, 1.0, 1.0);

vec4()は、RGBaで表されています。最後はアルファ、透明度です。

シェーダーは主に2種類

Three.jsで使用されるシェーダーは、WebGLにおけるカスタム描画処理の基本です。シェーダーは主に2種類あります:頂点シェーダー(vertexShader)とフラグメントシェーダー(fragmentShader)です。これらのシェーダーは、GPU上で実行される小さなプログラムで、3Dオブジェクトの描画方法を定義します。

以下に詳しく記述しますが、覚えておきたいこととしては、以下です。
頂点の位置を操作したいときには、vertexShaderを触ります。
fragmentShaderは、色を操作したいときに触ります。

頂点シェーダー(vertexShader)

頂点シェーダーは、メッシュの頂点に対する処理を行います。
vertexShaderには、立方体の頂点がそれぞれ渡ってきます。
メッシュは頂点(点)で構成され、頂点シェーダーはこれらの頂点の最終的な位置を計算します。
詳しくは、頂点シェーダー(vertexShader バーテックスシェーダー)とはを参考にしてみてください。

  • varying vec2 vUv;:フラグメントシェーダーにデータを渡すための変数。この場合、テクスチャ座標を渡します。vec2は2つの要素を持った変数を定義するときに使います。つまりvUvという変数が2つの要素を持つと宣言しています。
  • void main() {…}:頂点シェーダーのメイン関数。すべての頂点シェーダーにはこれが必要です。
  • vUv = uv;:頂点のUV座標を vUv に割り当てます。これにより、フラグメントシェーダーで使用できます。
  • gl_Position = …;:頂点の最終的な位置を計算します。projectionMatrix * modelViewMatrix * vec4(position, 1.0) は、3D空間内での頂点の位置を2Dスクリーン上の位置に変換します。

varyingの意味や挙動に関しては、【WebGL】の曲者varyingとは?その挙動の説明と使い方についてを参考にしてみてください。
void main() {…}は、シェーダーで自動的に実行される関数です。
projectionMatrixやmodelViewMatrixやpositionはここでは定義していないのですが、使えます。
なぜならThree.jsの中ですでに定義してくれているからです。
BoxGeometryなどジオメトリをインスタンス化することが、頂点データを定義することになります。

modelViewMatrixとは

modelViewMatrixは立方体などのオブジェクトの座標からrotateやscaleなどを考慮したMeshの座標に変換して(モデル変換)、
それを3D空間上の座標(ワールド座標)に配置して、カメラの場所などを考慮した座標(視点座標)に変換します。(ビュー変換)
要するに、オブジェクトが3D空間上の座標を得るまでをモデルビュー変換といい、それがmodelViewMatrixという行列で計算されています。

projectionMatrixとは

そして、projectionMatrixが投影変換と言われるもので、PerspectiveCameraとしった視点のカメラの座標を変換して、クリップ座標にします。

WebGLのクリップ座標について

クリップ座標は、頂点シェーダーでgl_Positionで処理される座標のことです。
クリップ座標は-1から1の範囲をとります。

ここに書いたコードと、Three.jsのコードが結合されて、3Dグラフィクスに反映させることができます。

フラグメントシェーダー(fragmentShader)

フラグメントシェーダーは、メッシュの各ピクセル(またはフラグメント)の色を計算します。

  • varying vec2 vUv;:頂点シェーダーから受け取ったデータ。
  • void main() {…}:フラグメントシェーダーのメイン関数。
  • gl_FragColor = vec4(vUv, 0.5, 0.8);:フラグメントの色を設定します。この例では、vUv の座標をベースにRGB色を計算し、それに固定のアルファ値(0.8)を適用しています。

頂点シェーダとほぼ同じです。
頂点シェーダで定義した頂点を線で結んだ内側の色を指定することができます。

Three.js内のシェーダコードをコンソールで確認する方法

renderer.debug.onShaderError = ( gl, program, vertexShader, fragmentShader ) => {

  const vertexShaderSource = gl.getShaderSource( vertexShader );
  const fragmentShaderSource = gl.getShaderSource( fragmentShader );

  console.groupCollapsed( "頂点シェーダ (vertexShader)" )
  console.log( vertexShaderSource )
  console.groupEnd()

  console.groupCollapsed( "フラグメントシェーダ (fragmentShader)" )
  console.log( fragmentShaderSource )
  console.groupEnd()
}

上のコードをThree.jsを利用しているJavaScriptに記述します。
そして、void main() {…}の中で、debugなどの不要な文字列をいれてエラーを発生させると
コンソールでvertexShaderやgragmentShaderのコードが確認できます。

二つを合わせる

このシェーダーのペアは、メッシュの各頂点の位置を計算し、それぞれのピクセルに色を適用しています。
結果として、メッシュは特定の色のグラデーションを持つように見えます。これは、基本的なWebGLシェーダープログラミングの一例です。
グラデーションになる理由については、varyingの意味や挙動に関しては、【WebGL】の曲者varyingとは?その挙動の説明と使い方についてを参考にしてみてください。

基本的なGLSLの計算方法については、GLSLで使う数学(三角関数、ベクトル)を文系エンジニアのために実務でいるところだけ解説を参考にしてみてください。
また定義の方法は、GLSLで定数を定義する方法(define, constとその違いと注意点)を参考にしてみてください。

WebGLのUV座標(テクスチャ座標)について

UV座標(テクスチャ座標)は原点(0,0)から(1,1)をとります。
sampler2Dなどから色情報を取得して使うときに使用する座標です。

フラグメントシェーダーではこのUV座標を使って色情報を操作するので頭の片隅に置いておくといいです。

GLSLの組み込み関数の調べ方

Shaderific for OpenGL
上のサイトに訪問して、Functionsの項目を確認すると使える関数がわかります。
目的別なので、必要なものを適宜チェックしていく感じです。