// 3D 지형 시각화 모듈 var Terrain3D = (function() { var scene, camera, renderer, terrain, water; var isInitialized = false; var animationId = null; // 지형 초기화 function init(containerId) { var container = document.getElementById(containerId); if (!container) return; // 이미 초기화된 경우 리사이즈만 처리 if (isInitialized && renderer) { onWindowResize(container); return; } // Scene 생성 scene = new THREE.Scene(); scene.background = new THREE.Color(0x1a1a2e); // 카메라 설정 var width = container.clientWidth; var height = container.clientHeight; camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000); camera.position.set(0, 40, 60); camera.lookAt(0, 0, 0); // 렌더러 설정 renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(width, height); renderer.setPixelRatio(window.devicePixelRatio); container.appendChild(renderer.domElement); // 조명 추가 var ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); var directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(50, 100, 50); scene.add(directionalLight); // 지형 생성 createTerrain(); // 물 표면 생성 createWater(); // 마우스 컨트롤 setupControls(container); // 리사이즈 이벤트 window.addEventListener('resize', function() { onWindowResize(container); }); isInitialized = true; // 애니메이션 시작 animate(); } // 지형 생성 function createTerrain() { var geometry = new THREE.PlaneGeometry(80, 80, 64, 64); geometry.rotateX(-Math.PI / 2); // 높이 데이터 생성 (Perlin noise 시뮬레이션) var vertices = geometry.attributes.position.array; for (var i = 0; i < vertices.length; i += 3) { var x = vertices[i]; var z = vertices[i + 2]; // 여러 주파수의 노이즈 조합 var height = 0; height += Math.sin(x * 0.1) * Math.cos(z * 0.1) * 8; height += Math.sin(x * 0.2 + 1) * Math.cos(z * 0.15) * 4; height += Math.sin(x * 0.05) * Math.cos(z * 0.08) * 6; // 중앙에 분지(물이 고일 곳) 생성 var distFromCenter = Math.sqrt(x * x + z * z); if (distFromCenter < 15) { height = Math.min(height, -3 + distFromCenter * 0.2); } vertices[i + 1] = height; } geometry.computeVertexNormals(); // 높이에 따른 색상 적용 var colors = []; for (var i = 0; i < vertices.length; i += 3) { var height = vertices[i + 1]; var color = getColorForHeight(height); colors.push(color.r, color.g, color.b); } geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); var material = new THREE.MeshLambertMaterial({ vertexColors: true, side: THREE.DoubleSide }); terrain = new THREE.Mesh(geometry, material); scene.add(terrain); } // 높이에 따른 색상 반환 function getColorForHeight(height) { var color = new THREE.Color(); if (height < -2) { // 낮은 지역 - 심각 (빨강) color.setRGB(0.91, 0.30, 0.24); } else if (height < 2) { // 중간 낮음 - 높음 (주황) color.setRGB(0.90, 0.49, 0.13); } else if (height < 6) { // 중간 - 보통 (노랑) color.setRGB(0.95, 0.77, 0.06); } else { // 높은 지역 - 낮음 (초록) color.setRGB(0.15, 0.68, 0.38); } return color; } // 물 표면 생성 function createWater() { var waterGeometry = new THREE.CircleGeometry(12, 32); waterGeometry.rotateX(-Math.PI / 2); var waterMaterial = new THREE.MeshLambertMaterial({ color: 0x3498db, transparent: true, opacity: 0.7 }); water = new THREE.Mesh(waterGeometry, waterMaterial); water.position.y = -2; scene.add(water); } // 마우스 컨트롤 설정 function setupControls(container) { var isDragging = false; var previousMousePosition = { x: 0, y: 0 }; var rotationSpeed = 0.005; var targetRotationY = 0; container.addEventListener('mousedown', function(e) { isDragging = true; previousMousePosition = { x: e.clientX, y: e.clientY }; }); container.addEventListener('mousemove', function(e) { if (!isDragging) return; var deltaX = e.clientX - previousMousePosition.x; targetRotationY += deltaX * rotationSpeed; previousMousePosition = { x: e.clientX, y: e.clientY }; }); container.addEventListener('mouseup', function() { isDragging = false; }); container.addEventListener('mouseleave', function() { isDragging = false; }); // 마우스 휠 줌 container.addEventListener('wheel', function(e) { e.preventDefault(); var zoomSpeed = 0.1; camera.position.z += e.deltaY * zoomSpeed; camera.position.z = Math.max(30, Math.min(100, camera.position.z)); camera.position.y = camera.position.z * 0.67; }); // 애니메이션에서 회전 적용 setInterval(function() { if (terrain) { terrain.rotation.y += (targetRotationY - terrain.rotation.y) * 0.1; if (water) water.rotation.y = terrain.rotation.y; } }, 16); } // 윈도우 리사이즈 처리 function onWindowResize(container) { if (!camera || !renderer) return; var width = container.clientWidth; var height = container.clientHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); } // 애니메이션 루프 function animate() { animationId = requestAnimationFrame(animate); // 물 애니메이션 if (water) { water.position.y = -2 + Math.sin(Date.now() * 0.001) * 0.1; } if (renderer && scene && camera) { renderer.render(scene, camera); } } // 정리 function dispose() { if (animationId) { cancelAnimationFrame(animationId); } if (renderer) { renderer.dispose(); } isInitialized = false; } return { init: init, dispose: dispose }; })();