Three.js 加载 glb 模型实现散点标注

2,190 阅读4分钟

最终成果

Github 源码
Github Pages 预览

注:上面链接用 vpn 访问, 另外,写这篇文章是为了高德地图3D标注做准备。

Image_20250626172003.png

js添加动画效果:

2025-06-26 17-13-40_1.gif

模型下载并查看

首先,这个模型是网上寻找的, 并且格式是 GLB 格式。 下载了之后, 怎么预览呢?

查看 3D GLB 格式的文件, 可以将文件拖拽到下面这个网站进行预览:

sandbox.babylonjs.com/

预览结果:

2025-06-26 17-44-42_2.gif

可以看到,这个文件似乎是白色的, 我们可以重新设置下材质,改变其颜色。

一、创建 Scene、Camera、 Renderer、 OrbitControls

为了看上面这个模型,首先搭建 3D 场景, 最典型的 3D 场景, 包括:

  1. Scene 创建 3D 世界
  2. Camera 照相机, 拍摄每一帧的页面
  3. Renderer 渲染器,将相机拍摄的画面,渲染在网页上
  4. OrbitControls 轨道控制器, 控制鼠标拖拽, 相机位置

典型代码如下:


import * as THREE from 'three';
import {
  OrbitControls
} from "three/examples/jsm/controls/OrbitControls";

export class Basic {
  public dom: HTMLElement;
  public scene: THREE.Scene;
  public camera: THREE.PerspectiveCamera;
  public renderer: THREE.WebGLRenderer;
  public controls: OrbitControls;
  
  constructor(dom: HTMLElement) {
    this.dom = dom;
    this.initScenes();
    this.setControls();
  }

  initScenes() {
    //第1步,Scene,初始化场景
    this.scene = new THREE.Scene();
    //第2步,Camera,初始化照相机,并摆好照相机的位置
    this.camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 10000);
    this.camera.position.set(-450, 180, -600);
    //第3步,设置好渲染器
    this.renderer = new THREE.WebGLRenderer({
      //透明,设置整个canvas是否透明,true的话,会显示大背景颜色,false的话,会覆盖大背景颜色
      alpha: true,
      //抗锯齿,true的话,放大缩小后,线条更加圆润
      antialias: true, 
    });
    //设置屏幕像素比
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.dom.appendChild(this.renderer.domElement);
  }

  //设置轨道控制器,主要目的是实现放大缩小、拖拽、点击, 原理是控制照相机的运行轨迹
  setControls() {
    //初始化轨道控制器
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    //这个是用来干什么的,暂时不是很清楚
    this.controls.autoRotateSpeed = 3
    //使动画循环使用时阻尼或自转,意思是否有惯性,设置为true,拖拽有惯性,更加丝滑
    this.controls.enableDamping = true;
    //动态阻尼系数 就是鼠标拖拽旋转灵敏度(设置为0.05就可以,具体效果也不是很清楚)
    this.controls.dampingFactor = 0.05;
    //是否可以缩放
    this.controls.enableZoom = true;
    //设置相机距离原点的最近距离(如果想要放大,这个值可以缩小)
    this.controls.minDistance = 100;
    //设置相机距离原点的最远距离(如果想要缩小,这个值可以放大)
    this.controls.maxDistance = 400;
    //是否开启右键拖拽(设置为true时,动画效果不好控制,所以还是不要右键操作了)
    this.controls.enablePan = false;
  }
}

二、加载四棱锥模型

首先,加载上面的四棱锥模型, 然后,重新创建一个材质, 将颜色改成需要的颜色,这时候,由于材质是感光的, 最好还是加上光线。

加载模型的方法如下:

loadOneModel(sourceUrl) {
    const loader = new GLTFLoader();
    return new Promise(resolve => {
      loader.load(sourceUrl, (gltf) => {
        const mesh = gltf.scene.children[0];
        resolve(mesh);
      },
      function (xhr) {
        console.log(xhr);
      },
      function (error) {
        console.log('loader model fail' + error);
      })
    })
  }

模型加载完毕, 需要添加光线,改变其材质, 并加入到 3D 世界中, 代码如下:

