threejs 加载外部三维模型(gltf)

3,246 阅读10分钟

一、建模软件绘制3D场景(Blender)

三维建模软件的作用:对于简单的立方体、球体等模型,可以通过threejs的几何体api快速实现,但复杂的模型,比如汽车、楼房、工厂等,一般需要通过3D建模软件来实现

3D美术常用的三维建模软件:Blender、3dmax、C4D、maya等,Blender轻量开源

分工和流程:一般由3D美术使用三维建模软件绘制3D模型,导出如gltf等常见的格式,程序员通过threejs加载三维模型

二、gltf格式简介(web3d领域jpg)

gltf格式的重要性

gltf格式是2015新发布的三维模型格式,随着物联网、webGL、5G的发展,会有越来越多的互联网项目web端接入3d元素。gltf格式的三维模型就像jpg或png格式的图片,现在的网站,图片是标配,对于以后的网站,使用3d来替换图片表达也是正常的。图片有很多格式,三维模型也有很多格式,gltf格式在三维模型中的地位就像jpg在图片中的地位。不仅threejs,其他的webGL三维引擎cesium。babylonjs对gltf格式都有良好的支持。

gltf 2.0

Khronos Group组织2015发布了gltf 1.0版本,2017年发布了gltf 2.0版本

gltf包含的内容

相比较obj、stl等格式而言,gltf格式可以包含更多的模型信息

gltf格式几乎可以包含所有的三维模型相关信息的数据,比如模型层级关系、pbr材质、纹理贴图、骨骼动画、变形动画

gltf格式信息

gltf通过json键值对的方式表示模型信息,meshes表示网格模型信息,materials表示材质信息

{
  "asset": {
    "version": "2.0",
  },
...
// 模型材质信息
  "materials": [
    {
      "pbrMetallicRoughness": {//PBR材质
        "baseColorFactor": [1,1,0,1],
        "metallicFactor": 0.5,//金属度
        "roughnessFactor": 1//粗糙度
      }
    }
  ],
  // 网格模型数据
  "meshes": ...
  // 纹理贴图
  "images": [
        {
            // uri指向外部图像文件
            "uri": "贴图名称.png"//图像数据也可以直接存储在.gltf文件中
        }
   ],
     "buffers": [
    // 一个buffer对应一个二进制数据块,可能是顶点位置 、顶点索引等数据
    {
      "byteLength": 840,
     //这里面的顶点数据,也快成单独以.bin文件的形式存在   
      "uri": "data:application/octet-stream;base64,AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/.......
    }
  ],
}

bin文件

有些gltf文件会关联一个或多个bin文件,bin文件以二进制形式存储了模型的顶点数据等信息。bin文件中的信息就是对应gltf文件中的buffers属性,buffers.bin中的模型数据,可以存储在.gltf文件中,也可以单独一个二进制bin文件

二进制glb

gltf格式文件不一定以.gltf结尾,.glb就是gltf的二进制文件。

如果将gltf模型和贴图信息合成到.glb文件中,.glb文件的体积更小,网络传输更快

导出gltf

blender:最新版本可以直接导出gltf

blender绘制好的三维模型,可以打开和预览gltf格式文件模型

三、加载gltf文件(模型加载全流程)

gltf加载器 GlTFLoader()

model.js

import * as THREE from 'three'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'

const loader = new GLTFLoader()
const model = new THREE.Group()
loader.load('./工厂.gltf', gltf => {
  model.add(gltf.scene)
})

export default model
image.png

相机投影(正投影OrthographicCamera和透视投影PerspectiveCamera)

预览一个三维场景,一般有正投影和透视投影2个相机可供选择,大多数情况下,都是使用透视投影,比如游戏、物联网等项目。

透视投影符合人眼的近大远小的规律,如果不需要模拟人眼近大远小可以使用正投影

const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000)

尺寸概念

项目开发的时候,程序员要对一个模型或者一个三维场景有一个尺寸的概念,不用具体值,要有一个大概印象。

一般通过三维建模软件可以轻松测量模型尺寸,可以使用blender打开gltf模型,测量尺寸

单位概念

threejs没有任何单位,只有数组大小的运算

obj、gltf格式的模型只有尺寸,没有单位信息

实际项目开发的时候,一般会定义一个单位,一方面甲方、前端、美术之间更好协调,甚至你自己写代码也要有一个尺寸标准。比如一个园区、工厂,可以以m为单位建模,比如建筑、人、相机都用m为尺度去衡量,如果单位不统一,就需要用scale属性去缩放

设置合适的相机参数

