【JavaScript】three.jsで立方体にテクスチャを貼る。

「ウェブページでかっこいい3Dアニメーションを表示してみたい!」と思ったことはありませんか?

Three.js(スリー・ジェイエス)」は、そんな願いを叶えてくれる魔法の道具(JavaScriptライブラリ)です。これを使えば、まるでゲームのような3Dグラフィックスをウェブブラウザ上で簡単に作ることができます。

この記事では、Three.jsを使って、立方体の箱に好きな絵(テクスチャ)を貼り付けて、くるくる回す 方法を、ステップバイステップで解説します。

応用例

マップエディタ

セルをクリックして壁(黒)/通路(白)を切替
Shiftキーを押しながらクリックで開始地点(S)を設定

壁・床・天井のテクスチャを変更できます。

画面のセンタークリックで前進・左右クリックで左右回転



<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>ダンジョンエディタ&3Dダンジョン(左上原点)</title>
  <style>
    #editor {
      width: 100%;
      background: #f0f0f0;
      overflow: auto;
      padding: 10px;
      box-sizing: border-box;
    }
    #editor table {
      border-collapse: collapse;
      width: initial;
    }
    #editor td {
      width: 15px;
      height: 15px;
      border: 1px solid #ccc;
      text-align: center;
      cursor: pointer;
      user-select: none;
      padding: 0px;
    }
    #editor td.wall {
      background-color: #333;
      color: white;
    }
    #editor td.empty {
      background-color: #fff;
      color: #333;
    }
    #editor td.start {
      background-color: #4caf50;
      color: white;
      font-size: 10px;
      padding: 0;
    }
    /* 現在地を示すセル:青い枠 */
    #editor td.currentPos {
      border: 2px solid blue;
    }
    #dungeon {
      width: 100%;
      position: relative;
      height: 400px;
    }
    #textureControls {
      margin-top: 10px;
    }
    select {
      width: 50px;
    }
  </style>
