前面我们学了 6 种灯光,这 6 种灯光里有 3 种是可以产生阴影的:
包括平行光 DirectionalLight、点光源 PointLight、聚光灯 SpotLight:



其余的 3 种不支持阴影:

现实世界中有光源就有阴影,阴影能增加 3D 世界的真实感,这节我们来学下阴影。
不过在学习阴影之前,我们要先学一下正投影相机。
前面用过透视相机 PerspectiveCamera

它符合人眼的规律,近大远小。
而正投影相机 OrthographicCamera 是这样的:

就不管多远,看到的都一样大,它的范围就不需要角度啥的了,只需要 left、right、top、bottom、near、far 这 6 个值构成立方体。
相比透视投影相机,正投影相机确实用的少,但是在计算阴影的时候,会用到正投影相机。
我们来写代码试一下:
npx create-vite orthographic-camera-shadow

这节开始我们就不直接从 CDN 引入 threejs 了,因为最近遇到过一次 CDN 挂掉的问题,这次用 vite 构建。
我们用 create-vite 创建项目。
进入项目,安装 three.js
npm install
npm install --save three
npm install --save-dev @types/three
正好这次下载了 three 的包,你可以看一下它的 package.json

可以看到它把 /examples/jsm/* 映射成了 addons/*,所以前面我们用 cdn 引入的时候才这样映射。
改下 src/main.js
import './style.css';
import * as THREE from 'three';
import {
OrbitControls
} from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(100, 100, 100);
const material = new THREE.MeshLambertMaterial({
color: new THREE.Color('orange')
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const axesHelper = new THREE.AxesHelper(500);
scene.add(axesHelper);
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(400, 200, 300);
scene.add(directionalLight);
const ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
camera.position.set(400, 200, 300);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
document.body.append(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
创建 Scene、Light、Camera、Renderer。
这里有两种 Light,平行光和环境光。
在 scene 中添加一个立方体。
然后改下 style.css
body {
margin: 0;
}
跑一下:
npm run dev


记住现在这个角度看起来是这样的:

我们换成正交相机试一下:

const aspectRatio = width / height;
const num = 500;
const camera = new THREE.OrthographicCamera(
- num * aspectRatio,
num * aspectRatio,
num,
-num,
0.1,
10000
);
camera.position.set(400, 200, 300);
camera.lookAt(0, 0, 0);
我们同样按照网页的宽高比来设置宽高,先计算出宽高比 aspectRatio
-num 到 num 是高度,那乘以宽高比之后 -num * aspectRatio 到 num * aspectRatio 就是宽度
看下效果:

是不是感觉怪怪的。
对比下之前用透视相机的:

透视相机的近大远小符合人眼规律,而正投影相机的远近一样大确实会看着有点怪。
但是计算阴影的时候,会用到正投影相机。
而且正投影相机同样可以用 CameraHelper 来可视化。
用 CameraHelper 可视化正投影相机,那我们还需要一个透视相机来观察:

const camera2 = new THREE.OrthographicCamera(
- num * aspectRatio,
num * aspectRatio,
num,
-num,
0.1,
5000
);
camera2.position.set(400, 200, 300);
camera2.lookAt(0, 0, 0);
const cameraHelper = new THREE.CameraHelper(camera2);
scene.add(cameraHelper);
const camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
camera.position.set(1000, 2000, 1000);
camera.lookAt(0, 0, 0);
这个就是正投影相机的可视范围:

就像前面说的这样:

既然说正投影相机主要是用来产生阴影的,那如何产生阴影呢?
把正投影相机和立方体注释掉,我们在 mesh.js 里写个平面和立方体:


src/mesh.js
import * as THREE from 'three';
const planeGeometry = new THREE.PlaneGeometry(2000, 2000);
const planeMaterial = new THREE.MeshLambertMaterial({
color: new THREE.Color('skyblue')
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotateX(- Math.PI / 2);
plane.position.y = -50;
const boxGeometry = new THREE.BoxGeometry(200, 600, 200);
const boxMaterial = new THREE.MeshLambertMaterial({
color: new THREE.Color('orange')
});
const box = new THREE.Mesh(boxGeometry, boxMaterial);
box.position.y = 200;
const box2 = box.clone();
box2.position.x = 500;
const mesh = new THREE.Group();
mesh.add(plane);
mesh.add(box);
mesh.add(box2);
export default mesh;
创建一个平面,两个立方体,调整下位置。
看下效果:

现在没有阴影,看起来比较假,我们给它加上阴影。
因为阴影计算是消耗性能的所以默认没有开启
首先需要开启 Renderer 的阴影:
renderer.shadowMap.enabled = true;
然后给计算阴影的 Light 开启阴影:
light.castShadow = true;
给会产生阴影的物体开启阴影,比如那两个立方体:
mesh.castShadow = true;
给接收其他物体阴影的物体开启接收阴影,比如下面的平面:
mesh.receiveShadow = true;
最后要调整下灯光的阴影相机的范围。
其实这几个步骤还是容易理解的,因为阴影计算是消耗性能的,所以要开启的话就要设置 Renderer 和哪些 Light、Mesh 要计算阴影。
我们来试一下:

设置平面接收阴影,立方体产生阴影:
plane.receiveShadow = true;
box.castShadow = true;
然后设置 light 产生阴影:

directionalLight.position.set(1000, 1000, 500);
directionalLight.castShadow = true;
这里我顺便改了一下光源的位置。
然后 renderer 开启阴影计算:

renderer.shadowMap.enabled = true;
看下效果:

现在啥也没有,因为我们还没设置灯光的阴影相机范围。
先打印一下它:

可以看到,它是一个正投影相机:

很容易理解,平行光投射的光线都是平行的,那阴影的范围可不就是正投影相机么。
我们用 CameraHelper 可视化下它:

const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(cameraHelper);

可以看到,它的范围很小。
我们把它改大一点:

directionalLight.shadow.camera.left = -500;
directionalLight.shadow.camera.right = 500;
directionalLight.shadow.camera.top = 500;
directionalLight.shadow.camera.bottom = -500;
directionalLight.shadow.camera.near = 0.1;
directionalLight.shadow.camera.far = 3000;
正投影相机要设置上下左右,远近裁截面距离,这个我们设置过。
看下效果:

可以看到,产生阴影了。
阴影在正投影相机的范围内。
所以我们设置正投影相机的可视范围包含要显示阴影的物体就好了。
这就是平行光的阴影的设置方式。
我们用 dat.gui 改一下光源的位置试试:

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
const gui = new GUI();

directionalLight.shadow.camera.far = 10000;
gui.add(directionalLight.position, 'x', 0, 10000);
gui.add(directionalLight.position, 'y', 0, 10000);
gui.add(directionalLight.position, 'z', 0, 10000);
先把阴影相机的远裁截面设置的大一点。
然后用 gui 添加 light.position 的调试控件。
调下灯光位置看看效果:

有的时候阴影显示不全,这个是正投影相机的可视范围不够大导致的,再调解下正投影相机的可视范围就行。
前面说过,点光源、聚光灯也是可以产生阴影的,它们的阴影和平行光一样么?
试试就知道了:

new THREE.PointLight(0xffffff, 10000000);
换成点光源看看:

这明显是一个透视相机,从类型也可以看出来:

聚光灯也是一样:


所以说,平行光的阴影相机是正投影相机,点光源和聚光灯的都是透视投影相机。
案例代码上传了小册仓库。
总结
这节我们学了下正投影相机和阴影。
透视投影相机是近大远小效果,而正投影相机是远近一样大。
正投影相机确实用的比较少,但在设置平行光阴影的时候会用到。
6 种灯光里只有点光源、聚光灯、平行光可以产生阴影,需要在 renderer 开启阴影 shadowMap.enabled,在灯光处开启阴影 castShadow,在产生阴影的物体设置阴影 castShadow,在接收阴影的物体设置 receiveShadow。
之后还要设置阴影相机的大小,平行光的阴影相机是正投影相机,点光源和聚光灯的是透视投影相机。
阴影相机的可视范围覆盖住要产生阴影的物体即可。
为了增加 3D 场景的真实感,很多时候是需要渲染阴影的。