iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0

Day 20 我們已經把 Three.js 的 Shadow 系統準備好了。
今天要第一次加入「衛星」。
目前所有行星都是繞著太陽
今天加入月球之後,月球會繞著地球

一、今天完成目標

今天會完成:

  • 加入月球 Texture
  • 建立 Moon
  • 讓 Moon 自轉
  • 讓 Moon 繞著 Earth 公轉
  • 建立月球軌道
  • 讓月球軌道跟著地球移動
  • 讓 Moon 支援 castShadow
  • 讓 Moon 支援 receiveShadow
  • 理解「繞太陽」與「繞地球」兩種座標計算

今天需要多準備一張:
textures/
└── moon.jpg
如果 Day 17 的行星 Texture 是從 Solar System Scope 下載的,月球也可以繼續使用同一套素材。這邊一樣附上連結:點我前往

二、先記住 Earth

我們現在的八大行星都是透過 planetData.forEach(...) 自動建立。
但今天 月亮 必須知道 地球 現在在哪裡?
所以建立行星以前,先加入:

let earth = null;

接著建立行星時:

if (data.name === "Earth") {

    earth =
        planet;

}

這樣之後就可以透過 earth.position 取得目前地球的位置。

三、建立 Moon

這邊的步驟跟之前建立其他星球時一樣,
先載入月球 Texture:

const moonTexture =
    textureLoader.load(
        "textures/moon.jpg"
    );

moonTexture.colorSpace =
    THREE.SRGBColorSpace;

建立月球:

const moonGeometry =
    new THREE.SphereGeometry(
        0.2,
        64,
        64
    );

const moonMaterial =
    new THREE.MeshStandardMaterial({
        map: moonTexture,
        roughness: 1,
        metalness: 0
    });

const moon =
    new THREE.Mesh(
        moonGeometry,
        moonMaterial
    );

跟其他行星一樣,Moon 也需要 Shadow:

moon.castShadow =
    true;

moon.receiveShadow =
    true;

最後加入 Scene:

scene.add(
    moon
);

四、月球的距離與速度

我們準備三個資料:

const moonDistance =
    1.3;

const moonOrbitSpeed =
    2;

let moonAngle =
    0;

其中
moonDistance 代表月球距離地球多遠
moonOrbitSpeed 代表月球繞地球的速度
moonAngle 代表目前公轉到哪個角度

這和我們八大行星的差別只是:
八大行星的中心是太陽,而月亮的中心是地球。

五、讓 Moon 繞 Earth

八大行星的公轉是:

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

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

因為 Sun 在 (0, 0, 0) 所以可以直接從原點計算。
但是 Moon 不一樣。Moon 的中心是 earth.position
因此要寫成:

moon.position.x =
    earth.position.x +
    Math.cos(
        moonAngle
    ) *
    moonDistance;

moon.position.z =
    earth.position.z +
    Math.sin(
        moonAngle
    ) *
    moonDistance;

注意這裡多了 earth.position.x 和 earth.position.z,
這就是今天最重要的地方。
可以理解成:Moon 世界座標 = Earth 世界座標 + Moon 相對 Earth 的位置。

六、讓 Moon 自轉建立月球軌道

動畫裡加入:

moon.rotation.y +=
    0.4 *
    delta;

所以 Moon 現在也會 自轉 + 公轉 跟行星一樣。
既然八大行星都有 Orbit,Moon 當然也可以有。
建立軌道:

const moonOrbitPoints =
    [];

const moonOrbitSegments =
    64;

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

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

    const x =
        Math.cos(
            angle
        ) *
        moonDistance;

    const z =
        Math.sin(
            angle
        ) *
        moonDistance;

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

接著:

const moonOrbitGeometry =
    new THREE.BufferGeometry()
        .setFromPoints(
            moonOrbitPoints
        );

const moonOrbitMaterial =
    new THREE.LineBasicMaterial({
        color: 0x666666
    });

const moonOrbit =
    new THREE.LineLoop(
        moonOrbitGeometry,
        moonOrbitMaterial
    );

scene.add(
    moonOrbit
);

這裡又有一個很重要的問題,就是我們要讓月球的軌道跟著 Earth 的位置一直公轉
所以 Moon 的軌道不能永遠留在原本的位置。
動畫裡需要加入:

moonOrbit.position.copy(
    earth.position
);

也就是 Earth 移動到哪裡 Moon Orbit 就跟到哪裡。

七、動畫新增部分和為什麼不用 earth.add(moon)?

Day 21 最重要的動畫程式就是:

moonAngle +=
    moonOrbitSpeed *
    delta;


moon.position.x =
    earth.position.x +
    Math.cos(
        moonAngle
    ) *
    moonDistance;


moon.position.y =
    earth.position.y;


moon.position.z =
    earth.position.z +
    Math.sin(
        moonAngle
    ) *
    moonDistance;


moon.rotation.y +=
    0.4 *
    delta;


