Skip to content

基于物理的渲染和照明

此笔记记录于DISCOVER three.js,大多数为其中的摘要,少数为笔者自己的理解

起步

要打开物理上正确的照明,只需启用渲染器的 .physicallyCorrectLights设置:

js
function createRenderer() {
  const renderer = new WebGLRenderer();

  // turn on the physically correct lighting model
  renderer.physicallyCorrectLights = true;

  return renderer;
}

默认情况下禁用此设置以保持向后兼容性。但是,打开它没有缺点,因此我们将始终启用它。

three.js中的大小单位是米

使用米为单位是一种约定,而不是规则。如果您不遵循它,那么除了物理上精确的照明之外的一切都仍然有效。 事实上,在某些情况下使用不同的比例是有意义的。例如,如果您正在构建一个大规模的空间模拟,您可能会决定使用 1单位=1000公里。但是,如果您想要物理上准确的照明,那么您必须使用以下公式将场景构建到真实世界的规模:

three.js中的光照

  1. 直接照明:直接来自灯泡并撞击物体的光线。
  2. 间接照明:光线在击中物体之前已经从墙壁和房间内的其他物体反弹,每次反弹都会改变颜色并失去强度。

与这些相匹配,three.js 中的灯光类分为两种类型:

  1. 直接光照,模拟直接光照。
  2. 环境光,这是 一种 廉价且可信的间接照明方式。

间接照明很难模拟,因为这样做需要计算从场景中所有表面永远反射的无限数量的光线。没有足够强大的计算机来做到这一点,即使我们限制自己仅计算几千条光线,每条光线只产生几次反弹( 光线追踪 ),实时计算通常仍然需要很长时间。因此,如果我们想要场景中的真实光照,我们需要某种方式(环境光)来伪造间接光照。

three.js 核心中总共有四种直接光源类型可用,每一种都模拟一个常见的现实世界光源:

  • DirectionalLight => 阳光
  • PointLight => 灯泡
  • RectAreaLight => 条形照明或明亮的窗户
  • SpotLight => 聚光灯

默认下禁用阴影

即使我们使用 PBR,现实世界和 three.js 之间的一个区别是默认情况下对象不会阻挡光线。光路径中的每个物体都会收到照明,即使路上有一堵墙。落在物体上的光会照亮它,但也会直接穿过并照亮后面的物体。物理正确性就这么多!

我们可以逐个对象的、逐个光照的手动启用阴影。但是,阴影很昂贵,因此我们通常只为一盏灯或两盏灯启用阴影,尤其是当我们的场景需要在移动设备上工作时。只有直接光类型可以投射阴影,环境光不能。

在代码中增加光源

DirectionalLight设计的目的是模仿遥远的光源,例如太阳。来自DirectionalLight的光线不会随着距离而消失。场景中的所有对象都将被同样明亮地照亮,无论它们放在哪里——即使是在灯光后面

创建components/light

js
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中导入并将其添加到场景中:

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

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