一文带你悟道 Threejs 3D模型开发

5,104 阅读12分钟

本文为稀土掘金技术社区首发签约文章,14 天内禁止转载,14 天后未获授权禁止转载,侵权必究!
本篇是 Three 从入门到进阶的第 4 篇,关注专栏

前言

模型是 Three 开发中的明珠,如何广泛的获取模型和灵活的使用模型是日常开发中的关键,小包本文借助三只飞鸟的案例,带你来一起悟道模型,打通 Three 模型的开发难关。

学习本文,你将掌握:

  • 理解模型是什么
  • 掌握获取 3D 模型的方式
  • 理解常用的模型格式及 GLTF 格式的强大之处
  • 学会如何使用 GLTF 模型及动画效果的启用

模型

模型,广义来讲就是场景中的一切物体,包括 3D 模型、光源、相机等;狭义来讲,就特指 3D 模型。本专栏中后续模型的理解都按照狭义方式来理解。

上一篇文章介绍了场景图的知识,我们根据场景图的简易或者复杂,可以将模型粗略的分成两类:简易模型和复杂模型。

简易模型通常就是几个独立的 Mesh 网格对象,例如太阳、地球、简单立方体及其组合等;复杂模型则开启多层局部空间,多组 Mesh 构成一个模型,例如太阳-地球-月亮星系模型、房屋模型等。后者通常由专业建模师构建,更加精美,拿来即用,开发中我们以后者为主。

Mesh 网格对象是 Three 操纵的核心元素,通常由图元(几何体)、材质构成。

Three 官方提供了丰富的图元 API,借助这些 API 可以实现各式各样的 3D 模型。例如下面的案例,可以实现各式各样的几何体:

code.juejin.cn/pen/7167931…

利用官方提供的图元 API,可以创建很多炫酷的模型,但创造像猫或人等更接近真实世界的场景,官方提供的 API 就有些捉襟见肘,单纯通过 Three 来绘制这些,复杂度不堪想象。

面对这种情况,Three 的出发点就发生了改变,我们无需纠结模型开发的难度,只需要将设计精美的模型导入到程序中,设置模型的各项动效及显示,最终成功在浏览器中渲染出炫酷的效果即可。

那么问题来了,我们如何能获取 3D 模型那?

3D 模型获取

精美的 3D 模型几乎就等价于炫酷的效果,因此各式各样的模型网站是每个 Three 开发者流连忘返的天堂,下面小包推荐几个常用的模型下载网站,保证让你以后再也不会缺精美 3D 模型

sketchfab

  • 推荐程度: ⭐⭐⭐⭐⭐
  • 网址: sketchfab.com/feed
  • 简单介绍:
    • 大名鼎鼎的 CG 类模型网站,分类全面,免费的模型数量特别多,设计都十分精美,初学者在这个平台简直可以遨游
    • 国内访问速度就特别快,注册特别简单,注册后就可以下载免费的模型
    • 如果有钱的话,付费的模型简直精美到恐怖
    • 支持多种模型格式的导出
    • 小包最喜欢的模型网站

sketchfab-export.png

sketchfab-model.png

sketchfab.png

cgmodel

  • 推荐程度: ⭐⭐⭐⭐⭐
  • 网址: sourl.cn/8sQhTE
  • 简单介绍:
    • CG 模型综合网站,模型的是三维动画、游戏、数字艺术等领域里的模型
    • 中文网站,访问速度很快
    • 免费模型数量很多
    • 模型的精度和造型比较精细和复杂
    • 还提供教程和学习等服务

cgmodel.png

free3D

  • 推荐程度: ⭐⭐⭐⭐
  • 网址: free3d.com/
  • 简单介绍:
    • 也是一个非常全面的 3D 模型网站,分类很多,但很多小包点进去并没有模型
    • 提供免费模型,据小包目前使用不算特别多
    • 访问速度不快,不知道是不是小包网速的原因
    • 模型相较于 sketchfab,没有那么精美和好看。模型的格式更全面