moonOrbit.position.copy(
    earth.position
);

Three.js 其實可以寫:

earth.add(
    moon
);

這樣 Moon 就會直接成為 Earth 的子物件。
但目前我們的 Earth 本身正在 earth.rotation.y += ... 也就是不停自轉。
如果 Moon 直接成為 Earth 的子物件,
Earth rotation 可能連 Moon 的整個座標系一起轉,
這會把 Earth 自轉和 Moon 公轉綁得太緊。
所以目前我們先採用比較容易理解的方法:
計算 Earth 目前位置,之後讓 Moon 使用 Earth 位置作為中心。
後面如果專案架構繼續變大,也可以再改成 THREE.Group來管理完整的地月系統。

八、Shadow 開始派上用場

Day 20 我們設定:

planet.castShadow =
    true;

planet.receiveShadow =
    true;

Moon 今天也加入:
moon.castShadow =
    true;

moon.receiveShadow =
    true;

所以現在理論上當 Moon 剛好位於 Sun 和 Earth 中間時,Moon 可以遮擋太陽光。
反過來Earth 也可能讓 Moon 進入陰影。
這就是 Day 20 Shadow 系統在地月系統中的實際用途。

九、完整程式碼

import * as THREE from
    "three";

import { OrbitControls } from
    "three/addons/controls/OrbitControls.js";


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

const scene =
    new THREE.Scene();

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


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

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

camera.position.set(
    30,
    25,
    30
);

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
    )
);

renderer.shadowMap.enabled =
    true;

renderer.shadowMap.type =
    THREE.PCFSoftShadowMap;

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


// ========================
// OrbitControls
// ========================

const controls =
    new OrbitControls(
        camera,
        renderer.domElement
    );

controls.enableDamping =
    true;

controls.dampingFactor =
    0.05;

controls.minDistance =
    10;

controls.maxDistance =
    80;

controls.target.set(
    0,
    0,
    0
);

controls.update();


// ========================
// Clock
// ========================

const clock =
    new THREE.Clock();


// ========================
// Texture Loader
// ========================

const textureLoader =
    new THREE.TextureLoader();


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

const sunTexture =
    textureLoader.load(
        "textures/sun.jpg"
    );

sunTexture.colorSpace =
    THREE.SRGBColorSpace;

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

const sunMaterial =
    new THREE.MeshBasicMaterial({
        map: sunTexture
    });

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

scene.add(
    sun
);


// ========================
// Point Light
// ========================

const sunLight =
    new THREE.PointLight(
        0xffffff,
        500,
        100
    );

sunLight.position.set(
    0,
    0,
    0
);

sunLight.castShadow =
    true;

sunLight.shadow.mapSize.width =
    512;

sunLight.shadow.mapSize.height =
    512;

sunLight.shadow.camera.near =
    0.1;

sunLight.shadow.camera.far =
    100;

scene.add(
    sunLight
);


// ========================
// Ambient Light
// ========================

const ambientLight =
    new THREE.AmbientLight(
        0xffffff,
        0.08
    );

scene.add(
    ambientLight
);


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

const planetData = [

    {
        name: "Mercury",
        size: 0.45,
        distance: 4,
        rotationSpeed: 0.8,
        orbitSpeed: 1.2,
        texture:
            "textures/mercury.jpg"
    },

    {
        name: "Venus",
        size: 0.65,
        distance: 6,
        rotationSpeed: -0.3,
        orbitSpeed: 0.95,
        texture:
            "textures/venus.jpg"
    },

    {
        name: "Earth",
        size: 0.7,
        distance: 8,
        rotationSpeed: 1.0,
        orbitSpeed: 0.8,
        texture:
            "textures/earth.jpg"
    },

    {
        name: "Mars",
        size: 0.55,
        distance: 10,
        rotationSpeed: 0.9,
        orbitSpeed: 0.65,
        texture:
            "textures/mars.jpg"
    },

    {
        name: "Jupiter",
        size: 1.3,
        distance: 13,
        rotationSpeed: 2.0,
        orbitSpeed: 0.4,
        texture:
            "textures/jupiter.jpg"
    },

    {
        name: "Saturn",
        size: 1.1,
        distance: 16,
        rotationSpeed: 1.8,
        orbitSpeed: 0.3,
        texture:
            "textures/saturn.jpg"
    },

    {
        name: "Uranus",
        size: 0.9,
        distance: 19,
        rotationSpeed: 1.2,
        orbitSpeed: 0.22,
        texture:
            "textures/uranus.jpg"
    },

    {
        name: "Neptune",
        size: 0.85,
        distance: 22,
        rotationSpeed: 1.1,
        orbitSpeed: 0.16,
        texture:
            "textures/neptune.jpg"
    }

];


// ========================
// 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 =
    [];