</head>
<body>
  <div id="editor">
    <strong>マップエディタ</strong>
    <p>
      セルをクリックして壁(黒)/通路(白)を切替<br>
      Shiftキーを押しながらクリックで開始地点(S)を設定
    </p>
    <div id="mapEditor"></div>
    <div id="textureControls">
      <select id="textureTarget">
        <option value="wall"></option>
        <option value="floor"></option>
        <option value="ceiling">天井</option>
      </select>
      <input type="file" id="textureFile">
      <p>壁・床・天井のテクスチャを変更できます。</p>
    </div>
  </div>
  <div id="dungeon"></div>
  画面のセンタークリックで前進・左右クリックで左右回転
  <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.141.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.141.0/examples/jsm/"
      }
    }
  </script>
  <script type="module">
    import * as THREE from 'three';

    // マップデータ(1=壁、0=通路、"S"=開始地点)
    let map = [
      [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
      [1,"S",0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,1,1,0,1,0,1,0,1,1,0,1,0,1,0,1],
      [1,0,1,0,0,0,0,0,0,0,1,0,1,0,1,0,1],
      [1,0,1,0,1,1,1,1,1,0,1,0,1,0,1,0,1],
      [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,1,1,0,1,1,1,0,1,1,1,0,1,1,0,1],
      [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,1,0,1,1,1,0,1,1,1,0,1,0,1,0,1],
      [1,0,1,0,0,0,0,0,0,0,1,0,1,0,1,0,1],
      [1,0,1,1,0,1,0,1,0,1,1,0,1,0,1,0,1],
      [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1],
      [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
      [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
    ];

    let camera, scene, renderer;
    let wallObjects = [];
    let objects = [];
    let prevTime = performance.now();

    // セルサイズ(シーン側)
    const cellSize = 20;

    // 回転・移動のアニメーション関連
    let isRotating = false;
    let isMoving = false;
    let rotationAngle = 0;
    let moveDistance = 0;
    const rotationSpeed = Math.PI / 30;
    const moveSpeed = 0.5;

    // テクスチャ関連
    const textureFile = document.getElementById('textureFile');
    const textureTarget = document.getElementById('textureTarget');
    const textureLoader = new THREE.TextureLoader();
    const defaultTexture = textureLoader.load('https://threejs.org/examples/textures/crate.gif');
    const wallMaterial = new THREE.MeshPhongMaterial({ map: defaultTexture });
    const floorMaterial = new THREE.MeshPhongMaterial({ map: defaultTexture });
    const ceilingMaterial = new THREE.MeshPhongMaterial({ map: defaultTexture });

    createMapEditor();
    initScene();
    animate();

    // マップエディタ作成:各セルにIDを付与
    function createMapEditor() {
      const editorDiv = document.getElementById('mapEditor');
      editorDiv.innerHTML = '';
      const table = document.createElement('table');
      for (let i = 0; i < map.length; i++) {
        const tr = document.createElement('tr');
        for (let j = 0; j < map[i].length; j++) {
          const td = document.createElement('td');
          td.id = `cell-${i}-${j}`;
          td.dataset.row = i;
          td.dataset.col = j;
          if (map[i][j] === 1) {
            td.className = 'wall';
            td.textContent = '';
          } else if (map[i][j] === "S") {
            td.className = 'start';
            td.textContent = 'S';
          } else {
            td.className = 'empty';
            td.textContent = '';
          }
          td.addEventListener('click', function(event) {
            if (event.shiftKey) {
              removeExistingStart();
              map[i][j] = "S";
            } else {
              map[i][j] = (map[i][j] === 1) ? 0 : 1;
            }
            updateDungeon();
            createMapEditor();
          });
          tr.appendChild(td);
        }
        table.appendChild(tr);
      }
      editorDiv.appendChild(table);
    }

    // 既存の開始地点削除
    function removeExistingStart() {
      for (let i = 0; i < map.length; i++) {
        for (let j = 0; j < map[i].length; j++) {
          if (map[i][j] === "S") {
            map[i][j] = 0;
          }
        }
      }
    }

    // セル中心にスナップする補助関数(左上原点)
    function snapToCellCenter(pos) {
      const col = Math.round((pos.x - cellSize/2) / cellSize);
      const row = Math.round((pos.z - cellSize/2) / cellSize);
      return new THREE.Vector3(
        col * cellSize + cellSize/2,
        pos.y,
        row * cellSize + cellSize/2
      );
    }

    // シーン初期化
    function initScene() {
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0xcccccc);
      const dungeonDiv = document.getElementById('dungeon');
      camera = new THREE.PerspectiveCamera(75, dungeonDiv.clientWidth / dungeonDiv.clientHeight, 1, 1000);
      camera.position.y = 10;
      renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.setSize(dungeonDiv.clientWidth, dungeonDiv.clientHeight);
      dungeonDiv.appendChild(renderer.domElement);

      const cols = map[0].length;
      const rows = map.length;
      const mapWidth = cols * cellSize;
      const mapDepth = rows * cellSize;

      // 床:シーン内のセル (0,0)~(cols-1, rows-1) の中心を覆うように、床の中心は (mapWidth/2, mapDepth/2)
      const floorGeometry = new THREE.PlaneGeometry(mapWidth, mapDepth);
      const floor = new THREE.Mesh(floorGeometry, floorMaterial);
      floor.rotation.x = -Math.PI / 2;
      floor.position.set(mapWidth/2, 0, mapDepth/2);
      floor.name = 'floor';
      scene.add(floor);

      // 天井
      const ceilingGeometry = new THREE.PlaneGeometry(mapWidth, mapDepth);
      const ceiling = new THREE.Mesh(ceilingGeometry, ceilingMaterial);
      ceiling.rotation.x = Math.PI / 2;
      ceiling.position.set(mapWidth/2, 20, mapDepth/2);
      ceiling.name = 'ceiling';
      scene.add(ceiling);

      const light = new THREE.HemisphereLight(0xffffff, 0x444444);
      light.position.set(mapWidth/2, 200, mapDepth/2);
      scene.add(light);

      updateDungeon();

      // Sセルがある場合、カメラ位置をそのセル中心に設定(左上原点)
      for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
          if (map[i][j] === "S") {
            camera.position.set(j * cellSize + cellSize/2, 10, i * cellSize + cellSize/2);
            break;
          }
        }
      }
      // スナップ(念のため)
      camera.position.copy(snapToCellCenter(camera.position));

      window.addEventListener('resize', onWindowResize);
      renderer.domElement.addEventListener('click', onClick);
      renderer.domElement.addEventListener('touchstart', onTouchStart);
      renderer.domElement.addEventListener('touchend', onTouchEnd);
      renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());

      textureFile.addEventListener('change', (event) => {
        const file = event.target.files[0];
        if (file) {
          const reader = new FileReader();
          reader.onload = (e) => {
            const texture = textureLoader.load(e.target.result);
            const target = textureTarget.value;
            if (target === 'wall') {
              wallMaterial.map = texture;
              wallMaterial.needsUpdate = true;
            } else if (target === 'floor') {
              floorMaterial.map = texture;
              floorMaterial.needsUpdate = true;
            } else if (target === 'ceiling') {
              ceilingMaterial.map = texture;
              ceilingMaterial.needsUpdate = true;
            }
          };
          reader.readAsDataURL(file);
        }
      });
    }

    function onWindowResize() {
      const dungeonDiv = document.getElementById('dungeon');
      camera.aspect = dungeonDiv.clientWidth / dungeonDiv.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(dungeonDiv.clientWidth, dungeonDiv.clientHeight);
    }

    // ダンジョン更新:壁の位置もセル中心に合わせる
    function updateDungeon() {
      wallObjects.forEach(obj => scene.remove(obj));
      wallObjects = [];
      objects = [];
      const cols = map[0].length;
      const rows = map.length;
      const wallGeometry = new THREE.BoxGeometry(cellSize, 20, cellSize);
      for (let i = 0; i < rows; i++) {
        for (let j = 0; j < cols; j++) {
          if (map[i][j] === 1) {
            const wall = new THREE.Mesh(wallGeometry, wallMaterial);
            wall.position.set(j * cellSize + cellSize/2, 10, i * cellSize + cellSize/2);
            scene.add(wall);
            wallObjects.push(wall);
            objects.push(wall);
          }
        }
      }
      // Sセルがある場合、カメラ位置をそのセル中心に合わせる
      let found = false;
      for (let i = 0; i < rows && !found; i++) {
        for (let j = 0; j < cols; j++) {
          if (map[i][j] === "S") {
            camera.position.set(j * cellSize + cellSize/2, 10, i * cellSize + cellSize/2);
            found = true;
            break;
          }
        }
      }
      const floor = scene.getObjectByName('floor');
      if (floor) { floor.material = floorMaterial; }
      const ceiling = scene.getObjectByName('ceiling');
      if (ceiling) { ceiling.material = ceilingMaterial; }
    }

    // クリック・タッチ時の処理(左回転/右回転/前進)
    function onClick(event) {
      if (isRotating || isMoving) return;
      const rect = renderer.domElement.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const width = renderer.domElement.clientWidth;
      if (x < width / 3) {
        rotateLeft();
      } else if (x > width * 2 / 3) {
        rotateRight();
      } else {
        moveForwardFunc();
      }
    }
    function onTouchStart(event) {
      if (event.touches.length === 1) {
        onClick(event.touches[0]);
      }
    }
    function onTouchEnd(event) {}

    // 左回転
    function rotateLeft() {
      rotationAngle = Math.PI / 2;
      isRotating = true;
      animateRotation();
    }
    // 右回転
    function rotateRight() {
      rotationAngle = -Math.PI / 2;
      isRotating = true;
      animateRotation();
    }
    // 回転アニメーション
    function animateRotation() {
      if (Math.abs(rotationAngle) < 0.01) {
        camera.rotation.y += rotationAngle;
        camera.rotation.y %= Math.PI * 2;
        isRotating = false;
        return;
      }
      const deltaAngle = Math.min(rotationSpeed, Math.abs(rotationAngle));
      camera.rotation.y += Math.sign(rotationAngle) * deltaAngle;
      rotationAngle -= Math.sign(rotationAngle) * deltaAngle;
      requestAnimationFrame(animateRotation);
    }

    // 前進(セル単位の移動)
    function moveForwardFunc() {
      moveDistance = cellSize;
      isMoving = true;
      animateMovement();
    }
    // 移動アニメーション:終了時に必ずセルセンターにスナップ
    function animateMovement() {
      if (Math.abs(moveDistance) < 0.01) {
        isMoving = false;
        const snapped = snapToCellCenter(camera.position);
        camera.position.copy(snapped);
        return;
      }
      const deltaMove = Math.min(moveSpeed, Math.abs(moveDistance));
      const direction = new THREE.Vector3();
      camera.getWorldDirection(direction);
      direction.y = 0;
      direction.normalize();
      const moveVec = direction.multiplyScalar(Math.sign(moveDistance) * deltaMove);
      const newPos = camera.position.clone().add(moveVec);
      const playerCollider = new THREE.Sphere(newPos, 5);
      let collision = false;
      for (let i = 0; i < objects.length; i++) {
        const wallBox = new THREE.Box3().setFromObject(objects[i]);
        if (wallBox.intersectsSphere(playerCollider)) {
          collision = true;
          break;
        }
      }
      if (!collision) {
        camera.position.copy(newPos);
      } else {
        // 衝突時はキャンセルしてスナップ
        const snapped = snapToCellCenter(camera.position);
        camera.position.copy(snapped);
        moveDistance = 0;
        isMoving = false;
        return;
      }
      moveDistance -= Math.sign(moveDistance) * deltaMove;
      requestAnimationFrame(animateMovement);
    }

    // 現在地インジケーター更新:カメラ位置をそのままセル番号に変換
    function updateCurrentPositionIndicator() {
      const cols = map[0].length;
      const rows = map.length;
      const currentCol = Math.floor(camera.position.x / cellSize);
      const currentRow = Math.floor(camera.position.z / cellSize);
      document.querySelectorAll('#mapEditor td.currentPos').forEach(td => td.classList.remove('currentPos'));
      const cell = document.getElementById(`cell-${currentRow}-${currentCol}`);
      if (cell) {
        cell.classList.add('currentPos');
      }
    }

    // アニメーションループ
    function animate() {
      requestAnimationFrame(animate);
      renderer.render(scene, camera);
      updateCurrentPositionIndicator();
    }
  </script>
