ViteでThree.jsの開発環境を構築する。

2024.08.29
TABLE OF CONTENTS

Viteを使ってThree.jsの簡易的な開発環境を構築します。
以前の記事のViteで静的サイトの開発環境を構築する。のViteの開発環境をThree.js用に改良していきます。

今回作成する開発環境、いわゆるHello Worldは https://sho1374k.github.io/mpa-vite-threejs/ になります。

開発環境構築の改修

1. パッケージをインストールする。

shaderを読み込めるように vite-plugin-glsl をインストールします。

npm i -D vite-plugin-glsl

次にthree.jsをインストールします。

npm i three

2. vite.config.jsの設定に追記をする。

先ほどインストールしたvite-plugin-glslの設定をします。

import { defineConfig } from "vite";

// ・・・省略・・・

import glsl from "vite-plugin-glsl"; // 追記

// ・・・省略・・・

export default defineConfig(({ mode }) => {

// ・・・省略・・・

  plugins: [
    glsl({
      include: /\.(vs|fs|frag|vert|glsl)$/,
      compress: true,
    }),
  ],

// ・・・省略・・・

});

拡張子を「vs、fs、vert、frag、glsl」をこのプロジェクトでインポートできるようにしました。
また、shaderの圧縮も有効化しておきます。
圧縮をbuild時のみにしたい場合は compress: mode === "production" のようにするといいと思います。

3. 新しくディレクトリとファイルを作成する。

□ ディレクトリ作成する。

  • src/assets/js/webglにthree.jsのコードを格納していきます。
  • src/assets/shaderにshaderのコードを格納していきます。
mkdir src/assets/js/webgl src/assets/shader

□ ファイルを作成する

  • controller.js:stage.jsやobjects.js等をコントロールするクラスを格納する。
  • stage.js:3d空間のステージ部分を作成する。
  • objects.js:ステージに配置するオブジェクトたちを作成する。
  • object.vs:追加するオブジェクトの頂点シェーダを記述する。
  • object.fs:追加するオブジェクトのピクセルシェーダを記述する。
touch src/assets/js/webgl/controller.js src/assets/js/webgl/stage.js src/assets/js/webgl/objects.js src/assets/shader/object.fs src/assets/shader/object.vs

Three.jsの設定をする。

1. htmlとscssに追加する。

<div id="webgl"></div>
#webgl {
  width: 100vw;
  height: 100vh;
}

2. controller.jsでベースを記述する。

const FPS = 30; // FPSの最大値を設定する。
export class Controller {
  constructor() {
    this.element = document.getElementById("webgl"); // canvasをラッパーする要素

    // 解像度をまとめる
    this.resolution = {
      x: this.element.clientWidth,
      y: this.element.clientHeight,
      aspect: this.element.clientWidth / this.element.clientHeight,
      devicePixelRatio: Math.min(2, window.devicePixelRatio),
    };

    this.time = {
      now: 0,
      delta: 0,
      elapsed: 0,
    };

    this.fps = {
      lastTime: 0,
      frameCount: 0,
      startTime: null,
      nowTime: 0,
      limit: FPS,
      interval: 1000 / FPS,
    };

    window.addEventListener("resize", this.resize.bind(this), { passive: true });
  }

  resize() {
    this.resolution.x = this.element.clientWidth;
    this.resolution.y = this.element.clientHeight;
    this.resolution.aspect = this.resolution.x / this.resolution.y;
  }

  update(now) {
    requestAnimationFrame(this.update.bind(this));
    if (!this.fps.startTime) this.fps.startTime = now;
    this.time.now = now;
    const elapsed = now - this.fps.lastTime;
    this.time.elapsed = elapsed * 0.001;
    this.time.delta = (now - this.fps.startTime) * 0.001;

    if (elapsed > this.fps.interval) {
      this.fps.lastTime = now - (elapsed % this.fps.interval);
      this.fps.frameCount++;
      // console.log(this.fps.frameCount / this.time.delta); // fpsの値を確認する

      // ここに処理を追加していく。
    }
  }

  init() {
    this.update();
    this.resize();
  }
}

上記がベースになります。
それぞれの関数の中にstage.jsやobjects.js等のコードを追加していきます。

src/assets/js/index.js に controller.jsをimportしていきます。

import "@scss/index.scss";

import { Controller } from "./webgl/controller";

window.addEventListener("load", (e) => {
  const controller = new Controller();
  controller.init();
});

3. stage.jsを記述する。

レンダラーとカメラとシーンを作成します。

import * as THREE from "three";

