24.Three.js 解决方案之加载.gLTF模型
24 Three.js 解决方案之加载.gltf 模型
上一章节,我们讲解了加载 .obj 模型,本文将讲解加载 .gltf 模型。
注:虽然我们标题写的是 “加载.gltf 模型”,但更加准确地说法应该是 “加载 gtTF 文件”
首先我们先回顾一下 .obj 文件格式的模型一些特征:文件格式简单(纯文本)、除模型外无法提供其他场景元素(例如摄像机、灯光等)。
本文要讲解的 .gltf 格式文件可以包含的数据内容和类型要比 .obj 多很多。
常见 3D 文件格式 和 gltf 的区别
我们先将常见的 3D 文件格式进行划分
第 1 类(原始文件):
3D 建模软件本身特有的、原始的文件格式,例如:
- Blender 对应的是 .blend
- 3D Max 对应的是 .max
- Maya 对应的是 .ma
- C4D 对应的是 .c4d
第 2 类(中转文件):
多个 3D 建模软件彼此都可以打开,能够读取的文件格式,例如:
- .obj
- .dae
- .fbx
所谓“中转”,是指这些格式的作用实际上相当于将某个模型从 A 软件 导出 然后再 B 软件中可以打开并读取。
第 3 类(特定格式):
某些 3D 应用独有的文件格式。
例如王者荣耀这款游戏中 3D 模型就可能是自己独有的文件格式。
第 4 类(传输格式):
这里的 “传输” 是英文单词 “Transmission” 的翻译
gltf 就属于传输格式类型的文件。gltf 格式可以做到其他格式都无法做到的事情。
GLTF 文件格式简介
GLTF 是英文:Graphics Language Transmission Format 的缩写
WebGL、OpenGL 中的 “GL” 和 GLTF 中的 “GL” 是相同的单词。
GLTF 中文全称为:图形语言传输格式
GLTF 本身就是由 OpenGL 和 Vulkan 背后的 3D 图形标准组织 Khronos 定义的。
所以你可以想象得到,gltf 本身就是为了网络传输、浏览器渲染 3D 而生的。
关于更多 gltf 信息,可以查看其官网:https://www.khronos.org/gltf/
GLTF 的支持度:几乎所有的 Web 3D 图形框架都支持 GLTF
除了 Three.js 框架外 ,其他 3D JS 引擎框架也都支持 GLTF
gltf 优点 1:体积小,便于传输
gltf 文件中模型的数据都以二进制存储,当下载(使用) gltf 文件时可以将这些二进制数据直接在 GPU 中使用。
反观 vrml、.obj 或 .dae 等格式,他们是将数据存储为文本(例如纯文本或 JSON 格式),也就是说 GPU 在读取这些模型文件时还需要进行文本解析。
在文件体积方面,通常相同的模型顶点数据如果用文本形式存储,要比二进制存储体积大 3 到 5 倍。
gltf 优点 2:直接渲染
gltf 文件中模型的数据是直接要渲染的,而不是要再次编辑的。
换句话说 gltf 文件中的模型是不可以再次编辑的
而其他类型的文件,例如 .obj 中模型是可以在 Three.js 中加载完成后二次编辑的
你可以简单的把 gltf 想象成 jpg 图片,而其他格式的 3D 文件是 PSD 文件,当我们仅仅是为了看到图片,无需编辑该图片时,肯定是 .jpg 图片体积小,打开速度快。
正因为是不可编辑,所以一些对于渲染而言不重要的数据通常都已被删除,例如多边形都已转化为三角形。
gltf 优点 3:内嵌材质信息
gltf 文件中模型的材质信息是被内嵌进去。
请注意我们这里说的是 “材质信息”,也就是相当于 .obj 对应的 .mtl 中的数据,但是对于纹理图片资源(xxx.jpg)本身来说,并不会内嵌进去。
所以,这里隐含的一个事情就是,我们依然需要将 .gltf 文件对应的纹理图片资源 .jpg 放在 pulic 目录中。
结论:gltf 格式非常有针对性,是专门为渲染而设计的,文件体积小,且 GPU 读取快速。
因此,推荐使用 gltf 格式。
在 Blender 中导出 gltf 文件
讲了这么多 gltf 文件的优点,那么我们打开之前创建的 hello.blend 文件,导出一下 gltf 文件看看。
导出步骤:
-
打开 hello.blend
-
文件 > 导出 > glTF 2.0(.glb/.gltf)
-
在弹窗对话框中,使用默认导出项,我们直接点
导出
尽管我们使用的是默认导出项,但是还请你留意一下这几项内容:
包括
:选定的物体(未勾选)、自定义属性(未勾选)、相机(未勾选)、精确灯光(未勾选)变换
:Y 向上(已勾选)几何数据
: 应用修改器(未勾选)、UV(已勾选)、法向(已勾选)、切向(未勾选)、顶点色(已勾选)、材质(导出)、图像(自动)、压缩(未勾选)动画
:动画(已勾选)、形态键(已勾选)、蒙皮(已勾选)尽管我们创建的 hello.blend 中并未设置任何动画,你可以选择取消动画相关的勾选
此时去导出目录里,我们会发现多出来了一个 hello.glb
的文件。
特别强调:由于纹理图片资源 metal_texture.jpg 本身就在目录中,所以我们只是从直观上感觉多出了 1 个文件而已。
.glb ?不是 .gltf ?
额~,别着急,我们补充一下 GLTF 格式的知识。
GLTF 是一种 3D 文件格式规范,但是却有 3 种表现形式
3 种表现形式分别为:分离式、二进制、嵌入式
第 1 种表现形式(分离式):.gltf + .bin + 纹理贴图资源(.jpg、.png)
-
gltf:3D 场景的所有概要信息,包括灯光、纹理贴图等信息
该文件的内容具体形式为 JSON
-
.bin:模型的二进制数据
-
纹理贴图资源:这个就不多说了,就是纹理图片 xxx.jpg 或 .png
第 2 种表现形式(二进制):.glb
.glb:包含场景所有的信息的二进制数据。
.glb === .gltf + .bin + 纹理图片资源
第 3 种表现形式(嵌入式):.gltf
.gltf:以 JSON 形式保存所有场景信息数据,包括材质和纹理信息。
这种形式由于文件内容是 json,因此是可以通过文本再次编辑的
Blender 默认导出 glTF 2.0 格式时,采用的是 .glb 后缀形式。
想要更改成别的导出形式,我们可以在 Blender 导出项 格式
下拉框中更改为 “.gltf 分离(.gltf + .bin + 纹理)” 或 “glTF 嵌入式(.gltf)"。
那么此时导出的文件格式就是 .gltf 后缀形式。
3 种形式的对比:
以下纯粹是我个人的观点,仅供参考
比较常见的是前 2 种:分离式(.gltf + .bin + 纹理)、二进制形式(.glb)
如果你的项目中,模型数据不会发生变化,但是纹理贴图可能容易发生变化,那么可以选择 分离式的。
分离式的贴图资源本身就是单独存在的,因此方便替换修改。
如果你是要发送给其他人使用、且不会发生材质变更的,则可以采用 .glb 形式的。
由于所有数据都只在 .glb 文件中,就 1 个文件也利于文件发送。
补充一点:有一个网站 https://gltf-viewer.donmccurdy.com/ ,他可以提供 .glb 文件在线预览。
同时在 NPM 上面,有很多针对 .glb 和 .gltf 格式互转的工具包,例如:gltf-import-export
对于 Three.js 来说,加载 glTF 格式的文件,无论哪种形式,均支持。
使用 GLTFLoader 加载 glTF 文件的示例
在 Three.js 中负责加载 glTF 格式文件的加载器为 GLTFLoader。
用法和之前 OBJLoader 用法完全相同,废话不多说,直接看代码。
我们先加载 .glb 格式的文件,代码如下:
src/components/hello-gltfloader
由于 .glb 文件是单独 1 个存在,所以我们这次可以将 hello.glb 文件放在 src/assces/model/ 目录下了。
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"
...
const loader = new GLTFLoader()
loader.load(require('@/assets/model/hello.glb').default, (gltf) => {
scene.add(gltf.scene)
})
请注意当加载完成后,执行的是 scene.add(gltf.scene)
加载完成得到的 gltf 中包含的数据有:
- gltf.animations; // Array<THREE.AnimationClip>
- gltf.scene; // THREE.Group
- gltf.scenes; // Array<THREE.Group>
- gltf.cameras; // Array<THREE.Camera>
- gltf.asset; // Object
我们此刻只是将 gltf.scene 添加到了场景中而已,其他数据暂时并未使用到。
和加载 .glb 类似,如果我们的 3D 数据文件为 分离式的 .gltf,则将上述代码修改为:
loader.load('./model/hello.gltf', (gltf) => {
scene.add(gltf.scene)
})
注意:我们只需将 hello.gltf 传递给 loader 即可,loader 会读取 .gltf 中的数据,自动去加载对应的 hello.bin 和 纹理图片 hello.jpg。
由于牵扯到不同的文件 webpack 编译,所以我们选择将 .gltf、.bin、.jpg 文件放在 src/public/ 目录中。
至此,加载 .gltf 文件讲解完成。
就这?明明就几行代码的事情,为什么还要花这样大的篇幅来讲解 .obj 和 .glb、gltf ?
答:要想学得深入,就一定要知道原理,知道 obj 和 gltf 的差异,知其然也要知其所以然。
我这里提供几个上找到的 glTF 文件资源,方便自己练习使用。
一个黄色的小鸭子:
- https://vr.josh.earth/assets/models/duck/duck.gltf
- https://vr.josh.earth/assets/models/duck/duck.bin
- https://vr.josh.earth/assets/models/duck/duck.png
一个简易 3D 社区
这个小区模型比较大,你需要适当调整一下镜头参数,才可以看清楚全貌
一个酷酷的头盔
https://cdn.khronos.org/assets/api/gltf/DamagedHelmet.glb
一个宇航员
https://modelviewer.dev/shared-assets/models/Astronaut.glb
真的好酷!
谷歌开源的一个 JS 库:model-viewer
在搜索 glTF 相关文章时,我无意中发现另外谷歌公司开源的一个 JS 项目: model-viewer
项目 Github 地址:https://github.com/google/model-viewer
项目官网:https://modelviewer.dev/
项目介绍:Easily display interactive 3D models on the web & in AR
简单来说就是:在 Web 或 AR 中,一个简单的用来显示 3D 模型的 JS 库。
具体用法:
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
<model-viewer src="shared-assets/models/Astronaut.glb" alt="A 3D model of an astronaut" auto-rotate camera-controls></model-viewer>
确实够简单了,就是引入 viewer ,然后可以使用
标签插入模型渲染显示标签。 简直和插入图片标签 没啥区别。
交互效果:除了可以渲染出 3D 模型文件,还默认配备有类似 OrbitControls 相同的交互效果。
兼容性:目前 苹果浏览器 Safari、火狐 Firefox 并不支持。
至此,关于如何加载 glTF 文件已讲解完毕。
但是有一点我们没有提到,就是使用 glTF 中自带的灯光、镜头、动画等内容。
由于目前我还不会在 Blender 创建动画,所以这一块我们暂且保留,等待以后有机会再继续学习。
在 Three.js 中,还有很多其他文件格式的加载器,我们就不逐个讲解了,具体的可以查阅官方文档。
你以为本文结束了?没有!
在上面示例中,我们实际上漏掉了一个非常重要的知识点:加载被压缩过的 .glb 文件
glTF 文件压缩和加载(解压)——Draco
在本文的示例中,所演示加载的 .glb 文件是我自己在 Blender 中创建导出的。
如同图片文件一样,也有专门针对 .glb 文件压缩的工具,最为著名的就是谷歌公司开源的:draco
Draco 简介
Draco 是一种库,用于压缩和解压缩 3D 几何网格(geometric mesh) 和 点云(point cloud)
draco 官网:https://google.github.io/draco/
draco 源码:https://github.com/google/draco
draco 底层是使用 c++ 编写的。
draco 可以在不牺牲模型效果的前提下,将 .glb 文件压缩体积减小很多。
就好像将普通文件压缩成 .zip 一样
至于文件减少多少,这个暂时没有查询到
Draco 使用流程是:
-
使用 Draco 将模型压缩,最终压缩后的文件格式为 .drc 或 .glb
Draco 可以压缩众多 3D 格式文件,.glb 仅仅是其中一种
-
在 .glb 文件内部有一个特殊字段,用来表述本文件是否经过了 draco 压缩
-
当客户端(JS) 使用 GLTFLoader 去加载某个 .glb 文件时会去读取该标识
-
若判断该 .glb 文件未被压缩则直接进行加载和解析
-
若判断该 .glb 文件是被 draco 压缩过的,则会尝试调用 draco 解压类,下载 .glb 文件的同时进行解压,最终将下载、解压后的 .glb 数据传递给 GLTFLoader 使用
这就引申出来一个事情:我们需要提前将负责 draco 解压的类传递给 GLTFLoader,具体如何做请看后面的讲解。
如何使用 Draco 压缩 .glb 文件?
具体如何操作实现,暂时我也没有学习,先搁置一下。
敬请期待以后的更新
如何在 Three.js 中加载压缩过的 .glb 文件?
关于 Draco 的介绍,可以查看 Three.js 对于 Draco 的介绍描述:
https://github.com/mrdoob/three.js/tree/dev/examples/js/libs/draco
Three.js 源码包中 draco 针对 gltf 文件的解压文件库:
-
draco/ 目录下有 4 个文件:draco_decoder.js、draco_decoder.wasm、draco_encoder.js、draco_wasm_wrapper.js
-
draco/gltf/ 目录下面同样有 4 个文件
请注意 draco/ 和 draco/gltf/ 目录下的 4 个文件虽然是名字一样,但是他们内容并不相同。
分别解释一下这 4 个文件:
-
draco_decoder.js
draco 解压(解码) 相关 js
-
draco_decoder.wasm
.wasm 文件是 WebAssembly 解码器
关于 WebAssembly 更多知识,请执行查阅:https://www.wasm.com.cn/
-
draco_encoder.js
draco 压缩(编码) 相关 js
-
draco_wasm_wrapper.js
用于封装 .wasm 解码器的 js
重点来了…
第 1 步:拷贝 draco 文件到项目 public 中
我们将 Three.js 中 examples/js/libs/draco 目录拷贝到 React 项目的 public 目录中。
draco 属于第 3 方库,我们目前暂时采用拷贝到 public 目录中这种形式
请记得一定拷贝的是 draco/,其中包含 draco/gltf/ 目录
第 2 步:实例化一个 DRACOLoader,并传递给 GLTFLoader
关于 DRACOLoader 的详细解释,请参考官方文档:
我们将之前 GLTFLoader 的代码修改如下:
+ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
const gltfLoader = new GLTFLoader()
+ const dracoLoader = new DRACOLoader()
+ dracoLoader.setDecoderPath('./examples/js/libs/draco/')
+ dracoLoader.setDecoderConfig({ type: 'js' })
+ gltfLoader.setDRACOLoader(dracoLoader)
gltfLoader.load('./model/vivo.glb', (gltf) => {
scene.add(gltf.scene)
})
下面就针对上面 4 行核心代码进行解释说明:
-
const dracoLoader = new DRACOLoader()
实例化一个 DRACOLoader
-
dracoLoader.setDecoderPath('./examples/js/libs/draco/')
设置 dracoLoader 应该去哪个目录里查找 解压(解码) 文件
-
dracoLoader.setDecoderConfig({ type: 'js' })
设置 dracoLoader 的配置项
-
gltfLoader.setDRACOLoader(dracoLoader)
将 dracoLoader 传递给 gltfLoader,供 gltfLoader 使用
至此,结束!
虽然 draco 非常复杂,但是对于我们使用者而言却很简单,仅仅上面 4 行代码即可实现加载被 draco 压缩过的 .glb 文件。
加载.drc 模型文件
在上面示例中,我们加载的是被 draco 压缩过的 .glb 文件。
那如果是被 draco 压缩过的 .drc 文件呢?
答:更加简单,直接使用 DRACOLoader 即可。
DRACOLoader 使用示例代码如下:
const loader = new DRACOLoader();
loader.setDecoderPath( '/examples/js/libs/draco/' );
loader.preload();
loader.load('./xxx/model.drc',
function ( geometry ) {
const material = new THREE.MeshStandardMaterial( { color: 0x606060 } );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
}
}
补充说明:修改模型位置偏差
无论加载 .obj 文件,还是本章讲解的加载 .gltf 文件,假设模型在建模软件中位置中心并不是原点,而是非常偏远的位置。
那么文件加载完成后,将模型添加到场景中,模型的位置并不在场景视角的中心位置,如果位置过于偏远,甚至有可能根本看不见模型。
我们可以通过以下方式,计算模型的位置偏差,并修正模型的位置,使其出现在视野中心位置。
const loader = new GLTFLoader()
loader.load('./model/lddq.gltf', (gltf) => {
const group = gltf.scene
const box = new Three.Box3().setFromObject(group)
const center = box.getCenter(new Three.Vector3())
group.position.x += (group.position.x - center.x)
group.position.y += (group.position.y - center.y)
group.position.z += (group.position.z - center.z)
scene.add(group)
})
Box3 的介绍请执行查阅官方文档。
下一章节,我们要学习如何添加 场景背景,呵, VR 看房效果要来了!