iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Modern Web

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

Day 22 - Planet Effects:地球夜景 × 金星大氣層

  • 分享至 

  • xImage
  •  

今天開始替不同的行星加入自己的特色。
這次會處理兩顆

Earth → 夜間城市燈光
Venus → 金星大氣/雲層 Texture

而且金星的大氣層不再只是單純的透明黃色球。
我們會準備 venus.jpg + venus_atmosphere.jpg 把大氣 Texture 套在金星外層。
同時會先把程式架構準備好,之後做 UI 時,就能直接控制來顯示或隱藏金星的大氣層。

一、今天完成目標

今天會完成:

  • 替 Earth 加入夜間城市燈光
  • 使用 emissiveMap
  • 使用 emissiveIntensity
  • 替 Venus 建立第二層 Sphere
  • 將金星大氣 Texture 貼在外層球體
  • 使用透明材質呈現雲層效果
  • 讓金星大氣層慢慢旋轉
  • 儲存 venusAtmosphere
  • 為之後 UI 的顯示/隱藏功能做準備
  • 保留 Moon、Lighting、Shadow 與 OrbitControls

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

二、先處理 Earth Night

之前地球其實和其他行星一樣,都是:

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

今天 Earth 不能再完全和其他行星共用同一套 Material。
因為 Earth 還要多一張 earth_night.jpg,所以先載入:

const earthNightTexture =
    textureLoader.load(
        "textures/earth_night.jpg"
    );


earthNightTexture.colorSpace =
    THREE.SRGBColorSpace;

三、什麼是 emissiveMap?

今天地球最重要的新東西就是 missiveMap,
我們原本的 map 代表正常表面 Texture,
而 emissiveMap 可以理解成哪些區域自己會發亮。
所以地球材質可以改成:

const material =
    new THREE.MeshStandardMaterial({
        map: planetTexture,

        emissiveMap:
            earthNightTexture,

        emissive:
            0xffffff,

        emissiveIntensity:
            0.8,

        roughness:
            1,

        metalness:
            0
    });

概念就是:
map 一般表面
emissiveMap 自發光區域

四、emissiveIntensity

今天設定:

emissiveIntensity:
    0.6

它控制夜景的亮度。
如果你的 earth_night.jpg 本身就很亮,可以再把數值降低。
要注意,目前這種做法是比較簡單的視覺模擬 emissiveMap 本身不會真正判斷
現在這個位置是不是背對太陽因此亮面也可能稍微看到夜景 Texture。
只是受到 PointLight 照亮後,白天 Texture 通常會比較明顯。
後面如果想做到非常精準的只有背光面出現城市燈就會需要 Shader。
目前先使用比較適合這個系列難度的做法。

五、載入金星大氣 Texture 並建立金星大氣層

接著是今天第二個重點 Venus Atmosphere(金星大氣層)
先加入:

const venusAtmosphereTexture =
    textureLoader.load(
        "textures/venus_atmosphere.jpg"
    );

venusAtmosphereTexture.colorSpace =
    THREE.SRGBColorSpace;

另外在建立行星以前準備:

let venusAtmosphere =
    null;

這個變數很重要。因為之後我們要直接控制:

venusAtmosphere.visible =
    false;

來把大氣關掉。
建立金星大氣層當建立到 Venus 時:

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

建立另一個稍大的球:

const atmosphereGeometry =
    new THREE.SphereGeometry(
        data.size *
        1.03,
        64,
        64
    );

Venus 本體 = 100%
Atmosphere = 103%
所以外層只比本體大一點點。

六、替大氣層加入 Texture

Material:
const atmosphereMaterial =
    new THREE.MeshBasicMaterial({
        map:
            venusAtmosphereTexture,

        transparent:
            true,

        opacity:
            0.55,

        side:
            THREE.DoubleSide,

        depthWrite:
            false
    });

這裡有幾個重要設定。

1.map

map:
    venusAtmosphereTexture

代表使用 venus_atmosphere.jpg
作為金星外層的雲層 Texture。

2.transparent

transparent:
    true

告訴 Three.js這個材質允許透明

3.opacity

opacity:
    0.55

讓我們還能透過 Atmosphere 看到下面的金星表面。
如果使用 opacity:1 Atmosphere 就可能完全蓋住下面的 Surface。

4.DoubleSide

side:
    THREE.DoubleSide

代表球體的內外兩面都可以被看見。
對這種透明的大氣層比較方便。

5.depthWrite
我們另外設定:

depthWrite:
    false

透明物件有時候會發生深度排序問題。
讓 Atmosphere 不寫入 Depth Buffer,可以降低某些透明材質互相遮擋時的怪異效果。

七、把 Atmosphere 加到 Venus

建立:

venusAtmosphere =
    new THREE.Mesh(
        atmosphereGeometry,
        atmosphereMaterial
    );

接著:

planet.add(
    venusAtmosphere
);

