最近基于物理的渲染(PBR)已成为实时和电影3D场景渲染的行业标准方法。顾名思义,这种渲染技术基于现实世界物理定律,根据场景中的材料和照明设置来计算表面对光的反应方式。PBR是Disney公司为其动画制作发明的技术,也用于现代游戏引擎,如Unreal和Frostbite。令人惊讶的是,压缩后仅 600kb的three.js核心使我们能够使用与这些巨头一样的渲染技术,甚至还可以在智能手机等低功耗设备上运行。就在几年前,这还是需要强大算力支撑的尖端技术,而现在我们可以在任何地方只需网络浏览器就可以运行。

使用Three.js的PBR很简单,只需切换我们使用的材料并添加光源即可。我们将介绍最重要的three.js PBR 材质,MeshStandardMaterial。在这个教程里我们不会深入PBR技术的实现细节,但如果你有兴趣了解更多,Physically Based Rendering: From Theory To Implementation 这本书是完全免费的。

1、照明和材料

在计算机图形渲染系统照明和材料中有着内在的联系。我们不能只谈论一个而避开另一个,这就是为什么在这个教程中我们需要介绍DirectionalLight 。这种光模仿来自遥远的光源(如太阳)的光线。稍后我们将更详细地探讨灯光和材料是如何相互作用的。要使用 MeshStandardMaterial这样的PBR 材料,我们必须为现场添加光线。这是有道理的 - 在现实世界中,如果没有光,我们看不见任何东西。与之相反,MeshBasicMaterial 材料不是基于物理的,不需要光线。

2、白天和夜间模式的切换问题

使用老派的、非PBR渲染来创建好看的场景需要大量乏味的参数调整。考虑这个场景:你为建筑展示设置了白天的起居室场景,阳光透过窗户,在房间周围营造出美丽的高光和阴影。稍后,你决定添加夜间模式,以展示房间周围的照明装置。使用非 PBR 技术的话,这种调整需要做大量工作。所有的照明和材料参数都需要调整,然后重新不停调整,直到夜景看起来和白天的场景一样好。

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

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

3、启用物理正确的照明

在为场景添加光线之前,我们将切换到使用物理正确的照明强度计算。物理正确的照明与基于物理的渲染不是一回事,但是,将两者同时使用以便给我们一个完整的物理准确的场景是有意义的。物理正确的照明意味着使用真实世界的物理方程计算光线如何随着距离光源(衰减)而衰减。这是相当简单的计算,你可以在任何物理教科书找到这些方程。另一方面,基于物理的渲染涉及以物理正确的方式计算光线与表面的反应。这些方程要复杂得多。幸运的是,我们不必完全理解原理就可以使用它们!

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

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

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

  return renderer;
}

默认情况下,此设置将禁用以保持向后兼容性。但是,开启它并没有什么不良的后果,因此我们将一直启用这个选项。为了使颜色和照明以物理上正确的方式工作,我们还需要调整一些参数,然而通过启用此设置,我们朝着生产级物理精确的场景照明迈出了重要的第一步。

4、创建物理大小的场景

要使物理上正确的照明准确无误,我们需要构建物理大小的场景。如果你的房间有1000公里长,使用来自真实灯泡的数据是没有意义的!如果你想用100瓦的灯泡照亮一个房间,就像在同等真实房间的灯泡一样,你必须以米为单位的正确尺度来建造房间。

在three.js中,单位是米

  • 我们之前创建的2×2×2立方体,每边有两米长。
  • camera.far = 100意味着我们可以看到100米远。
  • camera.near = 0.1意味着离相机0.1米以内的物体将看不见。

使用米是一种惯例而不是规则。如果你不遵循这一惯例,除了物理上正确的照明不能正常运作之外,其他特性仍然会工作。事实上,在某些情况下,使用不同的尺度是有意义的。例如,如果你正在构建一个大规模的空间模拟,可能会决定使用1 个单位=1000 公里。但是,如果你想要物理上正确的照明,那么就必须使用此公式将场景构建为真实世界的规模:

1 个单位=1 米

如果引入了由另一位艺术家构建的以英尺、英寸、厘米等单位计量的模型,则应将其重新缩放到米。

5、Three.js中的照明

如果你在黑暗的房间里打开灯泡,该房间中的物体将以两种方式接收光线:

  1. 直接照明:直接来自灯泡并击中物体的光线。
  2. 间接照明:光线在集中物体之前从墙壁和房间内的其他物体上反射出来,改变颜色,每次反射都会损失一部分强度。

与之匹配,three.js中的光分为两种类型:

  1. Direct lights,模拟直接照明。
  2. Ambient lights,这是一种廉价且大致可信的方式模拟间接照明。

