iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Modern Web

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

Day 23 - Saturn Ring × Starfield:加入土星環與宇宙星空

  • 分享至 

  • xImage
  •  

今天要繼續處理另一顆非常有辨識度的行星:土星
除了加入 Saturn Ring,我們也會把原本單純的黑色背景升級成星空背景
另外,隨著 Moon、Venus Atmosphere、Saturn Ring 等物件逐漸加入,原本的行星距離開始顯得比較擁擠,所以今天也會重新調整整個太陽系的距離與 Camera 範圍。

由於目前專案程式碼已逐漸增加,從本篇開始只列出當日新增或修改的程式碼。
完整整合版將於 Day 30 最終完成時提供。

一、今天完成目標

今天會完成:

  • 重新調整八大行星之間的距離
  • 調整 Camera 觀看範圍
  • 使用 THREE.RingGeometry 建立 Saturn Ring
  • 替 Saturn Ring 加入 Texture
  • 修正 RingGeometry 的 UV Mapping
  • 使用 MeshStandardMaterial 讓土星環保有光照效果
  • 使用 emissiveMap 避免土星環過暗
  • 保留 Earth、Moon 與 Saturn 的 Shadow 設定
  • 使用 THREE.Points 建立 Starfield
  • 讓原本純黑背景更像真正的宇宙空間

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

二、本日修改摘要

項目 修改內容
Planet Distance 拉開外行星之間的距離
Camera 增加觀看距離與縮放範圍
Shadow Earth、Moon、Saturn 保留主要 Shadow
Saturn Ring Ring RingGeometry + Texture + UV 修正
Ring Material 加入 Emissive 避免過暗
Starfield 使用 THREE.Points 建立 2000 顆星星

三、重新調整行星距離

隨著太陽系裡的物件越來越多,原本比較靠近的行星軌道開始顯得擁擠。
因此今天重新調整 distance:

Mercury distance: 4
Venus distance: 6
Earth distance: 8.5
Mars distance: 12
Jupiter distance: 18
Saturn distance: 24
Uranus distance: 30
Neptune distance: 36

目前外行星的距離明顯拉開,讓 Saturn Ring、Moon 等附屬物件有更多顯示空間。
這裡仍然不是真實天文距離,而是適合網頁展示的視覺化比例
我們希望做到的是:
內行星 → 比較集中
外行星 → 距離逐漸拉開

四、Camera 也要跟著調整

因為 Neptune 已經移動到 distance: 36
原本的 Camera 範圍就稍微太小。
因此 OrbitControls 的最大距離改成:

controls.maxDistance =
    85;

原本鍵盤視角也跟著調整。
例如正面視角:

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

斜上方:

camera.position.set(
    30,
    50,
    40
);

俯視:

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

這樣才能完整看到距離已經拉大的太陽系。

五、重新整理 Shadow

前面 Day 20 我們先讓所有行星都具有 Shadow 能力。
但 Shadow 其實會增加額外的運算。
而目前真正比較需要 Shadow 的主要是:

Earth
Moon
Saturn

所以今天改成:

planet.castShadow =
    data.name === "Earth" ||
    data.name === "Saturn";

planet.receiveShadow =
    data.name === "Earth";

代表:

Earth → 投射 Shadow 接收 Shadow
Saturn → 投射 Shadow
其他行星 → 目前不參與主要 Shadow 計算

這也是目前程式裡使用的 Shadow 判斷方式。
Moon 則繼續保留:

moon.castShadow =
    true;

moon.receiveShadow =
    true;

這樣可以減少一些沒有必要的 Shadow 計算。

六、載入 Saturn Ring Texture

今天新增:
textures/
└── saturn_ring.png

程式:

const saturnRingTexture =
    textureLoader.load(
        "textures/saturn_ring.png"
    );

saturnRingTexture.colorSpace =
    THREE.SRGBColorSpace;

這裡使用 PNG 會比 JPG 更適合。
因為 Saturn Ring 本身有很多 透明、半透明、不同密度的環帶,
PNG 可以保留這些透明資訊。

七、使用 RingGeometry

土星本體使用的是 THREE.SphereGeometry
但土星環則使用 THREE.RingGeometry
先設定:

const innerRadius =
    data.size *
    1.3;

const outerRadius =
    data.size *
    2.2;

再建立:

const ringGeometry =
    new THREE.RingGeometry(
        innerRadius,
        outerRadius,
        128
    );

八、Saturn Ring 的 Texture 為什麼會變形?

一開始如果直接:

map:
    saturnRingTexture

可能會發現 Ring 出現 扇形、分塊、圖片被拉扯
原因其實不是 Texture 壞掉。
而是 RingGeometry 預設 UV 不適合我們現在這種從
內環到外環排列的 Saturn Ring Texture。
所以今天需要重新設定 UV。

九、重新計算 Ring UV

首先取得 Geometry 裡的:

const position =
    ringGeometry
        .attributes
        .position;

const uv =
    ringGeometry
        .attributes
        .uv;

接著逐一取得 Vertex:
const x =
    position.getX(
        i
    );

const y =
    position.getY(
        i
    );

計算它距離圓心多遠:

const distance =
    Math.sqrt(
        x * x +
        y * y
    );

再將距離轉換成 0 ~ 1

const normalizedRadius =
    (
        distance -
        innerRadius
    ) /
    (
        outerRadius -
        innerRadius
    );

最後:

uv.setXY(
    i,
    normalizedRadius,
    0.5
);

以及:

uv.needsUpdate =
    true;

你目前的完整版本已經使用這種依照半徑重新分配 UV 的方式。
可以簡單理解成:
以前
Texture按照 → RingGeometry 原本 UV → 扇形變形
現在
Texture → 按照距離圓心的半徑 → 內環 → 外環

