第十三章使用Blender和Three.js

在本章中,我们将更深入地了解如何将Blender和Three.js一起使用。我们会解释的 本章中的以下概念:

  • 从Three.js导出并导入到Blender:我们将创建一个简单的场景,从Three..js导出,然后在Blender中加载和渲染。
  • 从Blender导出静态场景并将其导入Three.js:在这里,我们将在Blender中创建一个场景,将其导出到Three.js,并在Three.js中渲染。
  • 从Blender导出动画并将其导入Three.js:Blender允许我们创建动画,我们将创建一个简单的动画,并在Three.js中加载和显示。
  • 在Blender中烘焙光照贴图和环境遮挡贴图:Blender允许我们烘焙可以在Three.js中使用的不同类型的贴图。
  • Blender中的自定义UV建模:通过UV建模,我们可以确定纹理如何应用于几何体。Blender提供了很多工具来简化这一过程。我们将探讨如何使用Blender的UV建模功能,并使用Three.js中的结果。

在我们开始学习本章之前,请确保安装Blender,以便您可以继续学习。您可以从这里下载操作系统的安装程序来安装Blender:https://www.blender.org/download/.本章中显示的Blender屏幕截图是使用macOS版本的Blender拍摄的,但Windows和Linux版本看起来是一样的。

让我们从第一个主题开始,我们在Three.js中创建一个场景,将其导出为中间格式,最后将其导入Blender。

从Three.js导出并导入Blender

对于这个例子,我们将只取一个简单的例子,重用我们在第6章“探索高级几何”中看到的参数几何。如果在浏览器中打开export-to-blender.html,则可以创建一些参数化几何图形。在右侧菜单的底部,我们添加了一个exportScene按钮:

image-20230605101830195

图13.1–我们将导出的简单场景

当您点击该按钮时,模型将以GLTF格式保存并下载到你的电脑。要使用Three.js导出模型,我们可以使用GLTFexporter,如下所示:

