// Three.jsを使用した3Dマップの実装 let scene, camera, renderer, buildings = {}, residents = {}; let controls; function init() { // シーンの作成 scene = new THREE.Scene(); scene.background = new THREE.Color(0x87CEEB); // 空色の背景 // カメラの設定 camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(50, 50, 50); camera.lookAt(0, 0, 0); // レンダラーの設定 renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; document.getElementById('three-container').appendChild(renderer.domElement); // 光源の追加 const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(50, 50, 50); directionalLight.castShadow = true; scene.add(directionalLight); // 地面の作成 const groundGeometry = new THREE.PlaneGeometry(100, 100); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x90EE90, side: THREE.DoubleSide }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground); // カメラコントロールの追加 controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; // アニメーションループの開始 animate(); // ウィンドウリサイズのハンドリング window.addEventListener('resize', onWindowResize, false); } function createBuilding(name, position, type) { let geometry, material, color; // 建物の種類に応じて形状と色を設定 switch(type) { case '住宅': geometry = new THREE.BoxGeometry(8, 6, 8); color = 0xFFB6C1; // ピンク系 break; case '公園': geometry = new THREE.CylinderGeometry(5, 5, 2, 32); color = 0x228B22; // 森の緑 break; case '図書館': geometry = new THREE.BoxGeometry(12, 8, 12); color = 0x8B4513; // 茶色 break; case 'カフェ': geometry = new THREE.BoxGeometry(10, 6, 10); color = 0xDEB887; // ベージュ break; case '商店': geometry = new THREE.BoxGeometry(10, 5, 10); color = 0x4682B4; // スチールブルー break; default: geometry = new THREE.BoxGeometry(10, 10, 10); color = 0x808080; } material = new THREE.MeshStandardMaterial({ color: color, roughness: 0.7, metalness: 0.2 }); const building = new THREE.Mesh(geometry, material); building.position.set(position.x, geometry.parameters.height / 2, position.z); building.castShadow = true; building.receiveShadow = true; scene.add(building); buildings[name] = building; // 建物名のラベル const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = 256; canvas.height = 64; context.fillStyle = '#ffffff'; context.font = '24px Arial'; context.fillText(name, 10, 32); const texture = new THREE.CanvasTexture(canvas); const labelMaterial = new THREE.SpriteMaterial({ map: texture }); const label = new THREE.Sprite(labelMaterial); label.position.set(position.x, geometry.parameters.height + 2, position.z); scene.add(label); } function createResident(name, position, profession) { let geometry, material, color; // 職業に応じて色を設定 switch(profession) { case '教師': color = 0x0000FF; // 青 break; case '看護師': color = 0xFF0000; // 赤 break; case 'エンジニア': color = 0x00FF00; // 緑 break; case 'デザイナー': color = 0xFF00FF; // マゼンタ break; case '商店主': color = 0xFFA500; // オレンジ break; default: color = 0xFFFFFF; // 白 } // 頭部 const headGeometry = new THREE.SphereGeometry(1, 32, 32); const headMaterial = new THREE.MeshStandardMaterial({ color: 0xFFE4C4 }); const head = new THREE.Mesh(headGeometry, headMaterial); head.position.y = 2; head.castShadow = true; // 胴体 const bodyGeometry = new THREE.CylinderGeometry(0.7, 0.7, 2, 32); const bodyMaterial = new THREE.MeshStandardMaterial({ color: color }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.position.y = 0.5; body.castShadow = true; // グループ化 const resident = new THREE.Group(); resident.add(head); resident.add(body); resident.position.set(position.x, 0, position.z); scene.add(resident); residents[name] = resident; // 住民名のラベル const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = 256; canvas.height = 64; context.fillStyle = '#ffffff'; context.font = '24px Arial'; context.fillText(name, 10, 32); const texture = new THREE.CanvasTexture(canvas); const labelMaterial = new THREE.SpriteMaterial({ map: texture }); const label = new THREE.Sprite(labelMaterial); label.position.set(position.x, 4, position.z); scene.add(label); } function updatePositions(state) { // 建物の位置を更新 state.locations.forEach(location => { if (!buildings[location.name]) { const type = location.name.includes('住宅') ? '住宅' : location.name.includes('公園') ? '公園' : location.name.includes('図書館') ? '図書館' : location.name.includes('カフェ') ? 'カフェ' : location.name.includes('商店') ? '商店' : 'その他'; createBuilding(location.name, location.position, type); } }); // 住民の位置を更新 state.residents.forEach(resident => { if (!residents[resident.name]) { createResident(resident.name, resident.position, resident.profession); } else { // 現在位置から目標位置への移動をアニメーション const currentPos = residents[resident.name].position; const targetPos = new THREE.Vector3( resident.position.x, 0, resident.position.z ); // 移動速度 const speed = 0.1; // 現在位置から目標位置への方向ベクトル const direction = targetPos.sub(currentPos).normalize(); // 移動 currentPos.add(direction.multiplyScalar(speed)); // 住民の向きを移動方向に合わせる if (direction.length() > 0) { residents[resident.name].lookAt(targetPos); } } }); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } // 初期化 init();