今天要繼續處理另一顆非常有辨識度的行星:土星
除了加入 Saturn Ring,我們也會把原本單純的黑色背景升級成星空背景
另外,隨著 Moon、Venus Atmosphere、Saturn Ring 等物件逐漸加入,原本的行星距離開始顯得比較擁擠,所以今天也會重新調整整個太陽系的距離與 Camera 範圍。
由於目前專案程式碼已逐漸增加,從本篇開始只列出當日新增或修改的程式碼。
完整整合版將於 Day 30 最終完成時提供。
今天會完成:
今天需要多準備一張:
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 等附屬物件有更多顯示空間。
這裡仍然不是真實天文距離,而是適合網頁展示的視覺化比例
我們希望做到的是:
內行星 → 比較集中
外行星 → 距離逐漸拉開
因為 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
);
這樣才能完整看到距離已經拉大的太陽系。
前面 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 計算。
今天新增:
textures/
└── saturn_ring.png
程式:
const saturnRingTexture =
textureLoader.load(
"textures/saturn_ring.png"
);
saturnRingTexture.colorSpace =
THREE.SRGBColorSpace;
這裡使用 PNG 會比 JPG 更適合。
因為 Saturn Ring 本身有很多 透明、半透明、不同密度的環帶,
PNG 可以保留這些透明資訊。
土星本體使用的是 THREE.SphereGeometry
但土星環則使用 THREE.RingGeometry
先設定:
const innerRadius =
data.size *
1.3;
const outerRadius =
data.size *
2.2;
再建立:
const ringGeometry =
new THREE.RingGeometry(
innerRadius,
outerRadius,
128
);
一開始如果直接:
map:
saturnRingTexture
可能會發現 Ring 出現 扇形、分塊、圖片被拉扯
原因其實不是 Texture 壞掉。
而是 RingGeometry 預設 UV 不適合我們現在這種從
內環到外環排列的 Saturn Ring Texture。
所以今天需要重新設定 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
這裡我們也遇到另一個問題。
原本使用 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
});
這裡:
map:
saturnRingTexture
負責正常表面。而:
emissiveMap:
saturnRingTexture
則額外讓 Texture 提供一點自發光效果。
搭配:
emissive:
0xffffff
以及:
emissiveIntensity:
0.35
讓 Ring 不會因為 MeshStandardMaterial + 太空環境很暗 直接變成黑色。
可以理解成 PointLight → 正常光照 + Emissive → 少量補亮 = 仍然有光影但不會黑成一片
這個做法比把整個 AmbientLight 調得非常亮好很多。
因為 AmbientLight 如果提高 所有行星 都會一起變亮。
現在只補亮 Saturn Ring
目前 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
三者的平衡。
建立完成後:
saturnRing.rotation.x =
Math.PI /
2;
讓 Ring 平躺再加入:
saturnRing.rotation.z =
THREE.MathUtils.degToRad(
27
);
製造土星環傾斜的效果。
最後:
planet.add(
saturnRing
);
Saturn 公轉時 Ring 也會自動跟著一起移動。
完成 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 之間。
所以星星不會全部跑到行星附近,而是包在太陽系外圍。
接著:
starGeometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(
starPositions,
3
)
);
其中 3 代表:x、y、z 三個座標。
也就是每三個 Number = 一顆 Star 的位置
建立 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:
讓原本只有黑色背景的畫面正式加入星空
從今天開始,主要的視覺效果已經差不多完成。
接下來就要正式進入 使用者互動
「UI Controls:替太陽系加入控制面板」
Day 24 我們會開始加入真正可以操作的 UI。
其中 Day 22 特別保存的 venusAtmosphere
就可以直接透過 venusAtmosphere.visible 控制。
另外也會開始把 Planet Orbit 保存起來,讓使用者可以:
顯示軌道 / 隱藏軌道
從 Day 24 開始,我們的作品就不再只是,
觀看太陽系而是開始變成操作太陽系。