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,物理正确
注意:点光源数量不宜过多。每个点光源都需要计算光照,多了会严重影响性能。我曾经在一个场景里放了20个点光源,结果帧率直接掉到个位数。

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 开启阴影

要使用阴影,需要三步走:

  1. 渲染器开启阴影:renderer.shadowMap.enabled = true
  2. 光源开启阴影:light.castShadow = true
  3. 物体设置投射和接收:mesh.castShadow = truemesh.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;
避坑指南:阴影贴图大小不是越大越好。2048x2048已经够用了,再大性能扛不住。我曾经为了追求极致效果设成4096,结果移动端直接卡死。

还有一个常见的坑:阴影相机范围要刚好包裹住场景。范围太大,阴影精度会下降;范围太小,阴影会被裁切掉。

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();
最后说一句:光照和阴影是3D场景的「化妆术」。光源用得好,模型再丑也能救回来;光源用得烂,再好的模型也白搭。多调、多看、多对比,慢慢就有感觉了。