free3D.png

free3D-model.png

CG 小霸王

  • 推荐程度: ⭐⭐⭐
  • 网址: www.cgbawang.com/
  • 简单介绍:
    • 国内的 3D 模型站,提供了 500+页免费模型,也是平常模型的不错选择
    • 免费模型不算特别精美
    • 类别比较丰富,模型的格式选择视模型而定

yunmoming

  • 推荐程度: ⭐⭐⭐
  • 网址: www.yunmoxing.com/
  • 简单介绍:
    • 模型素材比较全面,有各行各业的相关素材
    • 全中文,提供关键词搜索
    • 模型质量参差不齐,在下载模型时需要筛选,筛选成本较高

GLTF 格式的模型

一些官方或者个人收集的 GLTF 格式的模型。

turbosquid

  • 推荐程度: ⭐⭐⭐
  • 简单介绍:
    • 提供 20000+ 个免费模型,虽然官方说提供了 50000+,但免费页面中每一行都充斥着几个花钱的,建议折半考虑
    • 模型偏向于写实,大多都是真实世界的模型
    • 过滤条件比较全
    • 精美模型的价格真是不便宜

turbsquid.png

复杂的 3D 模型格式

在过去的几十年中,3D 模型经过了繁荣的发展,各种软件和公司都曾推出个各式各样的扩展格式,不夸张地说,较真一下 3D 模型格式甚至得有几十种。

如果对所有的格式感兴趣,传送门 -> en.wikipedia.org/wiki/List_o…

大浪淘沙,经过时间的历练,目前比较流行的模型格式大概有 3-5 种,下面小包来大致对比一下,后面大家使用起来能有一个整体的概念。

OBJ 格式

OBJ 格式适用于 3D 软件模型之间的互导。几乎所有的 3D 软件都支持 OBJ 格式的读取,本质上它是一种文本文件。

OBJ 格式只是 3D 模型文件,它不包含动画、材质特性、贴图路径等 3D 模型特质。

FBX 格式

FBX 格式可以视为 OBJ 格式的升级版,它不仅是一个 3D 模型格式,更可以视为一个通用的模型格式,它包含动画、材质特性、贴图、骨骼动画、灯光、摄像机等信息。FBX 格式支持多边形游戏模型、曲线、表面、点组材质。

FBX 格式经常用于游戏,是一种比较成熟的 3D 模型格式。

FBX 缺点也很明显,它是一个封闭格式,除开创者 Autodesk 外,其余很难对它进行更新。

STL 格式

STL 不能算是一种 3D 模型格式,它不支持颜色、材质、光照、纹理等 3D 模型信息,只能描述三维物体的几何信息,因此在 Three 导入时,通常不会把它视为第一序列的选择。

GLTF(推荐)

GLTF 格式几乎所有的框架都可以支持,也是官方极力推荐的格式。可以说,它就是为 3D 模型格式而诞生的,GLTF 跨平台格式已成为 Web 上的 3D 对象标准及 Web 导出的通用标准。

GLTF 文件可以包含模型、动画、几何体、材质、灯光、相机等 3D 模型属性,甚至它还可以包含整个场景。也就是说,你可以在外部创建整个模型的场景,然后直接加载到 Three 中。

为什么官方极力推荐 GLTF 格式呐?

官方套话是这么说的:
减少了 3D 格式中除了与渲染无关的冗余信息,最小化 3D 文件资源;
优化了应用程序读取效率和和减少渲染模型的运行时间

知其根一直是小包的特点,咱们来细致的挖掘一下,GLTF 格式为什么有上述优点?

GLTF 并不算是一个 3D 数据格式,更像一个中转格式,将 3D 模型的几何数据、纹理数据等信息以二进制格式批量化存储在外部文件中,使用 JSON 文件存储外部文件的链接。这使得核心数据以二进制数据形式进行存储方便互联网传输,并且可以直接被渲染程序使用,无需额外的解码、预处理。

