Discover three.js is now open source!
Word Count:572, reading time: ~3minutes

基于物理的渲染和照明

最近, 基于物理的渲染 (PBR)已成为渲染实时和电影 3D 场景的行业标准方法。顾名思义,这种渲染技术使用真实世界的物理学来计算表面对光的反应方式,从而避免在场景中设置材质和照明时进行猜测。PBR 是迪士尼为其长篇动画创建的,也用于现代游戏引擎,如 Unreal 和 Frostbite。令人惊讶的是,微小的(压缩后为 600kb)three.js 内核允许我们使用与这些行业领先巨头相同的物理正确渲染技术,不仅如此,我们甚至可以在智能手机等低功耗设备上运行这些技术。就在几年前,这是一项需要大量功能强大的计算机的尖端技术,而现在我们可以在任何地方的网络浏览器中运行它。

在 three.js 中使用 PBR 就像切换我们使用的材质并添加光源一样简单。我们将在下面介绍最重要的 three.js PBR 材料,即MeshStandardMaterial。我们不会在本书中深入探讨基于物理渲染的技术细节,但如果您有兴趣了解更多信息,请阅读这本出色的奥斯卡获奖书(是的,他们显然将奥斯卡奖授予书籍) 基于物理的渲染:从理论到实现是完全免费的。

照明和材料

光照和材质在计算机图形渲染系统中有着内在的联系。我们不能谈论一个而不说另一个,这就是为什么在本章中,我们还介绍了一种新的光源:DirectionalLight。这种光类型模仿来自遥远光源(如太阳)的光线。我们将在本书后面更详细地探讨灯光和材质如何相互作用。要使用诸如MeshStandardMaterial的 PBR 材质,我们必须在场景中添加灯光。这是很显然的——如果没有光,我们就看不到。到目前为止,我们一直在使用的材料MeshBasicMaterial不是基于物理的,也不需要灯光。

轻按开关即可昼夜交替

使用老式的、非基于物理的渲染创建好看的场景需要进行大量繁琐的调整。考虑一下这种情况:您已经为建筑展示设置了一个日间餐厅场景,阳光透过窗户在房间周围形成美丽的高光和阴影。稍后,您决定添加夜间模式以展示房间周围的照明设备。使用非 PBR 技术进行设置将是一项繁重的工作。所有照明和材质参数都需要调整,然后重新调整,然后再次重新调整,直到夜景看起来和白天一样好。

现在,想象同样的场景,但这次您使用的是物理上正确的照明和材料。要将白天切换到夜间,您只需关闭代表太阳的灯并打开灯具中的灯。那个主吸顶灯是一个百瓦的白炽灯泡?检查现实世界中等效灯泡的包装,注意它输出多少流明,然后在代码中使用该值,就完成了。

精心制作的基于物理的材料在所有照明条件下看起来都很棒。

启用物理上正确的光照

在向场景添加灯光之前,我们将切换到使用物理上正确的光照强度计算。物理上正确的 照明 与基于物理的 渲染 不同,但是,将两者结合使用来为我们提供完整的物理上准确的场景是有意义的。物理上正确的照明意味着使用真实世界的物理方程计算 光如何随着与光源的距离(衰减)而衰减。这计算起来相当简单,你可以在任何物理教科书中找到这些方程。另一方面,基于物理的渲染涉及以物理上正确的方式 计算光与表面的反应。这些方程要复杂得多,至少对于任何比镜子更复杂的表面来说是这样。幸运的是,我们不必了解它们即可使用它们!

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

renderer.js: 启用物理正确的照明
    function createRenderer() {
  const renderer = new WebGLRenderer();

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

  return renderer;
}

  

默认情况下禁用此设置以保持向后兼容性。但是,打开它没有缺点,因此我们将始终启用它。我们还需要调整一些参数,以使颜色和照明以物理上正确的方式工作。但是,通过启用此设置,在我们的场景中,我们在产品级别、物理精确照明方面迈出了重要的第一步。

创建物理大小的场景

