Day 13 我們已經替八大行星加入公轉軌道,現在每顆行星都可以沿著自己的軌道繞太陽運行。
不過目前還有一個問題,我們的動畫速度是使用:
planet.angle +=
planet.orbitSpeed;
這代表每執行一次畫面更新,就增加固定數值。
如果不同電腦每秒可以更新的畫面數量不同,動畫速度也可能跟著受到影響。
所以今天除了整理八大行星不同的運行速度,也會加入 THREE.Clock
讓動畫開始改成使用「時間」控制。
今天會完成:
index.html
與 Day 13 相同,不需要修改。(完整在 Day 9)
css/style.css
與 Day 13 相同,不需要修改。(完整在 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(
75,
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
);
// ========================
// Clock
// ========================
const clock =
new THREE.Clock();
// ========================
// 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.8,
orbitSpeed: 1.2
},
{
name: "Venus",
size: 0.65,
color: 0xd9a066,
distance: 6,
rotationSpeed: -0.3,
orbitSpeed: 0.95
},
{
name: "Earth",
size: 0.7,
color: 0x2277ff,
distance: 8,
rotationSpeed: 1.0,
orbitSpeed: 0.8
},
{
name: "Mars",
size: 0.55,
color: 0xcc4422,
distance: 10,
rotationSpeed: 0.9,
orbitSpeed: 0.65
},
{
name: "Jupiter",
size: 1.3,
color: 0xd8b38a,
distance: 13,
rotationSpeed: 2.0,
orbitSpeed: 0.4
},
{
name: "Saturn",
size: 1.1,
color: 0xe6c87a,
distance: 16,
rotationSpeed: 1.8,
orbitSpeed: 0.3
},
{
name: "Uranus",
size: 0.9,
color: 0x66ccdd,
distance: 19,
rotationSpeed: 1.2,
orbitSpeed: 0.22
},
{
name: "Neptune",
size: 0.85,
color: 0x3355ff,
distance: 22,
rotationSpeed: 1.1,
orbitSpeed: 0.16
}
];
// ========================
// 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
);
// 取得上一幀到這一幀
// 經過多少秒
const delta =
clock.getDelta();
// 太陽自轉
sun.rotation.y +=
0.5 * delta;
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;
}
);
renderer.render(
scene,
camera
);
}
animate();
今天最大的改變就是:
const clock =
new THREE.Clock();
THREE.Clock 可以幫我們記錄程式運行時經過的時間。
接著在 animate() 裡加入:
const delta =
clock.getDelta();
delta 代表上一個畫面到現在這個畫面,中間經過了多少秒。
60 FPS 大約每 0.016 秒更新一次畫面
所以 delta 可能會是 0.016
如果電腦只有大約 30 FPS 大約每 0.033 秒更新一次畫面
這時 delta 就可能接近 0.033
這樣我們就可以根據真正經過的時間更新動畫。
planet.angle +=
planet.orbitSpeed;
假設 orbitSpeed: 0.02 每更新一次畫面就增加 0.02
如果一台電腦每秒更新 60 次 0.02 × 60
另一台電腦每秒只更新 30 次 0.02 × 30
兩台電腦實際看到的動畫速度就可能不一樣。
所以今天開始不再單純依靠「畫面更新幾次」。
今天改成:
planet.mesh.rotation.y +=
planet.rotationSpeed *
delta;
例如地球 rotationSpeed: 1.0 and delta = 0.016
這一次動畫實際增加: 1.0 × 0.016 = 0.016
如果下一幀比較慢 delta = 0.03
則會增加: 1.0 × 0.03 = 0.03
也就是:
時間經過比較多 → 這一幀就移動比較多
時間經過比較少 → 這一幀就移動比較少
最後整體動畫速度就會比較穩定。
原本:
planet.angle +=
planet.orbitSpeed;
今天改成:
planet.angle +=
planet.orbitSpeed *
delta;
所以 orbitSpeed 現在為每秒增加多少公轉角度。
例如:
{
name: "Earth",
orbitSpeed: 0.8
}
代表地球每秒大約增加 0.8 radians 的公轉角度。
Day 13時地球是 orbitSpeed = 0.025
Day 14變成 orbitSpeed = 0.8
這不是突然讓地球快很多?
答案是不會原因是
前面的 0.025 是每一幀增加 0.025
而今天 0.8 則會再乘上 delta
例如 0.8 × 0.016 = 0.0128
所以兩種數值不能直接拿來比較。
從今天開始,我們統一採用 speed * delta 控制動畫速度。
之後新增其他動畫,也盡量使用這種方式。
附圖
今天的畫面看起來可能和 Day 13 沒有非常大的差異。
因為八大行星原本就已經會 自轉 + 公轉 沿著軌道運行,
但今天真正重要的是動畫背後的計算方式改變了。
前幾天 angle += speed;
現在 > angle += speed * delta;
而 delta 來自:
const clock =
new THREE.Clock();
const delta =
clock.getDelta();
所以目前我們已經完成:
全螢幕場景、太陽、八大行星、不同大小、顏色、距離、Wireframe、自轉、公轉、八大行星軌道、不同運行速度,以及使用 THREE.Clock 控制動畫時間。
從今天開始,整個太陽系動畫的基礎就更加完整了。
「Camera:從不同角度觀察太陽系」
目前我們的 Camera 一直固定的,
也就是固定從一個角度看太陽系。
Day 15 我們會正式認識 THREE.PerspectiveCamera
裡面幾個重要設定:
FOV
Aspect
Near
Far
也會實際改變 camera.position 比較:
正面視角
俯視視角
斜角視角
近距離
遠距離
看看同一個太陽系,從不同 Camera 位置觀看會有什麼差別。