這就是今天第一次真正碰到 UV Mapping

十、Ring Material

這裡我們也遇到另一個問題。
原本使用 THREE.MeshBasicMaterial
雖然 Texture 很亮,但它完全不受到 Light 影響。
後來改成 THREE.MeshStandardMaterial
結果 Ring 又變得太黑。
因此目前的做法是

const ringMaterial =
    new THREE.MeshStandardMaterial({

        map:
            saturnRingTexture,

        transparent:
            true,

        side:
            THREE.DoubleSide,

        depthWrite:
            false,

        roughness:
            1,

        metalness:
            0,

        emissive:
            0xffffff,

        emissiveMap:
            saturnRingTexture,

        emissiveIntensity:
            0.35

    });

十一、為什麼加入 emissiveMap?

這裡:

map:
    saturnRingTexture

負責正常表面。而:

emissiveMap:
    saturnRingTexture

則額外讓 Texture 提供一點自發光效果。
搭配:

emissive:
    0xffffff

以及:

emissiveIntensity:
    0.35

讓 Ring 不會因為 MeshStandardMaterial + 太空環境很暗 直接變成黑色。
可以理解成 PointLight → 正常光照 + Emissive → 少量補亮 = 仍然有光影但不會黑成一片
這個做法比把整個 AmbientLight 調得非常亮好很多。
因為 AmbientLight 如果提高 所有行星 都會一起變亮。
現在只補亮 Saturn Ring

十二、Saturn Ring 的 Shadow

目前 Saturn 本體:

planet.castShadow =
    true;

但是 Ring 在現在這個版本中:

saturnRing.receiveShadow =
    false;

這是目前刻意留下的設定。
原因是我們剛剛測試 Ring 接收 Shadow + MeshStandardMaterial
之後,環很容易變得過暗。
所以目前 Day 23 先選擇:

Saturn
→ 保留 castShadow
Saturn Ring
→ 保留 Lighting
→ 暫時不接收 Shadow

先讓畫面的 Texture 與亮度穩定。
之後如果還想強化 Saturn Shadow,再回來調整:

Light
Shadow
Emissive

三者的平衡。

十三、調整 Saturn Ring 角度

建立完成後:

saturnRing.rotation.x =
    Math.PI /
    2;

讓 Ring 平躺再加入:

saturnRing.rotation.z =
    THREE.MathUtils.degToRad(
        27
    );

製造土星環傾斜的效果。
最後:

planet.add(
    saturnRing
);

Saturn 公轉時 Ring 也會自動跟著一起移動。

十四、加入 Starfield

完成 Saturn 後,再處理整個宇宙背景。
原本只有:

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

也就是黑色背景
今天我們保留黑色背景,再另外建立 2000 顆 Star

十五、建立星星位置

先準備:

const starGeometry =
    new THREE.BufferGeometry();

const starPositions =
    [];

const starCount =
    2000;

接著使用亂數:

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

    const radius =
        100 +
        Math.random() *
        140;

    const theta =
        Math.random() *
        Math.PI *
        2;

    const phi =
        Math.acos(
            2 *
            Math.random() -
            1
        );

    const x =
        radius *
        Math.sin(phi) *
        Math.cos(theta);

    const y =
        radius *
        Math.cos(phi);

    const z =
        radius *
        Math.sin(phi) *
        Math.sin(theta);

    starPositions.push(
        x,
        y,
        z
    );

}

目前每顆星星距離中心大約會落在 80 ~ 200 之間。
所以星星不會全部跑到行星附近,而是包在太陽系外圍。

十六、BufferGeometry

接著:

starGeometry.setAttribute(
    "position",

    new THREE.Float32BufferAttribute(
        starPositions,
        3
    )
);

其中 3 代表:x、y、z 三個座標。
也就是每三個 Number = 一顆 Star 的位置

十七、使用 THREE.Points

建立 Material:

const starMaterial =
    new THREE.PointsMaterial({

        color:
            0xffffff,

        size:
            0.25,

        sizeAttenuation:
            true

    });

再建立:

const stars =
    new THREE.Points(
        starGeometry,
        starMaterial
    );

scene.add(
    stars
);

目前就是使用 2000 個 Point 組成整個 Starfield。
THREE.Points 很適合大量粒子效果,例如:
星星、雪花、火花、灰塵
因為不用每一顆都建立一個完整 Mesh。

十八、今天小結

今天主要完成三件事情。

首先重新調整 Planet Distance + Camera:
讓越來越完整的太陽系有更多空間。

第二個是土星環而今天也遇到兩個很實際的問題:
Texture 變成扇形 → UV Mapping 問題
Ring 變得太黑 → Lighting 與 Material 問題
最後分別使用 重新計算 UV + emissiveMap 改善。

第三個則是THREE.Points:
讓原本只有黑色背景的畫面正式加入星空
從今天開始,主要的視覺效果已經差不多完成。
接下來就要正式進入 使用者互動

Day 24 預告

「UI Controls:替太陽系加入控制面板」

Day 24 我們會開始加入真正可以操作的 UI。
其中 Day 22 特別保存的 venusAtmosphere
就可以直接透過 venusAtmosphere.visible 控制。
另外也會開始把 Planet Orbit 保存起來,讓使用者可以:
顯示軌道 / 隱藏軌道
從 Day 24 開始,我們的作品就不再只是,
觀看太陽系而是開始變成操作太陽系。


上一篇
Day 22 - Planet Effects:地球夜景 × 金星大氣層
下一篇
Day 24 - UI Controls:替太陽系加入控制面板
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言