为了使物理上正确的照明准确,您需要构建物理大小的场景。如果你的房间有 1000 公里宽,那么使用真实灯泡的数据是没有意义的!如果你想让一个百瓦的灯泡以与等效真实房间中的等效灯泡相同的方式照亮房间,则必须使用米将房间建造成正确的比例。

three.js 中的大小单位是米

  • 我们之前创建的$2\times 2 \times 2$的立方体每边长为两米。
  • camera.far = 100意味着我们可以看到一百米的距离。
  • camera.near = 0.1意味着距离相机不到十厘米的物体将不可见。

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

$ 1 \text{单位} = 1 \text{米}$

如果您引入由另一位艺术家制作的以英尺、英寸、厘米或弗隆为单位的模型,您应该将它们重新缩放为米。 我们将在下一章向您展示如何缩放对象

Three.js 中的光照

如果你在一个黑暗的房间里打开一个灯泡,那个房间里的物体会以两种方式接收到光:

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

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

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

我们可以轻松模拟直接照明。直接光线从光源出来并沿直线继续,直到它们击中或不击中物体。然而,间接照明更难模拟,因为这样做需要计算从场景中所有表面永远反射的无限数量的光线。没有足够强大的计算机来做到这一点,即使我们限制自己仅计算几千条光线,每条光线只产生几次反弹( 光线追踪 ),实时计算通常仍然需要很长时间。因此,如果我们想要场景中的真实光照,我们需要某种方式来伪造间接光照。在 three.js 中有几种技术可以做到这一点,其中环境光就是其中之一。其他的几种技术分别是基于图像的照明 (IBL) 和光探测器,我们将在本书后面看到。

直接照明

在本章中,我们将添加DirectionalLight,它模拟来自太阳或另一个非常明亮的遥远光源的光。我们将在本节稍后部分回到 环境照明。three.js 核心中总共有四种直接光源类型可用,每一种都模拟一个常见的现实世界光源:

  • DirectionalLight => 阳光

  • PointLight => 灯泡

  • RectAreaLight => 条形照明或明亮的窗户

  • SpotLight => 聚光灯

默认情况下禁用阴影

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

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

介绍DirectionalLight

来自定向光的光线

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

DirectionalLight的光线是平行的,从一个位置照向一个目标。默认情况下,目标放置在我们场景的中心(点$(0, 0, 0)$),所以当我们移动周围的光线时,它总是会向中心照射。

添加一个DirectionalLight到我们的场景

说得够多了,让我们在场景中添加一个DirectionalLight。打开或创建 components/lights.js 模块,该模块将遵循与此文件夹中其他组件相同的模式。首先,我们将导入DirectionalLight类,然后我们将设置一个createLights函数,最后,我们将导出该函数:

lights.js: 初始化模块结构
    
import { DirectionalLight } from 'three';

function createLights() {
const light = null; // TODO

return light;
}

export { createLights };

  

创建一个DirectionalLight

DirectionalLight构造函数有两个参数,颜色color和强度intensity。在这里,我们创建一个强度为 8 的纯白光:

lights.js: 创建一个DirectionalLight
    
function createLights() {
// Create a directional light
const light = new DirectionalLight('white', 8);

return light;
}

  

所有 three.js 灯都有颜色和强度设置,继承自 Light基类

定位灯光

DirectionalLightlight.position照向light.target.position。正如我们上面提到的,灯光和目标的默认位置都是我们场景的中心,$(0, 0, 0)$。这意味着光线当前正在从$(0, 0, 0)$照向$(0, 0, 0)$。 这确实有效,但看起来不太好。我们可以通过调整light.position来改善灯光的外观。我们将通过将位置设置为$(10, 10, 10)$来达到向左、向上和朝向我们移动它的效果。

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


  

现在灯光从$(10, 10, 10)$照向$(0, 0, 0)$。

World.js 设置

World.js 中,导入新模块:

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';
...
  

然后创建一个灯光并将其添加到场景中。向场景中添加灯光就像 添加网格一样:

World.js: 创建一个灯光并将其添加到场景中
    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);
  }

  

请注意,我们仅在一次scene.add调用中就添加了灯光和网格。我们可以添加任意数量的对象,用逗号分隔。

切换到基于物理的MeshStandardMaterial

