【Three.js】lil-gui(dat-gui)のカスタマイズとstat-jsでパフォーマンスを測る方法(デモつき)

2023年11月24日WebGL & Three.js

Three.jsで数値をGUIで変更することができるようになるlil-gui(dat-gui)のカスタマイズ方法と、
パフォーマンスを測ることができるstat-jsの使い方を紹介します。
lil-guiを使えると、見た目で直感的に調整できるので大変便利です。
また、Webサイトが遅いな、などと思ったらstat-jsでチェックできるのでこちらも便利です。

デモを用意したので、確認してみてください。

lil-guiとstat-jsのサンプルデモ

ソースコード

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import GUI from "lil-gui";
import Stats from "stats-js";

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

  // antialias: trueにするとギザギザが滑らかになる
  const renderer = new THREE.WebGLRenderer({
    antialias: true,
  });
  renderer.setSize(window.innerWidth, window.innerHeight);
  // 背景色を変更する
  renderer.setClearColor(0x090909);
  // document.body.appendChild(renderer.domElement);
  document.getElementById("modal-container").appendChild(renderer.domElement);

  const meshes = []; // メッシュを格納する配列。
  const MESH_NUM = 250; // 生成するメッシュの数。
  const POS_RANGE = 150; // 位置をランダムに設定する範囲。
  const MAX_SCALE = 1.1; // メッシュの最大スケール。
  const TARGET_MESH_NUM = 20; // 動かすメッシュの数。
  // 色の設定
  const COLORS = {
    COLOR1: "#27005D",
    COLOR2: "#9400FF",
    COLOR3: "#AED2FF",
    COLOR4: "#E4F1FF",
  };
  /**
   * 最小値と最大値の間でランダムな数を生成します。
   *
   * @param {number} min - 生成する数の最小値。
   * @param {number} max - 生成する数の最大値。
   * @param {boolean} [isInt=false] - 整数値を生成するかどうか。
   * @returns {number} 生成されたランダムな数。
   */
  function mapRand(min, max, isInt = false) {
    let rand = Math.random() * (max - min) + min;
    rand = isInt ? Math.round(rand) : rand;
    return rand;
  }

  /**
   * ランダムな属性を持つメッシュを生成します。
   *
   * @returns {THREE.Mesh} 生成されたランダムなメッシュ。
   */
  function randomMesh() {
    // 利用可能なジオメトリの配列。
    const geometries = [
      new THREE.BoxGeometry(8, 8, 8),
      new THREE.PlaneGeometry(8, 8),
      new THREE.TorusGeometry(8, 3, 200, 20),
      new THREE.SphereGeometry(8),
      new THREE.TorusKnotGeometry(8, 2),
    ];
    // ランダムな色を生成。
    // const color = new THREE.Color(
    //   mapRand(0.2, 1),
    //   mapRand(0.2, 1),
    //   mapRand(0.2, 1)
    // );
    // 色の選択を COLORS オブジェクトから行う
    const colors = [COLORS.COLOR1, COLORS.COLOR2, COLORS.COLOR3, COLORS.COLOR4];
    // 色のインデックスをランダムに選択
    const colorIndex = mapRand(0, colors.length - 1, true); // ここで整数値を生成
    const color = new THREE.Color(colors[colorIndex]);
    // ランダムな位置を生成。
    const pos = {
      x: mapRand(-POS_RANGE, POS_RANGE),
      y: mapRand(-POS_RANGE, POS_RANGE),
      z: mapRand(-POS_RANGE, POS_RANGE),
    };
    // ランダムなスケールを生成。
    const scale = mapRand(1, MAX_SCALE);

    // ランダムなマテリアルを生成。
    const material = new THREE.MeshStandardMaterial({ color });
    // ランダムなジオメトリを選択。
    const gIndex = mapRand(0, geometries.length - 1, true);
    // メッシュを生成。
    const mesh = new THREE.Mesh(geometries[gIndex], material);
    // 色のインデックスをメッシュに関連付け
    mesh.colorIndex = colorIndex; // メッシュに色のインデックスを設定
    // メッシュの位置を設定。
    mesh.position.set(pos.x, pos.y, pos.z);
    // メッシュのスケールを設定。
    mesh.geometry.scale(scale, scale, scale);
    return mesh;
  }
  // メッシュの生成とシーンへの追加
  for (let i = 0; i < MESH_NUM; i++) {
    const mesh = randomMesh();
    meshes.push(mesh);
  }
  scene.add(...meshes);

  // 座標軸ヘルパーの追加
  const axis = new THREE.AxesHelper(500);
  scene.add(axis);

  // カメラの初期位置設定
  camera.position.z = 50;
  // オービットコントロールの初期化
  const control = new OrbitControls(camera, renderer.domElement);

  // ライト
  const amLight = new THREE.AmbientLight(0xa9a9a9);
  scene.add(amLight);
  // 点光源1の追加
  const pointLight1 = new THREE.PointLight(0xdfdfdf, 1, 500);
  pointLight1.position.set(10, 110, 120);
  // ヘルパーの色と大きさを変更
  const pHelper1 = new THREE.PointLightHelper(pointLight1, 5, 0x00ff00);
  pHelper1.visible = false;

  // 点光源2(青色)の追加
  const pointLight2 = new THREE.PointLight(0xffff00, 1, 700);
  pointLight2.position.set(-100, -110, -220);
  const pHelper2 = new THREE.PointLightHelper(pointLight2, 5, 0x00ff00);
  pHelper2.visible = false;

  // 点光源3(赤色)の追加
  const pointLight3 = new THREE.PointLight(0x00ffff, 1, 2900);
  pointLight3.position.set(550, 100, 220);
  // 回転させる
  // radius, phi(x軸への回転), theta(y軸への回転)
  const spherical = new THREE.Spherical(28, 0.5, 1);
  pointLight3.position.setFromSpherical(spherical);
  const pHelper3 = new THREE.PointLightHelper(pointLight3, 5, 0x00ff00);
  pHelper3.visible = false;

  scene.add(
    pointLight1,
    pHelper1,
    pointLight2,
    pHelper2,
    pointLight3,
    pHelper3
  );

  /**
   * 物体に適用するランダムなアクションを生成します。
   * この関数は、物体がランダムに移動する方向を決定します。
   *
   * @param {{ x: number, y: number, z: number }} position - 物体の現在位置。
   * @returns {Function} 物体に適用するアクション関数。
   */
  function getAction({ x, y, z }) {
    const rand = mapRand(0.5, 2);
    const ACTIONS = [
      function () {
        const direction = x < 0 ? rand : -rand;
        this.position.x += direction;
      },
      function () {
        const direction = y < 0 ? rand : -rand;
        this.position.y += direction;
      },
      function () {
        const direction = z < 0 ? rand : -rand; this.position.z += direction; }, ]; const action = ACTIONS[mapRand(0, ACTIONS.length - 1, true)]; return action; } // 物体の移動設定 let targetMeshes = []; setInterval(() => {
    // 次で初期化されるけども残っているとバグの元になるので初期化する
    targetMeshes.forEach((mesh) => (mesh.__action = null));
    // 初期化する
    targetMeshes = [];

    for (let i = 0; i < TARGET_MESH_NUM; i++) { const mesh = meshes[mapRand(0, meshes.length - 1, true)]; mesh.__action = getAction(mesh.position); targetMeshes.push(mesh); } }, 1500); const gui = new GUI({ container: document.querySelector("#modal-container") }); // pointLight1のx軸を-500〜500まで、1刻みで変更できるように設定 const folder1 = gui.addFolder("ポイントライト1"); folder1.open(); folder1.add(pointLight1.position, "x", -500, 500, 1); folder1.add(pointLight1.position, "y", -500, 500, 1); folder1.add(pointLight1.position, "z", -500, 500, 1); // const folder2 = gui.addFolder("色"); // folder2.open(); // folder2.addColor(colors, "color1").onChange(() => {
    //   color1.color.set(colors.color1);
    // });
    // COLORS オブジェクトをGUIに追加
    const folder2 = gui.addFolder("色");
    folder2.open();
    folder2.addColor(COLORS, "COLOR1").onChange((newValue) => {
      meshes.forEach((mesh) => {
        if (mesh.colorIndex === 0) {
          // COLOR1に関連するメッシュ
          mesh.material.color.set(newValue);
        }
      });
    });
    folder2.addColor(COLORS, "COLOR2").onChange((newValue) => {
      meshes.forEach((mesh) => {
        if (mesh.colorIndex === 1) {
          // COLOR2に関連するメッシュ
          mesh.material.color.set(newValue);
        }
      });
    });
    folder2.addColor(COLORS, "COLOR3").onChange((newValue) => {
      meshes.forEach((mesh) => {
        if (mesh.colorIndex === 2) {
          // COLOR3に関連するメッシュ
          mesh.material.color.set(newValue);
        }
      });
    });
    folder2.addColor(COLORS, "COLOR4").onChange((newValue) => {
      meshes.forEach((mesh) => {
        if (mesh.colorIndex === 3) {
          // COLOR4に関連するメッシュ
          mesh.material.color.set(newValue);
        }
      });
    });

  // stats(パフォーマンス確認)
    const stats = new Stats();
    stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
    document.body.appendChild( stats.dom );


  // アニメーション関数
  function animate() {
    requestAnimationFrame(animate);
    stats.begin();
    // 移動させる
    targetMeshes.forEach((mesh) => mesh.__action());
    // カメラを移動させる
    if (POS_RANGE > camera.position.z) {
      camera.position.z += 0.01;
    }
    // 点光源を回転させる
    spherical.theta += 0.01;
    pointLight3.position.setFromSpherical(spherical);

    control.update();

    renderer.render(scene, camera);
    stats.end();
  }

  animate();

