iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
Modern Web

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

Day 29 - Integration × Debug:最終整合與問題修正

  • 分享至 

  • xImage
  •  

一路做到 Day 28,目前我們功能其實已經相當完整。
但專案做到這個階段後,最重要的事情反而不是繼續加功能,而是:
把所有功能一起操作找出彼此衝突的地方並且修正 Bug
還有整理程式確認 Desktop / Mobile 都能正常操作
所以 Day 29 的主題就是:Integration + Debug

Day 29 不加入大型新功能,而是把前面做過的功能全部重新測試與修正。
Day 30 就會正式進入最終完整作品。

一、今天完成目標

今天主要處理:

  • 全面測試目前所有功能
  • 修正 Planet Focus 時 Camera 操作不自然的問題
  • 修正拖曳 OrbitControls 時可能誤選 Planet
  • 讓 Focus Mode 中仍然可以自由旋轉 Camera
  • 整理 Planet Selection 程式
  • 檢查 Pause 與 Focus 是否能同時運作
  • 檢查 Orbit Toggle 與 Highlight
  • 改善手機版 UI 高度問題
  • 建立最終功能測試清單

二、本日修改摘要

問題 Day 29 修正
Focus Camera 不再每幀強制重設 Camera Offset
OrbitControls Focus 時仍可自由旋轉
Planet Selection Pointer 移動超過一定距離就不算 Click
Mobile Touch 避免滑動畫面時誤選 Planet
UI 小螢幕控制面板可捲動
Debug 建立完整功能測試流程

三、第一個問題:Focus Camera 被「鎖死」

Day 27 我們使用:

camera.position.copy(
    selectedPlanet.mesh.position
);

camera.position.add(
    focusCameraOffset
);

每一幀讓 Camera 跟著 Planet 的這個概念沒有錯。
但實際操作後會發現:
進入 Planet Focus 使用滑鼠旋轉 Camera
而下一幀 Camera 又被原本的 focusCameraOffset 拉回去
原因就是我們每一幀都重新指定 Camera Position
所以 OrbitControls 剛剛做出的 Camera 旋轉結果,很容易被覆蓋。

三 - 1、Focus Mode 應該只追蹤「Planet 移動量」

我們真正需要的是:
Planet 往前移動多少 → Camera 跟著移動多少
而不是:
每一幀 → 重新設定 Camera 固定位置

所以今天改成記錄:

const previousFocusPosition =
    new THREE.Vector3();

它負責記錄:
上一幀 Planet 在哪裡

三 - 2、進入 Focus 時先記住 Planet Position

修改 javascript:

function focusPlanet(
    planet
) {

    selectedPlanet =
        planet;

    focusMode =
        true;


    const focusDistance =
        planet.size *
        6 +
        3;


    camera.position.set(

        planet.mesh.position.x +
            focusDistance,

        planet.mesh.position.y +
            focusDistance *
            0.5,

        planet.mesh.position.z +
            focusDistance

    );


    controls.target.copy(
        planet.mesh.position
    );


    previousFocusPosition.copy(
        planet.mesh.position
    );


    controls.update();


    showPlanetInfo(
        planet
    );

}

注意今天已經不需要原本的 focusCameraOffset 了。

三 - 3、計算 Planet 每一幀移動多少

建立:

const focusMovement =
    new THREE.Vector3();

Animation 裡:
if (
    focusMode &&
    selectedPlanet
) {

    focusMovement
        .copy(
            selectedPlanet
                .mesh
                .position
        )
        .sub(
            previousFocusPosition
        );

}

代表現在位置 - 上一幀位置 = 這一幀移動多少

例如:
上一幀 Earth

X = 5.00
Z = 3.00

這一幀 Earth

X = 5.05
Z = 3.02

那 Movement

X = 0.05
Z = 0.02

三 - 4、Camera 跟著相同距離移動

取得 Movement 後:

camera.position.add(
    focusMovement
);

也就是 Planet 往右移動 0.05
Camera 也往右移動 0.05
接著:

controls.target.copy(
    selectedPlanet.mesh.position
);

Camera 仍然以 Planet 為中心。
最後更新:

previousFocusPosition.copy(
    selectedPlanet.mesh.position
);

三 - 5、這樣有什麼不同?

以前:

Planet 移動 Camera 被強制放回固定 Offset
OrbitControls 操作容易被覆蓋

現在:

Planet 移動計算 Planet Movement
Camera 只跟著平移 OrbitControls 自己控制旋轉角度

因此進入 Earth Focus 後仍然可以
拖曳滑鼠繞 Earth 觀看 + 滾輪可以靠近 / 遠離 Earth
同時 Earth 繼續公轉,
這才比較符合我們想要的 Planet Tracking + OrbitControls。

四、第二個問題:旋轉 Camera 可能誤選 Planet

Day 26 我們使用:

renderer.domElement.addEventListener(
    "pointerdown",
    ...
);

