一、建模软件绘制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
相机投影(正投影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
新版属性名变为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)
}
然后在页面中拖动模型,达到自己想要的效果,查看此时的位置信息,比如这样一个位置就是我想要的效果
咱们拿着控制台的数据去设置相机的位置,这样来的更精确
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)
}
初始化时的目标原点就是坐标系的原点
如果说希望工厂渲染时位置靠上一点,这个时候xyz的值为多少?
可以设置控制器的target
controls.target.set(-26, -63, 24)
controls.update()
五、gltf不同文件形式(.glb)
gltf格式模型文件,有不同的组织形式:
- 单独gltf文件
- 单独glb文件
- gltf+bin+贴图文件
这些不同形式的gltf模型,加载代码没啥区别
loader.load('./工厂.glb', gltf => {
model.add(gltf.scene)
})
导出gltf+glb+贴图文件,文件夹中有gltf,也有bin文件,和需要用到的贴图
使用时还是导入gltf文件
loader.load('./工厂/工厂.gltf', gltf => {
model.add(gltf.scene)
})
六、模型命名(程序与美术协作)
开发一些web3d项目,比如一个小区、工厂,场景中会有很多个模型对象,程序员加载三维模型的时候,通过什么方式获取到自己想要的模型节点是个问题
三维软件模型命名
模型命名可以类比前后端api接口命名,web3d前端和后端对接需要命名接口,和3D美术对接,同样需要给一些模型节点命名
- 模型命名可以使用汉字、英文、拼音和其他语言形式
- 如果使用汉字,可能会出现导出乱码问题
浏览器控制台查看3D模型树结构
加载gltf模型,通过gltf.scene可以获取模型的数据,你可以通过浏览器控制台打印gltf.scene,和blender中的模型目录树进行对比,比较2者的结构是否相同
- 模型父对象节点可以用Object3D对象表示,也可以用组对象Group表示
- 通过children属性可以查看一个父对象模型的所有子对象
- 通过name属性可以查看模型节点的名称
getObjectByName根据name获取模型节点
一般三维建模软件的目录树,都有模型的名称,threejs加载外部模型,外部模型的名称体现为threejs对象的name属性,threejs可以通过getObjectByName方法,把模型节点的名字name作为函数参数,快速查找某个模型对象
const shuimian = gltf.scene.getObjectByName('水面')
shuimian.material.color.set('black')
分组管理
对于大类,可以进行分组,这样更好管理。比如大货车就是一个组,程序员可以通过组名,找到组下所有的模型,批量设置颜色
const huoche1 = gltf.scene.getObjectByName('大货车1')
for (const item of huoche1.children) {
item.material.color.set('red')
}
七、递归遍历层级模型修改材质
加载一个外部模型,比如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' })
}
})
})
八、外部模型材质共享
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属性是同一个对象
解决问题的方式
- 在建模软件中,设置mesh的材质不要共享,要独享材质
- 通过代码克隆材质对象,重新赋值给mesh的材质属性
代码方式解决多个mesh材质共享的问题
如果我改变某一个mesh的材质,其他的mesh的材质也会跟着变
mesh1.material.color.set('red')
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
- THREE.LinearEncoding:线性颜色空间
- THREE.sRGBEncoding:sRGB颜色空间
控制台查看线性颜色空间和sRGB颜色空间的值分别为3000和3001
它们都来自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
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
为了解决单独加载的颜色贴图颜色偏差,可以给纹理对象的encoding设置为THREE.sRGBEncoding:texture.encoding = THREE.sRGBEncoding
注意,新版本的threejs纹理对象的encoding已经变更为colorSpace