iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Modern Web

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

Day 19 - Lighting:調整太陽系的明暗效果

  • 分享至 

  • xImage
  •  

現在會遇到一個很明顯的問題,
背對太陽的一面可能太黑。
所以今天我們要再加入另一種光源 THREE.AmbientLight (環境光)
讓整個場景保留一點基本亮度,同時繼續保留太陽光造成的明暗差異。

一、今天完成目標

今天會完成:

  • 認識 AmbientLight
  • 比較 PointLight 與 AmbientLight
  • 讓行星暗面保留一些細節
  • 調整太陽光強度
  • 調整環境光強度
  • 保留亮面與暗面的差異
  • 了解為什麼 AmbientLight 不能開太強

二、完整程式碼

index.html

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

css/style.css

與 Day 18 相同,不需要修改。(完整在 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
);


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

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

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

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

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

三、程式碼簡單說明

  1. 今天真正新增的核心程式:
const ambientLight =
    new THREE.AmbientLight(
        0xffffff,
        0.08
    );

scene.add(
    ambientLight
);

也就是 THREE.AmbientLight 它和昨天的 THREE.PointLight 有很大的不同。

  1. AmbientLight

今天加入:

new THREE.AmbientLight(
    0xffffff,
    0.08
);

AmbientLight 可以理解成整個場景都有一點點基礎亮度。
它沒有特定位置,所以整顆行星都會稍微受到影響。

四、為什麼要加入 AmbientLight (環境光)?

如果只有太陽的 PointLight 亮面很好看,
但是暗面可能會變成 完全黑掉。
例如地球背光的一側,有可能幾乎看不到 Texture,
所以我們加入非常弱的 AmbientLight,
讓暗面仍然保留一些資訊。
效果會比較接近暗面還是暗,只是沒有完全消失。

五、Light 和 Material 的關係

今天也順便整理一下前幾天學過的 Material。
太陽現在使用 THREE.MeshBasicMaterial
行星使用 THREE.MeshStandardMaterial
兩者最大的差異就是
MeshBasicMaterial 不受 Light 影響
MeshStandardMaterial 會受到 Light 影響

六、小試身手

今天最適合測試的就是 ambientLight。
目前是:

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

可以試著改成不同數值比較看看暗面會變什麼樣。
最後再改回 0.08
你就會很清楚知道 AmbientLight 的作用。
也可以暫時把 sunLight 註解掉

// scene.add(
// sunLight
// );

只留下 AmbientLight。
這時候你會發現行星還是看得到,
但是沒有明顯的亮面與暗面方向。
這就是 PointLight 和 AmbientLight 最明顯的差別。

七、今天小結

今天 Day 19 加入 THREE.AmbientLight
替整個場景增加非常微弱的補光。
其中
PointLight → 決定主要明暗方向
AmbientLight → 讓暗面不要完全消失
我們也要記住 AmbientLight 不是越亮越好

如果 AmbientLight 太強,反而會失去太陽照射的立體感。
所以今天的重點不是單純 「多加一盞燈」 而是開始學習,
如何搭配不同光源,控制整個 3D 場景的明暗。

Day 20 預告

「Shadow:讓行星開始產生陰影」

目前我們已經有:
Texture + MeshStandardMaterial + PointLight + AmbientLight
但 Three.js 的 Shadow 並不會自動出現。
Day 20 我們會開始設定
renderer.shadowMap.enabled 以及 castShadow 和 receiveShadow
讓物件真正參與陰影計算。
也會了解 Light 和 Shadow 其實不是同一件事情。


上一篇
Day 18 - Sun Light:讓太陽真正成為光源
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言