這裡我們使用的是 planet.add(...) 而不是 scene.add(...)
因為 Atmosphere 本來就應該屬於 Venus,
因此 Venus 公轉時 Atmosphere 自動跟著移動,
不需要像 Moon Orbit 一樣每一幀重新計算位置。

八、讓金星大氣自己旋轉

因為 Atmosphere 是 Venus 的子物件,所以本來就會跟著 Venus 一起移動。
但我們還可以額外讓雲層慢慢移動:

if (
    venusAtmosphere
) {

    venusAtmosphere.rotation.y +=
        0.15 *
        delta;

}

這樣畫面會變成:
Venus Surface → 自轉
Venus Atmosphere → 另外慢慢轉動
讓雲層與表面之間出現一些相對運動。

九、Animation 新增 Venus Atmosphere

原本所有行星完成自轉與公轉之後,再加入:

if (
    venusAtmosphere
) {

    venusAtmosphere.rotation.y +=
        0.15 *
        delta;

}

所以現在動畫大致變成

Sun 自轉
Planets 自轉 + 公轉
Venus Atmosphere 雲層旋轉
Moon 自轉 + 繞 Earth

十、完整程式碼

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

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


// ========================
// Lights
// ========================

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


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"
    }

];


// ========================
// Planet Orbits
// ========================

planetData.forEach(
    (data) => {

        const points =
            [];

        const segments =
            128;


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

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


            points.push(
                new THREE.Vector3(

                    Math.cos(
                        angle
                    ) *
                    data.distance,

                    0,

                    Math.sin(
                        angle
                    ) *
                    data.distance

                )
            );

        }


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


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


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


        scene.add(
            orbit
        );

    }
);


// ========================
// Special Textures
// ========================

const earthNightTexture =
    textureLoader.load(
        "textures/earth_night.jpg"
    );

earthNightTexture.colorSpace =
    THREE.SRGBColorSpace;


const venusAtmosphereTexture =
    textureLoader.load(
        "textures/venus_atmosphere.jpg"
    );

venusAtmosphereTexture.colorSpace =
    THREE.SRGBColorSpace;


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

const planets =
    [];

let earth =
    null;

let venusAtmosphere =
    null;


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

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

        planetTexture.colorSpace =
            THREE.SRGBColorSpace;


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


        let material;


        // Earth Night

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

            material =
                new THREE.MeshStandardMaterial({

                    map:
                        planetTexture,

                    emissiveMap:
                        earthNightTexture,

                    emissive:
                        0xffffff,

                    emissiveIntensity:
                        0.6,

                    roughness:
                        1,

                    metalness:
                        0

                });

        } else {

            material =
                new THREE.MeshStandardMaterial({

                    map:
                        planetTexture,

                    roughness:
                        1,

                    metalness:
                        0

                });

        }


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


        planet.castShadow =
            true;

        planet.receiveShadow =
            true;


        // Venus Atmosphere

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

            const atmosphereGeometry =
                new THREE.SphereGeometry(
                    data.size *
                    1.03,
                    64,
                    64
                );


            const atmosphereMaterial =
                new THREE.MeshBasicMaterial({

                    map:
                        venusAtmosphereTexture,

                    transparent:
                        true,

                    opacity:
                        0.55,

                    side:
                        THREE.DoubleSide,

                    depthWrite:
                        false

                });


            venusAtmosphere =
                new THREE.Mesh(
                    atmosphereGeometry,
                    atmosphereMaterial
                );


            planet.add(
                venusAtmosphere
            );

        }


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


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;


    moonOrbitPoints.push(
        new THREE.Vector3(

            Math.cos(
                angle
            ) *
            moonDistance,

            0,

            Math.sin(
                angle
            ) *
            moonDistance

        )
    );

}


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

    sun.rotation.y +=
        0.5 *
        delta;


    // Planets

    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;

        }
    );


    // Venus Atmosphere

    if (
        venusAtmosphere
    ) {

        venusAtmosphere.rotation.y +=
            0.15 *
            delta;

    }


    // Moon

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


    controls.update();


    renderer.render(
        scene,
        camera
    );

}

animate();

十一、今天小結

今天我們開始真正讓不同星球產生自己的特色。
Earth 現在變成主要使用 emissiveMap
讓夜間城市具有自發光效果。
Venus 則變成 Venus + venusAtmosphere,
讓金星有了大氣層的效果。
今天最大的變化就是讓不同星球開始真正有自己的視覺特色。

Day 23 預告

「Saturn Ring × Starfield:加入土星環與宇宙星空」

Day 23 我們會再處理另一顆非常有特色的行星土星環
這次會使用 THREE.RingGeometry 建立土星環。
而且跟今天的 Venus 一樣,不只使用單一顏色,而是加入 土星環 + 透明效果,
另外也會把目前純黑背景升級成星空背景,讓整個太陽系正式開始有宇宙空間的感覺。


上一篇
Day 21 - Moon:建立地球與月球系統
下一篇
Day 23 - Saturn Ring × Starfield:加入土星環與宇宙星空
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言