iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Modern Web

從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作系列 第 13 篇

Day 13 - 行星軌道:把看不見的公轉路線畫出來

  • 分享至 

  • xImage
  •  

Day 12 我們已經讓八大行星開始繞太陽公轉。
今天要做的是把每顆行星的公轉路線畫出來。
目前行星雖然有在繞太陽移動,但是沒有軌道線時,畫面會比較難看出它到底沿著什麼路線運行,
所以今天我們會讓每顆行星都有自己的圓形軌道。

一、今天完成目標

今天會完成:

  • 使用 THREE.BufferGeometry() 建立軌道幾何
  • 使用 THREE.LineBasicMaterial() 設定軌道線材質
  • 使用 THREE.LineLoop() 畫出完整圓形軌道
  • 根據每顆行星的 distance 自動產生不同大小的軌道
  • 讓八大行星的軌道同時顯示在場景中並讓行星沿著自己對應的軌道運行

二、完整程式碼

index.html

與 Day 12 相同,不需要修改。(完整在 Day 9)

css/style.css

與 Day 12 相同,不需要修改。(完整在 Day 9)

js/main.js

import * as THREE from
    "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js";


// ========================
// Scene
// ========================

const scene =
    new THREE.Scene();

scene.background =
    new THREE.Color(
        0x000000
    );


// ========================
// Camera
// ========================

const camera =
    new THREE.PerspectiveCamera(
        65,
        window.innerWidth /
        window.innerHeight,
        0.1,
        1000
    );

camera.position.set(
    0,
    20,
    36
);

camera.lookAt(
    0,
    0,
    0
);


// ========================
// Renderer
// ========================

const renderer =
    new THREE.WebGLRenderer({
        antialias: true
    });

renderer.setSize(
    window.innerWidth,
    window.innerHeight
);

renderer.setPixelRatio(
    Math.min(
        window.devicePixelRatio,
        2
    )
);

document
    .getElementById("scene")
    .appendChild(
        renderer.domElement
    );


// ========================
// Sun
// ========================

const sunGeometry =
    new THREE.SphereGeometry(
        2,
        32,
        16
    );

const sunMaterial =
    new THREE.MeshBasicMaterial({
        color: 0xffaa00
    });

const sun =
    new THREE.Mesh(
        sunGeometry,
        sunMaterial
    );

const sunwireMaterial =
    new THREE.MeshBasicMaterial({
        color: 0xffffff,
        wireframe: true
    });

const sunwire =
    new THREE.Mesh(
        sunGeometry,
        sunwireMaterial
    );

sunwire.scale.set(
    1.03,
    1.03,
    1.03
);

sun.add(
    sunwire
);

scene.add(
    sun
);


// ========================
// Planet Data
// ========================

const planetData = [

    {
        name: "Mercury",
        size: 0.45,
        color: 0xaaaaaa,
        distance: 4,
        rotationSpeed: 0.008,
        orbitSpeed: 0.04
    },

    {
        name: "Venus",
        size: 0.65,
        color: 0xd9a066,
        distance: 6,
        rotationSpeed: -0.003,
        orbitSpeed: 0.03
    },

    {
        name: "Earth",
        size: 0.7,
        color: 0x2277ff,
        distance: 8,
        rotationSpeed: 0.01,
        orbitSpeed: 0.025
    },

    {
        name: "Mars",
        size: 0.55,
        color: 0xcc4422,
        distance: 10,
        rotationSpeed: 0.009,
        orbitSpeed: 0.02
    },

    {
        name: "Jupiter",
        size: 1.3,
        color: 0xd8b38a,
        distance: 13,
        rotationSpeed: 0.02,
        orbitSpeed: 0.012
    },

    {
        name: "Saturn",
        size: 1.1,
        color: 0xe6c87a,
        distance: 16,
        rotationSpeed: 0.018,
        orbitSpeed: 0.009
    },

    {
        name: "Uranus",
        size: 0.9,
        color: 0x66ccdd,
        distance: 19,
        rotationSpeed: 0.012,
        orbitSpeed: 0.007
    },

    {
        name: "Neptune",
        size: 0.85,
        color: 0x3355ff,
        distance: 22,
        rotationSpeed: 0.011,
        orbitSpeed: 0.005
    }

];