</body>
</html>

1. Three.jsを使う準備をしよう

まず、Three.jsをHTMLファイルで使えるようにします。一番簡単なのは、インターネット上にあるThree.jsのファイルを直接読み込む方法(CDN)です。

HTMLファイルの <head> タグの中か、<body> タグの最後に、以下のコードを追加してください。これは「これからThree.jsという道具を使いますよ」という宣言のようなものです。

<script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.141.0/build/three.module.js"
    }
  }
</script>

これでThree.jsを使う準備ができました。次に、実際に3Dを描画するためのJavaScriptコードを書いていきましょう。別の<script>タグを作って、その中に書いていきます。

2. 3D空間を作るための3つの要素:舞台・カメラ・映写機

Three.jsで3Dを表示するには、基本的に以下の3つが必要です。

  • シーン(Scene):オブジェクト(物)を置くための仮想的な「舞台」です。
  • カメラ(Camera):その舞台をどこから、どのように見るかを決める「視点」です。
  • レンダラー(Renderer):カメラが見た映像を、実際にウェブページに描画する「映写機」です。

これをコードで書いてみます。

// Three.jsの道具箱から必要なものを取り出すおまじない
import * as THREE from 'three';

// 1. シーン(舞台)の作成
const scene = new THREE.Scene();
// 舞台の背景を灰色っぽくする
scene.background = new THREE.Color(0x333333);

