2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptAdvent Calendar 2024

Day 18

Three.jsで、チームラボを再現【基本〜ユーザ操作】完成品とソースコード公開

Last updated at Posted at 2024-12-18

はじめに

初投稿!チームラボが作り出す空間って素敵ですよね!
せっかくなら新しい技術に触れて表現したいと思いました!私の好きはこれです😁
是非「いいね」お願いします。励みになります。

私の完成品はこちら!

開発工程

実際の植物を観察

  1. 実際に植物園に行き、写真撮影
  2. 好きな花を20種、草を8種類ピックアップ

image.png

image.png

まずは簡単に作る。

基本のセットアップ

// 必要なモジュールをインポート
import * as THREE from 'three';

// シーン、カメラ、レンダラーを初期化
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
    75, // 視野角 (FOV)
    window.innerWidth / window.innerHeight, // アスペクト比
    0.1, // 近距離クリップ
    1000 // 遠距離クリップ
);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight); // レンダラーのサイズを設定
document.body.appendChild(renderer.domElement); // レンダラーの DOM を追加

// カメラの初期位置を設定
camera.position.z = 10; // カメラを遠ざける位置

作り方を学ぶ。

const groundGeometry = new THREE.PlaneGeometry(50, 50);
const groundMaterial = new THREE.MeshBasicMaterial({ color: 0x228B22 }); 
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2; // 平面を横に倒す
scene.add(ground);

const loader = new THREE.TextureLoader();

const createPlant = (texturePath, x, y, z) => {
    loader.load(texturePath, (texture) => {
        const planeGeometry = new THREE.PlaneGeometry(1, 1); // サイズは調整可能
        const planeMaterial = new THREE.MeshBasicMaterial({
            map: texture,
            transparent: true, // 透過PNGをサポート
        });
        const plant = new THREE.Mesh(planeGeometry, planeMaterial);
        plant.position.set(x, y, z);
        scene.add(plant);
    });
};

// 複数の草花を配置
createPlant('flower1.png', 0, 0.5, 0);
createPlant('flower2.png', 2, 0.5, -1);
createPlant('flower3.png', -2, 0.5, 1);

image.png
うっすらしか見えてないけど、ここから改良していきます!

うっすらしか見えてないけど、改良していこう!

上下左右を植物で囲む

植物の配置をランダムにして地面作成.

textures.Texturesには、描写する画像のURLが入っている。
座標にランダム要素を入れて、前後(startZ,endZ) に描写範囲を広げる。描写する植物もランダムに選ばれるようにしている。

function createGround(startZ, endZ) {
  // x 軸と z 軸に沿ってスプライトを生成
  for (let x = -40; x < 40; x += 2) {
    for (let z = startZ; z < endZ; z += 5) {
      // ランダムにテクスチャを選択
      const textureUrl = textures.Textures[Math.floor(Math.random() * textures.topTextures.length)];

      // キャッシュからテクスチャを取得
      const cachedTexture = textureCache.get(textureUrl);

      if (cachedTexture) {
        // スプライトマテリアルを作成
        const material = new THREE.SpriteMaterial({ map: cachedTexture });

        // スプライトを作成し、ランダムな位置に配置
        const sprite = new THREE.Sprite(material);
        sprite.position.set(
          x + random_1 + 5, // x 座標にランダムオフセットを追加
          Math.random() * 5 - 20, // y 座標をランダムに設定
          z + random_1 // z 座標にランダムオフセットを追加
        );

        // シーンにスプライトを追加
        scene.add(sprite);
      }
    }
  }
}

アーチ型の天井を作成

x座標を少し工夫。アーチの形に合わせて画像に傾きをつけるためベースroationOffsetを設定しています。

function createTop(startZ, endZ) {
  // x 軸と z 軸に沿ってスプライトを生成
  for (let x = -12; x < 80; x += Math.random() * 5) { // x 軸のランダムな間隔
    for (let z = startZ; z < endZ; z += Math.random() * 5) { // z 軸のランダムな間隔
      // ランダムにテクスチャを選択
      const textureUrl = textures.groundTextures[
        Math.floor(Math.random() * textures.groundTextures.length)
      ];

      // キャッシュからテクスチャを取得
      const cachedTexture = textureCache.get(textureUrl);

      if (cachedTexture) {
        // スプライトマテリアルを作成
        const material = new THREE.SpriteMaterial({ map: cachedTexture });

        // スプライトを作成
        const sprite = new THREE.Sprite(material);

        // スプライトの位置を設定
        sprite.position.set(
          x + random_1 - 35, // x 座標をランダムにオフセット
          10 + 10 * Math.sin((x / 70) * Math.PI) + Math.random() * 5, // sin 関数で高さを調整
          z + random_1 // z 座標をランダムにオフセット
        );

        // スプライトの回転を設定
        const baseRotation = Math.PI - 5.5; // 基本回転角
        const rotationOffset = (Math.random() - 0.5) * Math.PI / 6; // ランダムな回転オフセット
        sprite.material.rotation = baseRotation + rotationOffset;

        // シーンにスプライトを追加
        scene.add(sprite);
      }
    }
  }
}

