基于vue3+threejs實現(xiàn)可視化大屏

本文主要主要講述對 threejs 的一些 api 進行基本的封裝,在 vue3 項目中來實現(xiàn)一個可視化的3d項目。包含了一些常用的功能,場景、燈光、攝像機初始化,模型、天空盒的加載,以及鼠標點擊和懸浮的事件交互。


項目截圖:




Github 地址:github.com/fh332393900…


項目預覽地址:stevenfeng.cn/threejs-dem…


基礎(chǔ)功能

1.場景 Viewer 類

首先我們第一步需要初始化場景、攝像機、渲染器、燈光等。這些功能只需要加載一次,我們都放到 Viewer 類中可以分離關(guān)注點,在業(yè)務(wù)代碼中就不需要關(guān)注這一部分邏輯。業(yè)務(wù)代碼中我們只需要關(guān)注數(shù)據(jù)與交互即可。


1.1 初始化場景和攝像機


js


復制代碼


private initScene() {

  this.scene = new Scene();

}


private initCamera() {

  // 渲染相機

  this.camera = new PerspectiveCamera(25, window.innerWidth / window.innerHeight, 1, 2000);

  //設(shè)置相機位置

  this.camera.position.set(4, 2, -3);

  //設(shè)置相機方向

  this.camera.lookAt(0, 0, 0);

}

1.2 初始化攝像機控制器


js


復制代碼


private initControl() {

  this.controls = new OrbitControls(

    this.camera as Camera,

    this.renderer?.domElement

  );

  this.controls.enableDamping = false;

  this.controls.screenSpacePanning = false; // 定義平移時如何平移相機的位置 控制不上下移動

  this.controls.minDistance = 2;

  this.controls.maxDistance = 1000;

  this.controls.addEventListener('change', ()=>{

    this.renderer.render(this.scene, this.camera);

  });

}

1.3 初始化燈光

這里放了一個環(huán)境燈光和平行燈光,這里是寫在 Viewer 類里面的,如果想靈活一點,也可以抽出去。



js


復制代碼


private initLight() {

  const ambient = new AmbientLight(0xffffff, 0.6);

  this.scene.add(ambient);


  const light = new THREE.DirectionalLight( 0xffffff );

  light.position.set( 0, 200, 100 );

  light.castShadow = true;


  light.shadow.camera.top = 180;

  light.shadow.camera.bottom = -100;

  light.shadow.camera.left = -120;

  light.shadow.camera.right = 400;

  light.shadow.camera.near = 0.1;

  light.shadow.camera.far = 400;

  // 設(shè)置mapSize屬性可以使陰影更清晰,不那么模糊

  light.shadow.mapSize.set(1024, 1024);


  this.scene.add(light);

}

1.4 初始化渲染器


js


復制代碼


private initRenderer() {

  // 獲取畫布dom

  this.viewerDom = document.getElementById(this.id) as HTMLElement;

  // 初始化渲染器

  this.renderer = new WebGLRenderer({

    logarithmicDepthBuffer: true,

    antialias: true, // true/false表示是否開啟反鋸齒

    alpha: true, // true/false 表示是否可以設(shè)置背景色透明

    precision: 'mediump', // highp/mediump/lowp 表示著色精度選擇

    premultipliedAlpha: true, // true/false 表示是否可以設(shè)置像素深度(用來度量圖像的分辨率)

    // preserveDrawingBuffer: false, // true/false 表示是否保存繪圖緩沖

    // physicallyCorrectLights: true, // true/false 表示是否開啟物理光照

  });

  this.renderer.clearDepth();


  this.renderer.shadowMap.enabled = true;

  this.renderer.outputColorSpace = SRGBColorSpace; // 可以看到更亮的材質(zhì),同時這也影響到環(huán)境貼圖。

  this.viewerDom.appendChild(this.renderer.domElement);

}

Viewer 里面還加了一些 addAxis 添加坐標軸、addStats 性能監(jiān)控等輔助的公用方法。具體可以看倉庫完整代碼。







1.5 鼠標事件

里面主要使用了 mitt 這個庫,來發(fā)布訂閱事件。


threejs里面的鼠標事件主要通過把屏幕坐標轉(zhuǎn)換成 3D 坐標。通過raycaster.intersectObjects方法轉(zhuǎn)換。



