基于vue3+threejs實(shí)現(xiàn)可視化大屏效果
前言
Three.js是一款基于原生WebGL封裝通用Web 3D引擎,在小游戲、產(chǎn)品展示、物聯(lián)網(wǎng)、數(shù)字孿生、智慧城市園區(qū)、機(jī)械、建筑、全景看房、GIS等各個(gè)領(lǐng)域基本上都有three.js的身影。
本文需要對(duì) threejs 的一些基本概念和 api 有一定了解。
本文主要主要講述對(duì) threejs 的一些 api 進(jìn)行基本的封裝,在 vue3 項(xiàng)目中來實(shí)現(xiàn)一個(gè)可視化的3d項(xiàng)目。包含了一些常用的功能,場(chǎng)景、燈光、攝像機(jī)初始化,模型、天空盒的加載,以及鼠標(biāo)點(diǎn)擊和懸浮的事件交互。
項(xiàng)目截圖:
Github 地址:GitHub - fh332393900/threejs-demo
項(xiàng)目預(yù)覽地址:Three Demo (stevenfeng.cn)
基礎(chǔ)功能
1.場(chǎng)景 Viewer 類
首先我們第一步需要初始化場(chǎng)景、攝像機(jī)、渲染器、燈光等。這些功能只需要加載一次,我們都放到 Viewer 類中可以分離關(guān)注點(diǎn),在業(yè)務(wù)代碼中就不需要關(guān)注這一部分邏輯。業(yè)務(wù)代碼中我們只需要關(guān)注數(shù)據(jù)與交互即可。
1.1 初始化場(chǎng)景和攝像機(jī)
private initScene() { this.scene = new Scene(); } private initCamera() { // 渲染相機(jī) this.camera = new PerspectiveCamera(25, window.innerWidth / window.innerHeight, 1, 2000); //設(shè)置相機(jī)位置 this.camera.position.set(4, 2, -3); //設(shè)置相機(jī)方向 this.camera.lookAt(0, 0, 0); }
1.2 初始化攝像機(jī)控制器
private initControl() { this.controls = new OrbitControls( this.camera as Camera, this.renderer?.domElement ); this.controls.enableDamping = false; this.controls.screenSpacePanning = false; // 定義平移時(shí)如何平移相機(jī)的位置 控制不上下移動(dòng) this.controls.minDistance = 2; this.controls.maxDistance = 1000; this.controls.addEventListener('change', ()=>{ this.renderer.render(this.scene, this.camera); }); }
1.3 初始化燈光
這里放了一個(gè)環(huán)境燈光和平行燈光,這里是寫在 Viewer 類里面的,如果想靈活一點(diǎn),也可以抽出去。
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 初始化渲染器
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ì),同時(shí)這也影響到環(huán)境貼圖。 this.viewerDom.appendChild(this.renderer.domElement); }
Viewer 里面還加了一些 addAxis 添加坐標(biāo)軸、addStats 性能監(jiān)控等輔助的公用方法。具體可以看倉庫完整代碼。
1.5 鼠標(biāo)事件
里面主要使用了 mitt 這個(gè)庫,來發(fā)布訂閱事件。
threejs里面的鼠標(biāo)事件主要通過把屏幕坐標(biāo)轉(zhuǎn)換成 3D 坐標(biāo)。通過raycaster.intersectObjects
方法轉(zhuǎn)換。
/**注冊(cè)鼠標(biāo)事件監(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); }; // 初始化常用的幾種鼠標(biāo)事件 initRaycasterEvent('click'); initRaycasterEvent('dblclick'); initRaycasterEvent('mousemove'); } /**自定義鼠標(biāo)事件觸發(fā)的范圍,給定一個(gè)模型組,對(duì)給定的模型組鼠標(biāo)事件才生效 */ 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 方法,傳遞一個(gè)觸發(fā)鼠標(biāo)事件的模型范圍,可以避免在整個(gè)場(chǎng)景中都去觸發(fā)鼠標(biāo)事件。這里也可以用一個(gè) Map 去存不同模型的事件,在取消訂閱時(shí)再移除。
使用方式:
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 這兩個(gè)類。
模型加載器 ModelLoder 初始化的時(shí)候需要把 Viewer 的實(shí)例傳進(jìn)去。
需要注意的是,需要把 draco 從 node_modules 拷貝到項(xiàng)目的 public 目錄中去。
實(shí)現(xiàn)代碼:
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(); // 提供一個(gè)DracLoader實(shí)例來解碼壓縮網(wǎng)格數(shù)據(jù) // 沒有這個(gè)會(huì)報(bào)錯(cuò) dracolPath 默認(rèn)放在public文件夾當(dāng)中 this.dracoLoader.setDecoderPath(dracolPath); this.gltfLoader.setDRACOLoader(this.dracoLoader); } /**模型加載到場(chǎng)景 */ 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 類
這里對(duì)模型外面包了一層,做了一些額外的功能,如模型克隆、播放動(dòng)畫、設(shè)置模型特性、顏色、材質(zhì)等方法。
/** * 設(shè)置模型動(dòng)畫 * @param i 選擇模型動(dòng)畫進(jìn)行播放 */ 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ù)分開,在運(yùn)行時(shí)填入 this.animaObject = { fun: this.updateAnima, content: this, }; this.viewer.addAnimate(this.animaObject); } private updateAnima(e: any) { e.mixer.update(e.clock.getDelta()); }
還有一些其他方法的實(shí)現(xiàn),可以看倉庫代碼。
4.天空盒 SkyBoxs 類
import * as THREE from 'three'; import type Viewer from '../Viewer'; import { Sky } from '../type'; /** 場(chǎng)景天空盒*/ 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; } /** * 添加默認(rèn)天空盒 * @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 可以實(shí)現(xiàn)簡單的鼠標(biāo)選中的特效。
也可以通過 OutlinePass 實(shí)現(xiàn)發(fā)光的特效。
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; }); } }
使用方式:
let modelLoader = new ModelLoader(viewer); boxHelperWrap = new BoxHelperWrap(viewer); boxHelperWrap.setVisible(false);
以上就是基于vue3+threejs實(shí)現(xiàn)可視化大屏效果的詳細(xì)內(nèi)容,更多關(guān)于vue3+threejs 可視化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
uni-app無限級(jí)樹形組件簡單實(shí)現(xiàn)代碼
文章介紹了如何在uni-app中簡單封裝一個(gè)無限級(jí)樹形組件,該組件可以無線嵌套,展開和收縮,并獲取子節(jié)點(diǎn)數(shù)據(jù),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-01-01vue中ref引用操作DOM元素的實(shí)現(xiàn)
本文主要介紹了vue中ref引用操作DOM元素的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01Vue.js條件渲染和列表渲染以及Vue中key值的內(nèi)部原理
這篇文章主要介紹了Vue.js條件渲染和列表渲染,以及Vue中key值的內(nèi)部原理,文中有詳細(xì)的代碼示例,感興趣的同學(xué)可以參考閱讀2023-04-04vue-electron使用serialport時(shí)問題解決方案
這篇文章主要介紹了vue-electron使用serialport時(shí)問題解決方案,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09Vue過渡效果之CSS過渡詳解(結(jié)合transition,animation,animate.css)
Vue 在插入、更新或者移除 DOM 時(shí),提供多種不同方式的應(yīng)用過渡效果。本文將從CSS過渡transition、CSS動(dòng)畫animation及配合使用第三方CSS動(dòng)畫庫(如animate.css)這三方面來詳細(xì)介紹Vue過渡效果之CSS過渡2020-02-02Vue中使用定時(shí)器(setInterval、setTimeout)的兩種方式
js中定時(shí)器有兩種,一個(gè)是循環(huán)執(zhí)行?setInterval,另一個(gè)是定時(shí)執(zhí)行?setTimeout,這篇文章主要介紹了Vue中使用定時(shí)器?(setInterval、setTimeout)的兩種方式,需要的朋友可以參考下2023-03-03VUEX 數(shù)據(jù)持久化,刷新后重新獲取的例子
今天小編就為大家分享一篇VUEX 數(shù)據(jù)持久化,刷新后重新獲取的例子,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11