介绍

Three.js允许我们创建许多基本几何图形,但是当涉及到更复杂的形状时,我们最好使用专业的3D软件。这次课将使用已经制作完成的模型,后面将会学习在3D软件中创建自己的模型。

格式

随着时间推移,已经有非常多的3D模型格式文件,这也是为什么到目前为止我们有数百种模型格式可供使用https://en.wikipedia.org/wiki/List_of_file_formats#3D_graphics

GLTF

GLTF即图形语言传输格式( Graphics Language Transmission Format or GL Transmission Format)。它在过去几年内非常流行。它支持非常多不同的数据集,可以很直观的具有几何体和材质等数据,同时也可以具有摄像机、灯光、场景图形、动画、骨架、变形甚至多场景等数据。不仅如此,它还支持各种文件格式,像是json、二进制文件(binary)、嵌入纹理(embed textures)。
glTF正在成为一种标准,大多数3D软件、游戏引擎和库都支持它。这意味着我们可以在不同的环境中轻松获得类似的结果。
当然这也并不是说所有情况下都得使用glTF,如果你只需要一个几何体,最好使用另一种格式,如OBJ、FBX、STL或PLY。
我们应该在每个项目上测试不同的格式,看看文件是否包含需要的所有数据,文件是否太大了,是否被压缩过了,被压缩后解压需要多长时间等等。

寻找模型

首先,我们需要一个模型。如前所述,我们将在稍后学习如何在3D软件中创建自己的模型,但现在,让我们使用预先制作的模型。
GLTF团队提供各种模型,从简单的三角形到逼真的模型,以及动画、变形、透明涂层材料等。https://github.com/KhronosGroup/glTF-Sample-Models

GLTF格式

虽然GLTF本身就是一种格式,但它也可以有不同的文件版本格式。
如果打开/static/models/Duck/文件夹,您将看到4个不同的文件夹。虽然每个都包含duck,但都采用不同的GLTF格式:

  • glTF
  • glTF-Binary
  • glTF-Draco
  • glTF-Embedded
    我们甚至可以找到其他格式,但这4种格式是最重要的,涵盖了我们需要学习的内容。

glTF

此格式是一种默认格式。Duck.gltf文件是可以在编辑器中打开的JSON文件。它包含各种信息,如相机、灯光、场景、材质、对象变换,但既不包含几何体geometry,也不包含纹理material。Duck0.bin文件是一个二进制文件,不能像上面那样去读取。它通常包含几何图形等数据以及与顶点相关的所有信息,如UV坐标、法线、顶点颜色等。DuckCM.png是鸭子的纹理。
当我们加载这种格式时,我们只加载Duck.gltf,其中包含对随后将自动加载的其他文件的引用。
在这里插入图片描述

glTF-Binary

此格式仅由一个glb文件组成。它包含我们在上面讨论的glTF默认文件格式中的所有数据。这是一个二进制文件,你不能在代码编辑器中打开它来查看其中的内容。
由于只有一个文件,这种格式可能会更轻一些,加载起来也更舒适,但你无法轻松更改其数据。例如,如果要调整纹理大小或压缩纹理,则无法将其与其他纹理合并,因为它位于该二进制文件中。
在这里插入图片描述

glTF-Draco

此格式类似于glTF默认格式,但缓冲区数据(通常是几何体)使用Draco算法进行压缩。如果你比较一下.bin文件大小,你将看到它要小得多。下面会详细讲。

glTF-Embedded

此格式类似于glTF-Binary格式,因为它只有一个文件,此文件实际上是一个JSON,因此可以在编辑器中打开。这种格式的唯一好处是有一个易于编辑的文件,通常不会使用,因为文件大小太大了。

格式的选择

如何选择正确的格式取决于我们要怎样处理资源。
如果希望能够在导出格式文件后还能更改纹理或灯光的坐标,最好使用默认的glTF。
如果只想一个模型对应一个文件,并且不关心去修改资源内容,那么最好选择glTF-Binary二进制格式文件。
这俩种情况下,还得决定是否要使用Draco压缩。

加载模型

在three.js中加载GLTF文件,必须使用GLTF加载器,因此要引入GLTFLoader

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

实例化GLTF加载器

const gltfLoader = new GLTFLoader()

console.log一下康康里面有些什么信息

gltfLoader.load('/models/Duck/glTF/Duck.gltf', gltf => {
    console.log(gltf)
})