通过gltf加载完成后,需要根据自身需要,设置合适的相机参数,就好比拍照,想要拍摄一个石头,肯定要把相机对着石头,如果希望石头在照片上的占比大,就要离石头近一些

相机位置

工厂的尺寸大概是200m数量级,如果想整体预览工厂全貌,就先就将camera.position的xyz统统设置为几百即可,camera.position.set(200, 200, 200)

具体的xyz值,可以通过OrbitControls可视化调整,然后在控制台查看相机的参数

某位置在canvas画布居中

需要工厂在哪个位置居中,就直接设置camera.lookAt指向哪个坐标camera.lookAt(0, 0, 0)

相机空间OrbitControls会影响lookAt的设置,需要手动设置OrbitControls的目标参数

camera.lookAt(100, 0, 0) // 仅设置这行没有效果

const controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(100, 0, 0)
controls.update()

远裁截面far参数

PerspectiveCamera(fov, aspect, near, far)

近裁截面near和远裁截面far,要能包含你想渲染的场景,否则超出视椎体模型会被裁剪掉,简单说near足够小,far足够大,主要是far

测量工厂的尺寸大概是几百的数量级,不用具体测量,需要far为3000就足够显示下工厂的全貌了

const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000)

纹理贴图颜色偏差解决

加载gltf模型的时候,可能会遇到threejs渲染结果颜色偏差,对于这种情况,需要修改WebGL渲染器默认编码方式即可 renderer.outputEncoding = THREE.sRGBEncoding

image.png

新版属性名变为outputColorSpace:

renderer.outputColorSpace = THREE.SRGBColorSpace

四、OrbitControls辅助设置相机参数

实例化OrbitControls,可以旋转缩放平移3D模型,本质上是改变camera的参数

OrbitControls可视化改变相机位置属性position

我们可以在render函数中打印相机的位置信息

function render() {
  renderer.render(scene, camera)
  requestAnimationFrame(render)
  console.log('camera.position',camera.position)
}

然后在页面中拖动模型,达到自己想要的效果,查看此时的位置信息,比如这样一个位置就是我想要的效果

image.png

咱们拿着控制台的数据去设置相机的位置,这样来的更精确

camera.position.set(-363, 245, 235)

OrbitControls可视化改变相机观察目标原点lookAt

在render函数中打印controls的target

function render() {
  renderer.render(scene, camera)
  requestAnimationFrame(render)
  console.log('controls.target',controls.target)
}

初始化时的目标原点就是坐标系的原点

image.png

如果说希望工厂渲染时位置靠上一点,这个时候xyz的值为多少?

image.png

可以设置控制器的target

controls.target.set(-26, -63, 24)
controls.update()

五、gltf不同文件形式(.glb)

gltf格式模型文件,有不同的组织形式:

  1. 单独gltf文件
  2. 单独glb文件
  3. gltf+bin+贴图文件

这些不同形式的gltf模型,加载代码没啥区别

loader.load('./工厂.glb', gltf => {
  model.add(gltf.scene)
})

导出gltf+glb+贴图文件,文件夹中有gltf,也有bin文件,和需要用到的贴图

image.png

使用时还是导入gltf文件

loader.load('./工厂/工厂.gltf', gltf => {
  model.add(gltf.scene)
})

六、模型命名(程序与美术协作)

开发一些web3d项目,比如一个小区、工厂,场景中会有很多个模型对象,程序员加载三维模型的时候,通过什么方式获取到自己想要的模型节点是个问题

三维软件模型命名

模型命名可以类比前后端api接口命名,web3d前端和后端对接需要命名接口,和3D美术对接,同样需要给一些模型节点命名

  1. 模型命名可以使用汉字、英文、拼音和其他语言形式
  2. 如果使用汉字,可能会出现导出乱码问题

浏览器控制台查看3D模型树结构

加载gltf模型,通过gltf.scene可以获取模型的数据,你可以通过浏览器控制台打印gltf.scene,和blender中的模型目录树进行对比,比较2者的结构是否相同

image.png

  1. 模型父对象节点可以用Object3D对象表示,也可以用组对象Group表示
  2. 通过children属性可以查看一个父对象模型的所有子对象
  3. 通过name属性可以查看模型节点的名称

getObjectByName根据name获取模型节点

一般三维建模软件的目录树,都有模型的名称,threejs加载外部模型,外部模型的名称体现为threejs对象的name属性,threejs可以通过getObjectByName方法,把模型节点的名字name作为函数参数,快速查找某个模型对象

  const shuimian = gltf.scene.getObjectByName('水面')
  shuimian.material.color.set('black')