js


復制代碼


/**注冊鼠標事件監(jiān)聽 */

public initRaycaster() {

  this.raycaster = new Raycaster();


  const initRaycasterEvent: Function = (eventName: keyof HTMLElementEventMap): void => {

    const funWrap = throttle(

      (event: any) => {

        this.mouseEvent = event;

        this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;

        this.mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;

        // @ts-expect-error

        this.emitter.emit(Events[eventName].raycaster, this.getRaycasterIntersectObjects());

      },

      50

    );

    this.viewerDom.addEventListener(eventName, funWrap, false);

  };


  // 初始化常用的幾種鼠標事件

  initRaycasterEvent('click');

  initRaycasterEvent('dblclick');

  initRaycasterEvent('mousemove');

}


/**自定義鼠標事件觸發(fā)的范圍,給定一個模型組,對給定的模型組鼠標事件才生效 */

public setRaycasterObjects (objList: THREE.Object3D[]): void {

  this.raycasterObjects = objList;

}


private getRaycasterIntersectObjects(): THREE.Intersection[] {

  if (!this.raycasterObjects.length) return [];

  this.raycaster.setFromCamera(this.mouse, this.camera);

  return this.raycaster.intersectObjects(this.raycasterObjects, true);

}

通過 setRaycasterObjects 方法,傳遞一個觸發(fā)鼠標事件的模型范圍,可以避免在整個場景中都去觸發(fā)鼠標事件。這里也可以用一個 Map 去存不同模型的事件,在取消訂閱時再移除。


使用方式:



js


復制代碼


let viewer: Viewer;

viewer = new Viewer('three');


viewer.initRaycaster();


viewer.emitter.on(Event.dblclick.raycaster, (list: THREE.Intersection[]) => {

  onMouseClick(list);

});


viewer.emitter.on(Event.mousemove.raycaster, (list: THREE.Intersection[]) => {

  onMouseMove(list);

});

2.模型加載器 ModelLoder 類

模型的加載我們需要用的threejs里面的,GLTFLoader、DRACOLoader 這兩個類。


模型加載器 ModelLoder 初始化的時候需要把 Viewer 的實例傳進去。


需要注意的是,需要把 draco 從 node_modules 拷貝到項目的 public 目錄中去。




實現(xiàn)代碼:



js


復制代碼


import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';

import BaseModel from '../BaseModel';

import type Viewer from '../Viewer';


type LoadModelCallbackFn<T = any> = (arg: T) => any;


/**模型加載器 */

export default class ModelLoder {

  protected viewer: Viewer;

  private gltfLoader: GLTFLoader;

  private dracoLoader: DRACOLoader;


  constructor(viewer: Viewer, dracolPath: string = '/draco/') {

    this.viewer = viewer;

    this.gltfLoader = new GLTFLoader();

    this.dracoLoader = new DRACOLoader();


    // 提供一個DracLoader實例來解碼壓縮網(wǎng)格數(shù)據(jù)

    // 沒有這個會報錯 dracolPath 默認放在public文件夾當中

    this.dracoLoader.setDecoderPath(dracolPath);

    this.gltfLoader.setDRACOLoader(this.dracoLoader);

  }


  /**模型加載到場景 */

  public loadModelToScene(url: string, callback: LoadModelCallbackFn<BaseModel>) {

    this.loadModel(url, model => {

      this.viewer.scene.add(model.object);

      callback && callback(model);

    });

  }


  private loadModel(url: string, callback: LoadModelCallbackFn<BaseModel>) {

    this.gltfLoader.load(url, gltf => {

      const baseModel = new BaseModel(gltf, this.viewer);

      callback && callback(baseModel);

    });

  }

}

3.模型 BaseModel 類

這里對模型外面包了一層,做了一些額外的功能,如模型克隆、播放動畫、設(shè)置模型特性、顏色、材質(zhì)等方法。



js


復制代碼


/**

* 設(shè)置模型動畫

* @param i 選擇模型動畫進行播放

*/

