今天開始替不同的行星加入自己的特色。
這次會處理兩顆
Earth → 夜間城市燈光
Venus → 金星大氣/雲層 Texture
而且金星的大氣層不再只是單純的透明黃色球。
我們會準備 venus.jpg + venus_atmosphere.jpg 把大氣 Texture 套在金星外層。
同時會先把程式架構準備好,之後做 UI 時,就能直接控制來顯示或隱藏金星的大氣層。
今天會完成:
今天需要多準備兩張:
earth_night.jpg、venus_atmosphere.jpg
如果 Day 17、21 的行星 Texture 是從 Solar System Scope 下載的,月球也可以繼續使用同一套素材。這邊一樣附上連結:點我前往
之前地球其實和其他行星一樣,都是:
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;
今天地球最重要的新東西就是 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:
0.6
它控制夜景的亮度。
如果你的 earth_night.jpg 本身就很亮,可以再把數值降低。
要注意,目前這種做法是比較簡單的視覺模擬 emissiveMap 本身不會真正判斷
現在這個位置是不是背對太陽因此亮面也可能稍微看到夜景 Texture。
只是受到 PointLight 照亮後,白天 Texture 通常會比較明顯。
後面如果想做到非常精準的只有背光面出現城市燈就會需要 Shader。
目前先使用比較適合這個系列難度的做法。
接著是今天第二個重點 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%
所以外層只比本體大一點點。
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,可以降低某些透明材質互相遮擋時的怪異效果。
建立:
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 → 另外慢慢轉動
讓雲層與表面之間出現一些相對運動。
原本所有行星完成自轉與公轉之後,再加入:
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,
讓金星有了大氣層的效果。
今天最大的變化就是讓不同星球開始真正有自己的視覺特色。
「Saturn Ring × Starfield:加入土星環與宇宙星空」
Day 23 我們會再處理另一顆非常有特色的行星土星環
這次會使用 THREE.RingGeometry 建立土星環。
而且跟今天的 Venus 一樣,不只使用單一顏色,而是加入 土星環 + 透明效果,
另外也會把目前純黑背景升級成星空背景,讓整個太陽系正式開始有宇宙空間的感覺。