1. gltf模型介绍
GLTF全称Graphics Language Transmission Forma(图形语言传输格式),是三维场景和模型的标准文件格式。 它由OpenGL和Vulkan背后的3D图形标准组织Khronos所定义,使得GLTF相当于是3D模型中的JPG格式、Web导出的通用标准。
该类文件以JSON(.gltf)格式或二进制(.glb)格式提供, 外部文件存储贴图(.jpg、.png)和额外的二进制数据(.bin)。一个glTF组件可传输一个或多个场景, 包括网格、材质、贴图、蒙皮、骨架、变形目标、动画、灯光以及摄像机。
2. gltf模型预览
很多线上网址支持模型预览,或者blender软件等。 此处推荐一个我找到的在线地址: glTF模型在线查看器
3. 代码实现
3.1 技术栈
vue 3、three.js、typescript
3.2 文件目录
gltf文件放在public/static文件夹里
- public
- static
- building // 模型的文件夹
- scene.bin
- scene.gltf
- src
- views
- demo1.vue // 单页面
- utils
- threeWorld.ts // ThreeWorld类
3.3 实现一个基本的ThreeWorld类
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
export default class ThreeWorld {
scene: THREE.Scene | null = null; // 场景
camera: THREE.PerspectiveCamera | null = null; // 摄像机
renderer: THREE.WebGLRenderer | null = null; // renderer
directionLight: THREE.DirectionalLight | null = null; // 灯光
ambientLight: THREE.AmbientLight | null = null;
dom: HTMLDivElement | null = null; // 放置canvas的父元素div
domRect: DOMRect | null = null; // dom的boundingRect
gltfLoader: GLTFLoader | null = null; // 加载gltf的loader
orbitControls: OrbitControls | null = null;
constructor(dom: HTMLDivElement | null) {
if (!dom) {
console.warn('请先提供画布所在的父元素dom');
return ;
}
this.dom = dom;
this.domRect = this.dom?.getBoundingClientRect();
this.init(); // 初始化函数
}
}
初始化,含场景、摄像机、renderer、loader、orbitControls
init(): void {
// 创建场景
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color('aquamarine'); // for test
this.setCamera();
this.setLight();
this.setRenderer();
this.gltfLoader = new GLTFLoader();
this.addControls(); // 增加 鼠标滚轮放大缩小模型 的交互
}
初始化摄像机
setCamera(): void {
const { width, height } = this.domRect as DOMRect;
const aspect = width / height;
this.camera = new THREE.PerspectiveCamera(
45,
aspect,
0.1,
1000
);
this.camera.position.set(0, 0, 60);
}
初始化Renderer
setRenderer(): void {
this.renderer = new THREE.WebGLRenderer();
const { width, height } = this.domRect as DOMRect;
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.dom?.appendChild(this.renderer.domElement);
}
初始化灯光
setLight(): void {
this.ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
this.ambientLight.position.set(0, 0, 0);
this.scene?.add(this.ambientLight);
this.directionLight = new THREE.DirectionalLight(0xffffff, 2);
this.scene?.add(this.directionLight);
}
render
render(): void {
if (this.scene && this.camera) {
this.renderer?.render(this.scene, this.camera);
}
}
animnate(): void {
this.render();
this.orbitControls?.update();
requestAnimationFrame(this.animnate.bind(this));
}
3.4 ThreeWorld类增加loadModel功能
通过promise.resolve的方式,实现异步回调
loadModel(modelPath: string) {
return new Promise(resolve => {
this.gltfLoader?.load(modelPath, (gltf) => {
const obj = gltf.scene;
this.scene?.add(obj);
resolve(obj);
}, process => {
const { lengthComputable, loaded, target, total } = process
console.log('loading:', lengthComputable, loaded, target, total)
}, err => {
const { colno, error, filename, lineno, message } = err;
console.warn('load failed:', colno, error, filename, lineno, message);
})
})
}
3.5 在demo1.vue中调用
<template>
<div class="demo1">
<!-- load model by gltf loader -->
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import ThreeWorld from '@/utils/threeWorld';
onMounted(() => {
const w = new ThreeWorld(document.querySelector('.demo1'));
w.loadModel(`./static/models/building/scene.gltf`).then(res => {
w.animnate()
})
})
</script>
<style scoped>
.demo1 {
width: 500px;
height: 600px;
}
</style>
现在我们就基本的实现了加载gltf文件了。
3.6 新的问题
但很快就有同学发现了新的问题,gltf的数据load出来了,但是画面渲染不出来,这是咋回事?
是模型数据异常的问题吗?可以排查一下,先用工具预览下模型。
若没有问题,就可能是显示问题。
有几种解决方法:
- 针对性调整参数,相机、材质、scale
- 让模型居中显示:包围盒Box3计算模型的中心位置和尺寸。这种适合加载大量的、未知不确定模型。
以下为第二种解决方法的代码,见mark1-mark2这几行代码
loadModel(modelPath: string) {
return new Promise(resolve => {
this.gltfLoader?.load(modelPath, (gltf) => {
const obj = gltf.scene;
this.scene?.add(obj);
const {x, y, z} = this.center(obj); // mark1
this.camera?.position.set(x, y, z + 150);
this.camera?.lookAt(x, y, z);
if (this.camera?.up) {
this.camera.up.x = 0;
this.camera.up.y = 1;
this.camera.up.z = 0;
}
this.orbitControls?.target.set(x, y, z);
this.orbitControls?.update(); // mark2
resolve(obj);
}, process => {
const { lengthComputable, loaded, target, total } = process
console.log('loading:', lengthComputable, loaded, target, total)
}, err => {
const { colno, error, filename, lineno, message } = err;
console.warn('load failed:', colno, error, filename, lineno, message);
})
})
}
/**
*让模型居中显示:包围盒Box3计算模型的中心位置和尺寸
* @param mesh
* @returns
*/
center(mesh: THREE.Object3D): THREE.Vector3 {
const box3 = new THREE.Box3();
box3.expandByObject(mesh);
// const size = new THREE.Vector3();
// box3.getSize(size);
const center = new THREE.Vector3();
box3.getCenter(center);
return center;
}