// ========================
// Create Orbits
// ========================

planetData.forEach(
    (data) => {

        const points = [];

        const segments = 128;

        for (
            let i = 0;
            i < segments;
            i++
        ) {

            const angle =
                (i / segments) *
                Math.PI * 2;

            const x =
                Math.cos(angle) *
                data.distance;

            const z =
                Math.sin(angle) *
                data.distance;

            points.push(
                new THREE.Vector3(
                    x,
                    0,
                    z
                )
            );
        }


        const orbitGeometry =
            new THREE.BufferGeometry()
                .setFromPoints(
                    points
                );


        const orbitMaterial =
            new THREE.LineBasicMaterial({
                color: 0x444444
            });


        const orbit =
            new THREE.LineLoop(
                orbitGeometry,
                orbitMaterial
            );


        scene.add(
            orbit
        );
    }
);


// ========================
// Create Planets
// ========================

const planets = [];

planetData.forEach(
    (data, index) => {

        // 行星球體
        const geometry =
            new THREE.SphereGeometry(
                data.size,
                12,
                8
            );


        // 行星本體材質
        const material =
            new THREE.MeshBasicMaterial({
                color: data.color
            });


        // 建立行星
        const planet =
            new THREE.Mesh(
                geometry,
                material
            );


        // ========================
        // Wireframe
        // ========================

        const wireMaterial =
            new THREE.MeshBasicMaterial({
                color: 0xffffff,
                wireframe: true
            });

        const wire =
            new THREE.Mesh(
                geometry,
                wireMaterial
            );

        wire.scale.set(
            1.03,
            1.03,
            1.03
        );

        planet.add(
            wire
        );


        // 加入場景
        scene.add(
            planet
        );


        // 每顆行星不同起始角度
        const startAngle =
            index * 0.8;


        // 保存資料
        planets.push({
            mesh: planet,

            distance:
                data.distance,

            rotationSpeed:
                data.rotationSpeed,

            orbitSpeed:
                data.orbitSpeed,

            angle:
                startAngle
        });
    }
);


// ========================
// Resize
// ========================

window.addEventListener(
    "resize",
    () => {

        camera.aspect =
            window.innerWidth /
            window.innerHeight;

        camera.updateProjectionMatrix();

        renderer.setSize(
            window.innerWidth,
            window.innerHeight
        );

        renderer.setPixelRatio(
            Math.min(
                window.devicePixelRatio,
                2
            )
        );
    }
);


// ========================
// Animation
// ========================

function animate() {

    requestAnimationFrame(
        animate
    );


    // 太陽自轉
    sun.rotation.y +=
        0.005;


    planets.forEach(
        (planet) => {

            // 自轉
            planet.mesh.rotation.y +=
                planet.rotationSpeed;


            // 公轉
            planet.angle +=
                planet.orbitSpeed;


            planet.mesh.position.x =
                Math.cos(
                    planet.angle
                ) *
                planet.distance;


            planet.mesh.position.z =
                Math.sin(
                    planet.angle
                ) *
                planet.distance;

        }
    );


    renderer.render(
        scene,
        camera
    );
}

animate();

三、建立行星軌道

今天最主要新增的是 Orbits 的:

planetData.forEach(
    (data) => {

    }
);

我們直接利用 Day 12 已經有的 distance 來產生不同大小的軌道。

Mercury distance = 4
Earth distance = 8
Jupiter distance = 13

所以不需要再另外設定八個軌道大小,
直接使用 data.distance 就可以建立對應的軌道。