左右側面の作成

左側を作成。右側も座標を変更すれば作成可能です。let xの幅はいい加減です。

function createGround_left(startZ, endZ) {
    for (let x = - 20; x < -5 ; x += 2) {
        for (let z = startZ; z < endZ; z += 2) {
            const textureUrl = textures.groundTextures4[Math.floor(Math.random() * textures.groundTextures4.length)];
            const cachedTexture = textureCache.get(textureUrl);
            if (cachedTexture) {
                const material = new THREE.SpriteMaterial({ map: cachedTexture });
                const sprite = new THREE.Sprite(material);
                const posX = x + random_1 - 20;
                const posY = - x + Math.random() * 30 - 30;
                const posZ = z + random_1;
                sprite.position.set(posX, posY, posZ);
                sprite.scale.set(scale, scale, 1);
                const maxTilt = - Math.PI / 4 + (posY + 40) / 100;
                const minTilt = - Math.PI / 8 + (posY + 40) / 200;
                sprite.material.rotation = Math.random() * (maxTilt - minTilt) + minTilt + 25 ;
                scene.add(sprite);
}}}}

現状はこんな感じ

絵を見てるとの同じで、少し残念...

image.png

ユーザー操作を実装

ここからは、ユーザーの動きに合わせて画面を変化させます。

画像を大きくすることで、植物が成長させる。
前回作った4方向の植物は、それぞれ配列に格納されている。
大きさの上限を決めて、ランダムな倍率で大きくする。

// 個別の花を成長させる関数
function growFlowers(flowers, maxScale) {
  flowers.forEach(flower => {
    const growth = Math.random() * 0.05 + 0.6; // ランダムな成長率 (0.6 ~ 0.65)
    if (flower.scale.x < maxScale) { // 最大スケールに達するまで成長
      flower.scale.set(
        flower.scale.x + growth, // x 軸のスケールを更新
        flower.scale.y + growth  // y 軸のスケールを更新
      );
    }
  });
}

ユーザー操作を感知して、先ほど作った関数に繋げる。

マウス操作で、カメラの視点を動かす。
はじめの4行で上下(Y)左右(X)の限界を決め、後ろを振り返ることができないようにしている。タッチの開始地点と終了地点を利用。

const maxYRotation = 1.9; 
const minYRotation = -1.9; 
const maxXRotation = Math.PI / 4;
const minXRotation = -Math.PI / 4;

function onTouchMove(event) {
  if (event.touches.length === 1) {
      const touch = event.touches[0];
      
      if (lastTouchX !== null && lastTouchY !== null) {
          const deltaX = (touch.clientX - lastTouchX) * 0.005;
          const deltaY = (touch.clientY - lastTouchY) * 0.005;
          camera.rotation.y += deltaX;
          camera.rotation.y = Math.max(minYRotation, Math.min(maxYRotation, camera.rotation.y));
          camera.rotation.x += deltaY;
          camera.rotation.x = Math.max(minXRotation, Math.min(maxXRotation, camera.rotation.x));
          growAllFlowers();
      }
      lastTouchX = touch.clientX;
      lastTouchY = touch.clientY;
}}
function onTouchEnd() {
  lastTouchX = null;
  lastTouchY = null;
}
document.addEventListener("touchmove", onTouchMove, { passive: false });
document.addEventListener("touchend", onTouchEnd);
document.addEventListener("touchcancel", onTouchEnd);

画面をクリックした時に画像を拡大させる。

window.addEventListener('click', () => growAllFlowers());

マウス移動で視点操作

window.addEventListener("mousemove", (event) => {
    const mouseX = (event.clientX / window.innerWidth) * 2 - 1;
    const mouseY = - (event.clientY / window.innerHeight) * 2 + 1;
    camera.rotation.y = - mouseX * 0.5;
    camera.rotation.x =  mouseY * 0.2;
    growAllFlowers();
});

スクロールした時に、カメラのZ座標を変化させる。手前/奥に移動。

window.addEventListener("wheel", (event) => {
    scrollSpeed -= event.deltaY * 0.05; //慣性を設定。
    scrollSpeed = Math.max(MIN_SCROLL_SPEED, Math.min(MAX_SCROLL_SPEED, scrollSpeed));
    growAllFlowers();
});

(おまけ) 画面のサイズが変更された時に、再計算を行う。

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

今回はここまで!!

問題点

次回は、これらを解決します!

  • 描写した画像が配列に残り続けるため、時間が経つごとに重い
  • 最初の画像読み込みに時間がかかる
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?