Three.jsによる3D倉庫可視化システムの構築

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倉庫の基本的な構築手法を紹介した。次回は、棚への商品配置、在庫管理機能、温湿度センサー表示、防火設備の可視化など、より高度な機能の実装について解説する。

タグ: Three.js WebGL 3Dモデリング javascript 3D可視化

5月14日 21:05 投稿