四、程式碼簡單說明

  1. 先建立很多個點

我們先建立 const points = [];
接著設定 const segments = 128;代表我們會在圓形軌道上建立 128 個點。
然後:

for (
    let i = 0;
    i < segments;
    i++
)

讓程式重複計算這些位置,每一個點的角度是:

const angle =
    (i / segments) *
    Math.PI * 2;

Math.PI * 2 就是一整圈。
所以 0 → Math.PI * 2 就等於繞完整個圓。

  1. 計算軌道上的 X 和 Z

跟 Day 12 的公轉一樣,我們使用 distance

const x =
    Math.cos(angle) *
    data.distance;

以及:

const z =
    Math.sin(angle) *
    data.distance;

這樣就可以得到圓形軌道上的位置。
再把每一個位置放進:

points.push(
    new THREE.Vector3(
        x,    //x
        0,    //y
        z     //z
    )
);

THREE.Vector3() 代表一個 3D 座標
因為我們的行星是在 X、Z 平面上公轉,所以 Y 固定使用 0

  1. BufferGeometry

有了很多個座標之後:

const orbitGeometry =
    new THREE.BufferGeometry()
        .setFromPoints(
            points
        );

BufferGeometry 可以把剛剛建立的座標整理成 Three.js 可以使用的 Geometry。
所以流程可以理解成:

Math.cos / Math.sin
↓
很多個座標
↓
points
↓
BufferGeometry

  1. LineBasicMaterial

接著我們建立軌道線的材質:

const orbitMaterial =
    new THREE.LineBasicMaterial({
        color: 0x444444
    });

這裡使用比較深的灰色,因為如果直接用白色,八條軌道全部顯示時會太搶眼。
我們希望太陽、行星才是畫面的主要焦點,而軌道只是輔助。

  1. THREE.LineLoop
const orbit =
    new THREE.LineLoop(
        orbitGeometry,
        orbitMaterial
    );

這裡使用的是 THREE.LineLoop ,
跟普通 Line 最大的差別是 LineLoop 最後一個點會自動連回第一個點。
所以非常適合用來畫:圓形、軌道、封閉線條。
最後再:

scene.add(
    orbit
);

軌道就會顯示在 Three.js 場景裡。

五、小試身手

目前我們使用 const segments = 128; 建立軌道。
大家可以試著改成 const segments = 16;
看看軌道會變成什麼樣子。
你會發現圓形開始變得有稜有角。
再試試:

const segments = 32;
const segments = 64;
const segments = 256;

觀察:

segments 越少 → 軌道越有稜角
segments 越多 → 軌道越接近圓形

六、今天小結

今天加入軌道之後現在整個太陽系的結構就更加清楚了。
目前我們已經完成:

全螢幕場景、太陽、八大行星、不同大小、顏色、距離、Wireframe、自轉、公轉、不同公轉速度、八大行星軌道。

而且今天我們第一次使用:

THREE.Vector3
THREE.BufferGeometry
THREE.LineBasicMaterial
THREE.LineLoop

讓 Three.js 不只是建立球體,也開始可以自己建立線條與路徑。
補上完成的畫面
螢幕錄製 2026-09-22 153258

Day 14 預告

「不同速度:調整八大行星的公轉節奏」

現在八大行星雖然已經有 orbitSpeed 但是目前的速度只是為了方便觀察所設定的數值。
Day 14 我們會進一步整理八大行星之間的公轉速度差異。
同時也會開始處理一個很重要的問題 「動畫速度會不會受到電腦 FPS 影響?」
我們會開始接觸 THREE.Clock ,讓動畫不要單純依賴每一幀增加固定數值,讓太陽系動畫的速度控制更穩定。


上一篇
Day 12 - 行星公轉:讓八大行星開始繞太陽運行
下一篇
Day 14 - 不同速度:讓八大行星擁有自己的運行節奏
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 16 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言