其他很多类型的格式,例如 OBJ 格式,需要首先解析场景结构,将数值型几何数据转换为图形 API 所需格式,会降低渲染的效率。

GLTF 格式有两种扩展名:

  • gltf: gltf 格式文件没有被压缩,会附带额外的 bin 数据文件,在文本编辑器中容易阅读,方便进行调试。
  • glb: 所有数据都包含在二进制文件中,文件小,推荐使用

GLTF 格式也有比较明显的缺点,其为了更简单的存储 3D 模型数据,不允许位置、UV 和法线数据的不同拓扑。虽然加快了模型的读取速度,但模型的修改变得困难起来,也就是说 GLTF 模型不可被编辑。

三只飞鸟

基本配置的设置参考用摄影的故事来敲开 Threejs 世界的大门

官方推荐使用 GLTF 格式来导入 3D 模型,因此小包以 GLTF 格式为主。

GLTF 导入并不困难,首先我们要加载 GLTF Loader 插件。

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const loader = new GLTFLoader();

Three 提供了两种加载方式: 基于回调模式的 load 方法和基于 Promise 的 loadAsync 方法,Promise 的模式更符合目前的开发模式,我们来导入小鸟 parrot。

const parrotData = await loader.loadAsync("/models/Parrot.glb");

将模型添加到场景之前,我们先打印一下加载的数据,观察模型的具体组成。

{
  animations: [AnimationClip],
  asset: {version: '2.0', generator: 'THREE.GLTFExporter'},
  cameras: [],
  parser: GLTFParser {json: {…}, extensions: {…}, plugins: {…}, options: {…}, cache: {…}, …},
  scene: Group {isObject3D: true, uuid: '06006dfc-5ae0-49b3-ba2e-cd32ae0dd256', name: 'AuxScene', type: 'Group', parent: null, …},
  scenes: [Group],
  userData: {},
}
  • animations: 动画剪辑数组,GLTF 格式是可以包含动画效果的,本文使用的三只小鸟就包含了简单的飞行动画,后面我们来启用它。
  • asset: GLTF 文件的元数据,由 Blender 导出时创建
  • cameras: 一组相机
  • paser: GLTFLoader 的技术细节
  • scene: 模型存放的位置
  • scenes: GLTF 可以将多个场景存储在一个文件中
  • userData: 额外的一些非标准数据

可以发现,在模型的操纵中,我们通常不只是关注 scene 属性,还会关注 animations 及 cameras 属性。

因此这里小包单独抽离出一个方法 setModel 来处理模型对应的逻辑,目前主要包含模型的提取。

function setupModel(data) {
  const model = data.scene.children[0];
  return model;
}

定义三只鸟儿的加载函数 loadBirds 函数

async function loadBirds() {
  const loader = new GLTFLoader();
  // 同时加载多个模型
  const [parrotData, storkData, flamingoData] = await Promise.all([
    loader.loadAsync("/model/Parrot.glb"),
    loader.loadAsync("/model/Stork.glb"),
    loader.loadAsync("/model/Flamingo.glb"),
  ]);

  const parrot = setupModel(parrotData);
  const flamingo = setupModel(flamingoData);
  const stork = setupModel(storkData);
  return {
    parrot,
    flamingo,
    stork,
  };
}

// 相应位置调用函数,将返回的模型加载至 scene 中

const { parrot, flamingo, stork } = await loadBirds();
scene.add(parrot, flamingo, stork);

birds-nolight.png

为什么只能看到黑色的三只鸟儿那?没有灯光,Three 中模型的渲染跟现实的光学原理相同,需要光。