我们可以轻松模拟直接照明。直接光线从光源中出来,并以直线继续,直到它们击中物体,或不击中物体。然而,间接照明更难模拟,因为这样做需要计算从场景中所有表面无限次反射的无限数量的光线。没有足够强大的计算机来做到这一点,即使我们把自己限制在仅仅计算几千光射线,每个光只考虑几次反射(射线跟踪),它仍然需要太长的时间来计算。因此,如果我们想要在我们的场景中实现逼真的照明,需要某种方式来模拟间接照明。在Three.js中有几种技术可以做到,环境光(Ambient Light)是其中之一。其他技术包括基于图像的照明 (IBL) 和光探头,我们将在后续教程中介绍。

6、直接照明

在这一章中,我们将添加DirectionalLight来模拟来自太阳或其他非常明亮的遥远光源。我们将在稍后部分再讨论Ambient Light。Three.js核心提供了四种直接光,每种光类型都模拟了共同的实际光源:

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

默认情况下,阴影会被禁用

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

我们可以逐个物体、逐个光源的手动启用阴影。但是,阴影成本很高,因此我们通常只启用一两盏灯的阴影,尤其是在我们的场景需要在移动设备上工作的情况下。只有直接的光线类型才能投射阴影,环境光不会产生阴影。

7、DirectionalLight介绍

定向光的光线
定向光的光线

DirectionalLight (定向光)旨在模仿遥远的光源,如太阳,光线不会随着距离而褪色。场景中的所有物体无论放置在哪里,都会被同样程度地照亮,即使是在灯光后面

定向光的光线是平行的,从光源位置向目标照射。 默认情况下,目标被放置在我们场景的原点(0,0,0),所以当我们移动光源,它将永远照向原点。

8、添加DirectionalLight

好了, 让我们给场景加一个DirectionalLight。打开或创建components/lights.js模块,该模块将遵循与该文件夹中其他组件相同的模式。首先,我们将导入该类,然后实现并导出createLights方法:

import { DirectionalLight } from 'three';

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

  return light;
}

export { createLights };

定向光构造器需要两个参数,颜色强度。在这里,我们创建一个强度为8的纯白光:

function createLights() {
  // Create a directional light
  const light = new DirectionalLight('white', 8);

  return light;
}

Three.js中所有的光都有颜色和强度设置,继承自Light基类。

9、放置光源

定向光light.position射向light.target.position。正如我们上面提到的,光目标的默认位置是我们场景的中心(0,0,0)。这意味着光目前从(0,0,0)射向(0,0,0)。这确实有效,但看起来不太好。我们可以通过调整light.position来提高表现力。我们将移动它到位置(10,10,10)。

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

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

然后创建一个光源并将其添加到场景中。为场景添加光线就像添加mesh对象

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 调用中同时添加了光源和mesh对象。

11、切换到PBR材质

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

顾名思义,MeshBasicMaterial是Three.js中最基本的材料,它根本不对灯光做出反应,网格对象的整个表面都用单一颜色着色,不考虑视角或距离,因此对象看起来甚至不是三维的。我们只能看到一个2D轮廓。

现在我们用MeshStandardMaterial替换基本材料。这是一种高质量、通用、物理精确的材料,使用真实世界的物理方程对光线做出反应。顾名思义,MeshStandardMaterial应该是几乎所有情况下的"标准"材料。辅以精心制作的纹理,我们可以使用MeshStandardMaterial 重建任何表面。

12、Three.js材料基类

如果在上述两个场景中打开"Material"菜单,就会看到两种材料具有许多相同的设置,例如transparent(材料是否透明)、opacity(透明度)、visible(显示/隐藏)等等。原因是所有的Three.js材料,都继承自Material基类。你不能直接使用Material,必须利用某个继承类,例如MeshStandardMaterialBasic

13、切换立方体的材料

前往cube.js我们将切换到此新材料。首先需要导入它:

import { BoxBufferGeometry, Mesh, MeshStandardMaterial } from 'three';

然后,更新createCube 函数,将老式、无聊的基本材料切换到新的标准材料:

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

14、更改材料的颜色

我们将在此模块中再进行一次更改,并将材料的颜色设置为紫色。设置材料参数与three.js中的其他类略有不同,因为我们需要使用具有命名参数的规范对象

const spec = {
  color: 'purple',
}

const material = new MeshStandardMaterial(spec);

为了保持代码简短可读,我们采用内联式对象声明:

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颜色名称设置颜色。

15、旋转立方体

作为教程的最后一部分,让我们旋转立方体,这样我们不再能从正面观察它。调整对象的旋转工作方式与设置其位置的方式大致相同。将以下代码添加到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;
}

可以设置为你喜欢的值。现在它终于看起来像一个立方体,而不是一个正方形。

The rotated cube

原文链接:Physically Based Rendering and Lighting

BimAnt翻译整理,转载请标明出处