// 2. カメラ(視点)の作成
// PerspectiveCamera(視野角, 画面の縦横比, 最も近い描画距離, 最も遠い描画距離)
// 難しく考えず、人が物を見るような自然な見え方をするカメラ、と覚えておけばOK
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// カメラの位置を少し手前に引く(Z軸方向に5)
camera.position.z = 5;

// 3. レンダラー(映写機)の作成
// WebGLRendererを使うと、コンピュータのグラフィック性能を活かしてキレイに描画できる
const renderer = new THREE.WebGLRenderer({ antialias: true }); // antialias: trueでギザギザを滑らかに
// 映写機のサイズをブラウザのウィンドウサイズに合わせる
renderer.setSize(window.innerWidth, window.innerHeight);
// 映写機が描画する場所(canvas要素)をHTMLのbodyに追加する
document.body.appendChild(renderer.domElement);

これで、3Dを表示するための基本的な舞台設定が完了しました!まだ何も表示されませんが、準備は整っています。

3. 主役の登場!立方体に絵を貼ろう

いよいよ、今回の主役である立方体を作り、それに絵(テクスチャ)を貼り付けます。

物を作るには、まず「形」を決めて、次に「見た目(色や模様)」を決める必要があります。

  • ジオメトリ(Geometry):物体の「形」を決めるデータです(今回は立方体)。
  • マテリアル(Material):物体の表面の「見た目」を決める設定です(色、質感、貼り付ける絵など)。
  • メッシュ(Mesh):形(ジオメトリ)と見た目(マテリアル)を組み合わせた、実際に表示される「物体」です。