問題是 Pointer Down
不一定代表使用者真的想選 Planet
他可能只是想按住滑鼠拖動畫面
如果剛好按在 Planet 上
Camera 還沒開始轉 Planet 就被選中了
在手機上也會發生類似情況:

所以今天要把 Click 與 Drag 區分開。

四 - 1、記錄 Pointer Down Position

新增:

const pointerStart =
    new THREE.Vector2();

按下滑鼠時:

renderer.domElement.addEventListener(
    "pointerdown",
    (event) => {

        pointerStart.set(
            event.clientX,
            event.clientY
        );

    }
);

只是單純記住滑鼠從哪裡開始

四 - 2、Pointer Up 時計算移動距離

放開時:

const pointerEnd =
    new THREE.Vector2(
        event.clientX,
        event.clientY
    );

再計算:

const pointerDistance =
    pointerStart.distanceTo(
        pointerEnd
    );

十二、移動太多就當作 Camera Drag
設定:

if (
    pointerDistance >
    5
) {

    return;

}

意思就是:

滑鼠移動 ≤ 5px → 視為 Click
滑鼠移動 > 5px → 視為 Drag 不執行 Raycaster

因此:

點 Planet → Select Planet
拖動畫面經過 Planet → 不 Select

操作會自然很多。

五、把 Raycaster 整理成 Function

Day 29 我們順便把 Planet Selection 整理一下。
建立:

function selectPlanet(
    event
) {

    const rect =
        renderer.domElement
            .getBoundingClientRect();


    mouse.x =
        (
            (
                event.clientX -
                rect.left
            ) /
            rect.width
        ) *
        2 -
        1;


    mouse.y =
        -(
            (
                event.clientY -
                rect.top
            ) /
            rect.height
        ) *
        2 +
        1;


    raycaster.setFromCamera(
        mouse,
        camera
    );


    const planetMeshes =
        planets.map(
            (planet) =>
                planet.mesh
        );


    const intersects =
        raycaster.intersectObjects(
            planetMeshes,
            false
        );


    if (
        intersects.length ===
        0
    ) {

        return;

    }


    const clickedMesh =
        intersects[0].object;


    const clickedPlanet =
        planets.find(
            (planet) =>
                planet.mesh ===
                clickedMesh
        );


    if (
        !clickedPlanet
    ) {

        return;

    }


    resetOrbitColors();


    highlightOrbit(
        clickedPlanet.name
    );


    focusPlanet(
        clickedPlanet
    );

}

六、Pointer Up 再真正選擇

現在:

renderer.domElement.addEventListener(
    "pointerup",
    (event) => {

        const pointerEnd =
            new THREE.Vector2(
                event.clientX,
                event.clientY
            );


        const pointerDistance =
            pointerStart.distanceTo(
                pointerEnd
            );


        if (
            pointerDistance >
            5
        ) {

            return;

        }


        selectPlanet(
            event
        );

    }
);

流程變成:
Pointer Down → 記住開始位置
Pointer Move → OrbitControls 可以正常處理
Pointer Up → 計算移動距離是否小於 5px?
→ IF YES → Raycaster → Planet Selection

七、Pause × Planet Focus 測試

這是今天很重要的整合測試。
假設現在 Focus Earth
再按 Pause 此時:
Earth → 停止公轉
Moon → 停止運動
Sun → 停止旋轉
但 OrbitControls 仍然在 Animation Loop 裡更新。
所以仍可以繞著 Earth 旋轉 Camera

這就是我們前面刻意沒有停止:

requestAnimationFrame(
    animate
);

的原因。
我們停止的是天體動畫更新而不是整個 Render Loop

七、Speed × Focus 也要測試

接著測 Earth Focus Speed 5x
Earth 會快速公轉。
現在 Focus Mode 使用的是 Planet Movement
所以 Camera 也會跟著 Earth 的移動量一起移動。
因此理論上任何速度都可以正常追蹤。
這就是整合 Debug 很重要的地方。
單獨看 Speed 沒有問題。
單獨看 Focus 也沒有問題。
但最後一定要測 Speed + Focus 一起執行。

八、Orbit Toggle × Highlight

還需要測點 Earth
Earth Orbit 黃色
關閉「顯示軌道」

因為我們是修改 orbit.line.visible
所以 Earth Orbit 雖然還保留黃色 Material,但會被隱藏。
重新開啟顯示軌道 Earth Orbit 還是黃色,這其實是合理的
因為 selectedPlanet 仍然是 Earth。

九、返回太陽系

點返回太陽系應該同時完成:

focusMode       = false
selectedPlanet  = null
Camera          回到初始位置
controls.target 回 Sun
Planet Info     消失
Orbit Highlight 清除

目前:

function returnToSolarSystem() {

    focusMode =
        false;

    selectedPlanet =
        null;


    if (
        window.innerWidth <=
        768
    ) {

        camera.position.set(
            45,
            40,
            55
        );

    } else {

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

    }


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


    controls.update();


    planetInfo.classList.remove(
        "active"
    );


    resetOrbitColors();

}