let earth =
    null;

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

        const planetTexture =
            textureLoader.load(
                data.texture
            );

        planetTexture.colorSpace =
            THREE.SRGBColorSpace;


        const geometry =
            new THREE.SphereGeometry(
                data.size,
                64,
                64
            );


        const material =
            new THREE.MeshStandardMaterial({
                map: planetTexture,
                roughness: 1,
                metalness: 0
            });


        const planet =
            new THREE.Mesh(
                geometry,
                material
            );


        planet.castShadow =
            true;

        planet.receiveShadow =
            true;


        const startAngle =
            index * 0.8;


        planet.position.x =
            Math.cos(
                startAngle
            ) *
            data.distance;

        planet.position.z =
            Math.sin(
                startAngle
            ) *
            data.distance;


        scene.add(
            planet
        );


        if (
            data.name === "Earth"
        ) {

            earth =
                planet;

        }


        planets.push({

            name:
                data.name,

            mesh:
                planet,

            distance:
                data.distance,

            rotationSpeed:
                data.rotationSpeed,

            orbitSpeed:
                data.orbitSpeed,

            angle:
                startAngle

        });

    }
);


// ========================
// Moon
// ========================

const moonTexture =
    textureLoader.load(
        "textures/moon.jpg"
    );

moonTexture.colorSpace =
    THREE.SRGBColorSpace;


const moonGeometry =
    new THREE.SphereGeometry(
        0.2,
        64,
        64
    );


const moonMaterial =
    new THREE.MeshStandardMaterial({
        map: moonTexture,
        roughness: 1,
        metalness: 0
    });


const moon =
    new THREE.Mesh(
        moonGeometry,
        moonMaterial
    );


moon.castShadow =
    true;

moon.receiveShadow =
    true;


scene.add(
    moon
);


// Moon Data

const moonDistance =
    1.3;

const moonOrbitSpeed =
    2;

let moonAngle =
    0;


// ========================
// Moon Orbit
// ========================

const moonOrbitPoints =
    [];

const moonOrbitSegments =
    64;

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

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


    const x =
        Math.cos(
            angle
        ) *
        moonDistance;


    const z =
        Math.sin(
            angle
        ) *
        moonDistance;


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

}


const moonOrbitGeometry =
    new THREE.BufferGeometry()
        .setFromPoints(
            moonOrbitPoints
        );


const moonOrbitMaterial =
    new THREE.LineBasicMaterial({
        color: 0x666666
    });


const moonOrbit =
    new THREE.LineLoop(
        moonOrbitGeometry,
        moonOrbitMaterial
    );


scene.add(
    moonOrbit
);


// ========================
// Camera Controls
// ========================

window.addEventListener(
    "keydown",
    (event) => {

        if (event.key === "1") {

            camera.position.set(
                0,
                0,
                40
            );

        }


        if (event.key === "2") {

            camera.position.set(
                30,
                25,
                30
            );

        }


        if (event.key === "3") {

            camera.position.set(
                0,
                45,
                0.1
            );

        }


        if (event.key === "4") {

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

        }


        controls.target.set(
            0,
            0,
            0
        );

        controls.update();

    }
);


// ========================
// 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
    );


    const delta =
        clock.getDelta();


    // Sun Rotation

    sun.rotation.y +=
        0.5 *
        delta;


    // Planet Animation

    planets.forEach(
        (planet) => {

            planet.mesh.rotation.y +=
                planet.rotationSpeed *
                delta;


            planet.angle +=
                planet.orbitSpeed *
                delta;


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


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

        }
    );


    // Moon Orbit

    moonAngle +=
        moonOrbitSpeed *
        delta;


    moon.position.x =
        earth.position.x +
        Math.cos(
            moonAngle
        ) *
        moonDistance;


    moon.position.y =
        earth.position.y;


    moon.position.z =
        earth.position.z +
        Math.sin(
            moonAngle
        ) *
        moonDistance;


    // Moon Rotation

    moon.rotation.y +=
        0.4 *
        delta;


    // Moon Orbit follows Earth

    moonOrbit.position.copy(
        earth.position
    );


    controls.update();


    renderer.render(
        scene,
        camera
    );

}

animate();

十、今天小結

今天我們第一次建立了衛星系統
而今天最重要的座標概念就是:

moon.position.x =
    earth.position.x +
    Math.cos(
        moonAngle
    ) *
    moonDistance;

也就是 月球的位置 = 地球的位置 + 月球自己的公轉位置
今日完成畫面
image

Day 22 預告

「Planet Effects:地球夜景 × 金星大氣層」

Day 22 會開始做我們之前決定加入的行星特殊效果。
首先是地球白天地球 Texture + 夜間城市燈光
我們會準備:earth.jpg、earth_night.jpg
並讓城市燈光出現在地球的暗面,另外還會處理金星:

金星
├── 金星本體
└── 半透明 Atmosphere(大氣層)

讓金星外面多一層淡黃色的大氣效果。


上一篇
Day 20 - Shadow:讓行星開始產生陰影
下一篇
Day 22 - Planet Effects:地球夜景 × 金星大氣層
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言