public startAnima(i = 0) {

  this.animaIndex = i;

  if (!this.mixer) this.mixer = new THREE.AnimationMixer(this.object);

  if (this.gltf.animations.length < 1) return;

  this.mixer.clipAction(this.gltf.animations[i]).play();

  // 傳入?yún)?shù)需要將函數(shù)與函數(shù)參數(shù)分開,在運行時填入

  this.animaObject = {

    fun: this.updateAnima,

    content: this,

  };

  this.viewer.addAnimate(this.animaObject);

}


private updateAnima(e: any) {

  e.mixer.update(e.clock.getDelta());

}

還有一些其他方法的實現(xiàn),可以看倉庫代碼。


4.天空盒 SkyBoxs 類


js


復制代碼


import * as THREE from 'three';

import type Viewer from '../Viewer';

import { Sky } from '../type';


/** 場景天空盒*/

export default class SkyBoxs {

  protected viewer: Viewer;

  

  constructor (viewer: Viewer) {

    this.viewer = viewer;

  }


  /**

   * 添加霧效果

   * @param color 顏色

   */

  public addFog (color = 0xa0a0a0, near = 500, far = 2000) {

    this.viewer.scene.fog = new THREE.Fog(new THREE.Color(color), near, far);

  }


  /**

   * 移除霧效果

   */

  public removeFog () {

    this.viewer.scene.fog = null;

  }


  /**

   * 添加默認天空盒

   * @param skyType

   */

  public addSkybox (skyType: keyof typeof Sky = Sky.daytime) {

    const path = `/skybox/${Sky[skyType]}/`; // 設(shè)置路徑

    const format = '.jpg'; // 設(shè)定格式

    this.setSkybox(path, format);

  }


  /**

   * 自定義添加天空盒

   * @param path 天空盒地址

   * @param format 圖片后綴名

   */

  private setSkybox (path: string, format = '.jpg') {

    const loaderbox = new THREE.CubeTextureLoader();

    const cubeTexture = loaderbox.load([

      path + 'posx' + format,

      path + 'negx' + format,

      path + 'posy' + format,

      path + 'negy' + format,

      path + 'posz' + format,

      path + 'negz' + format,

    ]);

    // 需要把色彩空間編碼改一下

    cubeTexture.encoding = THREE.sRGBEncoding;

    this.viewer.scene.background = cubeTexture;

  }

}






5.模型輪廓輔助線

通過 BoxHelper 可以實現(xiàn)簡單的鼠標選中的特效。


也可以通過 OutlinePass 實現(xiàn)發(fā)光的特效。


這里有一篇關(guān)于 threejs 中輪廓線、邊框線、選中效果實現(xiàn)的N種方法以及性能評估的文章:zhuanlan.zhihu.com/p/462329055



js


復制代碼


import {

  BoxHelper,

  Color,

  Object3D

} from 'three';

import type Viewer from '../Viewer';


export default class BoxHelperWrap {

  protected viewer: Viewer;

  public boxHelper: BoxHelper;


  constructor (viewer: Viewer, color?: number) {

    this.viewer = viewer;

    const boxColor = color === undefined ? 0x00ffff : color;

    this.boxHelper = new BoxHelper(new Object3D(), new Color(boxColor));

    // // @ts-expect-error

    // this.boxHelper.material.depthTest = false;


    this.initBoxHelperWrap();

  }


  private initBoxHelperWrap () {

    this.viewer.scene.add(this.boxHelper);

  }


  public setVisible (visible: boolean): void {

    this.boxHelper.visible = visible;

  }


  public attach (obj: Object3D): void {

    this.boxHelper.setFromObject(obj);

    this.setVisible(true);

  }


  public dispose (): void {

    const parent = this.boxHelper.parent;

    if (parent !== null) {

      parent.remove(this.boxHelper);

    }


    Object.keys(this).forEach(key => {

      // @ts-expect-error

      this[key] = null;

    });

  }

}

使用方式:



js


復制代碼


let modelLoader = new ModelLoader(viewer);


boxHelperWrap = new BoxHelperWrap(viewer);


boxHelperWrap.setVisible(false);

推薦項目

以上功能的封裝主要參考了以下幾個比較不錯的項目


github.com/alwxkxk/iot…


gitee.com/303711888/t…


還有一個用 vue3 hooks來寫的


github.com/fengtianxi0…





作者:前端Q


歡迎關(guān)注微信公眾號 :前端Q