在场景、光源、相机、渲染器这些基础都奠定好了之后,也了解了通过three.js的几何体相关API创建简单的立方体、球体等模型,不过复杂的模型,比如一辆轿车、一栋房子、一个仓库,一般需要通过3D建模软件来实现。现在我们来了解用GLTFLoader
来加载外部的模型。
GLTF格式简介
GLTF格式是新2015发布的三维模型格式,随着物联网、WebGL、5G的进一步发展,会有越来越多的互联网项目Web端引入3D元素,你可以把GLTF格式的三维模型理解为.jpg、.png格式的图片一样,现在的网站,图片基本是标配,对于以后的网站来说如果需要展示一个场景,使用3D来替换图片表达也是很正常的事情。图片有很多格式,对于三维模型自然也是如此,Web开发的时候图片会有常用格式,对于Web3D开发也一样,肯定会根据需要选择一个常见的大家都熟悉的格式,随时时间的发展,GLTF必然称为一个极为重要的标准格式。
gltf包含内容
glTF(gl传输格式)是一种开放格式的规范(open format specification),用于更高效地传输、加载3D内容。该类文件以JSON(.gltf)格式或二进制(.glb)格式提供,外部文件存储贴图(.jpg、.png)和额外的二进制数据(.bin)。一个glTF组件几乎包含所有的三维模型相关信息的数据,可传输一个或多个场景, 包括网格、材质、贴图、蒙皮、骨架、变形目标、动画、灯光以及摄像机。
GLTFLoader
three.js GLTFLoader是一个用于加载和解析GLTF格式3D模型文件的工具,它是three.js库的一部分,基于WebGL技术。
通过使用GLTFLoader,开发人员可以快速构建出复杂的3D场景,并实现交互和动画效果,进一步增强场景的真实感和交互性。
引入GLTFLoader.js
在three.js官方文件的 examples/jsm/子文件 loaders/目录下,可以找到一个文件GLTFLoader.js
,这个文件就是three.js的一个扩展库,专门用来加载gltf格式模型加载器。
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
gltf加载器new GLTFLoader()
执行new GLTFLoader()
就可以实例化一个gltf的加载器对象。
// 创建GLTF加载器对象
const loader = new GLTFLoader();
gltf加载器方法.load()
通过gltf加载器方法.load()
就可以加载外部的gltf模型。
.load( url, onLoad, onProgress, onError)
- url:包含有.gltf .glb文件路径URL的字符串。
- onLoad:加载成功完成后将会被调用的函数。返回一个gltf对象,该gltf对象包含
.scene
、.cameras
、.animations
和.asset
。 - onProgress:加载正在进行过程中会被调用的函数。其参数将会是XMLHttpRequest实例,包含有总字节数
.total
与已加载的字节数.loaded
。 - onError:若在加载过程发生错误,将被调用的函数。该函数接收
error
来作为参数。
gltf参数内容:
loader.load('gltf模型.gltf', (gltf)=> {// 加载成功的回调函数console.log('gltf对象场景属性',gltf.scene);// 返回的场景对象gltf.scene插入到threejs场景中scene.add( gltf.scene );},(xhr) => {// 加载过程中的回调函数},(error) => {// 加载错误回调函数}
);
onload函数返回参数gltf.scene
gltf的场景属性gltf.scene
,该属性包含的是模型信息,比如几何体BufferGometry、材质Material、网格模型Mesh。
通过浏览器控制打印gltf.scene
可以看出。
- 模型父对象节点可以用
Object3D
对象表示,也可以用组对象Group
表示。 - 通过
.children
属性可以查看一个父对象模型的的所有子对象。 - 通过
.name
属性可以查看模型节点的名称。
.getObjectByName()
根据.name
获取模型节点
three.js加载外部模型,外部模型的名称体现为three.js对象的.name
属性,three.js可以通过.getObjectByName()
方法,把模型节点的名字.name
作为函数参数,快速查找某个模型对象。
// 返回名.name为"HandL"对应的对象
const obj = gltf.scene.getObjectByName("HandL");
console.log('obj', obj); // 控制台查看返回结果
console.log('obj.children', obj.children);
// obj.children的所有子对象都是Mesh,改变Mesh对应颜色
obj.children.forEach(function (mesh) {mesh.material.color.set(0xffff00);
})
注意:.getObjectByName("HandL")
获取对象返回结果,包含了对象本身以及对象的所有子对象。
// 返回名.name为"HandL"对应的对象
const obj = gltf.scene.getObjectByName("HandL");
console.log('obj.children', obj.children);
// obj.children的所有子对象都是Mesh,改变Mesh对应颜色
obj.children.forEach(function (mesh) {mesh.material.color.set(0xffff00);
})
递归遍历方法.traverse()
加载一个外部模型,如果你想批量修改每个Mesh
的材质,一个一个设置比较麻烦,可以通过递归遍历方法.traverse()
批量操作更加方便。
// 递归遍历所有模型节点批量修改材质
gltf.scene.traverse(function(obj) {if (obj.isMesh) { // 判断是否是网格模型console.log('模型节点', obj);console.log('模型节点名字', obj.name);console.log('模型默认材质', obj.material);}
});
threejs解析gltf模型默认材质一般是MeshStandardMaterial
或MeshPhysicalMaterial
,相比较其它网格材质,这两个材质属于PBR物理材质,可以提供更加真实的材质效果。
gltf.scene.traverse(function(obj) {if (obj.isMesh) {// 重新设置材质obj.material = new THREE.MeshLambertMaterial({color:0xffffff,});}
});
onProgress
函数
一个可选的进度回调函数,通过此函数计算或者操作显示加载进度。
loader.load('gltf模型.gltf', (gltf)=> {// 加载成功的回调函数scene.add( gltf.scene );},(xhr) => {// 加载过程中的回调函数,计算模型加载进度const num = (xhr.loaded / xhr.total) * 100;console.log(`已加载:${num}%`)},(error) => {// 加载错误回调函数}
);
gltf加载不同文件形式
Blender三维建模软件,可以根据设置,以不同形式导出gltf模型,比如单独导出一个.gltf文件,比如单独导出一个.glb文件,比如导出形式为.gltf + .bin + 贴图多个文件。这些不同形式的gltf模型,加载代码其实没啥区别。
// 单独.gltf文件
loader.load("../../工厂.gltf", function (gltf) { scene.add(gltf.scene);
})
// 单独.glb文件
loader.load("../../工厂.glb", function (gltf) { scene.add(gltf.scene);
})
// .gltf + .bin + 贴图文件
loader.load("../../工厂/工厂.gltf", function (gltf) { scene.add(gltf.scene);
})
纹理贴图颜色偏差解决
纹理对象Texture颜色空间编码属性.encoding
有多个属性值,默认值是线性颜色空间THREE.LinearEncoding
。
- THREE.LinearEncoding:线性颜色空间,在threejs内部表示数字3000
- THREE.sRGBEncoding:sRGB颜色空间,在threejs内部表示数字3001
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);
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);
webGL渲染器.outputEncoding
的默认值是线性空间THREE.LinearEncoding
,和纹理对象.encoding
默认值一样,如果颜色贴图.encoding
的值是THREE.sRGBEncoding
,为了避免颜色偏差,.outputEncoding
的值也需要设置为THREE.sRGBEncoding
。
综上所述,three.js加载gltf模型的时候,可能会遇到three.js渲染结果颜色偏差,对于这种情况,你只需要修改WebGL渲染器默认的编码方式.outputEncoding
即可。
//解决加载gltf格式模型纹理贴图和原图不一样问题
renderer.outputEncoding = THREE.sRGBEncoding;
注意:最新版本属性名字有改变。渲染器属性名.outputEncoding
已经变更为.outputColorSpace
。
查WebGL渲染器文档,你可以看到.outputColorSpace
的默认值就是SRGB颜色空间THREE.SRGBColorSpace
,意味着新版本代码中,加载gltf,没有特殊需要,不设置.outputColorSpace
也不会引起色差。
//新版本,加载gltf,不需要执行下面代码解决颜色偏差
renderer.outputColorSpace = THREE.SRGBColorSpace;//设置为SRGB颜色空间
完整实例代码
结合React,GLTFLoader示例代码。
import React, { useRef, useEffect } from 'react';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
const modelUrl = require('@/static/files/RobotExpressive.glb');let scene, camera, renderer, controls;// three.js加载3D场景 模型glb
export default function InitModelPage() {const box = useRef(); // canvas盒子// 加载模型function setGltfModel() {// 导入GlTF模型let gltfLoader = new GLTFLoader();gltfLoader.load(modelUrl, (gltf) => {gltf.scene.traverse(obj => {// 可操作模型if (obj.isMesh) {if(obj.material.map){// 判断是否存在贴图}}});scene.add(gltf.scene);});}// 渲染动画function renderFn() {requestAnimationFrame(renderFn);// 用相机渲染一个场景renderer && renderer.render(scene, camera);}// 监听窗体变化、自适应窗体事件function onWindowResize() {let width = box.current.offsetWidth;let height = box.current.offsetHeight;camera.aspect = width / height;// 更新相机投影矩阵,在相机任何参数被改变以后必须被调用camera.updateProjectionMatrix();renderer.setSize(width, height); // 设置渲染区域尺寸}// 初始化环境、灯光、相机、渲染器useEffect(() => {scene = new THREE.Scene();// 添加光源const ambitlight = new THREE.AmbientLight(0x404040);scene.add(ambitlight)const sunlight = new THREE.DirectionalLight(0xffffff);sunlight.position.set(-20, 1, 1);scene.add(sunlight);// 加载模型setGltfModel();// 获取宽高设置相机和渲染区域大小let width = box.current.offsetWidth;let height = box.current.offsetHeight;let k = width / height;// 投影相机camera = new THREE.PerspectiveCamera(45, k, 0.1, 3000);camera.position.set(5, 10, 25);camera.lookAt(scene.position);// 创建一个webGL对象renderer = new THREE.WebGLRenderer();renderer.setSize(width, height); // 设置渲染区域尺寸renderer.setClearColor(0x333333, 1); // 设置颜色透明度renderer.outputEncoding = THREE.sRGBEncoding; // 解决纹理贴图颜色偏差box.current.appendChild(renderer.domElement);// 监听鼠标事件controls = new OrbitControls(camera, renderer.domElement);// 渲染renderFn();// 监听窗体变化window.addEventListener('resize', onWindowResize, false);}, []);useEffect(() => {return () => {// 清除数据scene = null;camera = null;renderer = null;controls = null;}}, []);return <div style={{ width: '100%', height: '100%' }} ref={box}></div>;
}