export class Stage {
  constructor(_element, _resolution) {
    this.element = _element;
    this.resolution = _resolution;

    this.renderer = null;
    this.camera = null;
    this.scene = null;
  }

  resizeRenderer() {
    this.renderer.setSize(this.resolution.x, this.resolution.y);
    this.renderer.setPixelRatio(this.resolution.pixelRatio);
  }

  setRenderer() {
    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    this.resizeRenderer();
    this.element.appendChild(this.renderer.domElement);
  }

  resizeCamera() {
    this.camera.aspect = this.resolution.x / this.resolution.y;
    this.camera.updateProjectionMatrix();
  }

  setCamera() {
    this.camera = new THREE.PerspectiveCamera();
    this.camera.near = 1;
    this.camera.far = 100;
    this.camera.fov = 60;
    this.camera.position.z = 4;
    this.resizeCamera();
  }

  setScene() {
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color("#000");

    // helper
    this.scene.add(new THREE.GridHelper(1000, 100));
    this.scene.add(new THREE.AxesHelper(100));
  }

  resize(_resolution) {
    this.resolution = _resolution;
    this.resizeRenderer();
    this.resizeCamera();
  }

  init() {
    this.setRenderer();
    this.setCamera();
    this.setScene();
  }
}

stage.jsをcontroller.jsで読み込みましょう。

import { Stage } from "./stage"; // 追記

// ・・・省略・・・

export class Controller {
  constructor() {
    // ・・・省略・・・

    this.stage = new Stage(this.element, this.resolution); // 追記

    window.addEventListener("resize", this.resize.bind(this), { passive: true });
  }

  resize() {
    // ・・・省略・・・
    this.stage.resize(this.resolution); // 追記
  }

  // ・・・省略・・・

  init() {
    this.stage.init(); // 追記
    // ・・・省略・・・
  }
}

4. objects.jsを記述する。

import * as THREE from "three";

import ObjectVs from "../../shader/object.vs"; // vertex shader を読み込む
import ObjectFs from "../../shader/object.fs"; // fragment shader を読み込む

export class Objects {
  constructor(_stage, _resolution) {
    this.stage = _stage;
    this.resolution = _resolution;
  }

  resize(_resolution) {
    this.resolution = _resolution;
  }

  update(_time) {
    const t = _time.elapsed * 0.25;
    if (this.cubeMesh) {
      this.cubeMesh.rotation.x += t;
      this.cubeMesh.rotation.y += t;
      this.cubeMesh.rotation.z += t;
    }
  }

  init() {
    const g = new THREE.BoxGeometry(1, 1, 1);
    const m = new THREE.ShaderMaterial({
      vertexShader: ObjectVs,
      fragmentShader: ObjectFs,
      // uniforms: shaderにjs側からアスセスできる変数
      uniforms: {
        uTime: { value: 0.0 },
        uProgress: { value: 0.0 },
      },
      side: THREE.DoubleSide, // 両面表示する
    });
    this.cubeMesh = new THREE.Mesh(g, m);
    this.stage.scene.add(this.cubeMesh);
  }
}

uniform変数は経過時間と適当にデバッグ等で使える進捗値をshader側に渡しておきます。
uTime や uProgress の u は uniform の u です。適当でいいと思います。

objects.jsをcontroller.jsにて読み込みます。

// ・・・省略・・・

import { Stage } from "./stage";
import { Objects } from "./objects"; // 追記

// ・・・省略・・・

export class Controller {
  constructor() {

    // ・・・省略・・・

    this.stage = new Stage(this.element, this.resolution);
    this.objects = new Objects(this.stage, this.resolution); // 追記

    // ・・・省略・・・
  }

  resize() {

    // ・・・省略・・・

    this.stage.resize(this.resolution);
    this.objects.resize(this.resolution); // 追記
  }

  update(now) {

    // ・・・省略・・・

    if (elapsed > this.fps.interval) {

      // ・・・省略・・・

      this.objects.update(this.time); // 追記
      this.stage.renderer.render(this.stage.scene, this.stage.camera); // 追記
    }
  }

  init() {
    this.stage.init();
    this.objects.init(); // 追記
    // ・・・省略・・・
  }
}

5. vertex shaderを記述する。

vertex shader とは3Dオブジェクト形成する各頂点に対して座標変換の処理をします。

varying vec2 vUv;
varying vec3 vNormal;
uniform float uProgress;

void main(){
  vUv = uv;
  vNormal = normal;

  vec3 p = position;

  p.xyz = mix(p.xyz, p.xyz + vNormal, uProgress);

  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0 );
}

