iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0

Day 17 我們已經使用 TextureLoader,把原本只有單一顏色的行星換成真正的表面貼圖。
現在地球、火星、木星等行星已經開始有比較真實的外觀。
但目前還有一個問題,所有行星整顆都是亮的。
這是因為 Day 17 使用的是 THREE.MeshBasicMaterial
MeshBasicMaterial 不會受到燈光影響。
所以今天我們要讓太陽不只是看起來像太陽,而是真正成為整個太陽系的光源。

一、今天完成目標

今天會完成:

  • 加入 THREE.PointLight
  • 將光源放在太陽的位置 (0, 0, 0)
  • 將行星材質從 MeshBasicMaterial 改成 MeshStandardMaterial
  • 讓行星開始受到太陽光影響
  • 讓行星出現亮面與暗面
  • 加入 colorSpace 讓行星貼圖顏色顯示更正常
  • 了解為什麼太陽自己不能使用一般受光材質

二、完整程式碼

index.html

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

css/style.css

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

js/main.js

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

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


// ========================
// Sun Light
// ========================

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

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

scene.add(
    sunLight
);


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

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


        scene.add(
            planet
        );


        const startAngle =
            index * 0.8;


        planets.push({
            mesh: planet,

            distance:
                data.distance,

            rotationSpeed:
                data.rotationSpeed,

            orbitSpeed:
                data.orbitSpeed,

            angle:
                startAngle
        });
    }
);


// ========================
// 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.y +=
        0.5 * delta;


    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;

        }
    );


    controls.update();


    renderer.render(
        scene,
        camera
    );
}

animate();

三、加入 PointLight

今天最重要的新程式就是:

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

PointLight 可以理解成 從一個位置往四面八方發光的光源。
這非常適合我們現在的太陽。
所以我們把它放在:

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

剛好跟太陽的位置一樣。接著:

scene.add(
    sunLight
);

把光源加入場景。

四、程式碼簡單說明

  1. PointLight 的三個參數

我們今天使用:

new THREE.PointLight(
    0xffffff, //顏色
    500,      //強度
    100       //距離
);

第一個 0xffffff 代表白色光。
第二個 500 代表光線強度。
第三個 100 代表光源可以影響的距離。
目前最遠的海王星 distance: 22
所以 100 已經足夠照到整個太陽系。

  1. 為什麼行星不能再用 MeshBasicMaterial?

Day 17 我們使用:

new THREE.MeshBasicMaterial({
    map: planetTexture
});

但是 MeshBasicMaterial 有一個特性完全不理會光源。
所以就算加入 THREE.PointLight 它還是會整顆一樣亮。
因此今天要改成:

new THREE.MeshStandardMaterial({
    map: planetTexture
});

MeshStandardMaterial 會受到光線影響,所以:
面向太陽的一側會亮。
背對太陽的一側會暗。

  1. roughness 與 metalness

今天材質裡另外加入:

roughness: 1,
metalness: 0

roughness 可以理解成:
表面有多粗糙。
roughness = 0 , 表面比較光滑
roughness = 1 , 表面比較粗糙
行星並不是金屬球,所以我們設定 metalness: 0
避免它看起來像金屬材質。

五、為什麼太陽還是 MeshBasicMaterial?

你可能會發現行星全部改成 MeshStandardMaterial,
但是太陽還是 MeshBasicMaterial。
原因很簡單,太陽本身就是光源。
如果我們讓太陽也必須依靠 PointLight,
才能被看見,概念上會有點奇怪。
所以現在
Sun 自己保持明亮而 Planets 接受太陽光
這樣比較符合我們目前的太陽系效果。

六、colorSpace

今天另外加入:

sunTexture.colorSpace =
    THREE.SRGBColorSpace;

以及:

planetTexture.colorSpace =
    THREE.SRGBColorSpace;

因為我們載入的:

Earth.jpg
Mars.jpg
Jupiter.jpg
...

都是一般用來顯示顏色的圖片。
告訴 Three.js:THREE.SRGBColorSpace
可以讓 Texture 的顏色顯示方式更加正常。
所以之後只要是這類行星表面顏色貼圖
就可以設定:

texture.colorSpace =
    THREE.SRGBColorSpace;

七、今天小結

今天 Day 18 加入:
THREE.PointLight
並把行星材質從 MeshBasicMaterial
改成 MeshStandardMaterial
目前我們已經完成:

全螢幕場景、太陽、八大行星、Texture、自轉、公轉、公轉軌道、不同速度、THREE.Clock、Camera、OrbitControls,以及真正的太陽光源。

現在行星終於開始有 亮面 + 暗面,
整個太陽系的立體感也會比 Day 17 明顯很多。
今天完成的效果:
螢幕錄製 2026-10-02 135454

Day 19 預告

「Lighting:進一步調整太陽系的明暗效果」

Day 18 我們已經成功讓太陽發光。
Day 19 我們會進一步處理 Lighting。

為什麼背光面會完全黑掉?

要不要保留一點環境光?

不同材質受到光線時會有什麼差別?

我們會開始加入 THREE.AmbientLight
並比較 PointLight 和 AmbientLight

兩種光源的差別,讓目前的光照效果更自然。


上一篇
Day 17 - Texture:替八大行星換上真正的表面
下一篇
Day 19 - Lighting:調整太陽系的明暗效果
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言