【Three.js】物体(メッシュ)を複数ランダムに生成して散りばめる方法(デモあり)

2023年11月20日WebGL & Three.js

Three.jsを使って、物体(メッシュ)を複数個、ランダムに生成して散りばめる方法を紹介します。
またその応用として、ライトが必要な物体や、物体をランダムに移動させる方法も紹介しています。
大きさも、ジオメトリの種類も、場所も、マテリアルもランダムに生成するデモを用意したので、
まずはデモを2つほど確認してみてください。

物体(メッシュ)を複数ランダムに生成して散りばめるデモ


Three.jsを使って、ランダムに物体を生成ためのソースコード

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

  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(0xf0f0f0);
  document.body.appendChild(renderer.domElement);


  /**
   * 最小値と最大値の間でランダムな数を生成します。
   *
   * @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;
  }
  const meshes = []; // メッシュを格納する配列。
  const MESH_NUM = 100; // 生成するメッシュの数。
  const POS_RANGE = 150; // 位置をランダムに設定する範囲。
  const MAX_SCALE = 1.6; // メッシュの最大スケール。

  /**
   * ランダムな属性を持つメッシュを生成します。
   *
   * @returns {THREE.Mesh} 生成されたランダムなメッシュ。
   */
  function randomMesh() {
    // 利用可能なジオメトリの配列。
    const geometries = [
      new THREE.BoxGeometry(10, 10, 10),
      new THREE.PlaneGeometry(10, 10),
      new THREE.TorusGeometry(10, 3, 200, 20),
      new THREE.SphereGeometry(10),
      new THREE.TorusKnotGeometry(10, 2),
    ];
    // ランダムな色を生成。
    const color = new THREE.Color(
      mapRand(0.2, 1),
      mapRand(0.2, 1),
      mapRand(0.2, 1)
    );
    // ランダムな位置を生成。
    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.MeshBasicMaterial({ color: color });
    const material = new THREE.MeshBasicMaterial({ color });
    // ランダムなジオメトリを選択。
    const gIndex = mapRand(0, geometries.length - 1, true);
    // メッシュを生成。
    const mesh = new THREE.Mesh(geometries[gIndex], material);
    // メッシュの位置を設定。
    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(20);
  scene.add(axis);

  camera.position.z = 50;

  const control = new OrbitControls(camera, renderer.domElement);

  function animate() {
    requestAnimationFrame(animate);

    control.update();

    renderer.render(scene, camera);
  }

  animate();

もしThree.jsの基本的な操作の一連の流れを知らないという場合は、Three.jsの基本的な操作の一連の流れがとても短くまとめた記事なので参考にしてみてください。
具体的なコードとデモが見たい場合は、【WebGL&Three.js入門】Three.jsの一番シンプルなサンプルコードのチュートリアルの解説でデモもみれる状態にしているので参考にしてみてください。
コードのポイントは以下です。

ランダムな数値を返す関数を作成

  /**
   * 最小値と最大値の間でランダムな数を生成します。
   *
   * @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;
  }

最小値、最大値、整数値にするかどうかのブール値を引数にとる関数です。
これにより、いろんな値を設定できるようになります。

それぞれの項目にランダムな数値を設定

  const meshes = []; // メッシュを格納する配列。
  const MESH_NUM = 100; // 生成するメッシュの数。
  const POS_RANGE = 150; // 位置をランダムに設定する範囲。
  const MAX_SCALE = 1.6; // メッシュの最大スケール。

  /**
   * ランダムな属性を持つメッシュを生成します。
   *
   * @returns {THREE.Mesh} 生成されたランダムなメッシュ。
   */
  function randomMesh() {
    // 利用可能なジオメトリの配列。
    const geometries = [
      new THREE.BoxGeometry(10, 10, 10),
      new THREE.PlaneGeometry(10, 10),
      new THREE.TorusGeometry(10, 3, 200, 20),
      new THREE.SphereGeometry(10),
      new THREE.TorusKnotGeometry(10, 2),
    ];
    // ランダムな色を生成。
    const color = new THREE.Color(
      mapRand(0.2, 1),
      mapRand(0.2, 1),
      mapRand(0.2, 1)
    );
    // ランダムな位置を生成。
    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.MeshBasicMaterial({ color: color });
    const material = new THREE.MeshBasicMaterial({ color });
    // ランダムなジオメトリを選択。
    const gIndex = mapRand(0, geometries.length - 1, true);
    // メッシュを生成。
    const mesh = new THREE.Mesh(geometries[gIndex], material);
    // メッシュの位置を設定。
    mesh.position.set(pos.x, pos.y, pos.z);
    // メッシュのスケールを設定。
    mesh.geometry.scale(scale, scale, scale);
    return mesh;
  }

ジオメトリは配列を用意して、インデックス番号(gIndex)をランダムにすることで、ランダムなジオメトリを選択可能にしています。
色や配置場所やスケールなどは、直接ランダムな数値を格納してします。

複数のメッシュを一括生成

  for (let i = 0; i < MESH_NUM; i++) {
    const mesh = randomMesh();
    meshes.push(mesh);
  }
  scene.add(...meshes);

一回ずつランダムな数値を設定できるように、関数を読んで、配列に格納します。
そして、scene.addは残余引数(可変引数)を取ることができるので、
スプレッド構文(...)で展開して渡せば生成可能です。

Three.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("canvas-container").appendChild(renderer.domElement);

  const meshes = []; // メッシュを格納する配列。
  const MESH_NUM = 250; // 生成するメッシュの数。
  const POS_RANGE = 150; // 位置をランダムに設定する範囲。
  const MAX_SCALE = 1.1; // メッシュの最大スケール。
  const TARGET_MESH_NUM = 20; // 動かすメッシュの数。

  /**
   * 最小値と最大値の間でランダムな数を生成します。
   *
   * @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)
    );
    // ランダムな位置を生成。
    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.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(0xe2e2e2, 0.9);
  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);

  // アニメーション関数
  function animate() {
    requestAnimationFrame(animate);
    // 移動させる
    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);
  }

  animate();

基本は上と同じです。
知っておくべき情報は2点です。
ライトと物体の移動方法です。
まず、ライトについて、知っておくべき情報は、基本のライト:AmbientLight, DirectionalLight, PointLightの使い方とサンプルデモを参考にしてみてください。
物体の位置を移動させる方法については、物体を回転、並行移動、スケールする方法(デモあり)を参考にしてみてください。

以下がポイントです。

ライトが必要なメッシュをインスタンス化する

    // ランダムなマテリアルを生成。
    const material = new THREE.MeshStandardMaterial({ color });

やってもやらなくてもいいですが、ライトで変化を見たい場合は、ライトが必要なメッシュを使います。

ライトを設置

 // ライト
  const amLight = new THREE.AmbientLight(0xe2e2e2, 0.9);
  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
  );

ライトについては、詳しくは基本のライト:AmbientLight, DirectionalLight, PointLightの使い方とサンプルデモを参考にしてもらえればすぐにわかると思います。
ライトのヘルパーは、シーンにaddする部分でコメントアウトしてもいいのですが、面倒な場合が多いので、visibleというプロパティで制御する方法があります。こちらをコメントアウトした方がちょっと楽になります。

物体を移動させる設定

  // 物体の移動設定
  let targetMeshes = [];
  setInterval(() => {
    // 次で初期化されるけども残っているとバグの元になるのでnullで初期化
    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);

setIntervalを使って定期的に処理しています。
最初に初期化しています。
物体を移動させるためには、物体のx軸、y軸、z軸の位置を変えてあげればOKです。
positionにx,y,zがあるので、それをgetActionという関数に渡して、数値を変える処理をしています。
あとは、移動させるとき、全部を移動させると処理が重くなるので、様子を見ながら、適宜調整します。
調整には、コードの上の方で設定した定数TARGET_MESH_NUMの数値をいじるだけです。

const TARGET_MESH_NUM = 20; // 動かすメッシュの数。

次にpositionにx,y,zの数値を変える関数をみてみましょう。

物体の位置を変更する処理

  /**
   * 物体に適用するランダムなアクションを生成します。
   * この関数は、物体がランダムに移動する方向を決定します。
   *
   * @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;
  }

まず、引数は、物体のpositionが渡されて、分割代入しています。position.x,position.y,position.zがpositionには含まれていて、必要なものは、x,y,zだけなので、分割代入するとスッキリします。
分割代入が不明な場合は、JavaScriptの分割代入の使い方を参考にしてみてください。
そして、配列に関数をいれて、ランダムにする関数にてその配列内の関数をランダムに実行しています。
3D空間自体を原点(0,0,0)を中心に作成しているため、0より大きい場合はマイナスの数値を足し、小さい場合はプラスの数値を足すことで、
大きく外れた物体を作らないようにしています。