goheung/app/static/js/terrain3d.js
2026-02-02 19:07:53 +09:00

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
};
})();