Three 提供了多种光:

  • DirectionalLight: 直射光,可以理解为手电筒,从放置处发出平行光。缺点也比较明显,只能照向一个位置,而且直射光很浪费性能,不建议使用太多。
  • AmbientLight: 环境光,从各个方向向场景中的每个对象添加恒定数量的光照,与现实中光的工作模式完全不同。用起来很简单,通常来配合 DirectionalLight 来使用。但环境光由于各方向相同,无法显示出深度信息。
  • HemisphereLight: 半球光,光源在场景顶部的天空颜色和场景底部的地面颜色之间渐变,比较接近现实的光。半球光性能非常高,但其不从某一特定方向照射,因此通常和直射光配合使用实现某区域的高光效果。
const ambientLight = new HemisphereLight("white", "darkslategrey", 5);

const mainLight = new DirectionalLight("white", 4);
mainLight.position.set(10, 10, 10);
scene.add(ambientLight, mainLight);

birds-light.png

三只鸟儿目前都位于 (0,0,0) 位置,在 loadBirds 函数中分别调整一下三只鸟儿的位置。

const parrot = setupModel(parrotData);
parrot.position.set(0, 0, 50);

const flamingo = setupModel(flamingoData);
flamingo.position.set(150, 0, -200);

const stork = setupModel(storkData);
stork.position.set(0, -50, -200);

birds-position.png

Three 的动画系统非常完善,这里咱们先不用详细的理解各部分的原理,先学会如何播放模型中带有的动画效果。

animations 属性存储了模型的动画,我们需要按照 Three 的模式来启动动画帧。

大致步骤是这样的: 首先使用 AnimationMixer 混合器将静态对象转化为动态对象,然后利用 AnimationAction 将动画连接至混合器,为每个鸟添加 tick 方法,在帧更新时同时更新鸟的混合器。

讲起来的确有些抽象,我们来细讲一下。

  • AnimationMixer: 混合器

Three 场景中的动画对象对需要使用混合器,混合器控制模型的移动,因此后续通过更新混合器来实现动画帧的切换效果。

  • AnimationAction: 动画的控制模块。

动画效果通常由多个动作组成,该组件负责将每个动作进行动画剪辑,每个动画剪辑对应一个动作,该组件来控制动作的开启与暂停等控制功能。

  • tick 方法: 动画剪辑更新

AnimationAction 只是启动了动画,但动画真正开始播放需要更新渲染循环中的混合器。AnimationMixer 提供了 update 方法,允许根据时间参数来进行更新,通常使用帧——渲染循环更新时进行更新。

// 在 setupModel 中实现上述逻辑
function setupModel(data) {
  const model = data.scene.children[0];
  // 获取到动画
  const clip = data.animations[0];
  // 声明混合器
  const mixer = new AnimationMixer(model);
  // 将动画按照动作进行动画剪辑
  const action = mixer.clipAction(clip);
  action.play();
  // 更新混合器
  model.tick = (delta) => mixer.update(delta);
  return model;
}

动画的基本配置就完成了,下面只需要在渲染循环中更新混合器即可。

// 鸟类模型的总数组
birds = [];

// 获取模型后,放置到 birds 中
const { parrot, flamingo, stork } = await loadBirds();
birds.push(parrot, flamingo, stork);

// 修改渲染循环逻辑,每一帧更新混合器
// 帧时间参数通过 Three 提供的 Clock 来获取
const clock = new Clock();
const render = () => {
  renderer.render(scene, camera);
  // 获取时间参数
  const delta = clock.getDelta();
  // 每一帧更新时更新混合器
  birds.forEach((bird) => {
    bird.tick(delta);
  });
  controls && controls.update();
  requestAnimationFrame(render);
};

三只鸟儿就可以快乐的挥舞翅膀了。

码上掘金

code.juejin.cn/pen/7170864…

后语

我是 战场小包 ,一个快速成长中的小前端,希望可以和大家一起进步。

如果喜欢小包,可以在 掘金 关注我,同样也可以关注我的小小公众号——小包学前端

一路加油,冲向未来!!!