コードを見てみましょう。

// --- テクスチャ(貼り付ける絵)の準備 ---
// TextureLoaderは画像を読み込むための道具
const textureLoader = new THREE.TextureLoader();
// 'crate.gif'という画像を読み込んで、テクスチャとして使えるようにする
// (このURLはThree.js公式のサンプル画像です。自分の好きな画像URLに変えてもOK!)
const texture = textureLoader.load('https://threejs.org/examples/textures/crate.gif');

// --- 立方体の作成 ---
// 1. 形(ジオメトリ)を作る:BoxGeometry(幅, 高さ, 奥行き)
const geometry = new THREE.BoxGeometry(2, 2, 2); // 2x2x2の大きさの立方体

// 2. 見た目(マテリアル)を作る:MeshPhongMaterialは光の当たり具合をリアルに表現できる
// mapプロパティに先ほど読み込んだテクスチャを指定する
const material = new THREE.MeshPhongMaterial({ map: texture });

// 3. 形と見た目を合体させて、物体(メッシュ)を作る
const cube = new THREE.Mesh(geometry, material);

// --- 舞台に登場させる ---
// 作った立方体メッシュをシーン(舞台)に追加する
scene.add(cube);

これで、テクスチャが貼られた立方体が作成され、シーンに追加されました。でも、まだ真っ暗かもしれません。なぜなら、「照明」がないからです。

4. ライトアップ!リアルに見せる工夫

現実世界と同じように、3D空間でも光がないと物は見えません。ライトを追加して、立方体を照らしてあげましょう。ここでは2種類のライトを使います。

  • 環境光(AmbientLight):空間全体を均一に、ふんわりと照らす光です。影はできませんが、全体的な明るさを確保します。
  • 平行光(DirectionalLight):特定の方向から降り注ぐ、太陽光のような光です。影を作り出し、オブジェクトに立体感を与えます。
// 1. 環境光(部屋全体の明かりのようなもの)
// AmbientLight(光の色, 光の強さ)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // 白色の光、強さ半分
scene.add(ambientLight); // シーンに追加

// 2. 平行光(太陽光のようなもの)
// DirectionalLight(光の色, 光の強さ)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); // 白色の光、強さ強め
// 光がどこから来るかを設定 (x, y, z座標)
directionalLight.position.set(5, 10, 7.5); // 右上前方から照らす感じ
scene.add(directionalLight); // シーンに追加

これでライトが追加され、立方体が明るく照らされているはずです!

5. 動きをつけよう!くるくる回る立方体

止まっているだけでは面白くないので、立方体をアニメーションで回転させてみましょう。

アニメーションは、パラパラ漫画と同じ原理です。少しずつ変化させた絵を連続で見せることで、動いているように見せます。Three.jsでは requestAnimationFrame という機能を使って、これを実現します。

