본문 바로가기
3Js

마우스 충돌 1

by 영감은어디에 2025. 3. 12.

<!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>