4、光照与阴影:环境光、点光源、平行光、聚光灯、阴影投射与接收
光照,是3D世界的灵魂。
没有光,你看到的只是一堆黑色剪影。有了光,模型才有了体积感、层次感,才有了「真实」的味道。我刚开始学Three.js那会儿,觉得光照就是加个光源完事,结果做出来的场景要么死白一片,要么黑得啥也看不见。后来踩了不少坑,才慢慢摸清这几种光源的脾气。
今天咱们就把环境光、点光源、平行光、聚光灯这四位请出来,挨个盘一遍。顺便把阴影投射和接收这块硬骨头也啃了。
4.1 环境光:给场景一个基础亮度
环境光,说白了就是「无影灯」。它没有方向,没有位置,均匀地照亮所有物体表面。
你想想看,现实世界里,光线在物体之间来回反弹,总会有一些微弱的光照亮暗部。环境光就是模拟这个效果的——虽然它很粗糙,但胜在简单高效。
// 创建一个柔和的环境光
const ambientLight = new THREE.AmbientLight(0x404060, 0.5);
scene.add(ambientLight);
我个人习惯把环境光的强度控制在0.3到0.6之间。太强了,场景会发灰,失去立体感;太弱了,暗部死黑一片。
4.2 点光源:像灯泡一样发光
点光源,你可以把它想象成一个悬在空中的灯泡。它从一个点向四面八方均匀发光。
我在项目中做过一个台灯展示的页面,用的就是点光源。效果很直观——离光源近的物体亮,远的暗,而且有清晰的衰减。
const pointLight = new THREE.PointLight(0xffaa00, 1, 100);
pointLight.position.set(10, 20, 10);
scene.add(pointLight);
// 可以加个小球体表示光源位置(调试用)
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16),
new THREE.MeshBasicMaterial({ color: 0xffaa00 })
);
sphere.position.copy(pointLight.position);
scene.add(sphere);
点光源有三个关键参数:颜色、强度、距离。距离控制着光能照多远,超过这个距离,物体就收不到光了。
| 参数 | 说明 | 我的建议值 |
|---|---|---|
| color | 光源颜色 | 根据场景氛围定 |
| intensity | 光照强度 | 0.5 ~ 2.0 |
| distance | 光照范围 | 0 表示无限远 |
| decay | 衰减速度 | 默认2,物理正确 |
4.3 平行光:模拟太阳光
平行光,顾名思义,所有光线都是平行的。它没有位置概念,只有方向。
太阳光就是最典型的平行光。你想想看,太阳离我们那么远,照到地球上的光线几乎就是平行的。所以平行光产生的阴影非常清晰、锐利。
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(50, 100, 50);
scene.add(directionalLight);
// 平行光需要指定目标点
directionalLight.target.position.set(0, 0, 0);
scene.add(directionalLight.target);
嗯,这里要注意:平行光的position决定了光的方向,而不是位置。光从position指向target的方向照射。
我一般用平行光做主光源,配合环境光做辅光。这样场景既有立体感,暗部也不会死黑。
4.4 聚光灯:舞台上的追光
聚光灯就像手电筒或者舞台上的追光灯。它从一个点发出,沿着一个方向照射,形成一个锥形的光照区域。
聚光灯的参数比较多,但搞懂了就很好用。我做产品展示的时候特别喜欢用聚光灯——它能精准地把观众的视线引到产品上。
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(10, 20, 10);
spotLight.angle = 0.3; // 光锥角度
spotLight.penumbra = 0.2; // 边缘模糊程度
spotLight.decay = 1; // 衰减
spotLight.distance = 50; // 照射距离
spotLight.target.position.set(0, 0, 0);
scene.add(spotLight.target);
scene.add(spotLight);
| 参数 | 作用 | 取值范围 |
|---|---|---|
| angle | 光锥半角 | 0 ~ Math.PI/2 |
| penumbra | 边缘半影比例 | 0 ~ 1 |
| decay | 光照衰减 | 0 ~ 2 |
| distance | 最大照射距离 | 0 表示无限 |
penumbra这个参数很妙。设为0时,光斑边缘是硬边;设为1时,边缘非常柔和。我一般设0.2到0.4之间,既有聚光效果,又不会太生硬。
4.5 阴影:让物体落地
没有阴影的3D场景,就像PS里抠图没抠干净——所有物体都飘在空中。
阴影的实现原理其实不复杂:从光源位置看过去,被遮挡的区域就是阴影。但计算量很大,所以Three.js默认是关闭阴影的。
4.5.1 开启阴影
要使用阴影,需要三步走:
- 渲染器开启阴影:
renderer.shadowMap.enabled = true - 光源开启阴影:
light.castShadow = true - 物体设置投射和接收:
mesh.castShadow = true和mesh.receiveShadow = true
// 1. 渲染器
const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 柔和阴影
// 2. 光源
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.castShadow = true;
dirLight.position.set(10, 20, 10);
// 3. 物体
const box = new THREE.Mesh(
new THREE.BoxGeometry(2, 2, 2),
new THREE.MeshStandardMaterial({ color: 0x44aaff })
);
box.castShadow = true;
box.receiveShadow = true;
const plane = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshStandardMaterial({ color: 0x888888 })
);
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true; // 地面只接收阴影
4.5.2 阴影的坑与优化
我曾经在一个项目里开了阴影,结果发现阴影边缘全是锯齿,特别难看。后来才知道要调阴影贴图的大小。
// 调整阴影贴图质量
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
// 调整阴影相机范围
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
dirLight.shadow.camera.top = 10;
dirLight.shadow.camera.bottom = -10;
还有一个常见的坑:阴影相机范围要刚好包裹住场景。范围太大,阴影精度会下降;范围太小,阴影会被裁切掉。
4.6 综合示例:一个完整的光照场景
说了这么多,咱们来写一个完整的例子。这个场景包含四种光源和阴影:
// 场景、相机、渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(15, 10, 15);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// 环境光 - 基础照明
const ambient = new THREE.AmbientLight(0x404060, 0.4);
scene.add(ambient);
// 平行光 - 主光源,产生阴影
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(10, 20, 5);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
scene.add(dirLight);
// 点光源 - 辅助照明
const pointLight = new THREE.PointLight(0xff4400, 0.5, 20);
pointLight.position.set(-5, 5, 5);
scene.add(pointLight);
// 聚光灯 - 重点照明
const spotLight = new THREE.SpotLight(0x44ff44, 0.3);
spotLight.position.set(0, 10, -5);
spotLight.angle = 0.4;
spotLight.penumbra = 0.3;
spotLight.castShadow = true;
scene.add(spotLight);
// 地面
const plane = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshStandardMaterial({ color: 0x666688 })
);
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true;
scene.add(plane);
// 物体
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshStandardMaterial({ color: 0x44aaff });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(0, 1, 0);
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);
// 动画循环
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();