Day 19 我們已經完成基本的光照系統,現在太陽會透過 PointLight 照亮行星,也加入了 AmbientLight 讓暗面保留一些細節。
今天不會加入新的行星,而是把 Three.js 的 Shadow 系統補上。
今天會完成:
今天順便更改一下文章的順序,之後都會將程式碼放在文章的後面。
在 Renderer 建立完成之後加入:
renderer.shadowMap.enabled =
true;
這一行可以理解成:
整個場景的 Shadow 總開關
如果沒有開啟它,即使後面設定 castShadow = true;
也不會出現陰影,我們另外再加入:
renderer.shadowMap.type =
THREE.PCFSoftShadowMap;
讓陰影邊緣看起來柔和一點。
所以 Renderer 這一段現在會變成:
const renderer =
new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(
window.innerWidth,
window.innerHeight
);
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
renderer.shadowMap.enabled =
true;
renderer.shadowMap.type =
THREE.PCFSoftShadowMap;
今天在 const sunLight 下面加入:
sunLight.castShadow =
true;
意思就是這個 Light 可以計算 Shadow
完整修改:
const sunLight =
new THREE.PointLight(
0xffffff,
500,
100
);
sunLight.position.set(
0,
0,
0
);
sunLight.castShadow =
true;
接著加入:
sunLight.shadow.mapSize.width =
512;
sunLight.shadow.mapSize.height =
512;
這裡設定的是 Shadow Map 的解析度。
可以簡單理解成:
解析度越高則陰影越清楚 GPU 就負擔越高
目前先使用 512 × 512
就可以了。
另外再設定:
sunLight.shadow.camera.near =
0.1;
sunLight.shadow.camera.far =
100;
代表 Shadow 計算的距離範圍。
所以 PointLight 完整修改會變成:
const sunLight =
new THREE.PointLight(
0xffffff,
500,
100
);
sunLight.position.set(
0,
0,
0
);
sunLight.castShadow =
true;
sunLight.shadow.mapSize.width =
512;
sunLight.shadow.mapSize.height =
512;
sunLight.shadow.camera.near =
0.1;
sunLight.shadow.camera.far =
100;
scene.add(
sunLight
);
今天在 const planet 下面加入:
planet.castShadow =
true;
意思是這顆行星可以擋住光線產生陰影
所以 castShadow = true;
可以記成 我可以製造陰影
接著再加入:
planet.receiveShadow =
true;
這代表這個物體可以顯示其他物體投射過來的陰影
所以現在行星建立的部分會變成:
const planet =
new THREE.Mesh(
geometry,
material
);
planet.castShadow =
true;
planet.receiveShadow =
true;
scene.add(
planet
);
兩個設定可以這樣記:
castShadow = 可以投射陰影
receiveShadow = 可以接收陰影
這裡有一個很重要的觀念。
我們讓地球出現 亮面 + 暗面
這是 Lighting 但暗面不代表 Shadow。
真正的 Shadow 是中間有一個物體把光擋住。
也就是:Light → Object A → 光線被擋住 → Object B 出現 Shadow
所以 Lighting ≠ Shadow
今天一樣只有修改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
)
);
renderer.shadowMap.enabled =
true;
renderer.shadowMap.type =
THREE.PCFSoftShadowMap;
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
);
// Point Light
const sunLight =
new THREE.PointLight(
0xffffff,
500,
100
);
sunLight.position.set(
0,
0,
0
);
sunLight.castShadow =
true;
sunLight.shadow.mapSize.width =
512;
sunLight.shadow.mapSize.height =
512;
sunLight.shadow.camera.near =
0.1;
sunLight.shadow.camera.far =
100;
scene.add(
sunLight
);
// Ambient Light
const ambientLight =
new THREE.AmbientLight(
0xffffff,
0.08
);
scene.add(
ambientLight
);
// 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
);
planet.castShadow =
true;
planet.receiveShadow =
true;
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();
今天真正新增的程式不多,
但它讓我們的太陽系多了一個新的系統:Lighting + Shadow。
目前 Shadow 還不一定非常明顯,但從今天開始,每顆行星都已經具備
投射陰影 + 接收陰影 的能力。
「Moon:建立地球與月球系統」
Day 21 我們會第一次建立衛星,並讓月亮繞著地球,
到時候今天建立的 castShadow 以及 receiveShadow
就會開始真正派上用場。