// アニメーション関数(繰り返し実行される処理)
function animate() {
  // 次のフレームを描画するタイミングで、もう一度animate関数を実行するようにお願いする
  requestAnimationFrame(animate);

  // 立方体を少しずつ回転させる
  cube.rotation.x += 0.01; // X軸(横軸)周りに回転
  cube.rotation.y += 0.01; // Y軸(縦軸)周りに回転

  // レンダラー(映写機)に、現在のシーン(舞台)とカメラ(視点)を使って描画するように命令する
  renderer.render(scene, camera);
}

// 最初にアニメーション関数を呼び出して、アニメーションを開始する
animate();

このコードを追加すると、立方体がX軸とY軸を中心にくるくると回転し始めます! += 0.01 の数字を大きくすると回転が速くなり、小さくすると遅くなります。

6. 画面サイズが変わっても大丈夫!

最後に、ブラウザのウィンドウサイズを変更したときに、表示が崩れないように調整しましょう。ウィンドウサイズが変わったら、カメラが見る範囲(アスペクト比)と、レンダラーが描画するサイズを更新する必要があります。

// ウィンドウサイズが変更されたときに実行される処理
window.addEventListener('resize', () => {
  // カメラのアスペクト比を、現在のウィンドウサイズに合わせて更新
  camera.aspect = window.innerWidth / window.innerHeight;
  // カメラの変更を適用するために必要な命令
  camera.updateProjectionMatrix();

  // レンダラーのサイズも、現在のウィンドウサイズに合わせて更新
  renderer.setSize(window.innerWidth, window.innerHeight);
});

これで、ブラウザのウィンドウを大きくしたり小さくしたりしても、3D表示が常にウィンドウサイズにフィットするようになります。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Three.js Textured Cube Sample</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      font-family: sans-serif;
      background-color: #222;
      color: #fff;
    }
    #container {
      width: 100vw;
      height: 100vh;
    }
  </style>
</head>
<body>
  <div id="container"></div>
  <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.141.0/build/three.module.js"
      }
    }
  </script>
  <script type="module">
    import * as THREE from 'three';

    // コンテナ取得
    const container = document.getElementById('container');

    // シーン、カメラ、レンダラーの作成
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x333333);
    
    const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
    camera.position.z = 5;

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(container.clientWidth, container.clientHeight);
    container.appendChild(renderer.domElement);

    // テクスチャ読み込み
    const textureLoader = new THREE.TextureLoader();
    // サンプルテクスチャ(three.js のサンプル画像)
    const texture = textureLoader.load('https://threejs.org/examples/textures/crate.gif');

    // 立方体ジオメトリとマテリアルを作成
    const geometry = new THREE.BoxGeometry(2, 2, 2);
    const material = new THREE.MeshPhongMaterial({ map: texture });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    // ライトの追加
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);
    
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(5, 10, 7.5);
    scene.add(directionalLight);

    // アニメーションループ
    function animate() {
      requestAnimationFrame(animate);
      // 立方体をゆっくり回転
      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;
      renderer.render(scene, camera);
    }
    animate();

    // ウィンドウリサイズ対応
    window.addEventListener('resize', () => {
      camera.aspect = container.clientWidth / container.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(container.clientWidth, container.clientHeight);
    });
  </script>
</body>
</html>

まとめ

お疲れ様でした!この記事では、Three.jsを使って以下のことを学びました。

  • Three.jsを使うための基本設定
  • シーン、カメラ、レンダラーの準備
  • 立方体の作り方と、テクスチャ(画像)の貼り方
  • ライトを当ててリアルに見せる方法
  • requestAnimationFrame を使ったアニメーションの実装
  • ウィンドウサイズ変更への対応

これで、ウェブページにテクスチャ付きの回転する立方体を表示させることができましたね!

ここからさらに、違う形のオブジェクトを使ってみたり、別のテクスチャ画像に変えてみたり、ライトの色や位置を変えてみたり… 色々試して、Three.jsの世界を探求してみてください!