236 lines
6.9 KiB
JavaScript
236 lines
6.9 KiB
JavaScript
// 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
|
|
};
|
|
})();
|