image.png

分组管理

对于大类,可以进行分组,这样更好管理。比如大货车就是一个组,程序员可以通过组名,找到组下所有的模型,批量设置颜色

  const huoche1 = gltf.scene.getObjectByName('大货车1')
  for (const item of huoche1.children) {
    item.material.color.set('red')
  }
image.png

七、递归遍历层级模型修改材质

加载一个外部模型,比如gltf模型,如果想批量修改么个mesh的材质,一个一个设置比较麻烦,可以通过递归方法traverse方法批量设置

递归方法traverse

将所有的模型节点的材质颜色都改掉

loader.load('./工厂/工厂.gltf', gltf => {
  model.add(gltf.scene)
  gltf.scene.traverse(obj => {
    if (obj.isMesh) {
      obj.material = new THREE.MeshLambertMaterial({ color: 'white' })
    }
  })
})
image.png

八、外部模型材质共享

loader.load('./简易小区-共享材质.glb', gltf => {
  const mesh1 = gltf.scene.getObjectByName('1号楼')
  const mesh2 = gltf.scene.getObjectByName('2号楼')
  console.log('1号楼名称', mesh1.material.name)
  console.log('2号楼名称', mesh2.material.name)

  model.add(gltf.scene)
})

可以看到,材质的名称都是一样的,说明mesh1和mesh2的material属性是同一个对象

image.png

解决问题的方式

  1. 在建模软件中,设置mesh的材质不要共享,要独享材质
  2. 通过代码克隆材质对象,重新赋值给mesh的材质属性

代码方式解决多个mesh材质共享的问题

如果我改变某一个mesh的材质,其他的mesh的材质也会跟着变

mesh1.material.color.set('red')

image.png

material.clone()返回一个新的材质对象,切断material之间的引用关系

  gltf.scene.getObjectByName('小区房子').traverse(obj => {
    if (obj.isMesh) {
      obj.material = obj.material.clone()
    }
  })

九、纹理encoding和渲染器

如果没有特殊需要,一般为了正常渲染,避免颜色偏差,threejs中需要颜色贴图.encoding和渲染器.outputEncoding属性值保持一致

纹理对象texture颜色空间编码属性encoding

encoding有多个属性值,默认值是线性颜色空间THREE.LinearEncoding

  1. THREE.LinearEncoding:线性颜色空间
  2. THREE.sRGBEncoding:sRGB颜色空间

控制台查看线性颜色空间和sRGB颜色空间的值分别为3000和3001 image.png

它们都来自constants.js

// constants.js源码部分截取
export const LinearEncoding = 3000;
export const sRGBEncoding = 3001;

gltf的贴图encoding

loader.load('./工厂/工厂.gltf', gltf => {
  model.add(gltf.scene)
  gltf.scene.traverse(obj => {
    if (obj.isMesh) {
      // 判断是否有贴图
      if (obj.material.map) {
        console.log('encoding', obj.material.map.encoding)
      }
    }
  })
})

可以看到,gltf模型中,颜色贴图的encoding的默认值是sRGB颜色空间,也就是THREE.sRGBEncoding

image.png

WebGL渲染器outputEncoding

outputEncoding的默认值是线性空间THREE.LinearEncoding,由于gltf模型的encoding是sRGB颜色空间THREE.sRGBEncoding,所以导入gltf模型时,会有颜色偏差

如果要去掉颜色偏差,需要将outputEncoding的值也设置为THREE.sRGBEncoding

renderer.outputEncoding = THREE.sRGBEncoding

注意,最新版本的threejs,渲染器属性已经由outputEncoding变成outputColorSpace

单独加载的颜色贴图颜色偏差时设置encoding属性

const geometry = new THREE.SphereGeometry(60)
const texLoader = new THREE.TextureLoader()
const texture = texLoader.load('./earth.jpg')
const material = new THREE.MeshLambertMaterial({ map: texture })
const mesh = new THREE.Mesh(geometry, material)

由于设置了renderer.outputEncoding = THREE.sRGBEncoding,这里会有颜色偏差。设置renderer.outputEncoding = THREE.sRGBEncoding的原因是为了解决gltf模型的颜色偏差,gltf默认的encoding是THREE.sRGBEncoding

image.png

为了解决单独加载的颜色贴图颜色偏差,可以给纹理对象的encoding设置为THREE.sRGBEncoding:texture.encoding = THREE.sRGBEncoding

注意,新版本的threejs纹理对象的encoding已经变更为colorSpace