Day 12 我們已經讓八大行星開始繞太陽公轉。
今天要做的是把每顆行星的公轉路線畫出來。
目前行星雖然有在繞太陽移動,但是沒有軌道線時,畫面會比較難看出它到底沿著什麼路線運行,
所以今天我們會讓每顆行星都有自己的圓形軌道。
今天會完成:
index.html
與 Day 12 相同,不需要修改。(完整在 Day 9)
css/style.css
與 Day 12 相同,不需要修改。(完整在 Day 9)
js/main.js
import * as THREE from
"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js";
// ========================
// Scene
// ========================
const scene =
new THREE.Scene();
scene.background =
new THREE.Color(
0x000000
);
// ========================
// Camera
// ========================
const camera =
new THREE.PerspectiveCamera(
65,
window.innerWidth /
window.innerHeight,
0.1,
1000
);
camera.position.set(
0,
20,
36
);
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
)
);
document
.getElementById("scene")
.appendChild(
renderer.domElement
);
// ========================
// Sun
// ========================
const sunGeometry =
new THREE.SphereGeometry(
2,
32,
16
);
const sunMaterial =
new THREE.MeshBasicMaterial({
color: 0xffaa00
});
const sun =
new THREE.Mesh(
sunGeometry,
sunMaterial
);
const sunwireMaterial =
new THREE.MeshBasicMaterial({
color: 0xffffff,
wireframe: true
});
const sunwire =
new THREE.Mesh(
sunGeometry,
sunwireMaterial
);
sunwire.scale.set(
1.03,
1.03,
1.03
);
sun.add(
sunwire
);
scene.add(
sun
);
// ========================
// Planet Data
// ========================
const planetData = [
{
name: "Mercury",
size: 0.45,
color: 0xaaaaaa,
distance: 4,
rotationSpeed: 0.008,
orbitSpeed: 0.04
},
{
name: "Venus",
size: 0.65,
color: 0xd9a066,
distance: 6,
rotationSpeed: -0.003,
orbitSpeed: 0.03
},
{
name: "Earth",
size: 0.7,
color: 0x2277ff,
distance: 8,
rotationSpeed: 0.01,
orbitSpeed: 0.025
},
{
name: "Mars",
size: 0.55,
color: 0xcc4422,
distance: 10,
rotationSpeed: 0.009,
orbitSpeed: 0.02
},
{
name: "Jupiter",
size: 1.3,
color: 0xd8b38a,
distance: 13,
rotationSpeed: 0.02,
orbitSpeed: 0.012
},
{
name: "Saturn",
size: 1.1,
color: 0xe6c87a,
distance: 16,
rotationSpeed: 0.018,
orbitSpeed: 0.009
},
{
name: "Uranus",
size: 0.9,
color: 0x66ccdd,
distance: 19,
rotationSpeed: 0.012,
orbitSpeed: 0.007
},
{
name: "Neptune",
size: 0.85,
color: 0x3355ff,
distance: 22,
rotationSpeed: 0.011,
orbitSpeed: 0.005
}
];
// ========================
// 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 = [];
planetData.forEach(
(data, index) => {
// 行星球體
const geometry =
new THREE.SphereGeometry(
data.size,
12,
8
);
// 行星本體材質
const material =
new THREE.MeshBasicMaterial({
color: data.color
});
// 建立行星
const planet =
new THREE.Mesh(
geometry,
material
);
// ========================
// Wireframe
// ========================
const wireMaterial =
new THREE.MeshBasicMaterial({
color: 0xffffff,
wireframe: true
});
const wire =
new THREE.Mesh(
geometry,
wireMaterial
);
wire.scale.set(
1.03,
1.03,
1.03
);
planet.add(
wire
);
// 加入場景
scene.add(
planet
);
// 每顆行星不同起始角度
const startAngle =
index * 0.8;
// 保存資料
planets.push({
mesh: planet,
distance:
data.distance,
rotationSpeed:
data.rotationSpeed,
orbitSpeed:
data.orbitSpeed,
angle:
startAngle
});
}
);
// ========================
// 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
);
// 太陽自轉
sun.rotation.y +=
0.005;
planets.forEach(
(planet) => {
// 自轉
planet.mesh.rotation.y +=
planet.rotationSpeed;
// 公轉
planet.angle +=
planet.orbitSpeed;
planet.mesh.position.x =
Math.cos(
planet.angle
) *
planet.distance;
planet.mesh.position.z =
Math.sin(
planet.angle
) *
planet.distance;
}
);
renderer.render(
scene,
camera
);
}
animate();
今天最主要新增的是 Orbits 的:
planetData.forEach(
(data) => {
}
);
我們直接利用 Day 12 已經有的 distance 來產生不同大小的軌道。
Mercury distance = 4
Earth distance = 8
Jupiter distance = 13
所以不需要再另外設定八個軌道大小,
直接使用 data.distance 就可以建立對應的軌道。
我們先建立 const points = [];
接著設定 const segments = 128;代表我們會在圓形軌道上建立 128 個點。
然後:
for (
let i = 0;
i < segments;
i++
)
讓程式重複計算這些位置,每一個點的角度是:
const angle =
(i / segments) *
Math.PI * 2;
Math.PI * 2 就是一整圈。
所以 0 → Math.PI * 2 就等於繞完整個圓。
跟 Day 12 的公轉一樣,我們使用 distance
const x =
Math.cos(angle) *
data.distance;
以及:
const z =
Math.sin(angle) *
data.distance;
這樣就可以得到圓形軌道上的位置。
再把每一個位置放進:
points.push(
new THREE.Vector3(
x, //x
0, //y
z //z
)
);
THREE.Vector3() 代表一個 3D 座標
因為我們的行星是在 X、Z 平面上公轉,所以 Y 固定使用 0
有了很多個座標之後:
const orbitGeometry =
new THREE.BufferGeometry()
.setFromPoints(
points
);
BufferGeometry 可以把剛剛建立的座標整理成 Three.js 可以使用的 Geometry。
所以流程可以理解成:
Math.cos / Math.sin
↓
很多個座標
↓
points
↓
BufferGeometry
接著我們建立軌道線的材質:
const orbitMaterial =
new THREE.LineBasicMaterial({
color: 0x444444
});
這裡使用比較深的灰色,因為如果直接用白色,八條軌道全部顯示時會太搶眼。
我們希望太陽、行星才是畫面的主要焦點,而軌道只是輔助。
const orbit =
new THREE.LineLoop(
orbitGeometry,
orbitMaterial
);
這裡使用的是 THREE.LineLoop ,
跟普通 Line 最大的差別是 LineLoop 最後一個點會自動連回第一個點。
所以非常適合用來畫:圓形、軌道、封閉線條。
最後再:
scene.add(
orbit
);
軌道就會顯示在 Three.js 場景裡。
目前我們使用 const segments = 128; 建立軌道。
大家可以試著改成 const segments = 16;
看看軌道會變成什麼樣子。
你會發現圓形開始變得有稜有角。
再試試:
const segments = 32;
const segments = 64;
const segments = 256;
觀察:
segments 越少 → 軌道越有稜角
segments 越多 → 軌道越接近圓形
今天加入軌道之後現在整個太陽系的結構就更加清楚了。
目前我們已經完成:
全螢幕場景、太陽、八大行星、不同大小、顏色、距離、Wireframe、自轉、公轉、不同公轉速度、八大行星軌道。
而且今天我們第一次使用:
THREE.Vector3
THREE.BufferGeometry
THREE.LineBasicMaterial
THREE.LineLoop
讓 Three.js 不只是建立球體,也開始可以自己建立線條與路徑。
補上完成的畫面
「不同速度:調整八大行星的公轉節奏」
現在八大行星雖然已經有 orbitSpeed 但是目前的速度只是為了方便觀察所設定的數值。
Day 14 我們會進一步整理八大行星之間的公轉速度差異。
同時也會開始處理一個很重要的問題 「動畫速度會不會受到電腦 FPS 影響?」
我們會開始接觸 THREE.Clock ,讓動畫不要單純依賴每一幀增加固定數值,讓太陽系動畫的速度控制更穩定。