Skip to main content

使用 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 按钮:

当您单击该按钮时,模型将以 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('在解析场景时发生错误', error);
},
options
);

在这里,您可以看到我们创建了一个 GLTFExporter, 可以用来导出 THREE.Scene。 我们可以将场景导出为 glTF 二进制格式或 JSON 格式。 在此示例中,我们导出为 JSONglTF 格式是一个复杂的格式, 虽然 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 删除立方体。 删除后,我们在其中加载我们导出的场景的空场景。

从顶部的文件菜单中选择 Import | glTF 2.0,然后会出现文件浏览器。

导航到您下载模型的位置,选择文件,然后单击导入 glTF 2.0。 这将打开文件,并显示如下内容:

如您所见,Blender 已经导入了我们完整的场景, 我们在 Three.js 中定义的 THREE.Mesh 现在在 Blender 中可用。 在 Blender 中,我们现在可以像对待任何其他网格一样使用它。 但是,在本示例中,让我们保持简单,只使用 Cycles Blender 渲染器渲染此场景。 为此,单击右侧菜单中的渲染属性(看起来像相机的图标), 对于渲染引擎,选择 Cycles:

接下来,我们需要正确定位相机,因此使用鼠标在场景中移动,直到您满意的视图, 然后按 Ctrl + Alt + 数字键盘 0 对齐相机。 此时,您将得到类似于以下的内容:

现在,我们可以按 F12 渲染场景。这将启动 Cycles 渲染引擎, 您将在 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. 在 Three.js 中导入 glTF 文件并将其添加到场景中。

让我们首先在 Blender 中创建一个简单的模型。我们将使用 Blender 使用的默认模型,可以通过在对象模式下选择添加 | 网格 | 猴子 来添加。点击猴子以选择它:

一旦选择了模型,在顶部菜单中选择文件->导出->glTF 2.0:

在此示例中,我们只导出网格。请注意,当您从 Blender 导出时,请始终检查应用修饰符复选框。这将确保在导出网格之前应用 Blender 中使用的任何高级生成器或修饰器。

一旦文件导出,我们可以使用 GLTFImporter 在 Three.js 中加载它:

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

最终结果是来自 Blender 的确切模型,但在 Three.js 中可视化(请参见 import-from-blender.html 示例):

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

从 Blender 导出动画并导入到 Three.js

从 Blender 导出动画的过程与导出静态场景几乎相同。 因此,为了演示这一过程,我们将创建一个简单的动画,再次以 glTF 格式导出它, 并在 Three.js 场景中加载和播放它。 为此,我们将创建一个简单的场景,其中一个立方体下落并破碎成碎片。 为此,我们首先需要一个地板和一个立方体。 因此,创建一个平面和一个悬挂在该平面上方的立方体:

在这里,我们只是将立方体上移一点(按 G 键移动立方体), 并添加一个平面(添加 | 网格 | 平面),然后我们缩放了这个平面以使其变得更大。 现在,我们可以为场景添加物理效果了。 在第 12 章“在场景中添加物理效果和声音”中,我们介绍了刚体的概念。 Blender 也使用了这种方法。 选择立方体并使用“对象 | 刚体 | 添加主动”来添加其刚体, 然后选择平面并添加其刚体,如下所示:“对象 | 刚体 | 添加被动”。 此时,当我们按空格键播放动画时,您会看到立方体下落:

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

在将立方体分解为更小的部分之前,让我们向模型添加一些顶点, 以便 Blender 有足够的顶点数,可以用来分割模型。 为此,请在编辑模式中选择立方体(使用 Tab 键), 然后在顶部菜单中选择“边缘 | 细分”。执行两次此操作,您将得到如下图所示的内容:

按 Tab 键返回对象模式,并选择立方体后,打开 Cell Fracture 窗口, 然后转到“对象 | 快速效果 | Cell Fracture”:

您可以尝试使用这些设置来获得不同类型的破裂效果。 使用图 13.3 中配置的设置,您将得到如下结果:

接下来,选择原始立方体,按 x 键删除它。 这将只保留分解的部分,我们将对其进行动画处理。 为此,请选择来自立方体的所有单元并再次使用“对象 | 刚体 | 添加主动”。 完成后,按空格键,您将看到立方体在碰撞时下落并破碎。

此时,我们的动画几乎准备好了。 现在,我们需要导出此动画,以便我们可以将其加载到 Three.js 并从那里重新播放。 在执行此操作之前,请确保将动画的结束帧(屏幕右下角)设置为第 80 帧, 因为导出完整的 250 帧并不那么有用。 除此之外,我们需要告诉 Blender 将来自物理引擎的信息转换为一组关键帧。 这需要做,因为我们无法导出物理引擎本身, 因此我们必须将所有网格的位置和旋转进行烘焙,以便导出它们。 为此,请再次选择所有单元,使用“对象 | 刚体 | 烘焙到关键帧”。 您可以选择默认设置并单击“导出 glTF2.0”按钮,以获取以下屏幕:

此时,我们将为每个单元都有一个动画,用于跟踪各个网格的旋转和位置。 有了这些信息,我们可以在 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
})
}

在渲染循环中,我们需要更新每个动画的混合器:

const clock = new THREE.Clock()
const onRender = () => {
const delta = clock.getDelta()
mixers.forEach((mixer) => {
mixer.update(delta)
})
}

结果如下图所示:

我们在这里展示的相同原理可以应用于 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 键删除它,对默认的光源执行相同的操作。 使用“添加 | 网格 | 平面”将一个简单的二维平面添加到场景中。 按 Tab 键进入编辑模式,选择三个顶点,然后使用 e 键和 z 键分别沿 z 轴拉伸, 以获得一个简单的形状,如下所示:

