現在整個專案已經有相當完整的互動功能。
但功能越來越多之後,也會開始遇到兩個很實際的問題:
不同螢幕尺寸
↓
UI 可能擠在一起
Geometry、Shadow、Pixel Ratio
↓
效能開始受到影響
所以今天不再新增大型功能,而是開始做:
Responsive + Performance Optimization
替 Day 29 的整合 Debug,還有 Day 30 最終完成做準備。
由於目前專案程式碼已逐漸增加,從 Day 23 開始文章只列出當日新增或修改的程式碼。
完整整合版將於 Day 30 最終完成時提供。
今天會完成:
| 項目 | 修改內容 |
|---|---|
| CSS | 新增 Responsive Media Query |
| Title | 小螢幕縮小字體 |
| Control Panel | 手機版縮小並改位置 |
| Planet Info | 小螢幕改成底部卡片 |
| Renderer | Pixel Ratio 維持最大 2 |
| Geometry | Planet Segment 從 64 降成 48 |
| Moon | Segment 從 64 降成 32 |
| Resize | 小螢幕時重新調整 Camera |
| Shadow | 繼續只保留必要物件 |
目前我們的 UI 大概是:
左上角 → Title
右上角 → Control Panel
左下角 → Planet Info
桌面螢幕看起來沒問題。
但如果螢幕變成手機、平板、小型筆電
可能就會發生:
Title 太大、Control Panel 太寬、Planet Info 擋住畫面、UI 互相重疊
所以今天會使用:@media
來調整不同螢幕尺寸。
目前:
.title {
top: 30px;
left: 40px;
}
.title h1 {
font-size: 36px;
}
桌面版沒問題,但是手機版 36px 就會有點太大。
所以加入:
@media (
max-width: 768px
) {
.title {
top: 18px;
left: 18px;
}
.title h1 {
font-size: 24px;
}
.title p {
font-size: 13px;
}
}
這樣:
Desktop → 36px
Mobile → 24px
目前 Control Panel:
width: 230px;
top: 30px;
right: 30px;
手機上可能太寬所以:
@media (
max-width: 768px
) {
.control-panel {
top: 90px;
right: 12px;
width: 190px;
padding: 14px;
}
}
這樣可以避免 Title + Control Panel 互相撞在一起。
Desktop 版左下角很適合。
但手機版畫面比較窄,所以可以改成底部左右留一點空間。
CSS:
@media (
max-width: 768px
) {
.planet-info {
left: 12px;
right: 12px;
bottom: 12px;
width: auto;
padding: 16px;
}
}
原本 width: 280px;
手機版改成 width: auto;
讓它自動填滿可用空間。
前面我們已經有:
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
這一段其實就是一個很重要的效能優化。
因為有些手機可能:
devicePixelRatio
=
3
4
如果直接:
renderer.setPixelRatio(
window.devicePixelRatio
);
GPU 需要處理的 Pixel 數量會大量增加。
假設螢幕:1920 × 1080
Pixel Ratio 1:1920 × 1080
Pixel Ratio 2:3840 × 2160
Pixel Ratio 3:5760 × 3240
差距非常大。
所以:
Math.min(
window.devicePixelRatio,
2
)
代表最多使用 2
通常畫面已經很清楚,又不會造成太高負擔。
這段我們繼續保留。
目前 Planet:
new THREE.SphereGeometry(
data.size,
64,
64
);
這會產生非常多 Vertex。
八顆行星全部使用 64 × 64 其實有點浪費。
因為我們的 Planet 在大部分畫面裡都不算非常大。
所以可以改成:
new THREE.SphereGeometry(
data.size,
48,
48
);
視覺差異通常不會太明顯。
Moon 本來只有 size = 0.2 非常小。
所以:
new THREE.SphereGeometry(
0.2,
64,
64
);
其實完全不需要這麼高。改成:
new THREE.SphereGeometry(
0.2,
32,
32
);
就已經很足夠。
Venus Atmosphere:
new THREE.SphereGeometry(
data.size * 1.03,
64,
64
);
可以改成:
new THREE.SphereGeometry(
data.size * 1.03,
48,
48
);
因為外層主要是 Texture + Transparent
通常不需要太高 Polygon。
Sun 是整個系統中心 + 比較大的球
而且 Camera 有時候會靠近。
所以 Sun 可以先保留 64,64
這樣中心天體仍然比較圓滑。
目前可以採用:
Sun → 64 × 64
Planet → 48 × 48
Venus Atmosphere → 48 × 48
Moon → 32 × 32
這樣比較合理。
我們前面已經把 Shadow 縮減成:
Earth
Moon
Saturn
這個策略今天不需要改回去。
因為 Shadow 通常是 Three.js 中:
比較昂貴的效果
尤其現在的 Light 是 THREE.PointLight
PointLight Shadow 比 DirectionalLight 更重,因為它需要處理多個方向。
所以目前:
planet.castShadow =
data.name === "Earth" ||
data.name === "Saturn";
planet.receiveShadow =
data.name === "Earth";
繼續保留。
Moon:
moon.castShadow =
true;
moon.receiveShadow =
true;
也繼續保留。
我們現在使用的是THREE.Points
不是 2000 個 THREE.Mesh 這本身也是一個效能優化。
因為 2000 個 Mesh → 2000 個獨立物件
THREE.Points
→ 一個 Geometry
→ 一次管理大量 Points
所以目前:
const starCount =
2000;
其實還算合理。
而且你現在的生成範圍是:
100 ~ 140
const radius =
100 +
Math.random() *
40;
今天可以直接保留。
現在 Resize 已經有:
camera.aspect =
window.innerWidth /
window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(
window.innerWidth,
window.innerHeight
);
這是正確的。
今天可以再加入:
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
確保視窗尺寸改變 Pixel Ratio 也重新確認
手機畫面比較窄。
如果 Camera 距離和 Desktop 一樣,
Planet 可能會太靠近畫面邊緣。
所以可以建立:
function updateCameraForScreen() {
if (
window.innerWidth <=
768
) {
if (
!focusMode
) {
camera.position.set(
45,
40,
55
);
}
}
}
然後 updateCameraForScreen();
以及 Resize updateCameraForScreen();
注意這個判斷:
if (
!focusMode
)
如果目前正在 Earth Focus
這時使用者旋轉手機或調整視窗。
如果 Resize Function 直接 camera.position.set(...)
就會把 Planet Focus 破壞掉。
所以只有沒有進入 Focus Mode 時,才重新設定 Camera。
完整一點可以寫:
function updateCameraForScreen() {
if (
focusMode
) {
return;
}
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();
}
這樣:
Desktop → 30, 25, 30
Mobile → 45, 40, 55
手機 Camera 稍微往外。
我們前面有 四種 Camera Preset。
如果每次 Resize 都執行 updateCameraForScreen();
會把使用者自己選的 Camera Preset 重設掉。
所以今天可以採取比較保守的做法:
頁面第一次載入 → 根據 Screen 設定 Camera
Resize → 只更新 aspect / renderer
這樣比較不會破壞:
Camera Shortcut
Planet Focus
OrbitControls
Planet:
const geometry =
new THREE.SphereGeometry(
data.size,
48,
48
);
Venus:
const atmosphereGeometry =
new THREE.SphereGeometry(
data.size *
1.03,
48,
48
);
Moon:
const moonGeometry =
new THREE.SphereGeometry(
0.2,
32,
32
);
Sun 仍然:
const sunGeometry =
new THREE.SphereGeometry(
2,
64,
64
);
主要加入:
@media (
max-width: 768px
) {
.title {
top: 18px;
left: 18px;
}
.title h1 {
font-size: 24px;
}
.title p {
font-size: 13px;
}
.control-panel {
top: 90px;
right: 12px;
width: 190px;
padding: 14px;
}
.control-panel h2 {
font-size: 14px;
}
.control-panel label,
.control-panel button {
font-size: 13px;
}
.planet-info {
left: 12px;
right: 12px;
bottom: 12px;
width: auto;
padding: 16px;
}
}
今天除了完成 Responsive 與 Performance Optimization,
最後也可以實際用「手機版」來檢查成果。
如果你是使用 VS Code + Live Server,可以用兩種方式測試。
先在瀏覽器開啟專案。
接著按 F12 開啟 Developer Tools。
再按 Ctrl + Shift + M 切換成 Device Toolbar。
接著就可以選擇 iPhone、Pixel、iPad 等不同裝置尺寸。
這時我們今天加入的:
@media (
max-width: 768px
)
就會開始生效。
可以檢查
Title 是否縮小
Control Panel 是否跑到適合的位置
Planet Info 是否正確顯示在畫面底部
整個 3D Scene 是否仍然可以正常操作
如果想確認真正手機上的操作感,
像是觸控、滑動、縮放、效能、UI 大小
就可以讓手機直接連到電腦上的 Live Server。
首先讓 電腦+手機 連到同一個 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/
就可以直接用手機瀏覽器打開目前的 Three.js 太陽系。
注意 127.0.0.1 代表目前這一台裝置自己。
所以手機不能直接使用 127.0.0.1 而是要使用電腦的區域網路 IP。
今天完成之後,我們不只是讓程式可以執行
而是開始檢查 Desktop + Tablet + Mobile 在不同裝置上是否都能正常操作。
這也是 Responsive Web Design 很重要的一部分。
Day 29 就會進入最後的 Integration + Debug 把目前所有功能一起檢查,
準備 Day 30 的最終完整作品。
「Integration × Debug:最終整合與問題修正」
Day 29 不會再加入大型新功能。
我們會把目前所有功能全部重新檢查。
包含:
選中 Planet 後 Orbit 是否正常?
Focus Mode 是否會跟著 Planet?
Pause 時 Focus 是否正常?
Orbit Toggle 是否影響 Highlight?
手機版 UI 是否會遮住 Planet Info?
返回太陽系是否會正確清除 selectedPlanet?
按 1~4 Camera Preset 是否會退出 Focus Mode?
Day 29 的目標就是找 Bug → 修 Bug → 整理程式 → 準備 Day 30 最終作品