22. Imported Models导入模型
介绍
Three.js 可以让你创建很多原始几何体,但是当涉及到更复杂的形状时,我们最好使用专用的 3D 软件建模。
在本课中,我们将使用已经制作好的模型,但我们将在以后的课程中学习如何完全在 3D 软件中创建模型。
格式
随着时间的推移,已经出现了许多 3D 模型格式。每个3D格式都在给予大家解决方案,比如模型中嵌入了什么数据、权重、压缩、兼容性、版权等。
这就是为什么今天我们可以访问数百种模型格式:https://en.wikipedia.org/wiki/List_of_file_formats#3D_graphics。
有些3D格式专用于一种软件。一些已知非常轻,但有时缺乏具体数据。众所周知,有些存储库几乎包含您可能需要的所有数据,但它们很重。有些格式是开源的,有些格式不是,有些是二进制的,有些是 ASCII,等等。
如果您需要精确的数据并且找不到您的软件支持的适当格式,您甚至可以很容易地创建自己的格式。
以下是您可能会遇到的流行格式列表:
- OBJ
- FBX
- STL
- PLY
- COLLADA
- 3DS
- GLTF
我们不会涵盖所有这些格式。这会很无聊,我们不需要这样做,因为已经有一种格式正在成为一种标准,应该可以满足您的大部分需求。
GLTF
GLTF 代表 GL 传输格式。它由 Khronos Group(OpenGL、WebGL、Vulkan、Collada 背后的人以及许多成员,如 AMD / ATI、Nvidia、Apple、id Software、Google、Nintendo 等)制作。
GLTF 在过去几年变得非常流行。
它支持不同的数据集。它可以包括几何体和材质等数据,也可以包括相机、灯光、场景图、动画、骨架、变形甚至多场景等数据。
它还支持各种文件格式,如 json、binary、embed textures。
GLTF 已成为实时标准。由于它正在成为一种标准,大多数 3D 软件、游戏引擎和库都要支持它。这意味着您可以在不同的环境中轻松获得相似的结果。
这并不意味着您必须在所有情况下都使用 GLTF。如果您只需要一个几何图形,您最好使用其他格式,如 OBJ、FBX、STL 或 PLY。你应该在每个项目上测试不同的格式,看看你是否拥有你需要的所有数据,文件是否太大,如果信息被压缩需要多长时间才能解压等等。
查找模型
首先,我们需要一个模型。正如我们之前所说,稍后我们将学习如何在 3D 软件中创建我们自己的模型,但现在,让我们使用预制模型。
GLTF 团队还提供各种模型,从简单的三角形到逼真的模型以及动画、变形、透明涂层材料等。
您可以在此存储库中找到它们:https://github.com/KhronosGroup/glTF-Sample-Models
如果你想测试这些模型,你必须下载或克隆整个存储库并获取你需要的文件。但我们将从一只简单的鸭子开始,您可以在/static/models/
启动器的文件夹中找到它。
GLTF格式
虽然 GLTF 本身是一种格式,但它也可以有不同的文件格式。这有点复杂,但有充分的理由。
如果您打开该/static/models/Duck/
文件夹,您将看到 4 个不同的文件夹。每个都包含鸭子,但 GLTF 格式不同:
- glTF
- glTF-Binary
- glTF-Draco
- glTF-Embedded
你甚至可以找到其他格式,但这 4 种是最重要的,涵盖了我们需要学习的内容。
当心; 您的操作系统可能会隐藏其中一些文件的扩展名。请参考代码编辑器中应显示扩展名的文件名。
glTF
这种格式是一种默认格式。该Duck.gltf
文件是一个 JSON格式,您可以在编辑器中打开它。它包含各种信息,如相机、灯光、场景、材质、对象转换,但既不包含几何体也不包含纹理。该Duck0.bin
文件是二进制文件,您无法打开阅读。它通常包含几何数据和与顶点相关的所有信息,如 UV 坐标、法线、顶点颜色等。DuckCM.png
文件只是鸭子的纹理。
当我们加载这种格式时,我们只要加载Duck.gltf
包含对其他文件的引用的文件,这些文件将被自动加载。
glTF-Binary
这种格式仅由一个文件组成。它包含我们在 glTF 默认格式中讨论的所有数据。那是一个二进制文件,您不能只在代码编辑器中打开它来查看里面的内容。
由于只有一个文件,因此这种格式可以更轻便且加载起来更舒适,但您将无法轻松更改其数据。例如,如果你想调整纹理大小或压缩纹理,你不能因为它在那个二进制文件中而与其他文件合并。
glTF-Draco
这种格式类似于glTF 默认格式,但缓冲区数据(通常是几何图形)是使用Draco 算法压缩的。如果比较.bin
文件大小,您会发现它要轻得多。
虽然此格式有一个单独的文件夹,但您可以将 Draco 压缩应用于其他格式。
这个先放一边,以后再说。
glTF-Embedded
这种格式类似于glTF-Binary格式,因为它只有一个文件,但这个文件实际上是一个 JSON,您可以在编辑器中打开它。
这种格式的唯一好处是只有一个易于编辑的文件。
选择
选择正确的格式取决于您希望如何处理资源。
如果你想在导出后能够改变纹理或灯光的坐标,你最好选择glTF-default
。它还具有分别加载不同文件的优势,从而提高了加载速度。
如果每个模型只需要一个文件并且不关心要不要修改资源,则最好选择glTF-Binary
。
在这两种情况下,您都必须决定是否要使用Draco
压缩,但我们稍后会介绍这一部分。
设置
启动器由一个空平面组成。
因为 GLTF 是一个标准,它显然支持灯光。通常,当您将 GLTF 导入 Three.js 项目时,您最终会得到具有MeshStandardMaterial的网格,您可能还记得,如果您的场景中没有灯光,您将看不到太多这些材料。
启动器中已经有一个AmbientLight和一个DirectionalLight 。
在 Three.js 中加载模型
要在 Three.js 中加载 GLTF 文件,我们必须使用GLTFLoader。此类在THREE
变量中默认不可用。我们需要从three
位于examples/
依赖项中的文件夹中导入它:
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
然后我们可以像为 TextureLoader 一样实例化它:
/**
* Models
*/
const gltfLoader = new GLTFLoader()
如果需要,我们也可以像在纹理课程中那样使用LoadingManager。
要加载模型,好消息,它几乎和加载纹理一样简单。我们调用该load(...)
方法并使用正确的参数:
- 文件的路径
- 成功回调函数
- 进度回调函数
- 错误回调函数
gltfLoader.load(
'/models/Duck/glTF/Duck.gltf',
(gltf) =>
{
console.log('success')
console.log(gltf)
},
(progress) =>
{
console.log('progress')
console.log(progress)
},
(error) =>
{
console.log('error')
console.log(error)
}
)
您应该看到进度和正在调用的成功函数。如果无法加载文件,可能会调用错误函数。检查路径,不要忘记我们不能添加路径是/static
的部分。
gltfLoader.load(
'/models/Duck/glTF/Duck.gltf',
(gltf) =>
{
console.log(gltf)
}
)
将加载的模型添加到我们的场景中
如果查看控制台中记录的对象,您会发现很多元素。最重要的部分是scene
属性,因为我们在导出的模型中只有一个场景。
这scene
包含了我们需要的一切。但它还包括更多。始终从研究其中可用的内容开始,并观察不同Groups、Object3D和Mesh的scale
缩放属性。
我们得到这样的东西:
THREE.Group: scene
└───Array: children
└───THREE.Object3D
└───Array: children
├───THREE.PerspectiveCamera
└───THREE.Mesh
mesh
网格应该是我们的鸭子。我们并不真正关心PerspectiveCamera。相机和鸭子似乎都在场景的子数组中的第一个也是唯一一个Object3D中。更糟糕的是,Object3D已将scale
设置为最小值。
正如您所看到的,即使是获取我们的鸭子也有点复杂,这是大多数初学者迷路的地方。
我们想要的只是让我们的鸭子出现在场景中。我们有多种方法可以做到这一点:
- 将整体添加到我们的
scene
场景中。我们可以这样做,因为即使它的名字是scene
,它实际上是一个Group。 - 将
scene
的子项添加到我们的场景中并忽略未使用的PerspectiveCamera。 - 在添加到场景之前过滤子项以删除不需要的对象,如PerspectiveCamera。
- 仅添加网格,但最终得到的鸭子可能会被错误地缩放、定位或旋转。
- 在 3D 软件中打开文件并删除PerspectiveCamera,然后再次导出GITF文件。
因为我们的模型结构简单,我们将Object3D添加到我们的场景中,而忽略里面未使用的PerspectiveCamera。在以后的课程中,我们会将整个场景添加为一个对象:
gltfLoader.load(
'/models/Duck/glTF/Duck.gltf',
(gltf) =>
{
scene.add(gltf.scene.children[0])
}
)
你应该看到渲染了一只鸭子。
您可以尝试其他格式,但不能尝试尚不能使用的 Draco
:
gltfLoader.load(
'/models/Duck/glTF/Duck.gltf', // Default glTF
// Or
gltfLoader.load(
'/models/Duck/glTF-Binary/Duck.glb', // glTF-Binary
// Or
gltfLoader.load(
'/models/Duck/glTF-Embedded/Duck.gltf', // glTF-Embedded
文件夹中提供了另一个名为FlightHelmet
(也取自glTF 模型示例/static/models/)的模型。该模型只有一种格式,即默认的 glTF。
尝试加载此模型:
gltfLoader.load(
'/models/FlightHelmet/glTF/FlightHelmet.gltf',
(gltf) =>
{
scene.add(gltf.scene.children[0])
}
)
我们没有得到漂亮的头盔,只渲染了几个零件。
问题是我们只将 loaded 的第一个child添加到我们的scene
场景中。
我们可以尝试的是循环孩子并将他们添加到场景中:
for(const child of gltf.scene.children)
{
scene.add(child)
}
这将产生更多元素,但不是全部。更糟糕的是,刷新时,您可能会得到不同的部分。
问题是当我们将一个child从一个场景添加到另一个场景时,它会自动从第一个场景中删除。这意味着现在第一个场景中的child更少了。
当我们添加第一个对象时,它会从第一个场景中移除,而第二个元素只是移动到第一个位置。但是您的循环现在采用数组的第二个元素。您将始终在children
数组中保留元素。
这个问题有多种解决方案。第一个解决方案是获取已加载场景的第一个子节点并将其添加到我们的场景中,直到没有剩余为止:
while(gltf.scene.children.length)
{
scene.add(gltf.scene.children[0])
}
我们现在得到了整个头盔。
另一种解决方案是复制children
数组以获得一个未更改的独立数组。为此,我们可以使用扩展运算符...
并将结果放入一个全新的数组中[]:
const children = [...gltf.scene.children]
for(const child of children)
{
scene.add(child)
}
这是一种原生 JavaScript 技术,可以在不触及原始数组的情况下复制数组。
最后,我们之前提到的一个简单好用的解决方案是添加属性scene
:
scene.add(gltf.scene)
我们的头盔太小了,我们只能增加比例,但我们会回到我们的 Duck 并尝试使用 Draco 压缩版本。
Draco compression 压缩
让我们回到我们的鸭子,但这一次,我们将使用 Draco 版本:
gltfLoader.load(
'/models/Duck/glTF-Draco/Duck.gltf',
可悲的是,我们没有得到任何鸭子。如果您查看日志,您应该会看到如下所示的警告No DRACOLoader instance provided
。我们需要为我们的GLTFLoader提供一个DRACOLoader实例,以便它可以加载压缩文件。
正如我们在浏览文件时看到的,Draco 版本比默认版本要轻得多。压缩应用于缓冲区数据(通常是几何图形)。使用默认的 glTF、二进制 glTF或嵌入式 glTF并不重要。
它甚至不是 glTF 独有的,您可以将它与其他格式一起使用。但是 glTF 和 Draco 同时流行起来,所以 glTF 导出器的实现速度更快。
谷歌在开源 Apache 许可下开发算法:
- 网站: https: //google.github.io/draco/
- Git 存储库: https: //github.com/google/draco
添加 DRACOLoader
Three.js 已经支持 Draco。我们必须从DRACOLoader导入开始:
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
然后我们可以实例化加载器(在 gltfLoader
之前):
const dracoLoader = new DRACOLoader()
解码器在原生 JavaScript 和 Web Assembly (wasm) 中可用,并且它可以在 worker 中运行(我们在物理课结束时看到的另一个线程)。这两个功能显著提高了性能,但它们意味着具有完全独立的代码。
Three.js 已经提供了这个分离的代码。要找到它,我们必须浏览到 Three.js 依赖项并将 Draco 解码器文件夹复制到我们的/static/
文件夹中。
这个 Draco 文件夹位于/node_modules/three/examples/js/libs/
. 获取整个/draco/
文件夹并将其复制到您的/static/
文件夹中。我们现在可以将此文件夹的路径提供给我们的dracoLoader
:
dracoLoader.setDecoderPath('/draco/')
最后,我们可以使用setDRACOLoader(...)
方法将DRACOLoader实例提供给GLTFLoader实例:
gltfLoader.setDRACOLoader(dracoLoader)
你的鸭子应该回来了,但这次是 Draco 压缩版本。
您仍然可以使用GLTFLoader加载未压缩的 glTF 文件,并且仅在需要时加载 Draco 解码器。
何时使用 Draco 压缩
虽然您可能认为 Draco 压缩是一个双赢的局面,但事实并非如此。是的,几何体更轻,但首先,您必须加载DRACOLoader类和解码器。其次,您的计算机需要花费时间和资源来解码压缩文件,这可能会导致体验开始时出现短暂的卡顿,即使我们使用的是 worker 和 Web Assembly 代码也是如此。
你必须适应并决定什么是最好的解决方案。如果你只有一个 100kB 几何模型,你可能不需要 Draco。但是,如果您有很大 MB 的模型要加载并且不关心用户体验开始时要进行一些等待,您可能需要 Draco 压缩。
动画
正如我们之前所说,glTF 也支持动画。Three.js 可以处理这些动画。
加载动画模型
首先,我们需要一个动画模型。我们可以使用位于文件夹中的狐狸/static/models/Fox/
(也取自glTF 模型示例)。
更改加载该狐狸的路径:
gltfLoader.load(
'/models/Fox/glTF/Fox.gltf',
我们渲染出现了问题; 狐狸太大了。如果您看不到它,请查看上方或缩小。
在处理动画之前,让我们修复比例。如果您查看导入场景的组成,狐狸由一个Object3D组成,它本身由一个Bone和一个SkinnedMesh组成。我不会解释它们是什么,但是我们不应该简单地缩放Object3D就解决问题了。即使现在可以用缩放解决,但是它可能不适用于更复杂的模型。
我们在这里可以做的是缩放加载的场景并将其直接添加到我们的场景中:
gltfLoader.load(
'/models/Fox/glTF/Fox.gltf',
(gltf) =>
{
gltf.scene.scale.set(0.025, 0.025, 0.025)
scene.add(gltf.scene)
}
)
处理动画
如果查看加载的对象,您会看到一个名为gltf
包含多个AnimationClip 的animations
属性。
这些AnimationClip不能轻易使用。我们首先需要创建一个AnimationMixer。AnimationMixer就像一个可以包含一个或多个AnimationClips 的对象相关联的播放器。这个想法是为每个需要动画的对象创建一个。
在 success 函数中,创建一个AnimationMixer
混音器并发送gltf.sceneas
参数:
const mixer = new THREE.AnimationMixer(gltf.scene)
我们现在可以使用clipAction(...)
该方法将AnimationClip添加到混合器中。让我们从第一个动画开始:
const action = mixer.clipAction(gltf.animations[0])
这个方法返回一个AnimationAction,我们终于可以调用play()
它的方法了:
action.play()
遗憾的是,还是没有动画。
要播放动画,我们必须告诉混音器在每一帧更新自己。问题是我们的mixer
变量已经在加载回调函数中声明了,我们在函数中无权访问它tick。为了解决这个问题,我们可以在加载回调函数之外声明一个mixer = null
值的变量,并在加载模型时更新它:
let mixer = null
gltfLoader.load(
'/models/Fox/glTF/Fox.gltf',
(gltf) =>
{
gltf.scene.scale.set(0.03, 0.03, 0.03)
scene.add(gltf.scene)
mixer = new THREE.AnimationMixer(gltf.scene)
const action = mixer.clipAction(gltf.animations[0])
action.play()
}
)
最后,我们可以用已经计算好的deltaTime
更新tick
函数中的混音器。
但在更新它之前,我们必须测试mixer
变量是否与null
不同。这样,如果模型已加载,我们不会更新混音器,这意味着动画尚未准备好:
const tick = () =>
{
// ...
if(mixer)
{
mixer.update(deltaTime)
}
// ...
}
动画应该正在运行。您可以通过更改clipAction(...)
方法中的值来测试其他动画。
const action = mixer.clipAction(gltf.animations[2])
Three.js在线编辑器
Three.js 拥有自己的在线编辑器。你可以在这里找到它: https: //threejs.org/editor/
它就像一个 3D 软件,但在线且功能较少。您可以创建图元、灯光、材质等。
因为您可以导入模型,所以这是测试您的模型是否正常工作的好方法。虽然要小心; 您只能测试由一个文件组成的模型。您可以尝试使用 glTF-Binary 或 glTF-Embedded 的文件格式。
将模型拖放到编辑器中。
你应该看到一只黑鸭子,因为没有光。从菜单中添加一个AmbientLight
和一个DirectionalLight
以更清楚地查看它。
最后,您可以以各种格式导出您的场景,您可以在您的代码中重复使用这些格式,但不在我们讨论范围内。
目前就是这样,但我们将在接下来的课程中多次使用加载的模型进行开发。