可以看到有许多元素,其中最重要的便是scene场景属性,因为导出的模型只有一个场景,这个场景便包含了我们需要的一切信息。
在这里插入图片描述
THREE.Group: scene
   └───Array: children
        └───THREE.Object3D
              └───Array: children
                    ├───THREE.PerspectiveCamera
                    └───THREE.Mesh
其中网格就是我们的小黄鸭模型。
在这里插入图片描述
想要把小黄鸭添加到我们的场景中,有多种办法可以实现:

  1. 往我们的场景中添加上面整个场景scene对象,虽然它也叫做scene,但实际上它是一个组Group
  2. 将scene对象中的children对象添加到场景中,并忽略未被使用到的透视摄像机
  3. 在添加到场景之前先过滤children对象,移除不需要的对象像是透视摄像机
  4. 仅仅添加网格到场景中,但结果可能会是鸭子模型被错误缩放、定位或者旋转
  5. 在3D软件中打开文件,移除透视摄像机,然后再导出文件

因为这次的模型结构很简单,所以我们会采用上面第二个方法。

加载小黄鸭模型

下面三种glTF格式都可以加载出来小黄鸭模型,只有glTF-Draco加载报错,下面会细讲。

gltfLoader.load('/models/Duck/glTF/Duck.gltf', gltf => {   //Default glTF
    scene.add(gltf.scene.children[0])
})