添加灯光不会有任何立竿见影的效果,因为我们目前使用的是MeshBasicMaterial。 正如我们前面提到的,这种材质会忽略场景中的任何灯光。在这里,我们将切换到MeshStandardMaterial

MeshBasicMaterial

顾名思义, MeshBasicMaterial是 three.js 中提供的最基本的材料。它根本不会对灯光做出反应,并且网格的整个表面都用单一颜色着色。不执行基于视角或距离的着色,因此对象看起来甚至不是三维的。我们所能看到的只是一个二维轮廓。

MeshBasicMaterial示例

在上面的控件中,Material菜单具有所有 three.js 材质共享的参数,而MeshBasicMaterial菜单具有来自此材质类型的参数。可以通过调整参数来改善这种材质的外观,特别是通过使用纹理,我们将在 1.8:纹理映射简介中进行探讨。您可以使用map参数测试颜色图的效果。或者,尝试使用参数envMap设置环境纹理。环境贴图是基于图像的照明的一种重要形式。但是,无论我们如何调整这些设置,我们都无法达到基于物理的材质的质量。

介绍MeshStandardMaterial

在本章中,我们将用 MeshStandardMaterial代替基本材料MeshBasicMaterial。这是一种高质量、通用、物理精确的材料,可以使用真实世界的物理方程对光做出反应。顾名思义,MeshStandardMaterial应该是几乎所有情况下的首选“标准”材料。通过添加精心制作的纹理,我们可以使用MeshStandardMaterial重建几乎任何常见的表面。

MeshStandardMaterial示例

如果你看这里的菜单,你会看到 three.js 的素材有很多设置!此场景中的控件仅显示了部分可用MeshStandardMaterial参数。

Material 基类

如果你在上面两个场景中打开 Material 菜单,你会看到这两种材质有很多相同的设置,比如透明(材质是否透明)、不透明度(透明程度)、可见(true/false 显示/隐藏材质),等等。这样做的原因是这两种材料,实际上,所有的 three.js 材料,都继承自 Material基类。你不能直接使用 Material。相反,您必须始终使用它的派生类中的某一个,例如MeshStandardMaterial或者MeshBasicMaterial

切换立方体的材质

转到 cube.js,我们将切换到这个新材料。首先,我们需要导入它:

cube.js: 导入新模块
    import { BoxBufferGeometry, Mesh, MeshStandardMaterial } from 'three';

  

然后,更新createCube函数并将旧的、枯燥的、基本的材料切换到花哨的新标准材料:

cube.js: 切换到MeshStandardMaterial
    
function createCube() {
const geometry = new BoxBufferGeometry(2, 2, 2);

// Switch the old "basic" material to
// a physically correct "standard" material
const material = new MeshStandardMaterial();

const cube = new Mesh(geometry, material);

return cube;
}

  

更改材质的颜色

我们将在这个模块中再做一个更改,并将材质的颜色设置为紫色。设置材质参数与盒子几何体等其他类略有不同,因为我们需要使用具有 命名参数规范对象

材料采用规范对象
    
const spec = {
color: 'purple',
}

const material = new MeshStandardMaterial(spec);

  

为了使我们的代码简短易读,我们将内联声明规范对象:

cube.js: 内联声明规范对象
    


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

  return cube;
}



  

当我们 设置场景的背景颜色时,我们使用了一个 CSS 颜色名称,我们在这里也做了同样的事情。

旋转立方体

最后,让我们旋转立方体,这样我们就不再直视它了。调整对象的旋转与设置位置的方式大致相同。将以下几行添加到立方体模块:

cube.js: 旋转立方体
    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;
}

  

现在把你喜欢的任何值放在那里。现在我们不再查看立方体的正面,它最终看起来像一个立方体而不是正方形。

旋转是我们遇到的第二种移动物体的方法,以及设置位置(平移)。移动物体 的技术术语是变换,我们将用于变换物体的第三种方法是缩放平移旋转缩放( TRS ) 是我们将用于在 3D 空间中定位对象的三个基本变换,我们将在 下一章中详细研究这些变换。

挑战

Import Style
Selected Texture