完成这个模型后,返回对象模式(使用 Tab 键),并将一些网格放置在房间内, 使其看起来类似于这样:

此时没有什么特别的 - 只是一个简单的没有任何照明的房间。 在我们继续添加一些照明之前,稍微改变对象的颜色。 因此,在 Blender 中,转到“材质属性”,为每个网格创建一个新的材质,并设置颜色。 结果将看起来类似于这样:

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

为场景添加照明

在这个场景中,我们将添加漂亮的基于 HDRI 的照明。 使用 HDRI 照明,我们不是有一个单一的光源,而是提供一个图像,将用作场景的光源。 在这个例子中,我们从这里下载了一个 HDRI 图像:https://polyhaven.com/a/thatch_chapel

下载后,我们有一个大的图像文件,可以在 Blender 中使用。 为此,请从属性编辑器面板中打开“世界”选项卡,选择“表面”下拉菜单,并选择“背景”。 在此下方,您将找到“颜色”选项,点击它,然后选择“环境纹理”:

正如您在这里所看到的,场景已经看起来相当不错,我们无需摆放单独的灯光。 现在我们在墙上有一些漂亮的柔和阴影,物体似乎从多个角度被照亮,物体看起来很好。 为了将灯光信息作为静态光照图使用,我们需要将光照烘焙到纹理上, 并将该纹理映射到 Three.js 中的对象上。

烘焙光照纹理

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

接下来,点击刚刚添加的节点的“New”按钮,并选择纹理的大小和名称:

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

  • 渲染引擎:Cycles。
  • 采样 | 渲染:将最大采样设置为 512,否则渲染光照图将需要很长时间。
  • 在“烘焙”菜单中,从“烘焙类型”菜单中选择漫反射, 并在“影响”部分中选择直接和间接。这将只渲染环境光照的影响。

现在,您可以点击“烘焙”,Blender 将为所选对象渲染光照图到纹理中:

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

您现在可以为其他网格重复此操作。 不过,在为盒子执行此操作之前,我们需要快速修复 UV 映射。 我们需要这样做,因为我们挤压了一些顶点以创建类似房间的结构, Blender 需要知道如何正确映射它们。 在此不展开太多细节,我们可以让 Blender 提出如何创建 UV 映射的建议。 点击顶部的 UV 编辑菜单,选择 Plane,在编辑模式中, 从 UV 菜单中选择 UV | 展开 | 智能展开:

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

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

导出场景并在 Blender 中导入

我们已经在“从 Blender 导出静态场景并导入 Three.js”部分中看到了 如何从 Blender 导出场景以在 Three.js 中使用的过程,因此我们将重复这些步骤。 点击“文件 | 导出 | glTF 2.0”。我们可以使用默认设置,因为我们没有动画, 可以禁用动画复选框。导出后,我们可以将场景导入到 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
})
}

现在,当我们查看同一场景(import-from-blender-lightmap.html)时, 我们有一个带有非常漂亮的照明的场景,尽管我们没有提供任何光源, 而是使用了 Blender 中烘焙的灯光:

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

在 Blender 中烘焙环境遮蔽贴图

如果我们回到已有的场景,我们还可以烘焙环境遮蔽贴图。 这个过程与烘焙光照图的过程相同:

  1. 设置一个场景。
  2. 添加所有投射阴影的光源和物体。
  3. 确保在 Shader Editor 中有一个空的图像纹理,我们可以将阴影烘焙到这个纹理中。
  4. 选择相关的烘焙选项,并将阴影渲染到图像中。

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

如您所见,您只需更改 Bake Type 下拉菜单为 Ambient Occlusion。 现在,您可以选择要为其烘焙这些阴影的网格,并单击 Bake 按钮。 对于房间网格,结果如下:

Blender 提供了许多其他烘焙类型,您可以使用它们来获得外观漂亮的纹理(特别是对于场景的静态部分), 这可以极大地提高渲染性能。

在这个关于 Blender 的部分,我们还将研究一个主题, 即如何使用 Blender 更改纹理的 UV 映射。

在 Blender 中进行自定义 UV 建模

在本节中,我们将从一个新的空白 Blender 场景开始,并使用默认的立方体进行实验。 为了对 UV 映射的工作原理有一个良好的概述,您可以使用一种称为 UV 网格的东西, 它看起来像这样:

当您将其应用为默认立方体的纹理时,您将看到网格的各个顶点如何映射到纹理上的特定位置。 为了使用这个,我们需要做的第一件事是定义这个纹理。 您可以从屏幕右侧的属性视图中的材质属性中轻松完成这个操作。 点击 Base Color 属性前面的黄点,并选择图像纹理。 这允许您浏览要用作纹理的图像:

您已经可以在主视口中看到这个纹理应用在立方体上的情况。 如果我们将这个包括材质的网格导出到 Three.js 并进行渲染, 我们将看到完全相同的映射, 因为 Three.js 将使用由 Blender 定义的 UV 映射(import-from-blender-uv-map-1.html):

现在,让我们切换回 Blender,并打开 UV 编辑选项卡。 在屏幕右侧使用 Tab 键进入编辑模式,并选择前面四个顶点。 选择了这些顶点后,您还将在屏幕左侧看到这四个顶点的位置。 在 UV 编辑器中,您现在可以移动(按 g)顶点并将它们移动到纹理上的不同位置。 例如,您可以将它们移动到纹理的边缘,如下所示:

移动顶点将导致一个看起来像这样的立方体:

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

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

总结

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

我们现在即将结束这本书。 在最后一章中,我们将讨论另外两个主题——如何将 Three.js 与 React.js 一起使用, 并且我们将简要了解 Three.js 对 VR 和 AR 的支持。