こちらのコードのほとんどは、【Three.js】物体(メッシュ)を複数ランダムに生成して散りばめる方法(デモあり)と同じで詳しく解説しているので確認してみてください。
今回は、lil-guiとstat-js部分に絞って解説します。

lil-gui(dat-gui)のカスタマイズ方法

dat-guiは非推奨になったのですが、ほぼ同じ使い方のため、参考になると思います。
今から使う場合は、dat-guiではなく、lil-guiを使いましょう。

lil-gui公式サイト: https://lil-gui.georgealways.com/

package.jsonにいれる

npm install lil-gui --save-dev

まずは使えるようにパッケージをインストールします。

インポートする

import GUI from "lil-gui";

インスタンス化する

    const gui = new GUI();
    const gui = new GUI({ container: document.querySelector("#modal-container") });

インスタンス化するときに、デフォルトではbody直下にDOMができます。
body以外にlil-guiを入れたい場合は、上のようにcontainerに値を設定するとそこにlil-guiをいれることができます。

ライトを直感的に調整できるようにする

    const folder1 = gui.addFolder("ポイントライト1");
    folder1.open();
    folder1.add(pointLight1.position, "x", -500, 500, 1);
    folder1.add(pointLight1.position, "y", -500, 500, 1);
    folder1.add(pointLight1.position, "z", -500, 500, 1);

