一、加载.gltf文件(模型加载全流程)
1、加载.gltf文件
- gltf模型加载器
GLTFLoader.js - 相机参数根据需要设置
- 加载gltf的时候,webgl渲染器编码方式设置
1.1、引入GLTFLoader.js
在three.js官方文件的**examples/jsm/子文件loaders/**目录下,可以找到一个文件GLTFLoader.js,这个文件就是three.js的一个扩展库,专门用来加载gltf格式模型加载器。
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
1.2、gltf加载器new GLTFLoader()
// 创建GLTF加载器对象
const loader = new GLTFLoader();
1.3、gltf加载器方法.load()
通过gltf加载器方法.load()就可以加载外部的gltf模型。
执行方法.load()会返回一个gltf对象,作为参数2函数的参数,改gltf对象可以包含模型、动画等信息,本节课你只需要先了解gltf的场景属性gltf.scene,该属性包含的是模型信息,比如几何体BufferGometry、材质Material、网格模型Mesh。
loader.load( 'gltf模型.gltf', function ( gltf ) {
console.log('控制台查看加载gltf文件返回的对象结构',gltf);
console.log('gltf对象场景属性',gltf.scene);
// 返回的场景对象gltf.scene插入到threejs场景中
scene.add( gltf.scene );
})
1.4、全部代码
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader;
const group = new THREE.Group()
loader.load('./工厂.gltf',function(gltf){
group.add(gltf.scene)
})
export default group

2、相机选择(正投影OrthographicCamera和透视投影PerspectiveCamera)
如果你想预览一个三维场景,一般有正投影相机OrthographicCamera和透视投影相机PerspectiveCamera可供选择。不过大部分3D项目,比如一般都是使用透视投影相机PerspectiveCamera,比如游戏、物联网等项目都会选择透视投影相机PerspectiveCamera。
如果你希望渲染的结果符合人眼的远小近大的规律,毫无疑问要选择透视投影相机,如果不需要模拟人眼远小近大的投影规律,可以选择正投影相机。
3、尺寸概念
项目开发的时候,程序员对一个模型或者说一个三维场景要有一个尺寸的概念,不用具体值,要有一个大概印象。
一般通过三维建模软件可以轻松测试测量模型尺寸,比如作为程序员你可以用三维建模软件blender打开gltf模型,测量尺寸。
4、单位问题
three.js的世界并没有任何单位,只有数字大小的运算。
obj、gltf格式的模型信息只有尺寸,并不含单位信息。
不过实际项目开发的时候,一般会定义一个单位,一方面甲方、前端、美术之间更好协调,甚至你自己写代码也要有一个尺寸标准。比如一个园区、工厂,可以m为单位建模,比如建筑、人、相机都用m为尺度去衡量,如果单位不统一,就需要你写代码,通过.scale属性去缩放
5、设置合适的相机参数
通过gltf加载完成,模型后,你还需要根据自身需要,设置合适的相机参数,就好比你拍照,你想拍摄一个石头,肯定要把相机对着石头,如果希望石头在照片上占比大,就要离石头近一些。
相机位置怎么设置,你就类比你的眼睛,如果你想模拟人在3D场景中漫游,那么很简单,你把相机放在地面上,距离地面高度和人身高接近即可。
如果你想看到工厂的全貌,你可以理解为你坐着无人机向下俯瞰,简单说,相比人漫游工厂,整体预览工厂相机距离工厂距离更远一些,否则你也看不到全貌,当然过于远了,你就看不清工厂了。
以课程工厂为例,先设定一个小目标,我们希望工厂能够居中显示在canvas画布上,并且保证可以整体预览。
下面以透视投影相机PerspectiveCamera为例说明。
5.1、相机位置.position
工厂尺寸范围大概200米数量级,那么如果想整体预览观察工厂所有模型,那很简单,第一步,把camera.position的xyz值统统设置为几百即可,比如(200, 200, 200)。
具体xyz值,你可以通过OrbitControls可视化操作调整,然后浏览器控制台记录相机参数即可。
camera.position.set(200, 200, 200);
5.2、某位置在canvas画布居中
你需要工厂那个位置在canavs画布上居中,直接把camera.lookAt()指向哪个坐标。
如果美术建模,把工厂整体居中,也就是说模型的几何中心,大概位于世界坐标原点。你设置camera.lookAt(0,0,0),相机视线指向坐标原点。
camera.lookAt(0, 0, 0);
注意相机控件OrbitControls会影响lookAt设置,注意手动设置OrbitControls的目标参数
camera.lookAt(100, 0, 0);
// 设置相机控件轨道控制器OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
// 相机控件.target属性在OrbitControls.js内部表示相机目标观察点,默认0,0,0
// console.log('controls.target', controls.target);
controls.target.set(100, 0, 0);
controls.update();//update()函数内会执行camera.lookAt(controls.targe)
5.3、远裁截面far参数
近裁截面near和远裁截面far,要能包含你想渲染的场景,否则超出视锥体模型会被剪裁掉,简单说near足够小,far足够大,主要是far。
PerspectiveCamera(fov, aspect, near, far)
测量工厂尺寸大概几百的数量级,这里不用测具体尺寸,有个大概数量级即可,然后far设置为3000足够了。
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
6、纹理贴图颜色偏差解决
three.js加载gltf模型的时候,可能会遇到three.js渲染结果颜色偏差,对于这种情况,你只需要修改WebGL渲染器默认的编码方式.outputEncoding即可
//解决加载gltf格式模型纹理贴图和原图不一样问题
renderer.outputEncoding = THREE.sRGBEncoding;
二、OrbitControls辅助设置相机参数
1、OrbitControls改变相机位置.position
通过OrbitControls旋转和缩放,本质上就是在改变透视投影相机PerspectiveCamera的位置.position。
渲染循环中不停地打印相机的位置属性,你可以通过相机控件旋转或缩放三维场景,同时通过浏览器控制台观察相机位置变化。
function render() {
requestAnimationFrame(render);
// 浏览器控制台查看相机位置变化
console.log('camera.position',camera.position);
}
render();
2、通过OrbitControls设置相机位置.position
上节课关于相机整体预览三维场景代码设置的时候,第一步是根据渲染范围的数量级,大概设置相机的位置参数,其实第二部,相机位置具体参数,可以借助OrbitControls可视化旋转或缩放,然后选择一个合适的渲染效果,浏览器控制台记录下此时的相机位置。
camera.position.set(200, 200, 200);//第1步:根据场景渲染范围尺寸设置
camera.position.set(-144, 95, 95); //第2步:通过相机控件辅助设置OrbitControls
3、OrbitControls改变相机.lookAt观察目标
通过OrbitControls平移,OrbitControls的.target属性会发生变化,.target属性对应的就是透视投影相机PerspectiveCamera的.lookAt观察目标`。
function render() {
requestAnimationFrame(render);
// 浏览器控制台查看controls.target变化,辅助设置lookAt参数
console.log('controls.target',controls.target);
}
render();
4、通过OrbitControls设置.lookAt()参数
camera.lookAt(100, 10, 10)
//相机控件
const controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(100, 10, 10)
controls.addEventListener('change', function () {
renderer.render(scene, camera)
})
5、全部代码:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
//引入性能监视器stats.js
import Stats from 'three/addons/libs/stats.module.js';
import cube from './group.js'
//场景
const scene = new THREE.Scene();
scene.add(cube);
//添加辅佐坐标系
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);
//相机
let width = window.innerWidth;
let height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000);
camera.position.set(132, 145, 10)
camera.lookAt(100, 10, 10)
scene.add(camera);
const pointLight = new THREE.PointLight('#fff', 5);
pointLight.intensity = 5
pointLight.decay = 0.0;
pointLight.position.set(400, 200, 100)
scene.add(pointLight)
const ml = new THREE.AmbientLight('#fff', 1.0);
scene.add(ml)
const pointLightHelper = new THREE.PointLightHelper(pointLight, 10);
scene.add(pointLightHelper)
//渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)
renderer.render(scene, camera)
renderer.outputEncoding = THREE.sRGBEncoding;
document.body.appendChild(renderer.domElement)
//相机控件
const controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(100, 10, 10)
controls.addEventListener('change', function () {
renderer.render(scene, camera)
})
function render() {
renderer.render(scene, camera)
requestAnimationFrame(render)
}
render()
window.onresize = function () {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix()
}

三、gltf不同文件形式(.glb)
.gltf格式模型文件,有不同的组织形式。
- 单独.gltf文件
- 单独.glb文件
- .gltf + .bin + 贴图文件
这些不同形式的gltf模型,加载代码其实没啥区别。
// 单独.gltf文件
loader.load("../../工厂.gltf", function (gltf) {
scene.add(gltf.scene);
})
2、Blender导出gltf不同形式
Blender三维建模软件,可以根据设置,以不同形式导出gltf模型,比如单独导出一个.gltf文件,比如单独导出一个.glb文件,比如导出形式为.gltf + .bin + 贴图多个文件
3、.glb文件
.glb是gltf格式的二进制形式文件,加载方式和.gltf没啥区别。
// 单独.glb文件
loader.load("../../工厂.glb", function (gltf) {
scene.add(gltf.scene);
})
4、.gltf + .bin + 贴图文件
gltf模型的一些数据,是可以以单独文件形式存在的,比如纹理贴图单独存在,比如.bin包含gltf的顶点数据。
要注意的就是贴图等数据单独是一个文件的时候,注意不随随意改变子文件相对父文件gltf的目录,以免找不到资源。
// .gltf + .bin + 贴图文件
loader.load("../../工厂/工厂.gltf", function (gltf) {
scene.add(gltf.scene);
})
四、模型命名(程序与美术协作)
1、三维软件模型命名
课程提供了一个Blender的模型例子,你可以打开查看。
其实模型节点命名可以类比前后端API接口命名,web3d前端和后端对接需要命名接口,和3D美术对接,同样需要给一些模型节点命名。
-
模型命名可以使用汉字、英文、拼音其他语言形式。
-
如果使用汉字注意,有些三维建模软件可能存在导出乱码问题。

2、浏览器控制台查看3D模型树结构
加载gltf模型,通过gltf.scene可以获取模型的数据,你可以通过浏览器控制打印gltf.scene,然后和你三维建模软件中的模型目录树对比,比较两者的结构是否相同。
- 模型父对象节点可以用
Object3D对象表示,也可以用组对象Group表示。 - 通过
.children属性可以查看一个父对象模型的的所有子对象。 - 通过
.name属性可以查看模型节点的名称
loader.load("./简易小区.glb", function (gltf) {
console.log('场景3D模型树结构', gltf.scene);
model.add(gltf.scene);
})

3、.getObjectByName()根据.name获取模型节点
一般三维建模软件的目录树,都有模型的名称,three.js加载外部模型,外部模型的名称体现为three.js对象的.name属性,three.js可以通过.getObjectByName()方法,把模型节点的名字.name作为改函数参数,快速查找某个模型对象。
// 返回名.name为"1号楼"对应的对象
const nameNode = gltf.scene.getObjectByName("1号楼");
nameNode.material.color.set(0xff0000);//改变1号楼Mesh材质颜色
import * as THREE from 'three';
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader()
const group = new THREE.Group();
loader.load('./简易小区.glb', function (glb) {
const nameNode = glb.scene.getObjectByName('1号楼')
nameNode.material.color.set('green')
group.add(glb.scene)
})
export default group

4、分组管理
对于大类,可以进行分组,这样更好管理,比如高层分为一组,洋房分为一组。如果这样做的好处是,程序员可以通过分类名称,快速获取所有模型,然后进行同样的渲染操作,比如洋房批量改变颜色。
//获得所有'洋房'房子的父对象
const obj = gltf.scene.getObjectByName('洋房');
console.log('obj', obj); //控制台查看返回结果
console.log('obj.children', obj.children);
// obj.children的所有子对象都是Mesh,改变Mesh对应颜色
obj.children.forEach(function (mesh) {
mesh.material.color.set(0xffff00);
})
import * as THREE from 'three';
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader()
const group = new THREE.Group();
loader.load('./简易小区.glb', function (glb) {
const nameNode = glb.scene.getObjectByName('洋房')
nameNode.children.forEach(function(mesh){
mesh.material.color.set('green')
})
group.add(glb.scene)
})
export default group

五、递归遍历层级模型修改材质
加载一个外部模型,比如gltf模型,如果你想批量修改每个Mesh的材质,一个一个设置比较麻烦,可以通过递归遍历方法.traverse()批量操作更加方便。
1、递归遍历方法.traverse()
递归遍历gltf所有的模型节点。
// 递归遍历所有模型节点批量修改材质
gltf.scene.traverse(function(obj) {
if (obj.isMesh) {//判断是否是网格模型
console.log('模型节点',obj);
console.log('模型节点名字',obj.name);
}
});
2、查看gltf默认的材质
.obj、.gltf、.fbx等不同格式的模型,threejs加载默认的材质可能不同,不过也不用刻意记忆,通过浏览器控制台log打印即可console.log(obj.material)。

threejs解析gltf模型默认材质一般是MeshStandardMaterial或MeshPhysicalMaterial,相比较其它网格材质,这两个材质属于PBR物理材质,可以提供更加真实的材质效果
// 递归遍历所有模型节点批量修改材质
gltf.scene.traverse(function(obj) {
if (obj.isMesh) {
console.log('gltf默认材质',obj.material);
}
});
3、批量修改gltf所有Mesh的材质
gltf.scene.traverse(function(obj) {
if (obj.isMesh) {
// 重新设置材质
obj.material = new THREE.MeshLambertMaterial({
color:0xffffff,
});
}
});
4、全部代码
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader()
const group = new THREE.Group()
loader.load('./build/gc.gltf', function (gltf) {
gltf.scene.traverse(function (obj) {
if (obj.isMesh) {
obj.material = new THREE.MeshLambertMaterial({
color: '#fff'
})
}
})
group.add(gltf.scene)
})
export default group

六、外部模型材质是否共享的问题
美术通过三维建模软件,比如Blender绘制好一个三维场景以后,一些外观一样的Mesh,可能会共享一个材质对象。
1、改变一个模型颜色其它模型跟着变化
由于楼房的Mesh共享了1号楼Mesh的材质,当你通过mesh1.material改变mesh1材质,本质上是改变所有楼Mesh的材质。
const mesh1 = gltf.scene.getObjectByName("1号楼");
//1. 改变1号楼Mesh材质颜色
mesh1.material.color.set(0xff0000);
全部代码
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader()
const group = new THREE.Group()
loader.load('./build/简易小区-共享材质.glb', function (gltf) {
const mesh1 = gltf.scene.getObjectByName('1号楼')
mesh1.material.color.set('green')
group.add(gltf.scene)
})
export default group

2、.name标记材质,判断两个mesh是否共享材质
通过.name标记材质,测试mesh1和mesh2是否共享了材质
const mesh1 = gltf.scene.getObjectByName('1号楼')
mesh1.material.name = '楼房材质'
const mesh2 = gltf.scene.getObjectByName('2号楼')
console.log('mesh2',mesh2.material.name);
3、解决问题方向
改变一个模型颜色其它模型跟着变化,是因为多个模型对象共享了材质,如果单独改变一个模型的材质,比如颜色,下面两个方案,可以任选其一。
- 三维建模软件中设置,需要代码改变材质的Mesh不要共享材质,要独享材质。
- 代码批量更改:克隆材质对象,重新赋值给mesh的材质属性
4、代码方式解决多个mesh共享材质的问题
//用代码方式解决mesh共享材质问题
gltf.scene.getObjectByName("小区房子").traverse(function (obj) {
if (obj.isMesh) {
// .material.clone()返回一个新材质对象,和原来一样,重新赋值给.material属性
obj.material = obj.material.clone();
}
});
mesh1.material.color.set(0xffff00);
mesh2.material.color.set(0x00ff00);
5、全部代码
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader()
const group = new THREE.Group()
loader.load('./build/简易小区-共享材质.glb', function (gltf) {
gltf.scene.getObjectByName("小区房子").traverse(function (obj) {
if (obj.isMesh) {
obj.material = obj.material.clone();
}
});
let mesh1 = gltf.scene.getObjectByName('1号楼')
mesh1.material.color.set('green')
let mesh2 = gltf.scene.getObjectByName('2号楼')
mesh2.material.color.set('red')
group.add(gltf.scene)
})
export default group

七、纹理encoding和渲染器
注意:如果没有特殊需要,一般为了正常渲染,避免颜色偏差,threejs代码中需要颜色贴图.encoding和渲染器.outputEncoding属性值保持一致。
1、纹理对象Texture颜色空间编码属性.encoding
纹理对象Texture颜色空间 编码属性.encoding有多个属性值,默认值是线性颜色空间THREE.LinearEncoding。
- THREE.LinearEncoding:线性颜色空间
- THREE.sRGBEncoding:sRGB 颜色空间
2、浏览器控制台查看Texture.encoding属性值
const texture = new THREE.TextureLoader().load('./earth.jpg');
texture.encoding = THREE.LinearEncoding;//默认值
// THREE.LinearEncoding变量在threejs内部表示数字3000
console.log('texture.encoding',texture.encoding);
// 修改为THREE.sRGBEncoding,
texture.encoding = THREE.sRGBEncoding;
// THREE.sRGBEncoding变量在threejs内部表示数字3001
console.log('texture.encoding',texture.encoding);
THREE.LinearEncoding、THREE.sRGBEncoding其实在theeejs内部都表示一个数字,具体可以查看src目录下constants.js的源码文件。
// constants.js源码部分截取
export const LinearEncoding = 3000;
export const sRGBEncoding = 3001;
3、gltfmap.encoding值
threejs加载gltf模型,颜色贴图map属性.encoding的默认值是sRGB颜色空间THREE.sRGBEncoding。
// 查看gltf所有颜色贴图的.encoding值
gltf.scene.traverse(function(obj) {
if (obj.isMesh) {
if(obj.material.map){//判断是否存在贴图
console.log('.encoding',obj.material.map.encoding);
}
}
});
// .encoding显示3001,说明是THREE.sRGBEncoding
console.log('.encoding',mesh.material.map.encoding);
4、WebGL渲染器.outputEncoding
.outputEncoding的默认值是线性空间THREE.LinearEncoding,和纹理对象.encoding默认值一样,如果颜色贴图.encoding的值是THREE.sRGBEncoding,为了避免颜色偏差,.outputEncoding的值也需要设置为THREE.sRGBEncoding。
//解决加载gltf格式模型颜色偏差问题
renderer.outputEncoding = THREE.sRGBEncoding;
注意!最新版本属性名字有改变。渲染器属性名.outputEncoding已经变更为.outputColorSpace,具体参考6.3小节最后说明
5、单独加载的颜色贴图设置.encoding = THREE.sRGBEncoding
如果webgl渲染器设置了renderer.outputEncoding = THREE.sRGBEncoding;,你单独加载图像返回的纹理对象需要设置 texture.encoding = THREE.sRGBEncoding;
//解决加载gltf格式模型颜色偏差问题
renderer.outputEncoding = THREE.sRGBEncoding;
const texture = new THREE.TextureLoader().load('./earth.jpg');
// 和webgl渲染器renderer.outputEncoding一致
texture.encoding = THREE.sRGBEncoding;
注意!!!最新版本,纹理对象属性名.encoding已经变更为.colorSpace。
texture.colorSpace = THREE.SRGBColorSpace;//设置为SRGB颜色空间
6、全部代码
import * as THREE from 'three';
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader()
const group = new THREE.Group();
loader.load('./工厂.gltf', function (glb) {
group.add(glb.scene)
})
const geometry = new THREE.SphereGeometry(30);
const v3 = new THREE.TextureLoader();
v3.colorSpace = THREE.SRGBColorSpace;
const material = new THREE.MeshLambertMaterial({
map: v3.load('./earth.jpg')
})
let mesh = new THREE.Mesh(geometry, material);
mesh.position.set(0, 100, 0)
group.add(mesh)
export default group

七、gltf模型更换.map(纹理.flipY)
1、加载颜色贴图.map
注意单独加载的纹理贴图的.encoding和webgl渲染器的.outputEncoding保持一致。
const texLoader = new THREE.TextureLoader();
const texture = texLoader.load('./黑色.png');// 加载手机mesh另一个颜色贴图
texture.encoding = THREE.sRGBEncoding; //和渲染器.outputEncoding一样值
2、更换gltf颜色贴图
执行mesh.material.map = texture;新的纹理对象Texture赋值给.material.map就可以更换材质贴图。
loader.load("../手机模型.glb", function (gltf) {
const mesh = gltf.scene.children[0]; //获取Mesh
mesh.material.map = texture; //更换不同风格的颜色贴图
})
注意:如果你直接给gltf模型材质设置.map属性更换贴图,会出现纹理贴图错位的问题,这主要和纹理对象Texture的翻转属性.flipY有关。
3、纹理对象Texture翻转属性.flipY默认值
.flipY表示是否翻转纹理贴图在Mesh上的显示位置。
纹理对象Texture翻转属性.flipY默认值是true。
// 纹理对象texture.flipY默认值
console.log('texture.flipY', texture.flipY);
4、gltf的贴图翻转属性.flipY默认值
gltf的贴图翻转属性.flipY默认值是false。
loader.load("../手机模型.glb", function (gltf) {
const mesh = gltf.scene.children[0]; //获取Mesh
console.log('.flipY', mesh.material.map.flipY);
})
如果更换单独加载的纹理贴图,比如颜色贴图.map,注意把纹理贴图.flipY的值设置给gltf中纹理的值false。
//是否翻转纹理贴图
texture.flipY = false;
5、全部代码
import * as THREE from 'three';
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader()
const group = new THREE.Group();
const v3 = new THREE.TextureLoader();
const v4 = v3.load('./黑色.png')
v4.encoding = THREE.sRGBEncoding;
v4.flipY = false;
loader.load('./手机模型.glb', function (glb) {
glb.scene.children[0].material.map = v4
group.add(glb.scene)
})
export default group

5313

被折叠的 条评论
为什么被折叠?



