Three.jsで太陽系っぽい3Dアプリを開発してみた

# はじめに
Three.jsで太陽系っぽい3Dアプリを開発しました。

[デモ](http://solar-system.wealthy-design.com/)

画面：
![スクリーンショット 2024-11-01 11.36.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682035/70efff95-2df0-258e-fb14-dc3026eed14b.png)

# 有名な衛星
有名な衛星も追加しました。
- 地球の月
- 木星のイオ
- 木星のエウロパ
- 木星のガニメデ
- 木星のカリスト
- 土星のエンケラドゥス

# コード

```html:solarSystem.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Solar System in 3D</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
        #controls { position: absolute; top: 10px; left: 10px; color: white; }
        #slider { width: 200px; }
        label { color: white; }
    </style>
</head>
<body>
    <div id="controls">
        <label for="speed">速度:</label>
        <input type="range" id="speed" name="speed" min="0.1" max="10" value="1" step="0.1">
        <br>
        <button id="camera-up">上</button>
        <button id="camera-down">下</button>
        <button id="camera-in">拡大</button>
        <button id="camera-out">縮小</button>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        // シーンのセットアップ
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // ライトの設定
        const pointLight = new THREE.PointLight(0xffffff, 1, 1000);
        pointLight.position.set(0, 0, 0);
        scene.add(pointLight);

        const ambientLight = new THREE.AmbientLight(0x404040, 1.5);
        scene.add(ambientLight);

        // 惑星作成関数
        function createPlanet(size, color, distance) {
            const geometry = new THREE.SphereGeometry(size, 32, 32);
            const material = new THREE.MeshPhongMaterial({ color: color });
            const planet = new THREE.Mesh(geometry, material);
            planet.userData.distance = distance;
            scene.add(planet);
            return planet;
        }

        // 軌道作成関数
        function createOrbit(distance) {
            const curve = new THREE.EllipseCurve(0, 0, distance, distance, 0, 2 * Math.PI, false, 0);
            const points = curve.getPoints(50);
            const geometry = new THREE.BufferGeometry().setFromPoints(points);
            const material = new THREE.LineBasicMaterial({ color: 0xffffff });
            const ellipse = new THREE.Line(geometry, material);
            ellipse.rotation.x = Math.PI / 2;  // XZ平面に回転
            scene.add(ellipse);
        }

        // 衛星作成関数
        function createMoon(size, color, distance, planet) {
            const geometry = new THREE.SphereGeometry(size, 32, 32);
            const material = new THREE.MeshPhongMaterial({ color: color });
            const moon = new THREE.Mesh(geometry, material);
            moon.userData = { distance: distance, parent: planet };
            scene.add(moon);
            return moon;
        }

        // 太陽と惑星の生成
        const sun = createPlanet(5, 0xffff00, 0);
        const mercury = createPlanet(0.5, 0xaaaaaa, 8);
        createOrbit(8);

        const venus = createPlanet(0.9, 0xffa500, 12);
        createOrbit(12);

        const earth = createPlanet(1, 0x0000ff, 16);
        createOrbit(16);

        const mars = createPlanet(0.7, 0xff4500, 20);
        createOrbit(20);

        const jupiter = createPlanet(2.2, 0xffa07a, 28);
        createOrbit(28);

        const saturn = createPlanet(2, 0xf4a460, 35);
        createOrbit(35);

        // 各惑星の有名な衛星の追加
        const earthMoon = createMoon(0.3, 0x888888, 2, earth);  // 地球の月
        const io = createMoon(0.4, 0xffddaa, 3, jupiter);        // 木星のイオ
        const europa = createMoon(0.35, 0xaaccff, 4, jupiter);   // 木星のエウロパ
        const ganymede = createMoon(0.5, 0xcccccc, 5, jupiter);  // 木星のガニメデ
        const callisto = createMoon(0.45, 0x999999, 6, jupiter); // 木星のカリスト
        const titan = createMoon(0.45, 0xffdd88, 4, saturn);     // 土星のタイタン
        const enceladus = createMoon(0.2, 0xffffff, 2.5, saturn); // 土星のエンケラドゥス

        // カメラの位置設定
        camera.position.set(30, 30, 50);
        camera.lookAt(0, 0, 0);

        // 公転周期の比率 (地球 = 1)
        const orbitSpeeds = {
            mercury: 4.15,  // 約88日
            venus: 1.62,    // 約225日
            earth: 1.0,     // 約365日
            mars: 0.53,     // 約687日
            jupiter: 0.084, // 約12年
            saturn: 0.034,  // 約29年
        };

        // アニメーションループ
        let speedMultiplier = 1;

        function animate() {
            requestAnimationFrame(animate);
            speedMultiplier = document.getElementById("speed").value;

            // 各惑星の公転を周期比率で調整
            mercury.position.x = mercury.userData.distance * Math.cos(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.mercury);
            mercury.position.z = mercury.userData.distance * Math.sin(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.mercury);

            venus.position.x = venus.userData.distance * Math.cos(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.venus);
            venus.position.z = venus.userData.distance * Math.sin(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.venus);

            earth.position.x = earth.userData.distance * Math.cos(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.earth);
            earth.position.z = earth.userData.distance * Math.sin(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.earth);

            mars.position.x = mars.userData.distance * Math.cos(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.mars);
            mars.position.z = mars.userData.distance * Math.sin(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.mars);

            jupiter.position.x = jupiter.userData.distance * Math.cos(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.jupiter);
            jupiter.position.z = jupiter.userData.distance * Math.sin(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.jupiter);

            saturn.position.x = saturn.userData.distance * Math.cos(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.saturn);
            saturn.position.z = saturn.userData.distance * Math.sin(Date.now() * 0.002 * speedMultiplier * orbitSpeeds.saturn);

            // 地球の月の回転
            earthMoon.position.x = earth.position.x + earthMoon.userData.distance * Math.cos(Date.now() * 0.005 * speedMultiplier);
            earthMoon.position.z = earth.position.z + earthMoon.userData.distance * Math.sin(Date.now() * 0.005 * speedMultiplier);

            // 木星の衛星
            io.position.x = jupiter.position.x + io.userData.distance * Math.cos(Date.now() * 0.004 * speedMultiplier);
            io.position.z = jupiter.position.z + io.userData.distance * Math.sin(Date.now() * 0.004 * speedMultiplier);

            europa.position.x = jupiter.position.x + europa.userData.distance * Math.cos(Date.now() * 0.003 * speedMultiplier);
            europa.position.z = jupiter.position.z + europa.userData.distance * Math.sin(Date.now() * 0.003 * speedMultiplier);

            ganymede.position.x = jupiter.position.x + ganymede.userData.distance * Math.cos(Date.now() * 0.0025 * speedMultiplier);
            ganymede.position.z = jupiter.position.z + ganymede.userData.distance * Math.sin(Date.now() * 0.0025 * speedMultiplier);

            callisto.position.x = jupiter.position.x + callisto.userData.distance * Math.cos(Date.now() * 0.002 * speedMultiplier);
            callisto.position.z = jupiter.position.z + callisto.userData.distance * Math.sin(Date.now() * 0.002 * speedMultiplier);

            // 土星の衛星
            titan.position.x = saturn.position.x + titan.userData.distance * Math.cos(Date.now() * 0.0015 * speedMultiplier);
            titan.position.z = saturn.position.z + titan.userData.distance * Math.sin(Date.now() * 0.0015 * speedMultiplier);

            enceladus.position.x = saturn.position.x + enceladus.userData.distance * Math.cos(Date.now() * 0.003 * speedMultiplier);
            enceladus.position.z = saturn.position.z + enceladus.userData.distance * Math.sin(Date.now() * 0.003 * speedMultiplier);

            renderer.render(scene, camera);
        }
        animate();

        // カメラ操作
        document.getElementById("camera-up").addEventListener("click", function() {
            camera.position.y += 5;
            camera.lookAt(0, 0, 0);
        });
        document.getElementById("camera-down").addEventListener("click", function() {
            camera.position.y -= 5;
            camera.lookAt(0, 0, 0);
        });
        document.getElementById("camera-in").addEventListener("click", function() {
            camera.position.z -= 5;
            camera.lookAt(0, 0, 0);
        });
        document.getElementById("camera-out").addEventListener("click", function() {
            camera.position.z += 5;
            camera.lookAt(0, 0, 0);
        });
    </script>
</body>
</html>
```