Three.js 简介

灯光与阴影

认识环境光、平行光、点光源三种常用灯光,学会组合布光,并给场景加上真实的阴影效果。

🎯 引言

上一篇我们发现 Standard 材质离开灯光就是一团黑。灯光是 3D 场景的“氛围组”,决定画面的明暗和层次。学完这篇文章,你能认识三种常用灯光,学会“环境光打底 + 平行光主光源”的组合套路,并给场景加上真实的阴影。


🧱 三种常用灯光

Three.js 内置了好几种灯光,日常高频使用的是这三种:

灯光类名特点生活类比
环境光AmbientLight均匀照亮所有物体,没有方向阴天的自然光
平行光DirectionalLight从一个方向平行照射太阳
点光源PointLight从一点向四周发散灯泡

三种光创建时都接收两个参数:颜色强度

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // 环境光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5); // 平行光
const pointLight = new THREE.PointLight(0xffffff, 30); // 点光源

环境光没有方向,放进场景就生效;平行光和点光源需要设置位置:

directionalLight.position.set(3, 5, 4); // 从右上方斜照下来
pointLight.position.set(2, 3, 2); // 挂在这个坐标的“灯泡”
点光源的强度量级和前两种光不同:新版 Three.js 中点光源按物理距离衰减,亮度掉得很快,30 这类偏大的值才够亮。平行光和环境光的强度一般在 0.5 ~ 2 之间调整。

🛠 组合布光:环境光打底 + 平行光主光源

只用平行光,背光面会黑得看不清;只用环境光,画面平得像纸片。实际开发中的常用套路是两种光配合:

  • 环境光:低强度,负责把背光面“兜底”提亮。
  • 平行光:高强度,负责制造明暗层次,当主光源。
// 环境光打底:强度低一点
scene.add(new THREE.AmbientLight(0xffffff, 0.4));

// 平行光当主光源:从右上方照下来
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(3, 5, 4);
scene.add(directionalLight);

下面是对比效果:左边只有平行光,背光面接近全黑;右边加上环境光后,背光面也能看清轮廓:

只有平行光

环境光 + 平行光


🧱 给场景加上阴影

有了光,自然就有影子。Three.js 的阴影默认是关闭的,需要三步手动开启:

// 1. 渲染器开启阴影贴图
renderer.shadowMap.enabled = true;

// 2. 灯光声明“产生阴影”
directionalLight.castShadow = true;

// 3. 物体声明“投出阴影”或“接收阴影”
cube.castShadow = true; // 立方体投出阴影
plane.receiveShadow = true; // 地面接收阴影

完整示例:一个悬空的立方体,影子投在地面上:

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 320 / 240, 0.1, 1000);
camera.position.set(2, 2, 4);
camera.lookAt(0, 0, 0);

scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(3, 5, 4);
directionalLight.castShadow = true;
scene.add(directionalLight);

// 悬空的立方体
const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0x44aa88 })
);
cube.position.y = 0.8;
cube.castShadow = true;
scene.add(cube);

// 地面:用一个大平面充当
const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(6, 6),
    new THREE.MeshStandardMaterial({ color: 0xdddddd })
);
plane.rotation.x = -Math.PI / 2; // 平面默认竖着,转成水平
plane.position.y = -0.5;
plane.receiveShadow = true;
scene.add(plane);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(320, 240);
renderer.shadowMap.enabled = true; // 别忘了渲染器这一步
containerRef.value.appendChild(renderer.domElement);
renderer.render(scene, camera);

代码里两个新面孔解释一下:

  • camera.lookAt(0, 0, 0):让相机转头盯住原点,不管相机摆在哪都朝向场景中心。
  • plane.rotation.x = -Math.PI / 2:新建的平面是竖立的(正对着相机),绕 x 轴转 90° 才平躺在地上。

下面是这段代码的真实渲染效果,注意立方体投在地面上的影子:

阴影是有渲染开销的。场景很大或物体很多时,可以只让关键物体 castShadow,小物件不投影,兼顾效果和性能。

🧾 小节总结

  • 三种常用灯光:环境光(均匀兜底)、平行光(像太阳,当主光源)、点光源(像灯泡)。
  • 组合套路:低强度环境光打底 + 高强度平行光制造层次。
  • 点光源按物理距离衰减,强度要给得比平行光大很多。
  • 阴影三步开启:渲染器 shadowMap.enabled、灯光 castShadow、物体 castShadow / receiveShadow
  • camera.lookAt() 让相机盯住指定点;平面默认竖立,要转 90° 才能当地面。

❓ 知识问答

Q1:灯光越多画面越亮吗?

总体趋势是变亮,但过亮会一片惨白。亮度主要靠一主一辅两盏灯控制,不够再微调强度,不建议堆数量。

Q2:为什么我的阴影边缘粗糙、有锯齿?

阴影贴图默认分辨率较低。可以提高灯光的 shadow.mapSize,比如 light.shadow.mapSize.set(1024, 1024),代价是多一点显存开销。

Q3:Basic 材质会受到灯光影响吗?

不会。MeshBasicMaterial 天生不响应灯光,加再多灯颜色也不变。想要明暗效果请用 MeshStandardMaterial

Q4:环境光会产生阴影吗?

不会。环境光没有方向,到处都一样亮,无从产生影子。能产生阴影的是平行光、点光源这类有方向的光。


🧪 小练习

搭一个“黄昏小场景”:

  1. 创建一个球体放在地面上,球体和地面都用 Standard 材质。
  2. 添加低强度环境光,再加一盏暖色调(如 0xffaa55)的平行光。
  3. 开启阴影,让球体的影子投在地面上。
views/DuskScene.vue
<template>
    <div ref="containerRef" class="container"></div>
</template>

<script setup>
import { onMounted, ref } from 'vue';
import * as THREE from 'three';

const containerRef = ref(null);

onMounted(() => {
    // 请在这里编写代码
});
</script>

<style scoped>
.container {
    width: 320px;
    height: 240px;
}
</style>

🎉 恭喜你已经学会布光和阴影啦!下一篇我们回头细看相机的四个参数,搞懂“看得清、看不见”背后的原理。