<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js - 5초마다 움직이는 구</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
body {
margin: 0;
width: 100%;
background: radial-gradient(#15094d, #000115);
min-height: 100vh;
}
canvas {
display: block;
width: 100%;
height: 100vh;
top: 0;
left: 0;
position: fixed;
z-index: 10;
background: transparent;
}
</style>
</head>
<body>
<canvas id="moonBox"></canvas>
<script>
const scene = new THREE.Scene();
const canvas = document.querySelector('#moonBox');
// 카메라 왜곡 줄이기 - FOV를 낮추고 거리를 조정
const camera = new THREE.PerspectiveCamera(
50,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 100;
const renderer = new THREE.WebGLRenderer(
{alpha: true, antialias: true, canvas}
);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0);
// 달 텍스처 로드
const textureLoader = new THREE.TextureLoader();
const moonTexture = textureLoader.load(
'Moon_002_basecolor.png',
function (texture) {
// 텍스처 로드 완료 후 구체들 생성
createSpheres();
}
);
const spheres = [];
const sphereSpeed = [];
const originalSizes = []; // 원래 크기 저장을 위한 배열 추가
const radiusMin = 4;
const radiusMax = 12;
const boundary = {
x: window.innerWidth / 20,
y: window.innerHeight / 20
};
let frameCount = 0;
let lastActionTime = 0; // 마지막으로 액션이 발생한 시간
// 크기와 속도 감소 관련 변수
const shrinkFactor = 0.998; // 각 프레임마다 크기가 줄어드는 비율
const minSize = 0.5; // 최소 크기
const initialSizeFactor = 1.0; // 초기 크기 비율
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function createSpheres() {
for (let i = 0; i < 15; i++) {
const radius = Math.random() * (radiusMax - radiusMin) + radiusMin;
originalSizes.push(radius); // 원래 크기 저장
const geometry = new THREE.SphereGeometry(radius, 32, 32);
// 달 텍스처를 적용한 재질
const material = new THREE.MeshStandardMaterial(
{map: moonTexture, roughness: 1, metalness: 0.2}
);
const sphere = new THREE.Mesh(geometry, material);
sphere
.position
.set(
(Math.random() - 0.5) * boundary.x * 2,
(Math.random() - 0.5) * boundary.y * 2,
(Math.random() - 0.5) * 5 // z축에도 약간의 깊이 추가
);
// 각 구체에 약간의 회전 추가
sphere.rotation.x = Math.random() * Math.PI;
sphere.rotation.y = Math.random() * Math.PI;
// 구체에 현재 크기 비율을 추적하는 속성 추가
sphere.userData.sizeFactor = initialSizeFactor;
scene.add(sphere);
spheres.push(sphere);
sphereSpeed.push(new THREE.Vector3(0, 0, 0)); // 초기 속도는 0
}
// 초기 랜덤 움직임 트리거
triggerRandomMovement();
// 애니메이션 시작
animate();
}
// 조명 설정 개선
const ambientLight = new THREE.AmbientLight(0x404040, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight
.position
.set(5, 10, 7)
.normalize();
scene.add(directionalLight);
// 약한 후면 조명 추가
const backLight = new THREE.DirectionalLight(0x404040, 0.6);
backLight
.position
.set(-5, -10, -7)
.normalize();
scene.add(backLight);
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
// 창 크기 변경 시 대응
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
boundary.x = window.innerWidth / 20;
boundary.y = window.innerHeight / 20;
});
// 5초마다 랜덤한 움직임 트리거
function triggerRandomMovement() {
spheres.forEach((sphere, i) => {
// 랜덤 속도 부여
sphereSpeed[i].x = (Math.random() - 0.5) * 1.0;
sphereSpeed[i].y = (Math.random() - 0.5) * 1.0;
sphereSpeed[i].z = (Math.random() - 0.5) * 0.3;
// 랜덤 회전 부여 sphere.rotation.x += Math.random() * Math.PI - Math.PI/2;
// sphere.rotation.y += Math.random() * Math.PI - Math.PI/2;
});
lastActionTime = Date.now();
}
function checkCollisions() {
for (let i = 0; i < spheres.length; i++) {
for (let j = i + 1; j < spheres.length; j++) {
let sphereA = spheres[i];
let sphereB = spheres[j];
let speedA = sphereSpeed[i];
let speedB = sphereSpeed[j];
// 현재 반지름 계산
let radiusA = originalSizes[i] * sphereA.userData.sizeFactor;
let radiusB = originalSizes[j] * sphereB.userData.sizeFactor;
let dx = sphereB.position.x - sphereA.position.x;
let dy = sphereB.position.y - sphereA.position.y;
let dz = sphereB.position.z - sphereA.position.z;
let distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
let minDist = radiusA + radiusB;
if (distance < minDist) {
let angle = Math.atan2(dy, dx);
let angleZ = Math.atan2(Math.sqrt(dx * dx + dy * dy), dz);
let overlap = minDist - distance;
// 충돌 해결 - 위치 조정
sphereA.position.x -= Math.cos(angle) * Math.sin(angleZ) * (overlap / 2);
sphereA.position.y -= Math.sin(angle) * Math.sin(angleZ) * (overlap / 2);
sphereA.position.z -= Math.cos(angleZ) * (overlap / 2);
sphereB.position.x += Math.cos(angle) * Math.sin(angleZ) * (overlap / 2);
sphereB.position.y += Math.sin(angle) * Math.sin(angleZ) * (overlap / 2);
sphereB.position.z += Math.cos(angleZ) * (overlap / 2);
// 충돌 해결 - 속도 조정
let speedAProj = speedA.x * Math.cos(angle) * Math.sin(angleZ) + speedA.y * Math.sin(
angle
) * Math.sin(angleZ) + speedA.z * Math.cos(angleZ);
let speedBProj = speedB.x * Math.cos(angle) * Math.sin(angleZ) + speedB.y * Math.sin(
angle
) * Math.sin(angleZ) + speedB.z * Math.cos(angleZ);
let newSpeedAProj = speedBProj;
let newSpeedBProj = speedAProj;
speedA.x += (newSpeedAProj - speedAProj) * Math.cos(angle) * Math.sin(angleZ);
speedA.y += (newSpeedAProj - speedAProj) * Math.sin(angle) * Math.sin(angleZ);
speedA.z += (newSpeedAProj - speedAProj) * Math.cos(angleZ);
speedB.x += (newSpeedBProj - speedBProj) * Math.cos(angle) * Math.sin(angleZ);
speedB.y += (newSpeedBProj - speedBProj) * Math.sin(angle) * Math.sin(angleZ);
speedB.z += (newSpeedBProj - speedBProj) * Math.cos(angleZ);
}
}
}
}
function checkBounds(sphere, speed, radius, index) {
if (sphere.position.x - radius < -boundary.x || sphere.position.x + radius > boundary.x) {
speed.x *= -0.9; // 약간의 에너지 손실
}
if (sphere.position.y - radius < -boundary.y || sphere.position.y + radius > boundary.y) {
speed.y *= -0.9; // 약간의 에너지 손실
}
if (sphere.position.z - radius < -20 || sphere.position.z + radius > 20) {
speed.z *= -0.9; // 약간의 에너지 손실
}
}
function animate() {
requestAnimationFrame(animate);
frameCount++;
// 현재 시간 체크
const currentTime = Date.now();
// 5초마다 랜덤 움직임 트리거
if (currentTime - lastActionTime > 5000) {
triggerRandomMovement();
}
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(spheres);
spheres.forEach((sphere, i) => {
// 마우스와 교차하는 객체 체크
if (intersects.some(intersect => intersect.object === sphere)) {
let dx = sphere.position.x - mouse.x * boundary.x;
let dy = sphere.position.y - mouse.y * boundary.y;
let escapeAngle = Math.atan2(dy, dx);
sphereSpeed[i].x += Math.cos(escapeAngle) * 0.5;
sphereSpeed[i].y += Math.sin(escapeAngle) * 0.5;
}
// 속도 감쇠
sphereSpeed[i].multiplyScalar(0.98);
// 현재 반지름 계산
const currentRadius = originalSizes[i] * sphere.userData.sizeFactor;
// 경계 체크
checkBounds(sphere, sphereSpeed[i], currentRadius, i);
// 위치 업데이트
sphere
.position
.add(sphereSpeed[i]);
});
checkCollisions();
renderer.render(scene, camera);
}
// 반응형 처리
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onWindowResize);
</script>
</body>
</html>