はじめに
初投稿!チームラボが作り出す空間って素敵ですよね!
せっかくなら新しい技術に触れて表現したいと思いました!私の好きはこれです😁
是非「いいね」お願いします。励みになります。
私の完成品はこちら!
開発工程
実際の植物を観察
- 実際に植物園に行き、写真撮影
- 好きな花を20種、草を8種類ピックアップ
まずは簡単に作る。
基本のセットアップ
// 必要なモジュールをインポート
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);
うっすらしか見えてないけど、改良していこう!
上下左右を植物で囲む
植物の配置をランダムにして地面作成.
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);
}}}}
現状はこんな感じ
絵を見てるとの同じで、少し残念...
ユーザー操作を実装
ここからは、ユーザーの動きに合わせて画面を変化させます。
画像を大きくすることで、植物が成長させる。
前回作った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);
});
今回はここまで!!
問題点
次回は、これらを解決します!
- 描写した画像が配列に残り続けるため、時間が経つごとに重い
- 最初の画像読み込みに時間がかかる