Desktop 與 Mobile 也會分別回到不同距離。

十、Camera Shortcut × Focus

我們還有四種 Camera View。
所以一定要測Focus Saturn 時按 3
這時應該:

退出 Focus
     ↓
Planet Info 消失
     ↓ 
Orbit 恢復灰色
     ↓
切換到 Camera View 3

因此 Keyboard Event 裡保留:

focusMode =
    false;

selectedPlanet =
    null;

planetInfo.classList.remove(
    "active"
);

resetOrbitColors();

十一、Mobile UI 再補一個小修正

手機高度比較短時 Control Panel 有可能超過畫面高度。
所以今天加入:

@media (
    max-width: 768px
) {

    .control-panel {
        max-height:
            calc(
                100vh -
                110px
            );

        overflow-y:
            auto;
    }

}

如果控制面板超出手機畫面
不會直接被裁掉可以上下捲動

十二、Touch 操作

今天完成 Pointer 修正後:

滑動 → OrbitControls
Tap Planet → Raycaster
Pinch → Zoom

在手機上會比直接使用 pointerdown 選 Planet 更容易操作。
這也是為什麼我們使用 Pointer Events 而不是只處理 mousedown
因為 Pointer Event 可以比較統一處理:

Mouse
Touch
Pen

十三、Day 29 功能測試

今天文章很適合讓讀者真正把功能逐項測一次。
先啟動 Live Server。
然後按照下面順序操作:

  • 太陽系是否正常運行?
  • Speed Slider 是否能控制速度?
  • Pause 是否能停止天體?
  • Pause 時 Camera 還能不能操作?
  • Orbit Checkbox 是否正常?
  • Venus Atmosphere 能不能關閉?
  • Starfield 能不能關閉?
  • 點 Earth 是否能 Focus?
  • Earth Orbit 是否變黃色?
  • Earth 公轉時 Camera 是否跟著?
  • Focus 時能不能自由旋轉 Camera?
  • Focus 時調成 5x 是否仍能正常追蹤?
  • 返回太陽系是否正常?
  • 按 1~4 是否能退出 Focus?
  • 拖曳 Camera 時是否不會誤選 Planet?
  • 手機 Touch 滑動是否正常?
  • 手機 Planet Info 是否正常顯示?

如果這些都正常代表核心功能整合完成

十四、怎麼用手機測 Day 29?

Day 28 已經提過手機測試,今天 Debug 時一定要真的再測一次。
首先手機 + 電腦連同一個 Wi-Fi。
Windows CMD:ipconfig
例如找到:
IPv4 Address 192.168.1.105
Live Server 如果是:
http://127.0.0.1:5500/
手機輸入:
http://192.168.1.105:5500/
接著實際測:

單指拖曳
雙指縮放
點擊 Planet
Focus Planet
Play / Pause
Slider
返回太陽系

因為 Chrome Device Mode 比較適合檢查 Responsive Layout
真正手機則可以檢查 Touch + Performance + 操作手感 兩種最好都測。

十五、今天小節

今天的重點不是又多做了一個功能
而是第一次真正以「使用者會怎麼操作?」來檢查整個作品。
我們修正了 Planet Focus + OrbitControls 互相衝突的問題。
以前 Camera 每幀都被強制放回固定 Offset。
現在改成 Planet Movement 時 Camera 可以跟著平移
因此使用者可以 Focus Planet + 自由旋轉 Camera 同時存在。
另外我們也把 Pointer Down
改成判斷:

Click
or
Drag

避免想旋轉畫面卻誤選 Planet。

最後今天最重要的工作就是把所有功能全部操作一次
如果你是讀者,也不要只確認畫面有出現
而是實際測每個功能是不是都正常
因為一個功能單獨正常不代表整合之後也一定正常
這就是 Debug 階段真正重要的地方。

Day 30 預告

「Final Project:30 天打造互動式太陽系」

終於剩最後一天,Day 30 我們會把 30 天的內容全部整合。
會整理完整:

solar-system/
│
├── index.html
│
├── css/
│   └── style.css
│
├── js/
│   └── main.js
│
└── textures/
    ├── sun.jpg
    ├── mercury.jpg
    ├── venus.jpg
    ├── venus_atmosphere.jpg
    ├── earth.jpg
    ├── earth_night.jpg
    ├── moon.jpg
    ├── mars.jpg
    ├── jupiter.jpg
    ├── saturn.jpg
    ├── saturn_ring.png
    ├── uranus.jpg
    └── neptune.jpg

並提供

完整 index.html
完整 style.css
完整 main.js
Texture 檔案說明
專案架構
功能整理
作品說明
30 天總結

Day 30 就會是這個系列真正的 Final Version


上一篇
Day 28 - Responsive × Performance:讓太陽系在不同裝置更順暢
下一篇
Day 30 - Final Project:30 天打造互動式太陽系
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言