Skip to content

31. 正投影相机和三种灯光的阴影

Published:

前面我们学了 6 种灯光,这 6 种灯光里有 3 种是可以产生阴影的:

包括平行光 DirectionalLight、点光源 PointLight、聚光灯 SpotLight:

image.png

image.png

image.png

其余的 3 种不支持阴影:

image.png

现实世界中有光源就有阴影,阴影能增加 3D 世界的真实感,这节我们来学下阴影。

不过在学习阴影之前,我们要先学一下正投影相机。

前面用过透视相机 PerspectiveCamera

image.png

它符合人眼的规律,近大远小。

而正投影相机 OrthographicCamera 是这样的:

image.png

就不管多远,看到的都一样大,它的范围就不需要角度啥的了,只需要 left、right、top、bottom、near、far 这 6 个值构成立方体。

相比透视投影相机,正投影相机确实用的少,但是在计算阴影的时候,会用到正投影相机。

我们来写代码试一下:

npx create-vite orthographic-camera-shadow

image.png

这节开始我们就不直接从 CDN 引入 threejs 了,因为最近遇到过一次 CDN 挂掉的问题,这次用 vite 构建。

我们用 create-vite 创建项目。

进入项目,安装 three.js

npm install
npm install --save three
npm install --save-dev @types/three

正好这次下载了 three 的包,你可以看一下它的 package.json

image.png

可以看到它把 /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

image.png

2025-03-31 18.43.42.gif

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

image.png

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

image.png

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 就是宽度

看下效果:

image.png

是不是感觉怪怪的。

对比下之前用透视相机的:

image.png

透视相机的近大远小符合人眼规律,而正投影相机的远近一样大确实会看着有点怪。

但是计算阴影的时候,会用到正投影相机。

而且正投影相机同样可以用 CameraHelper 来可视化。

用 CameraHelper 可视化正投影相机,那我们还需要一个透视相机来观察:

image.png

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);

这个就是正投影相机的可视范围:

2025-03-31 19.40.14.gif

就像前面说的这样:

image.png

既然说正投影相机主要是用来产生阴影的,那如何产生阴影呢?

把正投影相机和立方体注释掉,我们在 mesh.js 里写个平面和立方体:

image.png

image.png

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;

创建一个平面,两个立方体,调整下位置。

看下效果:

image.png

现在没有阴影,看起来比较假,我们给它加上阴影。

因为阴影计算是消耗性能的所以默认没有开启

首先需要开启 Renderer 的阴影:

renderer.shadowMap.enabled = true;

然后给计算阴影的 Light 开启阴影:

light.castShadow = true;

给会产生阴影的物体开启阴影,比如那两个立方体:

mesh.castShadow = true;

给接收其他物体阴影的物体开启接收阴影,比如下面的平面:

mesh.receiveShadow = true;

最后要调整下灯光的阴影相机的范围。

其实这几个步骤还是容易理解的,因为阴影计算是消耗性能的,所以要开启的话就要设置 Renderer 和哪些 Light、Mesh 要计算阴影。

我们来试一下:

image.png

设置平面接收阴影,立方体产生阴影:

plane.receiveShadow = true;

box.castShadow = true;

然后设置 light 产生阴影:

image.png

directionalLight.position.set(1000, 1000, 500);
directionalLight.castShadow = true;

这里我顺便改了一下光源的位置。

然后 renderer 开启阴影计算:

image.png

renderer.shadowMap.enabled = true;

看下效果:

image.png

现在啥也没有,因为我们还没设置灯光的阴影相机范围。

先打印一下它:

image.png

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

image.png

很容易理解,平行光投射的光线都是平行的,那阴影的范围可不就是正投影相机么。

我们用 CameraHelper 可视化下它:

image.png

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

2025-03-31 21.44.30.gif

可以看到,它的范围很小。

我们把它改大一点:

image.png

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;

正投影相机要设置上下左右,远近裁截面距离,这个我们设置过。

看下效果:

2025-03-31 21.47.54.gif

可以看到,产生阴影了。

阴影在正投影相机的范围内。

所以我们设置正投影相机的可视范围包含要显示阴影的物体就好了。

这就是平行光的阴影的设置方式。

我们用 dat.gui 改一下光源的位置试试:

image.png

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

const gui = new GUI();

image.png

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 的调试控件。

调下灯光位置看看效果:

2025-03-31 21.55.53.gif

有的时候阴影显示不全,这个是正投影相机的可视范围不够大导致的,再调解下正投影相机的可视范围就行。

前面说过,点光源、聚光灯也是可以产生阴影的,它们的阴影和平行光一样么?

试试就知道了:

image.png

new THREE.PointLight(0xffffff, 10000000);

换成点光源看看:

2025-03-31 22.10.15.gif

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

image.png

聚光灯也是一样:

image.png

2025-03-31 22.31.09.gif

所以说,平行光的阴影相机是正投影相机,点光源和聚光灯的都是透视投影相机。

案例代码上传了小册仓库

总结

这节我们学了下正投影相机和阴影。

透视投影相机是近大远小效果,而正投影相机是远近一样大。

正投影相机确实用的比较少,但在设置平行光阴影的时候会用到。

6 种灯光里只有点光源、聚光灯、平行光可以产生阴影,需要在 renderer 开启阴影 shadowMap.enabled,在灯光处开启阴影 castShadow,在产生阴影的物体设置阴影 castShadow,在接收阴影的物体设置 receiveShadow。

之后还要设置阴影相机的大小,平行光的阴影相机是正投影相机,点光源和聚光灯的是透视投影相机。

阴影相机的可视范围覆盖住要产生阴影的物体即可。

为了增加 3D 场景的真实感,很多时候是需要渲染阴影的。

评论