
光影,让场景有立体感
上节我们在场景中创建了立方体,学会了Three.js的基本使用,但是目前存在一个明显问题:场景看起来非常“平”,像一张纸片,缺乏 3D 场景应有的体积感和真实感。本节的重点是解决这个问题。
本节的重点就是解决这个问题。要让场景变得立体,核心在于构建光影系统。只有当场景中有了光线的明暗变化和阴影投射,物体才能表现出层次感。我们将基于初始化场景的代码,通过引入灯光、材质和阴影,将一个平面的 Demo 改造为具有真实质感的 3D 场景。
第一步:渲染器设置
渲染器(Renderer)是 Three.js 的画笔。默认情况下,出于性能考虑,Three.js 关闭了阴影计算。我们需要手动开启阴影支持,并调整背景色,避免纯黑背景吞没物体的暗部轮廓。
export default class Application {
// 初始化渲染器
initRenderer() {
this.renderer = new THREE.WebGLRenderer({
antialias: true, // 开启抗锯齿,使物体边缘更加柔和
canvas: this.canvas,
});
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
// 1. 开启阴影支持
// 计算机渲染阴影非常耗费性能,所以默认是关闭的
this.renderer.shadowMap.enabled = true;
// 2. 设置阴影类型
// PCFSoftShadowMap 能让阴影边缘带有模糊过渡,这是提升真实感的关键
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// 3. 设置背景色
// 使用深灰色代替纯黑色,能更好地衬托出物体的明暗交界线,避免暗部细节丢失
this.renderer.setClearColor(0x262626);
}
}第二步:材质更换
上节中的立方体用的材质是MeshBasicMaterial,这种材质是自发光的,它不受光照影响,因此无法产生阴影或明暗变化。我们需要将其更换为受光照影响的材质。
这里选择MeshStandardMaterial,这是一种基于物理渲染(PBR)的材质,它通过以下两个核心参数来模拟真实物理质感:
- roughness(粗糙度):决定表面反射光线的聚焦程度。数值越小表面越光滑,高光越明显。。
- metalness(金属度):决定表面像金属还是像塑料。
export default class World extends THREE.Object3D {
initCubeMesh() {
const geometry = new THREE.BoxGeometry(10, 10, 10);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const material = new THREE.MeshStandardMaterial({
color: 0x2196f3,
roughness: 0.3, // 表面比较光滑,会产生高光
metalness: 0.2, // 带有轻微的金属质感
});
const cube = new THREE.Mesh(geometry, material);
// 设置为“投射阴影”,即该物体挡住光线时可以产生阴影
cube.castShadow = true;
// 设置为“接收阴影”,即其他物体的影子可以投射在它身上
cube.receiveShadow = true;
this.cube = cube
this.add(cube);
}
}TIP
更换材质后,你会发现场景中的立方体变成了一团黑影。这是正常的,因为现在的材质需要光照才能显示颜色,而场景中目前还没有任何光源。

第三步:灯光布局
在 3D 渲染中,我们通常采用经典的主光 + 补光策略。这不仅仅是把场景照亮,更是为了通过明暗对比来塑造物体的体积感。 在现实物理世界中,即使你站在背对太阳的阴影里,你依然能看清自己的手,因为光线会撞击地面、墙壁并发生无数次反弹(漫反射),最终照亮暗部。 但在 WebGL 的默认世界里,光线不会自动反弹。如果不做特殊处理,背光面就是物理意义上的“绝对虚无”(纯黑色)。为了模拟真实的立体感,我们需要构建一套由环境光和平行光组成的灯光系统。
**1.环境光 (AmbientLight):世界的“底色” **
环境光是一种均匀照亮场景中所有物体的光。它没有方向,不能产生阴影。它的作用是,充当补光,确保了暗部也能被看见,保留了物体的轮廓细节。
TIP
环境光通常设置在 0.3 ~ 0.5 之间。太亮会让场景发白、变平;太暗则起不到补光作用。
2. 平行光 (DirectionalLight):立体的“雕刻刀”
平行光模拟的是极其遥远的光源(如太阳)。因为距离太远,到达地球的光线几乎是平行的。它是场景的主光源,有以下效果:。
- 塑造体积:它让物体产生明亮的受光面和深沉的背光面。有了这种明暗渐变,大脑才能感知到球是圆的,盒子是方的。
- 产生高光:配合 Standard 材质,它能在物体棱角处产生耀眼的反射光点,极大提升质感。
- 投射阴影:它是阴影的来源,确立了物体与地面的空间关系。
export default class World extends THREE.Object3D {
consttuctor() {
this.initLights()
}
// 新增加的初始化灯光的方法
initLights() {
// 环境光:这决定了场景中最暗的地方有多亮。
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.add(ambientLight);
// 平行光:这是模拟太阳,必须比环境光亮得多,才能产生对比度。
const sunLight = new THREE.DirectionalLight(0xffffff, 1.5);
// 平行光默认照射目标是原点 (0,0,0)。
// 设置位置 (10, 20, 10) 意味着光线是从右上方(x=10, y=20)射向原点。
// 这种 45 度角的侧顶光最容易展现物体轮廓。
sunLight.position.set(10, 20, 10);
// 只有主光源需要开启阴影,环境光无法产生阴影
sunLight.castShadow = true;
// 优化阴影的精细度
// 默认的阴影贴图可能比较糊,这里将其分辨率调大一倍 (默认是 512x512)
sunLight.shadow.mapSize.width = 1024;
sunLight.shadow.mapSize.height = 1024;
// 解决阴影截断问题
// 平行光的阴影计算区域是一个正方体盒子,如果场景很大,阴影可能会被切断。
// 这里扩大阴影相机的视锥体范围
const d = 50;
sunLight.shadow.camera.left = -d;
sunLight.shadow.camera.right = d;
sunLight.shadow.camera.top = d;
sunLight.shadow.camera.bottom = -d;
this.add(sunLight);
}
}
第四步:创建地面
为了观察到立方体的影子,还需要创建地面。这里创建一个灰色哑光地面,用来承接影子。
export default class World extends THREE.Object3D {
consttuctor() {
this.initGround()
}
// 新增创建地面的方法
initGround() {
const geometry = new THREE.PlaneGeometry(1000, 1000); // 地面设置大一点
const material = new THREE.MeshStandardMaterial({
color: 0x444444,
roughness: 0.8, // 地面粗糙一点,不反光
metalness: 0.0
});
const ground = new THREE.Mesh(geometry, material);
ground.rotation.x = -Math.PI / 2; // 旋转90度平铺
ground.position.y = -5; // 下沉到物体下方
// 地面必须设置为“接收阴影”
ground.receiveShadow = true;
this.add(ground);
}
}
现在,地面上已经清晰地出现了立方体的阴影,并且阴影会随着立方体的旋转实时变化,整个场景瞬间拥有了立足于地面的真实感。
完整在线运行示例:光影与立体感。