const exporter = new GLTFExporter()
const options = {
 trs: false,
 onlyVisible: true,
 binary: false
}
exporter.parse(
 scene,
 (result) => {
 const output = JSON.stringify(result, null, 2)
 save(new Blob([output], { type: 'text/plain' }),
 'out.gltf')
 },
 (error) => {
 console.log('An error happened during parsing of the
 scene', error)
 },
 options
)

在这里,你可以看到我们已经创建了一个GLTFExporter,我们可以使用它来导出THREE.Scene。我们可以以glTF二进制格式或JSON格式导出场景。对于本例,我们以JSON导出。glTF格式是一种复杂的格式,虽然GLTFExporter支持组成Three.js场景的许多对象,但在导出失败时仍可能遇到问题。更新到Three.js的最新版本通常是最好的解决方案,因为这个组件的工作一直在进行。

一旦我们得到了输出,我们就可以触发浏览器的下载功能,这将把它保存到您的本地机器上:

const save = (blob, filename) => {
 const link = document.createElement('a')
 link.style.display = 'none'
 document.body.appendChild(link)
 link.href = URL.createObjectURL(blob)
 link.download = filename
 link.click()
}

结果是一个glTF文件,它的前几行如下所示:

{
 "asset": {
 "version": "2.0"
      "generator": "THREE.GLTFExporter"
 },
 "scenes": [
 {
 "nodes": [
 0,
 1,
 2,
 3
 ]
 }
 ],
 "scene": 0,
 "nodes": [
 {},
     ...

现在我们有了一个包含场景的glTF文件,我们可以将其导入Blender。因此,打开Blender,您将看到带有单个立方体的默认场景。通过选择立方体并按x来移除立方体。移除后,我们将在一个空场景中加载导出的场景。

从顶部的“文件”菜单中,选择“导入|glTF 2.0”,您将看到一个文件浏览器。导航到下载模型的位置,选择文件,然后单击Import glTF 2.0。这将打开文件,并显示如下内容:

image-20230605102336001

图13.2–Blender中导入的Three.js场景

正如你所看到的,Blender已经导入了我们的完整场景,我们在THREE.js中定义的THREE.Mesh现在可以在Blender中使用了。在Blender中,我们现在可以像使用任何其他网格一样使用它。但是,对于本例,让我们保持简单,只使用Cycles Blender渲染器渲染此场景。要执行此操作,请单击右侧菜单中的“渲染属性”(看起来像摄影机的图标),然后选择“循环”作为“渲染引擎”:

image-20230605102451829

图13.3–使用Blender中的Cycles渲染引擎进行渲染

接下来,我们需要正确定位相机,因此使用鼠标在场景中移动,直到获得满意的视图,然后按Ctrl+Alt+numpad 0对齐相机。在这一点上,你会得到这样的东西:

image-20230605103110641

图13.4–显示摄影机看到的区域以及将要渲染的内容

现在,我们可以通过按F12来渲染场景。这将启动Cycles渲染引擎,您将在Blender中看到正在渲染的模型:

image-20230605103216116

图13.5-我们导出的Three.js模型在Blender中渲染的最终图像

正如您所看到的,使用glTF作为Three.js和Blender之间交换模型和场景的格式非常简单。只需使用GLTFExporter,在Blender中导入模型,您就可以使用Blender在您的模型上提供的一切。

当然,另一种方法也同样简单,我们将在下一节中向您展示。

从 Blender导出一个静态场景并将其导入到Three.js中

从Blender导出模型和导入模型一样简单。在旧版本的Three.js中,有一个特定的Blender插件,您可以使用它以Three.js-特定的JSON格式导出。然而,在后来的版本中,Three.js中的glTF已经成为与其他工具交换模型的标准。因此,要想让Blender发挥作用,我们所要做的就是:

  1. 在Blender中创建一个模型。
  2. 将模型导出到glTF文件中。
  3. 在Blender中导入glTF文件并将其添加到场景中。

让我们先在Blender中创建一个简单的模型。我们将使用Blender使用的默认模型,可以通过从菜单中选择 Add | Mesh | Monkey在对象模式中添加该模型。点击猴子选择它:

image-20230605103810175

图13.6–在Blender中创建要导出的模型

选择模型后,在顶部菜单中,选择“文件”->“导出”->“glTF 2.0”:

image-20230605103858292

图13.7–选择glTF导出

对于本例,我们仅导出网格。请注意,从Blender导出时,请始终选中“应用修改器”复选框。这将确保在导出网格之前应用Blender中使用的任何高级生成器或修改器。

image-20230605104023974

图13.8-将模型导出为glTF文件

导出文件后,我们可以使用GLTFImporter将其加载到Three.js中:

const loader = new GLTFLoader()
 return loader.loadAsync('/assets/gltf/
 blender-export/monkey.glb').then((structure) => {
 return structure.scene
 })

最终结果是Blender中的精确模型,但在Three.js中可视化(请参见从Blender.html导入的示例):

image-20230605104148918

图13.9–在Three.js中可视化的Blender模型

请注意,这不仅限于网格——使用glTF,我们还可以以相同的方式导出灯光、相机和纹理。

从Blender导出动画并将其导入Three.js

从Blender导出动画的工作方式与导出静态场景的工作方式大致相同。因此,在本例中,我们将创建一个简单的动画,再次以glTF格式导出,并将其加载到Three.js场景中。为此,我们将创建一个简单的场景,在该场景中,我们渲染一个立方体掉落并分解成多个部分。我们首先需要的是一个地板和一个立方体。因此,创建一个平面和一个略高于此平面的立方体:

image-20230605104332114

图13.10–一个空的 Blender 项目

在这里,我们只是将立方体向上移动了一点(按G获取立方体)并添加了一个平面(Add |Mesh | plane),然后缩放这个平面使其更大。现在,我们可以将物理添加到场景中。在第12章“为场景添加物理和声音”中,我们介绍了刚体的概念。Blender使用相同的方法。选择立方体并使用“Object | Rigid Body | Add Active”,然后选择平面并添加其刚体,如下所示:“Object |Rigid Body | Add Passive”。此时,当我们在Blender中播放(通过使用空格键)动画时,您将看到立方体掉落:

image-20230605105531388

图13.11——立方体坠落的中途动画

要创建断块效果,我们需要启用Cell Fracture插件。为此,请转到“编辑|首选项”屏幕,选择“加载项”,使用搜索选项搜索Cell Fractureplugin,然后选中复选框以启用该插件:

image-20230605105629345

图13.12-启用 Cell Fracture 插件

在我们将立方体分割成更小的部分之前,让我们向模型中添加一些顶点,以便Blender有大量的顶点,可以用来分割模型。为此,请在“编辑模式”下选择立方体(使用Tab键),然后从顶部的菜单中选择 Edge | Subdivide.。这样做两次,你会得到这样的东西:

image-20230605134228709

图13.13-显示了具有多个细分的多维数据集

点击Tab键返回“对象模式”,选中多维数据集后,打开“ Cell Fracture”窗口,然后转到 Object | Quick Effects | Cell Fracture:

image-20230605134311625

图13.14–配置 fractures

你可以使用这些设置来获得不同类型的 fractures。使用图13.3中配置的设置,您将得到如下内容:

image-20230605134540555

图13.15——显示 fractures 的立方体

接下来,选择原始立方体并点击x将其删除。这只会留下断裂的部分,我们将对其进行动画处理。若要执行此操作,请从多维数据集中选择所有单元,然后再次使用Object | Rigid Body | Add Active 。完成后,点击空格键,你会看到立方体在撞击时掉落并分解。

image-20230605134651829

图13.16:立方体掉落后

在这一点上,我们几乎已经准备好了我们的动画。现在,我们需要导出这个动画,这样我们就可以将其加载到Three.js中并从那里回放。在执行此操作之前,请确保将动画的结尾(屏幕的右下角)设置为第80帧,因为导出完整的250帧没有那么有用。除此之外,我们还需要告诉Blender将来自物理引擎的信息转换为一组关键帧。这需要完成,因为我们无法导出物理引擎本身,所以我们必须烘焙所有网格的位置和旋转,以便导出它们。若要执行此操作,请再次选择所有单元,然后使用 Object | Rigid Body | Bake to Keyframes 。您可以选择默认值,然后单击导出glTF2.0 按钮以获得以下屏幕:

image-20230605134813848

图13.17–动画导出设置

此时,我们将为每个单元创建一个动画,以跟踪各个网格的旋转和位置。有了这些信息,我们可以在Three.js中加载场景,并设置动画混合器进行播放:

const mixers = []
const modelAsync = () => {
 const loader = new GLTFLoader()
 return loader.loadAsync('/assets/models/blender-cells/fracture.glb').then((structure) => {
 console.log(structure)
 // 设置地面
 const planeMesh = structure.scene.
 getObjectByName('Plane')
 planeMesh.material.side = THREE.DoubleSide
    planeMesh.material.color = new THREE.Color(0xff5555)
 // 设置工件的材质
 const materialPieces = new THREE.MeshStandardMaterial({ color: 0xffcc33 })
 structure.animations.forEach((animation) => {
 const meshName = animation.name.substring
 (0, animation.name.indexOf('Action')).replace('.', '')
 const mesh = structure.scene.
 getObjectByName(meshName)
 mesh.material = materialPieces
 const mixer = new THREE.AnimationMixer(mesh)
 const action = mixer.clipAction(animation)
 action.play()
 mixers.push(mixer)
 })
 applyShadowsAndDepthWrite(structure.scene)
 return structure.scene
 })
}

结果如下所示:

image-20230605135228318

图13.18–Three.js中的分解立方体

我们在这里向您展示的相同原理可以应用于Blender支持的不同类型的动画。需要记住的主要一点是Three.js不会理解Blender或其他高级动画模型使用的物理引擎。因此,导出动画时,请确保烘焙动画,以便使用标准的Three.js工具播放这些基于关键帧的动画。

在下一节中,我们将更深入地了解如何使用Blender烘焙不同类型的纹理(贴图),然后将其加载到Three.js中。我们已经在第10章“加载和使用纹理”中看到了实际效果,但在本节中,将向您展示如何用Blender烘焙这些贴图。

在Blender中烘焙光照贴图和环境遮挡贴图

对于这个场景,我们将重新访问第10章中的示例,其中我们使用了Blender中的光照贴图。这个光照图提供了好看的照明,而无需在Three.js中实时计算。要使用Blender实现这一点,我们将采取以下步骤:

  1. 用几个模型在Blender中设置一个简单的场景。
  2. 在Blender中设置照明和模型。
  3. 在Blender中将灯光烘焙为纹理。
  4. 导出场景。
  5. 在Three.js中渲染所有内容

在下面的部分中,我们将详细讨论每个步骤。

在 Blender 中设置一个场景

对于本例,我们将创建一个简单的场景,在其中我们将在一些照明中烘焙。启动一个新项目,通过选择并点击x来删除默认立方体,并对默认灯光执行相同操作。使用 Add | Mesh | Plane 将简单的2D平面添加到场景中。按Tab键进入编辑模式,选择三个顶点,然后挤出e,然后挤出z,沿z轴挤出,得到一个简单的形状,如下所示:

image-20230605140704713

图13.19——创建一个简单的房间结构

获得此模型后,返回“对象模式”(使用Tab键),并在房间中放置几个网格,以获得与此处所示类似的内容:

image-20230605140747800

图 13.20 – 一个完整的房间,有一些网眼

在这一点上没有什么特别之处——只是一个没有任何照明的简单房间。在我们继续添加一些照明之前,请稍微更改对象的颜色。因此,在“Blender”中,转到“材质属性”,为每个网格创建一种新材质,然后设置一种颜色。结果将类似于此:

image-20230605141058292

图13.21–为场景中的不同对象添加颜色

接下来,我们将添加一些漂亮的照明。

向场景添加照明

对于此场景中的照明,我们将添加基于HDRI的良好照明。使用HDRI照明,我们没有单一的光源,但提供了一个将用作场景光源的图像。对于这个例子,我们从这里下载了一个HDRI图像:https://polyhaven.com/a/thatch_chapel.

image-20230605141811298

图13.22-从 Poly Haven 下载HDRI

下载后,我们有一个大的图像文件,我们可以在Blender使用。为此,请从 Properties Editor 面板中打开 World 选项卡,选择 Surface下拉列表,然后选择 Background。在此下方,您将找到 Color 选项,单击此选项,然后选择 Environment Texture::

image-20230605142747310

图13.23–为世界添加环境纹理

接下来,单击“打开”,浏览到下载图像的位置,然后选择该位置。在这一点上,我们可以渲染场景,并查看HDRI贴图提供的照明:

image-20230605142831537

图13.24–渲染场景以检查HDRI照明

正如你在这里看到的,在不需要放置单独的灯光的情况下,场景看起来已经很好了。我们现在在墙上有一些漂亮的柔和阴影,这些物体似乎是从多个角度照亮的,而且这些物体看起来很漂亮。要将灯光中的信息用作静态光照贴图,我们需要将灯光烘焙到纹理上,并将该纹理映射到Three.js中的对象。

烘焙灯光纹理

要烘焙灯光,首先,我们必须创建一个纹理来保存这些信息。选择立方体(或要为其烘焙照明的任何其他对象)。转到“着色”视图,然后在屏幕底部的“节点编辑器”中,添加一个新的“图像纹理”项目:“添加|纹理|图像纹理”。默认值应易于使用:

image-20230605143211962

图13.25–添加纹理图像以保存烘焙的光照贴图

接下来,单击刚刚添加的节点的“新建”按钮,然后选择纹理的大小和名称:

image-20230605143308852

图13.26–添加一个新图像以与纹理图像一起使用

现在,转到“特性编辑器”面板的“渲染”选项卡,并设置以下特性:

  • 渲染引擎:循环。
  • 采样|渲染:将“最大采样数”设置为512,否则渲染光照贴图将花费很长时间。
  • 在“烘焙”菜单中,从“烘焙类型”菜单中选择“漫反射”,然后在“影响”区域中,选择“直接”和“间接”。这只会渲染环境照明的影响。

现在,可以单击“烘焙”,“混合器”会将选定对象的光照贴图渲染到纹理:

image-20230605143505832

图13.27–立方体的渲染光照图

就这样。正如你在左下角的图像查看器中所看到的,我们现在为立方体提供了一个外观漂亮的渲染光照图。通过单击图像查看器中的汉堡包菜单,可以将此图像导出为独立纹理:

image-20230605143549710

图13.28–将光照贴图导出到外部文件

现在可以对其他网格重复此操作。不过,在对长方体执行此操作之前,我们需要快速修复UV贴图。我们需要这样做,因为我们挤出了几个顶点来形成类似房间的结构,Blender需要知道如何正确映射它们。在这里不需要太多细节,我们可以让Blender就如何创建UV贴图提出建议。单击顶部的“UV编辑”菜单,选择“平面”,转到“编辑模式”,然后从“UV”菜单中选择“UV |展开|智能展开”:

image-20230605143650385

图13.29–固定房间网格的UV

这将确保为房间的所有侧面生成光照贴图。现在,对所有网格重复此操作,您将获得该特定场景的光照贴图。导出所有光照贴图后,我们可以导出场景本身,然后使用Three.js中创建的光照贴图进行渲染:

image-20230605143753949

图13.30–所有创建的光照贴图

现在我们已经烘焙了所有贴图,下一步是从Blender导出所有内容,并在Three.js中导入场景和贴图。

导出场景并将其导入Blender

我们已经在“从Blender导出静态场景并将其导入Three.js”部分中看到了如何从Blender中导出场景以在Three.js中使用,因此我们将重复这些相同的步骤。单击文件|导出|glTF 2.0。我们可以使用默认值,因为我们没有动画,所以我们可以禁用动画复选框。导出后,我们可以将场景导入到Three.js中。如果我们不应用纹理(并使用我们自己的默认灯光),场景将如下所示:

image-20230605144006886

图13.31–使用默认灯光渲染的Three.js场景,不使用光照贴图

我们已经在第10章中看到了如何加载和应用光照贴图。以下代码片段显示了如何为我们从Blender导出的所有光照贴图加载光照贴图纹理:

const cubeLightMap = new THREE.TextureLoader().load('/assets/models/blender-lightmaps/cube-light-map.png')
const cylinderLightMap = new THREE.TextureLoader().load('/assets/models/blender-lightmaps/cylinder-light-map.png')
const roomLightMap = new THREE.TextureLoader().load('/assets/models/blender-lightmaps/room-light-map.png')
const torusLightMap = new THREE.TextureLoader().load('/assets/models/blender-lightmaps/torus-light-map.png')
const addLightMap = (mesh, lightMap) => {
 const uv1 = mesh.geometry.getAttribute('uv')
 const uv2 = uv1.clone()
 mesh.geometry.setAttribute('uv2', uv2)
 mesh.material.lightMap = lightMap
 lightMap.flipY = false
}
const modelAsync = () => {
 const loader = new GLTFLoader()
 return loader.loadAsync('/assets/models/blender-
 lightmaps/light-map.glb').then((structure) => {
 const cubeMesh = structure.scene.
 getObjectByName('Cube')
 const cylinderMesh = structure.scene.
 getObjectByName('Cylinder')
 const torusMesh = structure.scene.
 getObjectByName('Torus')
 const roomMesh = structure.scene.
 getObjectByName('Plane')
 addLightMap(cubeMesh, cubeLightMap)
 addLightMap(cylinderMesh, cylinderLightMap)
 addLightMap(torusMesh, torusLightMap)
 addLightMap(roomMesh, roomLightMap)
 return structure.scene
 })
}

现在,当我们看到同一个场景(从blender lightmap.html导入)时,我们有一个照明非常好的场景,尽管我们自己没有提供任何光源,而是使用了blender中的烘焙灯光:

image-20230605144215057

图13.32–相同的场景,但使用Blender应用的烘焙光照贴图

如果我们导出光照贴图,我们已经隐式地获得了关于阴影的信息,因为在这些位置,当然会有更少的光。我们还可以从Blender中获得更详细的阴影贴图。例如,我们可以生成环境遮挡贴图,这样我们就不必在运行时创建这些贴图。

在Blender中烘焙环境光遮挡贴图

如果我们回到现有的场景,我们还可以烘焙环境光遮挡贴图。其方法与用于烘焙光照贴图的方法相同:

  1. 设置场景。
  2. 添加所有投射阴影的灯光和对象。
  3. 确保在着色器编辑器中有一个空的图像纹理,我们可以将阴影烘焙到该纹理。
  4. 选择相关的烘焙选项并将阴影渲染到图像中。

由于前三个步骤与光照贴图的步骤相同,我们将跳过这些步骤,查看渲染阴影贴图所需的渲染设置:

image-20230605144516327

图13.33–烘焙环境光遮挡贴图的渲染设置

如您所见,您只需将“烘焙类型”的下拉列表更改为“环境光遮挡”即可。现在,可以选择要为其烘焙这些阴影的网格,然后单击“烘焙”按钮。对于房间网格,结果如下所示:

image-20230605144602391

图13.34-作为纹理的环境遮挡贴图

Blender提供了许多其他烘焙类型,可以用来获得好看的纹理(尤其是场景的静态部分),这可以大大提高渲染性能。

关于Blender,我们将在本节中再看一个主题,那就是如何使用Blender来更改纹理的UV贴图。

Blender中的自定义UV建模

在本节中,我们将从一个新的空Blender场景开始,并使用默认立方体进行实验。为了更好地了解UV贴图的工作原理,可以使用一种称为UV栅格的东西,它看起来像这样:

image-20230605144840502

图13.35–UV纹理样本

将其应用为默认立方体的纹理时,将看到网格的各个顶点如何映射到纹理上的特定位置。要使用这个,我们需要做的第一件事就是定义这个纹理。您可以从屏幕右侧“属性”视图中的“材质属性”轻松执行此操作。单击“基本颜色”特性之前的黄点,然后选择“图像纹理”。这允许您浏览要用作纹理的图像:

image-20230605145000425

图13.36–在Blender中向网格添加纹理

您已经可以在主视口中看到该纹理是如何应用于立方体的。如果我们将包括材质在内的网格导出到Three.js中并进行渲染,我们将看到完全相同的贴图,因为Three.jss将使用Blender定义的UV贴图(import-from-Blender-UV-map-1.html):

image-20230605145558077

图13.37–在Three.js中渲染的具有UV网格的长方体

现在,让我们切换回Blender,打开“UV编辑”选项卡。转到屏幕右侧的“编辑模式”(使用tab键),然后选择四个正面顶点。选择这些后,您将在屏幕左侧看到这四个顶点的位置。

image-20230605145239990

图13.38–UV编辑器显示了构成立方体正面的四个像素的映射

在UV编辑器中,现在可以抓取(g)顶点并将它们移动到纹理上的不同位置。例如,可以将它们移动到纹理的边缘,如下所示:

image-20230605145333777

图13.39–映射到完整纹理的一侧

移动顶点将生成如下所示的立方体:

image-20230605145421348

图13.40–具有自定义UV贴图的立方体的Blender渲染

当然,当我们导出和导入这个最小模型时,这也直接显示在Three.js中:

image-20230605145137253

图13.41–具有自定义UV映射的立方体的Three.js视图

使用这种方法,可以很容易地定义网格的哪些部分应该映射到纹理的哪个部分。

总结

在本章中,我们探讨了如何与Blender和Three.js协同工作。我们展示了如何使用glTF格式作为标准格式在Three.js和Blender之间交换数据。这非常适用于网格、动画和大多数纹理。然而,对于高级纹理属性,您可能需要在Three.js或Blender中进行一些微调。我们还展示了如何在Blender中烘焙特定纹理,如光照贴图和环境遮挡贴图,并在Three.js中使用它们。这使您可以在Blender中将这些信息渲染一次,将其导入Three.js,并创建出色的阴影、灯光和环境遮挡,而无需像Three.js正常情况下那样进行繁重的计算。请注意,这当然只适用于照明是静态的,并且几何体和网格不会移动或改变形状的场景。通常,可以将其用于场景的静态部分。最后,我们看了一下UV贴图是如何工作的,顶点映射到纹理上的某个位置的位置,以及如何使用Blender来处理此贴图。再次,通过使用glTF作为交换格式,Blender的所有信息都可以很容易地在Three.js中使用。

我们现在到了这本书的结尾。在最后一章中,我们将研究另外两个主题——如何将Three.js与React.js结合使用,我们将快速了解Three.js对VR和AR的支持。