まず、いろいろ追加するとわかりにくくなるので、フォルダーを作ってグループで管理して折りたためるようにします。
そのためにaddFolderを使います。
あとはそのaddForderに項目をadd()で追加していくだけです。
たとえば、x軸なら、pointLight1のx軸を-500〜500まで、1刻みで変更できるように設定しています。
ここの値を自由に変えることができます。

色を直感的に調整できるようにする

    const folder2 = gui.addFolder("色");
    folder2.open();
    folder2.addColor(COLORS, "COLOR1").onChange((newValue) => {
      meshes.forEach((mesh) => {
        if (mesh.colorIndex === 0) {
          // COLOR1に関連するメッシュ
          mesh.material.color.set(newValue);
        }
      });
    });
    folder2.addColor(COLORS, "COLOR2").onChange((newValue) => {
      meshes.forEach((mesh) => {
        if (mesh.colorIndex === 1) {
          // COLOR2に関連するメッシュ
          mesh.material.color.set(newValue);
        }
      });
    });
    folder2.addColor(COLORS, "COLOR3").onChange((newValue) => {
      meshes.forEach((mesh) => {
        if (mesh.colorIndex === 2) {
          // COLOR3に関連するメッシュ
          mesh.material.color.set(newValue);
        }
      });
    });
    folder2.addColor(COLORS, "COLOR4").onChange((newValue) => {
      meshes.forEach((mesh) => {
        if (mesh.colorIndex === 3) {
          // COLOR4に関連するメッシュ
          mesh.material.color.set(newValue);
        }
      });
    });

