Day 17 我們已經使用 TextureLoader,把原本只有單一顏色的行星換成真正的表面貼圖。
現在地球、火星、木星等行星已經開始有比較真實的外觀。
但目前還有一個問題,所有行星整顆都是亮的。
這是因為 Day 17 使用的是 THREE.MeshBasicMaterial
MeshBasicMaterial 不會受到燈光影響。
所以今天我們要讓太陽不只是看起來像太陽,而是真正成為整個太陽系的光源。
今天會完成:
index.html
與 Day 17 相同,不需要修改。(完整在 Day 16)
css/style.css
與 Day 16 相同,不需要修改。(完整在 Day 9)
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;
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
);
// ========================
// Sun Light
// ========================
const sunLight =
new THREE.PointLight(
0xffffff,
500,
100
);
sunLight.position.set(
0,
0,
0
);
scene.add(
sunLight
);
// ========================
// 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 = [];
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
);
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) => {
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.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;
}
);
controls.update();
renderer.render(
scene,
camera
);
}
animate();
今天最重要的新程式就是:
const sunLight =
new THREE.PointLight(
0xffffff,
500,
100
);
PointLight 可以理解成 從一個位置往四面八方發光的光源。
這非常適合我們現在的太陽。
所以我們把它放在:
sunLight.position.set(
0,
0,
0
);
剛好跟太陽的位置一樣。接著:
scene.add(
sunLight
);
把光源加入場景。
我們今天使用:
new THREE.PointLight(
0xffffff, //顏色
500, //強度
100 //距離
);
第一個 0xffffff 代表白色光。
第二個 500 代表光線強度。
第三個 100 代表光源可以影響的距離。
目前最遠的海王星 distance: 22
所以 100 已經足夠照到整個太陽系。
Day 17 我們使用:
new THREE.MeshBasicMaterial({
map: planetTexture
});
但是 MeshBasicMaterial 有一個特性完全不理會光源。
所以就算加入 THREE.PointLight 它還是會整顆一樣亮。
因此今天要改成:
new THREE.MeshStandardMaterial({
map: planetTexture
});
MeshStandardMaterial 會受到光線影響,所以:
面向太陽的一側會亮。
背對太陽的一側會暗。
今天材質裡另外加入:
roughness: 1,
metalness: 0
roughness 可以理解成:
表面有多粗糙。
roughness = 0 , 表面比較光滑
roughness = 1 , 表面比較粗糙
行星並不是金屬球,所以我們設定 metalness: 0
避免它看起來像金屬材質。
你可能會發現行星全部改成 MeshStandardMaterial,
但是太陽還是 MeshBasicMaterial。
原因很簡單,太陽本身就是光源。
如果我們讓太陽也必須依靠 PointLight,
才能被看見,概念上會有點奇怪。
所以現在
Sun 自己保持明亮而 Planets 接受太陽光
這樣比較符合我們目前的太陽系效果。
今天另外加入:
sunTexture.colorSpace =
THREE.SRGBColorSpace;
以及:
planetTexture.colorSpace =
THREE.SRGBColorSpace;
因為我們載入的:
Earth.jpg
Mars.jpg
Jupiter.jpg
...
都是一般用來顯示顏色的圖片。
告訴 Three.js:THREE.SRGBColorSpace
可以讓 Texture 的顏色顯示方式更加正常。
所以之後只要是這類行星表面顏色貼圖
就可以設定:
texture.colorSpace =
THREE.SRGBColorSpace;
今天 Day 18 加入:
THREE.PointLight
並把行星材質從 MeshBasicMaterial
改成 MeshStandardMaterial
目前我們已經完成:
全螢幕場景、太陽、八大行星、Texture、自轉、公轉、公轉軌道、不同速度、THREE.Clock、Camera、OrbitControls,以及真正的太陽光源。
現在行星終於開始有 亮面 + 暗面,
整個太陽系的立體感也會比 Day 17 明顯很多。
今天完成的效果:
「Lighting:進一步調整太陽系的明暗效果」
Day 18 我們已經成功讓太陽發光。
Day 19 我們會進一步處理 Lighting。
為什麼背光面會完全黑掉?
要不要保留一點環境光?
不同材質受到光線時會有什麼差別?
我們會開始加入 THREE.AmbientLight
並比較 PointLight 和 AmbientLight
兩種光源的差別,讓目前的光照效果更自然。