Day 14 我們已經使用 THREE.Clock,讓行星的自轉與公轉速度改成依照時間計算。
今天我們要處理另一個很重要的東西 Camera。
目前太陽系雖然已經會動,但我們一直都只是從固定位置觀看。
今天會開始認識 PerspectiveCamera 的幾個重要參數,也會實際改變 Camera 的位置,看看同一個太陽系從不同角度看起來有什麼差別。
今天會完成:
今天完成之後,太陽系本身功能不會改變,但我們會更了解:
「相機放在哪裡」會直接影響整個 3D 世界看起來的樣子。
index.html
與 Day 14 相同,不需要修改。(完整在 Day 9)
css/style.css
與 Day 14 相同,不需要修改。(完整在 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(
60,
window.innerWidth /
window.innerHeight,
0.1,
1000
);
// 斜上方觀看太陽系
camera.position.set(
0,
28,
38
);
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 camera =
new THREE.PerspectiveCamera(
60,
window.innerWidth /
window.innerHeight,
0.1,
1000
);
這四個數值分別代表:
PerspectiveCamera(
FOV,
Aspect,
Near,
Far
)
也就是:
FOV → 60
Aspect → window.innerWidth / window.innerHeight
Near → 0.1
Far → 1000
這四個參數會決定 Camera 可以看到多少東西,以及可以看到多近和多遠。
FOV 就是 Field of View 也就是相機的「視野範圍」。
可以簡單理解成相機鏡頭有多廣。
例如:
new THREE.PerspectiveCamera(
30,
...
);
視野比較窄而:
new THREE.PerspectiveCamera(
90,
...
);
視野比較廣。
FOV 越大,可以看到的範圍越多。
但畫面的透視感也會更強。
今天我們先使用 60 讓太陽系看起來比較自然。
Aspect 就是 畫面寬度 / 畫面高度。
這個比例必須和瀏覽器一致。
不然畫面可能會被拉長、壓扁、變形,
所以視窗大小改變時,我們還要:
camera.aspect =
window.innerWidth /
window.innerHeight;
camera.updateProjectionMatrix();
重新更新 Camera。
Near 代表 Camera 最近可以看到的位置。
Far 代表 Camera 最遠可以看到的位置。
如果物件距離 Camera 太近,小於 Near 會看不到,
如果距離太遠,超過 Far 也看不到,
目前我們的太陽系範圍大概只有幾十個 Three.js 單位,所以 0.1 、 1000 已經足夠使用。
今天我們把 Camera 改成:
camera.position.set(
0,
28,
38
);
也就是:
X = 0
Y = 28
Z = 38
其中 Y 比 Day 14 更高。
所以 Camera 會從比較高的位置往下看。
這樣可以更清楚看到八條軌道。
雖然 Camera 移動到了 0, 28, 38
但是我們還需要告訴它你要看哪裡?
所以使用:
camera.lookAt(
0,
0,
0
);
意思是讓 Camera 永遠看向:
X = 0
Y = 0
Z = 0
而這個位置剛好就是太陽所以 Camera 會一直看向太陽系中心。
今天新增一個功能就是:
window.addEventListener(
"keydown",
(event) => {
}
);
當使用者按下鍵盤按鍵時 event.key 就可以知道使用者按了什麼。
例如當按下 if (event.key === "1") 就代表使用者按下數字 1。
接著:
camera.position.set(
0,
0,
40
);
Camera 就會移動到新的位置。
目前我們準備四個視角:
1 正面視角 (0,0,40)
2 斜角視角 (30,25,30)
3 俯視視角 (0,45,0.1)
4 近距離視角 (0,12,20)
這也可以讓我們理解 Camera 距離對 3D 畫面的影響。
今天可以自己加入:
第 5 種 Camera 視角
按下 5 切換到另外一個角度。
可以在 keydown 裡加入:
if (event.key === "5") {
camera.position.set(
X,
Y,
Z
);
}
這樣就會從另外一側觀看太陽系。
也可以自己測試不同參數看看會得到什麼視角。
也可以試著改 FOV 比較不同鏡頭視野下,太陽系看起來有什麼不同。
今天太陽系本身沒有增加新的星球或新的運動方式。
但是我們加入了一個真正可以讓使用者操作的功能:
鍵盤切換 Camera
同時我們也正式認識 PerspectiveCamera、FOV、Aspect、Near、Far、camera.position、camera.lookAt
目前太陽系已經完成:
全螢幕場景、太陽、八大行星、Wireframe、自轉、公轉、公轉軌道、不同運行速度、THREE.Clock、Camera 視角,以及鍵盤切換 Camera。
不過現在 Camera 還是有一個限制。雖然比以前方便很多,但我們還是只能在四個固定位置之間切換。
如果想自己用滑鼠隨意轉動視角,目前還做不到。
「OrbitControls:用滑鼠自由觀看整個太陽系」
Day 16 我們會加入 OrbitControls 讓使用者可以直接滑鼠左鍵拖曳和旋轉觀看角度、
滑鼠滾輪放大 / 縮小、滑鼠右鍵拖曳平移畫面變成自由 3D 視角。
完成 Day 16 之後,這個太陽系就會第一次真正有「互動式 3D 網頁」的感覺。