色も同様で、まずはフォルダーを作ってあげます。
上の場合だと、ランダムに4種類の色から色を選択し、メッシュを生成しているため、
colorIndexという項目を目印にします。

    const colors = [COLORS.COLOR1, COLORS.COLOR2, COLORS.COLOR3, COLORS.COLOR4];
    // 色のインデックスをランダムに選択
    const colorIndex = mapRand(0, colors.length - 1, true); // ここで整数値を生成
    const color = new THREE.Color(colors[colorIndex]);

このcolorIndexの配列のインデックスをメッシュのインデックスと一致する場合に、とすることで、
複数メッシュに適用しているような色でも一括して変更することができます。
基本的に、addColorで配列を指定したら、onChange()で新しい色をセットする必要があります。

メッシュに色が固定されている場合

  COLORS = {
    COLOR1: "#27005D",
    COLOR2: "#9400FF",
    COLOR3: "#AED2FF",
    COLOR4: "#E4F1FF",
  };

  const boxGeometry = new THREE.BoxGeometry(SCALE, SCALE, SCALE);
  const material1 = new THREE.MeshStandardMaterial({ color: COLORS.COLOR1 });
  const material2 = new THREE.MeshStandardMaterial({ color: COLORS.COLOR2 });
  const material3 = new THREE.MeshStandardMaterial({ color: COLORS.COLOR3 });
  const material4 = new THREE.MeshStandardMaterial({ color: COLORS.COLOR4 });

上のような感じで色がメッシュに固定されている場合は、もっと簡単です。

    const folder2 = gui.addFolder("色");
    folder2.open();
    folder2.addColor(COLORS, "COLOR1").onChange(() => {
      COLOR1.color.set(COLORS.COLOR1);
    });

上のようにして変更することができます。

uniformsの値を変更する

  const material = new THREE.ShaderMaterial({
    uniforms: {
      uTex: { value: await loadTex("/imagepath/img.jpg") },
      uTick: { value: 0 },
      uSomething: { value: new THREE.Vector2(10, 20) },
    },
    vertexShader,
    fragmentShader,
  });

上のようなかたちでマテリアルでuniformsのプロパティを設定して、フラグメントシェーダーに渡したいときがあります。
その値をGUIでチェックできるようにするには、以下のようにします。

const gui = new GUI({ container: document.querySelector("#modal-container") });
  const folder1 = gui.addFolder("フォルダ名");
  folder1.open();
  folder1.add(material.uniforms.uSomething.value, "x", 0, 100, 1);
  folder1.add(material.uniforms.uSomething.value, "y", 0, 100, 1);

materialがインスタンス化されたオブジェクトなので、あとは単純にuniforms…以下を指定してあげます。
Vector2は値をみると分かるのですがxとyのプロパティが生成され、それに指定した値(上の例なら10と20)が入ります。

ラベル名を変更したいとき

folder1.add(material.uniforms.uProgress, "value", 0, 1, 0.1).name("進捗");

上のようにname関数を使うと、そこに指定した名称のラベルに変更できます。

数値のデータをチェックボックスで管理したいとき

画像の切り替えなどで0から1に変化するものをチェックボックスで表示管理したいときがあります。
その場合は以下のようにします。

  // booleanにしてチェックボックスにしたい
  const lilData = { next: Boolean(material.uniforms.uProgress.value) };
  folder1.add(lilData, "next").onChange(() => {
    gsap.to(material.uniforms.uProgress, {
      value: Number(lilData.next),
      duration: 1.0,
      ease: "Power2.inOut"
    })
  });

まずは、チェックボックスにするためブール値(true,false)に変更します。
そして、変更を検知するためにonChange()を使います。
あとはその中にコールバック関数を書いてあげます。
よくgsapなどで変化させたりしますが、その場合はブール値にした値を数値に変換してあげる必要があるので、Number()などを使います。

他の場所で変更された値と連動させたい

  // 数値で見たい場合
  folder1.add(material.uniforms.uProgress, "value", 0, 1, 0.1).name("進捗").listen();
  // booleanにしてチェックボックスにしたい
  const lilData = { next: Boolean(material.uniforms.uProgress.value) };
  folder1.add(lilData, "next").onChange(() => {
    gsap.to(material.uniforms.uProgress, {
      value: Number(lilData.next),
      duration: 1.0,
      ease: "Power2.inOut"
    })
  });

上のチェックボックスとたとえば進捗ラベル部分を連動させたいとします。
その場合、進捗ラベルの方にlisten関数をつけてあげると、チェックボックスのオンオフと連動して変化させることができます。