基于物理的渲染和照明
此笔记记录于DISCOVER three.js,大多数为其中的摘要,少数为笔者自己的理解
起步
要打开物理上正确的照明,只需启用渲染器的 .physicallyCorrectLights
设置:
function createRenderer() {
const renderer = new WebGLRenderer();
// turn on the physically correct lighting model
renderer.physicallyCorrectLights = true;
return renderer;
}
默认情况下禁用此设置以保持向后兼容性。但是,打开它没有缺点,因此我们将始终启用它。
three.js 中的大小单位是米
使用米为单位是一种约定,而不是规则。如果您不遵循它,那么除了物理上精确的照明之外的一切都仍然有效。 事实上,在某些情况下使用不同的比例是有意义的。例如,如果您正在构建一个大规模的空间模拟,您可能会决定使用 1 单位=1000 公里。但是,如果您想要物理上准确的照明,那么您必须使用以下公式将场景构建到真实世界的规模:
three.js 中的光照
- 直接照明:直接来自灯泡并撞击物体的光线。
- 间接照明:光线在击中物体之前已经从墙壁和房间内的其他物体反弹,每次反弹都会改变颜色并失去强度。
与这些相匹配,three.js 中的灯光类分为两种类型:
- 直接光照,模拟直接光照。
- 环境光,这是 一种 廉价且可信的间接照明方式。
间接照明很难模拟,因为这样做需要计算从场景中所有表面永远反射的无限数量的光线。没有足够强大的计算机来做到这一点,即使我们限制自己仅计算几千条光线,每条光线只产生几次反弹( 光线追踪 ),实时计算通常仍然需要很长时间。因此,如果我们想要场景中的真实光照,我们需要某种方式(环境光)来伪造间接光照。
three.js 核心中总共有四种直接光源类型可用,每一种都模拟一个常见的现实世界光源:
DirectionalLight
=> 阳光PointLight
=> 灯泡RectAreaLight
=> 条形照明或明亮的窗户SpotLight
=> 聚光灯
默认下禁用阴影:
即使我们使用 PBR,现实世界和 three.js 之间的一个区别是默认情况下对象不会阻挡光线。光路径中的每个物体都会收到照明,即使路上有一堵墙。落在物体上的光会照亮它,但也会直接穿过并照亮后面的物体。物理正确性就这么多!
我们可以逐个对象的、逐个光照的手动启用阴影。但是,阴影很昂贵,因此我们通常只为一盏灯或两盏灯启用阴影,尤其是当我们的场景需要在移动设备上工作时。只有直接光类型可以投射阴影,环境光不能。
在代码中增加光源
DirectionalLight
设计的目的是模仿遥远的光源,例如太阳。来自DirectionalLight
的光线不会随着距离而消失。场景中的所有对象都将被同样明亮地照亮,无论它们放在哪里——即使是在灯光后面。
创建components/light
import { DirectionalLight } from 'three';
function createLights() {
// Create a directional light
const light = new DirectionalLight('white', 8); // 颜色,强度
// move the light right, up, and towards us
light.position.set(10, 10, 10);
return light;
}
export { createLights };
所有 three.js 灯都有颜色和强度设置,继承自 Light
基类。
World.js
中导入并将其添加到场景中:
import { createCamera } from './components/camera.js';
import { createCube } from './components/cube.js';
import { createLights } from './components/lights.js';
import { createScene } from './components/scene.js';
import { createRenderer } from './systems/renderer.js';
import { Resizer } from './systems/Resizer.js';
let scene;
let camera;
let renderer;
class World {
constructor(container) {
camera = createCamera();
scene = createScene();
renderer = createRenderer();
container.append(renderer.domElement);
const cube = createCube();
const light = createLights();
scene.add(cube, light);
const resizer = new Resizer(container, camera, renderer);
}
render() {
// draw a single frame
renderer.render(scene, camera);
}
}
export { World };
材质: MeshStandardMaterial
添加灯光不会有任何立竿见影的效果,因为我们目前使用的是MeshBasicMaterial
。 正如我们前面提到的,这种材质会忽略场景中的任何灯光。
- 我们将用
MeshStandardMaterial
代替基本材料MeshBasicMaterial
。这是一种高质量、通用、物理精确的材料,可以使用真实世界的物理方程对光做出反应。 - 顾名思义,
MeshStandardMaterial
应该是几乎所有情况下的首选“标准”材料。通过添加精心制作的纹理,我们可以使用MeshStandardMaterial
重建几乎任何常见的表面。
如果你在上面两个场景中打开 Material 菜单,你会看到这两种材质有很多相同的设置,比如透明(材质是否透明)、不透明度(透明程度)、可见(true/false 显示/隐藏材质),等等。这样做的原因是这两种材料,实际上,所有的 three.js 材料,都继承自 Material
基类。你不能直接使用 Material。相反,您必须始终使用它的派生类中的某一个,例如MeshStandardMaterial
或者MeshBasicMaterial
。
在代码中切换材质
cuebe.js
import { BoxBufferGeometry, Mesh, MeshStandardMaterial } from 'three';
function createCube() {
const geometry = new BoxBufferGeometry(2, 2, 2);
// Switch the old "basic" material to
// a physically correct "standard" material
const material = new MeshStandardMaterial({ color: 'purple' });
const cube = new Mesh(geometry, material);
cube.rotation.set(-0.5, -0.1, 0.8); // 旋转
return cube;
}
export { createCube };