// 或者
gltfLoader.load(
    '/models/Duck/glTF-Binary/Duck.glb', // glTF-Binary

// 或者
gltfLoader.load(
    '/models/Duck/glTF-Embedded/Duck.gltf', // glTF-Embedded

在这里插入图片描述

加载飞行员头盔模型

一样是从glTF model samples拿的模型

gltfLoader.load('/models/FlightHelmet/glTF/FlightHelmet.gltf', gltf => {
      scene.add(gltf.scene.children[0])
})

在这里插入图片描述
可以发现没有一个完整的头盔模型,只有其中的一部分。这是因为上面的代码只是将已加载场景的第一个子元素给添加到我们的场景中。试下用for循环解决。

for(const child of gltf.scene.children)
        {
            scene.add(child)
        }

在这里插入图片描述
可以发现循环后的结果虽然可以看到有更多的模型元素,但一样不是完整的模型,而且更糟糕的是每当我们刷新页面,都会得到模型的不同部分。
问题出在于当我们把scene.children数组中的子元素从一个场景移到另一个场景的时候,它会自动从被移除的场景中删除掉,意味着我们循环的数组长度变小了。
当我们添加第一个对象时,它会从原来的场景中移除,然后第二个对象便会移动到替补上去。因此这里我们可以采用while循环来添加模型网格

gltfLoader.load('/models/FlightHelmet/glTF/FlightHelmet.gltf', gltf => {
  while (gltf.scene.children.length) {
      scene.add(gltf.scene.children[0])
  }
})

在这里插入图片描述
现在我们就得到一个完整的头盔了。当然也可以用下面的方式实现,将子元素提取出来放到一个新数组里面,这样就不会改变原来的数组了。

const children = [...gltf.scene.children]
for(const child of children){
      scene.add(child)
}

或者更简单粗暴的方式,因为gltf.scene是一个组Group,我们可以直接把组添加到场景里面

gltfLoader.load('/models/FlightHelmet/glTF/FlightHelmet.gltf', gltf => {
  scene.add(gltf.scene)
})

Draco压缩

  1. Draco版本glTF文件比默认的glTF文件体积更小
  2. 用于压缩缓冲区数据(通常是几何体)

添加DracoLoader

使用glTF-Draco版本加载模型报错,因此我们需要向GLTFLoader提供一个DracoLoader实例,以便它能够加载压缩文件。

gltfLoader.load('/models/Duck/glTF-Draco/Duck.gltf', gltf => {
  scene.add(gltf.scene)
})

在这里插入图片描述

引入DRACOLoader并实例化

import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'

const dracoLoader = new DRACOLoader()

three.js提供了Draco解码器的代码,可以去/node_modules/three/examples/js/libs/ 找到该文件夹,复制整个文件夹到static文件夹下,之后通过.setDecoderPath()方法将文件提供给dracoLoader。
最后,我们可以使用setDRACOLoader()将DRACOLoader实例dracoLoader提供给GLTFLoader实例gltfLoader:

const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')
const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)

gltfLoader.load('/models/Duck/glTF-Draco/Duck.gltf', gltf => {
  scene.add(gltf.scene)
})

之后便能看到小黄鸭模型了

什么时候使用Draco压缩

虽然我们可能会觉得使用Draco压缩是个双赢局面,但实际上并非如此。
确实它会让几何体更轻量,但首先要使用的时候必须加载DracoLoader类和解码器。其次,我们计算机解码一个压缩文件需要时间和资源,这可能会导致页面打开时有短暂冻结,即便我们使用了worker和WebAssembly。
因此我们必须根据实际来决定使用什么解决方案。如果一个模型具有100kb的几何体,那么则不需要Draco压缩,但是如果我们有MB大小的模型要加载,并且不关心在开始运行时有些许页面冻结,那么便可能需要用到Draco压缩。

动画

加载动画模型

因为加载出来的狐狸模型非常大,因此需要对其进行缩放

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属性,为此我们需要创建一个动画混合器AnimationMixerAnimationMixer类似于可以包含一个或多个AnimationClips对象的播放器

创建动画混合器

关于AnimationAction

  let mixer = null
  
  gltfLoader.load('/models/Fox/glTF/Fox.gltf', gltf => {
    // 创建混合器并传入gltf.scene
  	mixer = new THREE.AnimationMixer(gltf.scene)
  	// 使用clipAction()方法将索引为0的AnimationClip添加到混合器中
  	// 这个方法会返回一个AnimationAction
  	const action = mixer.clipAction(gltf.animations[0])
  	// 使用play()方法调用这个AnimationAction
  	action.play()
  	gltf.scene.scale.set(0.025, 0.025, 0.025)
  	scene.add(gltf.scene)
})

为看到动画我们必须回到动画函数里面在每一帧更新混合器

const tick = () => {
  ......
  //更新混合器
  if (mixer !== null) {
    mixer.update(deltaTime)
  }
  ......
}

在这里插入图片描述
当然可以试下索引1和2的AnimationClip
在这里插入图片描述
在这里插入图片描述

Three.js Editor

three.js有其自己的在线编辑器https://threejs.org/editor/
就像一款3D软件,只不过能够在线运行但是功能较少,可以创建基本的几何体、灯光、材质等。
你可以导入自己的模型,因此这是测试自己的模型是否正常工作的一个好方法。但是要注意,只能测试由一个文件组成的模型,像是glTF-Binary或glTF-Embedded版本的小黄鸭。
在这里插入图片描述
在菜单中添加环境光和平行光后便能看清楚小黄鸭了
在这里插入图片描述

模型来源

可以从这个网站获取各种模型,有收费也有免费的
https://sketchfab.com/feed

比如在里边下载一个gltf格式的模型,你会获得一个压缩包。解压缩后按照上边教程加载gltf文件即可。

由于网上模型包含网格较多,很难找出想要的部分网格,那么可以先去Three.js Editor,点击左上角File->import,选中你下载的模型的压缩包即可将模型加载出来,这时你可以选中模型慢慢找你要的网格的名称。
在这里插入图片描述
之后再回到traverse()方法中去操作你要的网格:

loadModel(gltfLoader) {
      gltfLoader.load(
        '/static/fictional_supercar_-_v12_goblin/scene.gltf',
        (gltf) => {
          gltf.scene.children[0].scale.set(1, 1, 1)
          gltf.scene.children[0].position.setY(0.1)
          gltf.scene.children[0].traverse((child) => {
            if (child.isMesh) {
              if (child.name === 'clearcoat_clearcoat_0') {
                child.material.color.set(this.debugParamters.clearcoatColor)
                this.gui
                  .addColor(this.debugParamters, 'clearcoatColor')
                  .name('车罩颜色')
                  .onChange(() => {
                    child.material.color.set(this.debugParamters.clearcoatColor)
                  })
              }
              if (child.name === 'car_body_car_body_0') {
                child.material.color.set(this.debugParamters.bodyColor)
                child.material.emissive = child.material.color
                child.material.emissiveMap = child.material.map
                this.gui
                  .addColor(this.debugParamters, 'bodyColor')
                  .name('车壳颜色')
                  .onChange(() => {
                    child.material.color.set(this.debugParamters.bodyColor)
                  })
              }
            }
          })
          this.scene.add(gltf.scene.children[0])
        },
        (xhr) => {
          console.log((xhr.loaded / xhr.total) * 100 + '%loaded')
        }
      )
    }

在这里插入图片描述

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