Threejs入门01 从gltf模型的加载开始

415 阅读3分钟

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出来了,但是画面渲染不出来,这是咋回事?

是模型数据异常的问题吗?可以排查一下,先用工具预览下模型。

若没有问题,就可能是显示问题。

有几种解决方法:

  1. 针对性调整参数,相机、材质、scale
  2. 让模型居中显示:包围盒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;
}