テクスチャ座標(uv)と法線(normal)の情報をvarying変数を使用してfragment shaderに渡せるようにします。
ちなみにテクスチャ座標は st と呼ぶこともあります。
なぜ、uv や st なのかは、3次元の座標が xyz とされていたり、4次元の座標が xyzw とされているように uv や st のような2次元の座標もその流れだと思います。(誰かがそんなことを言っていた気がします...)
ついでに法線とは、頂点や面がどの方向を向いているかを表す向きベクトルです。
vUv や vNormal の v は varying の v です。
適当でいいと思います。

p.xyz = mix(p.xyz, p.xyz + vNormal, uProgress);

上記のコードを簡単に解説します。
現在の頂点の座標をuProgressの0 ~ 1の量分、法線ベクトルの方に移動させるようにします。

▼ この状態がuProgressの値が0の時 ▼

▼ この状態がuPrgressの値が1の時 ▼

次は下記のコードについて簡単に説明します。(あんまり自信ないです...)

gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0 );
  • projectionMatrix:3d空間上の座標をカメラを基準に2dのスクリーン座標にあわせて変換する。
  • modelMatrix:オブジェクトのローカル座標をワールド座標に変換する。
  • viewMatrix:ワールド座標をカメラ基準の座標に変換する。

pで頂点の位置ベクトルの情報を保持する。
それにmodelViewMatrixをかけることで頂点をオブジェクトのローカル空間からカメラ空間に変換することができます。
最後にprojectionMatrixをかけてカメラ空間に座標をクリップできるように座標を変換します。
そうすることで3d空間にある頂点情報を2dスクリーン上に描画できるようになります。

ちなみに下記のようにmvp行列を取り除くと-1 ~ 1 の範囲の正規化デバイス座標空間になります。

gl_Position = vec4(p, 1.0 );

これはフレームバッファを使用してポストプロセシング等を実装するときに使用したりするので、覚えておくと便利かなと思います。

6. fragment shaderを記述する。

fragment shader とは3Dオブジェクトのポリゴンがスクリーン上でどのような色になるかをピクセルごとに処理をします。

varying vec2 vUv;
varying vec3 vNormal;
uniform float uTime;
uniform float uProgress;

void main( void ) {
  vec4 color = vec4(0);

  vec4 c1 = vec4(vUv, 1.0, 1.0);
  vec4 c2 = vec4(vNormal + vec3(0.5), 1.0);

  color = mix(c1, c2, uProgress);

  gl_FragColor = color;
}

varying変数でvertex shaderからuvとnormalの情報を受け取ります。

c1 は uv の色にblueを足した色味を表示しています。
c2 は normal の色に全体的に 0.5 足して明るくした色を表示しています。(黒い面が出てきて分かりづらくならないように加算しています。)

その c1 と c2 の色を uProgress 変数の 0 ~ 1 の間の値で mix関数で補完しています。

7. three.jsに内包されているデバッグツールを組み込む。

□ OrbitControls

□ Stats

□ GUI

上記の3点をcontroller.jsに追記したいと思います。

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import Stats from "three/examples/jsm/libs/stats.module";
import GUI from "three/examples/jsm/libs/lil-gui.module.min";

// ・・・省略・・・

export class Controller {
  constructor() {

    // ・・・省略・・・

    this.controls = null;
    this.stats = null;
    this.gui = null;

    // ・・・省略・・・

  }

  // ・・・省略・・・

  update(now) {

    // ・・・省略・・・

    if (elapsed > this.fps.interval) {
      // ・・・省略・・・

      if (this.stats) this.stats.update();

      // ・・・省略・・・
    }
  }

  init() {
   // ・・・省略・・・

    this.controls = new OrbitControls(this.stage.camera, this.stage.renderer.domElement);

    this.stats = new Stats();
    this.stats.domElement.style = `position: fixed; top: 0; left: 0; right: initial; bottom: initial; z-index: 9999;`;
    document.body.appendChild(this.stats.domElement);

    this.gui = new GUI();
    {
      // scene
      const f = this.gui.addFolder("scene");
      f.addColor(this.stage.scene, "background");
    }
    {
      // cube mesh
      const f = this.gui.addFolder("cube");
      f.add(this.objects.cubeMesh.material.uniforms.uProgress, "value", 0, 1).name("progress");
    }
  }
}

これでマウスでグリグリできるようになり、FPS等の確認もでき、GUIでパラメータ調整等できるようになりました。

最後に

コード全体はGitHubにあげています。
自己責任で自由に使ってください。

Reference

PICKUP ARTWORK