WebGL技術を活用し、Three.jsライブラリを用いてインタラクティブな3D倉庫管理システムを構築する手法について解説する。本記事では、倉庫の3Dモデル作成からパフォーマンス最適化までの実装プロセスを説明する。
開発環境のセットアップ
まず、HTMLファイルに必要なライブラリを導入する。Three.js本体に加え、モデル読み込み、アニメーション制御、統計情報表示などのユーティリティライブラリを準備する。
<script src="libs/three.min.js"></script>
<script src="libs/OrbitControls.js"></script>
<script src="libs/OBJLoader.js"></script>
<script src="libs/stats.min.js"></script>
シーンの初期化
3D描画を行うためのキャンバス要素とシーンを構築する。レンダラー、カメラ、ライトなどの基本コンポーネントを設定する。
<div id="viewport-container"></div>
class WarehouseBuilder {
constructor(containerId) {
this.scene = new THREE.Scene();
this.camera = null;
this.renderer = null;
this.controls = null;
this.initRenderer(containerId);
this.initCamera();
this.initLights();
this.initControls();
}
initRenderer(containerId) {
const container = document.getElementById(containerId);
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setClearColor(0x1a2a3a, 1);
container.appendChild(this.renderer.domElement);
}
initCamera() {
const aspect = window.innerWidth / window.innerHeight;
this.camera = new THREE.PerspectiveCamera(60, aspect, 1, 5000);
this.camera.position.set(0, 300, 800);
}
initLights() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(100, 200, 100);
this.scene.add(directionalLight);
}
initControls() {
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
}
start() {
this.animate();
}
animate() {
requestAnimationFrame(() => this.animate());
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
倉庫構造のモデリング
床面の作成
倉庫の床面を作成する。緑色の床面に黄色の区画線を組み合わせることで、リアルな床表現を実現する。
createFloor() {
// メインの床面
const floorGeometry = new THREE.BoxGeometry(2000, 10, 1500);
const floorMaterial = new THREE.MeshPhongMaterial({
color: 0x175A25,
side: THREE.DoubleSide
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.position.set(0, 0, 0);
this.scene.add(floor);
// 区画線の追加
this.createFloorMarkings();
}
createFloorMarkings() {
const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xFFCC00 });
const positions = [
{ x: 0, z: -500 },
{ x: 0, z: 0 },
{ x: 0, z: 500 },
{ x: -600, z: 0 },
{ x: 600, z: 0 }
];
positions.forEach(pos => {
const lineGeometry = new THREE.BoxGeometry(3, 2, 40);
const line = new THREE.Mesh(lineGeometry, lineMaterial);
line.position.set(pos.x, 6, pos.z);
this.scene.add(line);
});
}
壁面の作成
壁面を作成し、窓と扉の開口部を設ける。CSG(Constructive Solid Geometry)演算を用いて、壁から窓や扉の部分を削除する。
createWalls() {
const wallConfig = {
width: 2000,
height: 200,
depth: 10
};
// 背面の壁(窓と扉の開口部付き)
this.createWallWithOpenings(wallConfig, { x: 0, y: 100, z: -755 });
// 側面の壁
this.createSolidWall({ x: -1000, y: 100, z: 0, rotationY: Math.PI / 2 });
this.createSolidWall({ x: 1000, y: 100, z: 0, rotationY: Math.PI / 2 });
}
createWallWithOpenings(config, position) {
const wallGroup = new THREE.Group();
// 壁のベースメッシュ
const wallShape = new THREE.Shape();
wallShape.moveTo(-config.width / 2, 0);
wallShape.lineTo(config.width / 2, 0);
wallShape.lineTo(config.width / 2, config.height);
wallShape.lineTo(-config.width / 2, config.height);
wallShape.lineTo(-config.width / 2, 0);
// 開口部の定義(扉と窓)
const openings = [
{ x: -500, y: 100, width: 220, height: 200 }, // 左扉
{ x: 500, y: 100, width: 220, height: 200 }, // 右扉
{ x: -200, y: 80, width: 160, height: 120 }, // 窓1
{ x: 0, y: 80, width: 160, height: 120 }, // 窓2
{ x: 200, y: 80, width: 160, height: 120 }, // 窓3
{ x: -800, y: 80, width: 160, height: 120 }, // 窓4
{ x: 800, y: 80, width: 160, height: 120 } // 窓5
];
openings.forEach(opening => {
const hole = new THREE.Path();
hole.moveTo(opening.x - opening.width / 2, opening.y - opening.height / 2);
hole.lineTo(opening.x + opening.width / 2, opening.y - opening.height / 2);
hole.lineTo(opening.x + opening.width / 2, opening.y + opening.height / 2);
hole.lineTo(opening.x - opening.width / 2, opening.y + opening.height / 2);
wallShape.holes.push(hole);
});
const extrudeSettings = { depth: config.depth, bevelEnabled: false };
const wallGeometry = new THREE.ExtrudeGeometry(wallShape, extrudeSettings);
const wallMaterial = new THREE.MeshPhongMaterial({ color: 0xDDDDDD });
const wallMesh = new THREE.Mesh(wallGeometry, wallMaterial);
wallGroup.add(wallMesh);
wallGroup.position.set(position.x, position.y, position.z);
this.scene.add(wallGroup);
// 窓枠とドアフレームの追加
this.addWindowFrames(openings, position);
this.addDoorFrames(openings.slice(0, 2), position);
}
詳細パーツの追加
ドア、窓枠、観葉植物などの装飾要素を追加して、リアルな倉庫の外観を演出する。
addDoorFrames(doors, wallPosition) {
doors.forEach(door => {
// ドアフレーム
const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x8B4513 });
// 縦枠
const frameWidth = 15;
const frameHeight = 190;
const leftFrame = new THREE.Mesh(
new THREE.BoxGeometry(frameWidth, frameHeight, 15),
frameMaterial
);
leftFrame.position.set(door.x - door.width / 2 + frameWidth / 2, 100, wallPosition.z);
this.scene.add(leftFrame);
const rightFrame = new THREE.Mesh(
new THREE.BoxGeometry(frameWidth, frameHeight, 15),
frameMaterial
);
rightFrame.position.set(door.x + door.width / 2 - frameWidth / 2, 100, wallPosition.z);
this.scene.add(rightFrame);
// ドア本体
const doorMaterial = new THREE.MeshPhongMaterial({ color: 0x515151 });
const doorLeft = new THREE.Mesh(
new THREE.BoxGeometry(door.width / 2 - 8, frameHeight - 10, 4),
doorMaterial
);
doorLeft.position.set(door.x - door.width / 4 - 2, 95, wallPosition.z - 5);
doorLeft.userData = { type: 'door', side: 'left' };
this.scene.add(doorLeft);
const doorRight = new THREE.Mesh(
new THREE.BoxGeometry(door.width / 2 - 8, frameHeight - 10, 4),
doorMaterial
);
doorRight.position.set(door.x + door.width / 4 + 2, 95, wallPosition.z - 5);
doorRight.userData = { type: 'door', side: 'right' };
this.scene.add(doorRight);
});
}
addDecorativePlants() {
const plantPositions = [
{ x: -105, z: -735 },
{ x: -679, z: -735 },
{ x: 657, z: -735 },
{ x: -700, z: -735 },
{ x: 680, z: -735 }
];
plantPositions.forEach(pos => {
// 植木鉢
const potGeometry = new THREE.CylinderGeometry(16, 8, 40, 8);
const potMaterial = new THREE.MeshPhongMaterial({ color: 0xFFFFFF });
const pot = new THREE.Mesh(potGeometry, potMaterial);
pot.position.set(pos.x, 20, pos.z);
this.scene.add(pot);
// 植物(簡易的な球体で表現)
const plantGeometry = new THREE.SphereGeometry(20, 8, 6);
const plantMaterial = new THREE.MeshPhongMaterial({ color: 0x228B22 });
const plant = new THREE.Mesh(plantGeometry, plantMaterial);
plant.position.set(pos.x, 50, pos.z);
this.scene.add(plant);
});
}
棚の配置
倉庫内に複数の棚を配置する。ループ処理を用いて効率的にオブジェクトを生成する。
createShelves(count) {
const shelfGroup = new THREE.Group();
for (let i = 1; i <= count; i++) {
const shelf = this.createSingleShelf();
// 棚の位置を計算(グリッド配置)
const row = Math.floor((i - 1) / 6);
const col = (i - 1) % 6;
shelf.position.set(
-600 + col * 250,
100,
-400 + row * 200
);
shelf.name = `shelf_${i}`;
shelfGroup.add(shelf);
}
this.scene.add(shelfGroup);
return shelfGroup;
}
createSingleShelf() {
const group = new THREE.Group();
const frameColor = 0x4A90D9;
const frameMaterial = new THREE.MeshPhongMaterial({ color: frameColor });
// 棚のフレーム(4本の支柱)
const pillarHeight = 150;
const pillarGeometry = new THREE.BoxGeometry(5, pillarHeight, 5);
[[-40, -30], [-40, 30], [40, -30], [40, 30]].forEach(([x, z]) => {
const pillar = new THREE.Mesh(pillarGeometry, frameMaterial);
pillar.position.set(x, pillarHeight / 2, z);
group.add(pillar);
});
// 棚板(3段)
const shelfBoardGeometry = new THREE.BoxGeometry(85, 3, 65);
const shelfBoardMaterial = new THREE.MeshPhongMaterial({ color: 0xCCCCCC });
[20, 75, 130].forEach(y => {
const board = new THREE.Mesh(shelfBoardGeometry, shelfBoardMaterial);
board.position.set(0, y, 0);
group.add(board);
});
return group;
}
パフォーマンス最適化
多数のオブジェクトを配置すると描画パフォーマンスが低下する。これを解決するために、ジオメトリのマージ(統合)を行う。
optimizeScene(shelfGroup) {
// 統計情報の確認
console.log(`最適化前のドローコール: ${this.renderer.info.render.calls}`);
console.log(`最適化前の三角形数: ${this.renderer.info.render.triangles}`);
// 同一マテリアルを持つメッシュを統合
const geometriesByMaterial = new Map();
shelfGroup.children.forEach(shelf => {
shelf.traverse(child => {
if (child.isMesh) {
const materialId = child.material.uuid;
if (!geometriesByMaterial.has(materialId)) {
geometriesByMaterial.set(materialId, {
material: child.material,
geometries: []
});
}
// ワールド座標に変換
child.updateMatrixWorld();
const geometry = child.geometry.clone();
geometry.applyMatrix(child.matrixWorld);
geometriesByMaterial.get(materialId).geometries.push(geometry);
}
});
});
// 統合されたメッシュを作成
shelfGroup.clear();
geometriesByMaterial.forEach(data => {
const mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries(
data.geometries
);
const mergedMesh = new THREE.Mesh(mergedGeometry, data.material);
shelfGroup.add(mergedMesh);
});
console.log(`最適化後のドローコール: ${this.renderer.info.render.calls}`);
console.log(`最適化後の三角形数: ${this.renderer.info.render.triangles}`);
}
この最適化により、36個の棚を配置した状態でも、フレームレートを60FPSに維持することができる。
イベントハンドリング
ドアの開閉など、ユーザーインタラクションを実装する。
setupInteraction() {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
this.renderer.domElement.addEventListener('dblclick', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, this.camera);
const intersects = raycaster.intersectObjects(this.scene.children, true);
if (intersects.length > 0) {
const clickedObject = intersects[0].object;
if (clickedObject.userData.type === 'door') {
this.toggleDoor(clickedObject);
}
}
});
}
toggleDoor(doorMesh) {
const animationDuration = 1000;
const targetRotation = doorMesh.userData.isOpen ? 0 : Math.PI / 2;
const pivotPoint = doorMesh.userData.side === 'left'
? doorMesh.position.x - 50
: doorMesh.position.x + 50;
const startRotation = doorMesh.rotation.y;
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / animationDuration, 1);
const easeProgress = 1 - Math.pow(1 - progress, 3);
doorMesh.rotation.y = startRotation + (targetRotation - startRotation) * easeProgress;
if (progress < 1) {
requestAnimationFrame(animate);
} else {
doorMesh.userData.isOpen = !doorMesh.userData.isOpen;
}
};
animate();
}
実行例
これらのコンポーネントを統合し、メインの処理フローを構築する。
// メイン処理
const builder = new WarehouseBuilder('viewport-container');
// 倉庫構造の作成
builder.createFloor();
builder.createWalls();
builder.addDecorativePlants();
// 棚の配置
const shelves = builder.createShelves(36);
builder.optimizeScene(shelves);
// インタラクション設定
builder.setupInteraction();
// レンダリング開始
builder.start();
本記事では、Three.jsを用いた3D倉庫の基本的な構築手法を紹介した。次回は、棚への商品配置、在庫管理機能、温湿度センサー表示、防火設備の可視化など、より高度な機能の実装について解説する。