Day 20 我們已經把 Three.js 的 Shadow 系統準備好了。
今天要第一次加入「衛星」。
目前所有行星都是繞著太陽
今天加入月球之後,月球會繞著地球
今天會完成:
今天需要多準備一張:
textures/
└── moon.jpg
如果 Day 17 的行星 Texture 是從 Solar System Scope 下載的,月球也可以繼續使用同一套素材。這邊一樣附上連結:點我前往
我們現在的八大行星都是透過 planetData.forEach(...) 自動建立。
但今天 月亮 必須知道 地球 現在在哪裡?
所以建立行星以前,先加入:
let earth = null;
接著建立行星時:
if (data.name === "Earth") {
earth =
planet;
}
這樣之後就可以透過 earth.position 取得目前地球的位置。
這邊的步驟跟之前建立其他星球時一樣,
先載入月球 Texture:
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 也需要 Shadow:
moon.castShadow =
true;
moon.receiveShadow =
true;
最後加入 Scene:
scene.add(
moon
);
我們準備三個資料:
const moonDistance =
1.3;
const moonOrbitSpeed =
2;
let moonAngle =
0;
其中
moonDistance 代表月球距離地球多遠
moonOrbitSpeed 代表月球繞地球的速度
moonAngle 代表目前公轉到哪個角度
這和我們八大行星的差別只是:
八大行星的中心是太陽,而月亮的中心是地球。
八大行星的公轉是:
planet.mesh.position.x =
Math.cos(
planet.angle
) *
planet.distance;
planet.mesh.position.z =
Math.sin(
planet.angle
) *
planet.distance;
因為 Sun 在 (0, 0, 0) 所以可以直接從原點計算。
但是 Moon 不一樣。Moon 的中心是 earth.position
因此要寫成:
moon.position.x =
earth.position.x +
Math.cos(
moonAngle
) *
moonDistance;
moon.position.z =
earth.position.z +
Math.sin(
moonAngle
) *
moonDistance;
注意這裡多了 earth.position.x 和 earth.position.z,
這就是今天最重要的地方。
可以理解成:Moon 世界座標 = Earth 世界座標 + Moon 相對 Earth 的位置。
動畫裡加入:
moon.rotation.y +=
0.4 *
delta;
所以 Moon 現在也會 自轉 + 公轉 跟行星一樣。
既然八大行星都有 Orbit,Moon 當然也可以有。
建立軌道:
const moonOrbitPoints =
[];
const moonOrbitSegments =
64;
for (
let i = 0;
i < moonOrbitSegments;
i++
) {
const angle =
(i / moonOrbitSegments) *
Math.PI *
2;
const x =
Math.cos(
angle
) *
moonDistance;
const z =
Math.sin(
angle
) *
moonDistance;
moonOrbitPoints.push(
new THREE.Vector3(
x,
0,
z
)
);
}
接著:
const moonOrbitGeometry =
new THREE.BufferGeometry()
.setFromPoints(
moonOrbitPoints
);
const moonOrbitMaterial =
new THREE.LineBasicMaterial({
color: 0x666666
});
const moonOrbit =
new THREE.LineLoop(
moonOrbitGeometry,
moonOrbitMaterial
);
scene.add(
moonOrbit
);
這裡又有一個很重要的問題,就是我們要讓月球的軌道跟著 Earth 的位置一直公轉
所以 Moon 的軌道不能永遠留在原本的位置。
動畫裡需要加入:
moonOrbit.position.copy(
earth.position
);
也就是 Earth 移動到哪裡 Moon Orbit 就跟到哪裡。
Day 21 最重要的動畫程式就是:
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
);
Three.js 其實可以寫:
earth.add(
moon
);
這樣 Moon 就會直接成為 Earth 的子物件。
但目前我們的 Earth 本身正在 earth.rotation.y += ... 也就是不停自轉。
如果 Moon 直接成為 Earth 的子物件,
Earth rotation 可能連 Moon 的整個座標系一起轉,
這會把 Earth 自轉和 Moon 公轉綁得太緊。
所以目前我們先採用比較容易理解的方法:
計算 Earth 目前位置,之後讓 Moon 使用 Earth 位置作為中心。
後面如果專案架構繼續變大,也可以再改成 THREE.Group來管理完整的地月系統。
Day 20 我們設定:
planet.castShadow =
true;
planet.receiveShadow =
true;
Moon 今天也加入:
moon.castShadow =
true;
moon.receiveShadow =
true;
所以現在理論上當 Moon 剛好位於 Sun 和 Earth 中間時,Moon 可以遮擋太陽光。
反過來Earth 也可能讓 Moon 進入陰影。
這就是 Day 20 Shadow 系統在地月系統中的實際用途。
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
);
// ========================
// Point Light
// ========================
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
);
// ========================
// Ambient Light
// ========================
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"
}
];
// ========================
// Create Orbits
// ========================
planetData.forEach(
(data) => {
const points =
[];
const segments =
128;
for (
let i = 0;
i < segments;
i++
) {
const angle =
(i / segments) *
Math.PI *
2;
const x =
Math.cos(
angle
) *
data.distance;
const z =
Math.sin(
angle
) *
data.distance;
points.push(
new THREE.Vector3(
x,
0,
z
)
);
}
const orbitGeometry =
new THREE.BufferGeometry()
.setFromPoints(
points
);
const orbitMaterial =
new THREE.LineBasicMaterial({
color: 0x444444
});
const orbit =
new THREE.LineLoop(
orbitGeometry,
orbitMaterial
);
scene.add(
orbit
);
}
);
// ========================
// Create Planets
// ========================
const planets =
[];
let earth =
null;
planetData.forEach(
(data, index) => {
const planetTexture =
textureLoader.load(
data.texture
);
planetTexture.colorSpace =
THREE.SRGBColorSpace;
const geometry =
new THREE.SphereGeometry(
data.size,
64,
64
);
const material =
new THREE.MeshStandardMaterial({
map: planetTexture,
roughness: 1,
metalness: 0
});
const planet =
new THREE.Mesh(
geometry,
material
);
planet.castShadow =
true;
planet.receiveShadow =
true;
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
);
// Moon Data
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;
const x =
Math.cos(
angle
) *
moonDistance;
const z =
Math.sin(
angle
) *
moonDistance;
moonOrbitPoints.push(
new THREE.Vector3(
x,
0,
z
)
);
}
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 Rotation
sun.rotation.y +=
0.5 *
delta;
// Planet Animation
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;
}
);
// Moon Orbit
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
moon.rotation.y +=
0.4 *
delta;
// Moon Orbit follows Earth
moonOrbit.position.copy(
earth.position
);
controls.update();
renderer.render(
scene,
camera
);
}
animate();
今天我們第一次建立了衛星系統
而今天最重要的座標概念就是:
moon.position.x =
earth.position.x +
Math.cos(
moonAngle
) *
moonDistance;
也就是 月球的位置 = 地球的位置 + 月球自己的公轉位置
今日完成畫面
「Planet Effects:地球夜景 × 金星大氣層」
Day 22 會開始做我們之前決定加入的行星特殊效果。
首先是地球白天地球 Texture + 夜間城市燈光
我們會準備:earth.jpg、earth_night.jpg
並讓城市燈光出現在地球的暗面,另外還會處理金星:
金星
├── 金星本體
└── 半透明 Atmosphere(大氣層)
讓金星外面多一層淡黃色的大氣效果。