Day 15 我們已經讓 Camera 可以透過鍵盤切換不同視角。
但都還是固定好的位置。
今天我們要加入 OrbitControls
讓使用者可以直接用滑鼠自由操作 Camera。
今天會完成:
今天完成之後,太陽系就不再只能看固定角度。
可以直接拖曳,縮放、旋轉、平移自由探索整個 3D 場景。
index.html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>從 Web 到宇宙</title>
<link
rel="stylesheet"
href="css/style.css"
>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="scene"></div>
<div class="title">
<h1>從 Web 到宇宙</h1>
<p>DAY 16|OrbitControls</p>
</div>
<script
type="module"
src="js/main.js"
></script>
</body>
</html>
css/style.css
與 Day 15 相同,不需要修改。
js/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
)
);
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;
// Camera 主要看向太陽系中心
controls.target.set(
0,
0,
0
);
controls.update();
// ========================
// 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
});
}
);
// ========================
// Camera Controls
// ========================
window.addEventListener(
"keydown",
(event) => {
// 1:正面視角
if (event.key === "1") {
camera.position.set(
0,
0,
40
);
}
// 2:斜角視角
if (event.key === "2") {
camera.position.set(
30,
25,
30
);
}
// 3:俯視視角
if (event.key === "3") {
camera.position.set(
0,
45,
0.1
);
}
// 4:近距離視角
if (event.key === "4") {
camera.position.set(
0,
12,
20
);
}
// OrbitControls 的目標
// 回到太陽系中心
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.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;
}
);
// OrbitControls 更新
controls.update();
renderer.render(
scene,
camera
);
}
animate();
今天最重要的新功能就是 OrbitControls,
和前幾天有一個比較大的差別,
以前我們直接從 CDN 匯入 Three.js::
import * as THREE from
"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js";
但今天開始要使用 OrbitControls,它屬於 Three.js 的 Addons。
所以我們改成在 index.html 加入 importmap:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/"
}
}
</script>
之後 JavaScript 就可以簡化成:
import * as THREE from
"three";
import { OrbitControls } from
"three/addons/controls/OrbitControls.js";
接著建立:
const controls =
new OrbitControls(
camera,
renderer.domElement
);
這裡需要兩個東西 camera + renderer.domElement
camera 就是我們要控制的相機。
而 renderer.domElement 就是 Three.js 畫面中的 Canvas。
所以可以理解成:
滑鼠操作 → Canvas OrbitControls → 控制 Camera
加入 OrbitControls 之後,預設就可以使用:
滑鼠左鍵拖曳 → 旋轉 Camera
滑鼠滾輪 → 放大 / 縮小
滑鼠右鍵拖曳 → 平移 Camera
所以以前想換角度需要camera.position.set(...)現在可以直接用滑鼠完成。
我們加入:
controls.target.set(
0,
0,
0
);
這個 0, 0, 0
就是太陽的位置。
所以 OrbitControls 旋轉時,可以把它想像成
Camera 繞著太陽系中心旋轉,也就是使用者是在「繞著太陽系看」。
今天另外加入:
controls.enableDamping =
true;
如果沒有 Damping,滑鼠一放開 Camera 就會立刻停止。
開啟 enableDamping = true 之後,操作會多一點慣性。
效果會比較像:拖曳 → 放開滑鼠 → Camera 稍微滑動 → 慢慢停止
整體操作會比較自然。
我們設定:
controls.dampingFactor =
0.05;
這個數字控制阻尼效果。
數字比較小滑得比較久
數字比較大比較快停止
例如可以自己測試:
controls.dampingFactor =
0.15;
看看手感有什麼不同。
因為我們開啟:
controls.enableDamping =
true;
所以動畫中要持續執行 controls.update();
也就是:
function animate() {
...
controls.update();
renderer.render(
scene,
camera
);
}
這樣阻尼動畫才會持續更新。
controls.minDistance =
10;
以及:
controls.maxDistance =
80;
意思是 Camera 最靠近太陽系只能到 10,最遠只能到 80 。
如果完全不限制,使用者可能會,
滾輪一直往前 Camera 跑進太陽裡
或者滾輪一直往後整個太陽系變成一個小點
所以設定範圍之後會比較好控制。
Day 15 我們加入鍵盤控制四個 Camera 視角,今天沒有把它刪掉。
所以現在鍵盤滑鼠可以一起操作。
今天加入 OrbitControls 之後現在使用者已經可以真正自由探索整個太陽系。
目前我們完成:
全螢幕場景、太陽、八大行星、Wireframe、自轉、公轉、公轉軌道、不同運行速度、THREE.Clock、Camera、鍵盤視角切換,以及 OrbitControls。
做到 Day 16,其實整個太陽系的:
3D 基礎 + 動畫 + Camera + 使用者互動
已經大致完成。
接下來要開始處理目前最明顯的問題:
這些行星還只是彩色球。
「Texture:替八大行星換上真正的表面」
目前地球只是color: 0x2277ff
所以看起來只有一顆藍球
Day 17 我們會開始使用 THREE.TextureLoader 載入圖片。
接著把圖片貼到球體表面。
而且到了這一天之後,我們之前為了觀察自轉而加入的:
Wireframe 就可以開始準備移除。
因為行星有貼圖之後,自轉本身就會很明顯。