async createMainMesh() {
    const hemiLight = new HemisphereLight(0xffffff, 0x8d8d8d, 2);
    hemiLight.position.set(100, 0, 0);
    this.scene.add(hemiLight);

    const dirLight = new DirectionalLight(0xffffff, 1.5);
    dirLight.position.set(100, 10, 10);
    this.scene.add(dirLight);

    //加载模型
    const model: any = await this.loadOneModel('../../../static/models/taper2.glb');
    //给模型换一种材质
    const material = new MeshStandardMaterial({
      //自身颜色
      color: 0x1171ee,
      //透明度
      transparent: true,
      opacity: 1,
      //金属性
      metalness: 0.0,
      //粗糙度
      roughness: 0.5,
      //发光颜色
      emissive: new Color(0xff0000), 
      emissiveIntensity: 0.2,
      //blending: THREE.AdditiveBlending
    });
    //model.material = material;
    model.traverse((child: any) => {
      if (child.isMesh) {
        child.material = material;
      }
    });
    model.scale.set(1, 1, 1);
    model.position.set(0, 0, 1);
    model.rotateZ(Math.PI / 4);
    this.mainModel = model;
    this.scene.add(model);
  }

三、加载底座

底座的贴图如下:

Image_20250626205720.png

看到前面展示的效果, 底座应该是水波纹的圈圈, 为什么贴图是这个样子呢?

其实就是为了控制每一帧的样式, 画了个长帧图, 每一帧切换一副, 从而达到水波纹涟漪的效果。

第一步,加载水波纹平面

平面是类似一个正方形, 材质贴图用到上面的贴图, 代码如下:

 async createTrayMesh() {
    const model: any = await this.loadOneModel('../../../static/models/taper1-p.glb');
    const loader = new TextureLoader();
    const texture = await loader.loadAsync('../../../static/images/wave.png');
    const { width, height } = texture.image;
    this.frameX = width / height;
    texture.wrapS = texture.wrapT = RepeatWrapping;
    //设置xy方向重复次数,x轴有frameX帧,仅取一帧
    texture.repeat.set(1 / this.frameX, 1);
    const material = new MeshStandardMaterial({
      color: 0x1171ee,
      map: texture,
      transparent: true,
      opacity: 0.8,
      metalness: 0.0,
      roughness: 0.6,
      depthTest: true,
      depthWrite: false
    });
    model.material = material;
    this.waveTexture = texture;
    this.trayModel = model;
    this.scene.add(model);
  }

第二步,记录贴图帧数

上面代码中,记录了宽高比 frameX, 这样贴图 y 轴重复为 1, 即 y 轴全部由贴图的高度填充; x 轴的重复为 1/frameX , 即 x 轴由贴图长度的 1/frameX 来填充。 后面的关键帧动画会根据 frameX 这个参数,不断推进 offset, 达到一帧一帧移动的效果。

this.frameX = width / height;
texture.wrapS = texture.wrapT = RepeatWrapping;
//设置xy方向重复次数,x轴有frameX帧,仅取一帧
texture.repeat.set(1 / this.frameX, 1);

四、帧动画

动画目标如下

  1. 四棱柱不断旋转
  2. 底座实现水波纹效果

代码入下:

render() {
    requestAnimationFrame(this.render.bind(this));
    this.renderer.render(this.scene, this.camera);
    this.controls && this.controls.update();
    if(this.mainModel){
      this.mainModel.rotation.z += 0.02;
    }
    if(this.trayModel && this.waveTexture){
      this.offset += 0.6;
      this.waveTexture.offset.x = Math.floor(this.offset) / this.frameX;
    }
  }

彩蛋

为什么写的这么简单? 其实这只是做个小铺垫, 后面会在高德地图的基础上, 加上 three.js 图层, 并将上面这个四棱柱的模型, 应用在高德地图上面,后续会更新博文。

地图.png

Image_20250626220132.png

gif 效果如下, 3D 效果:

2025-06-26 22-15-35_2.gif

详细可以看下一篇:

Three.js+高德地图 实现 3D 散点标注