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