基于Vue3+Three.js實現(xiàn)一個3D模型可視化編輯系統(tǒng)
前言
1.因為之前工作過的可視化大屏項目開發(fā)3d大屏組件模塊需要用到Three.js來完成,其主功能是實現(xiàn)對3d模型的材質(zhì),燈光,背景,動畫。等屬性進行可視化的編輯操作以及模型編輯數(shù)據(jù)的存儲和模型在大屏上面的拖拽顯示
2.因為是第一次使用Three.js開發(fā)實際的項目,在開發(fā)這些功能的過程中也遇到了許多Three.js的坑(好在最終都解決了)
3.同時在開發(fā)這個項目模塊的過程中也發(fā)現(xiàn)github能夠搜索到的Three.js3d模型可視化編輯相關(guān)的開源項目非常的少,許多的three.js相關(guān)問題和功能的實現(xiàn)在網(wǎng)上也很難搜索到答案
4.因此在參考了之前工作項目中做過的可視化大屏項目的3D模型編輯模塊的功能和three.js 官方編輯器 https://threejs.org/editor/的部分功能的基礎(chǔ)之上開發(fā)了一款基于Three.js+Vue3的3d模塊可視化編輯器系統(tǒng),其主要目的是盡可能更多的將three.js提供的API結(jié)合在實際的項目中去使用,作為自己個人學習three.js的記錄,也供大家學習和參考
項目的在線訪問地址:https://zhang_6666.gitee.io/three.js3d/
系統(tǒng)界面圖:
實現(xiàn)的主要功能模塊
- 背景模塊:實現(xiàn)背景圖、全景圖、背景顏色的編輯功能
- 材質(zhì)模塊:實現(xiàn)模型材質(zhì)顏色、透明度、網(wǎng)格、材質(zhì)顯示/隱藏、材質(zhì)貼圖、模型材質(zhì)類型切換等編輯功能
- 后期處理模塊:實現(xiàn)模型材質(zhì)的輝光效果強度、半徑、閾值、色調(diào)曝光度、模型的拖拽和分解等編輯功能
- 燈光模塊:實現(xiàn)環(huán)境光、點光源、半球光、聚光燈等參數(shù)的編輯功能
- 動畫模塊:實現(xiàn)模型自帶動畫的播放、播放速度、播放類型、動作幅度和模型x,y,z軸動畫等編輯功能
- 輔助線/軸配置模塊:實現(xiàn)模型的軸坐標、軸位置、網(wǎng)格輔助線、模型骨架、模型坐標軸輔助線等編輯功能
- 幾何體模型配置模塊:實現(xiàn)對Three.js中的幾何體API函數(shù)的參數(shù)編輯功能
- 模型加載模塊:實現(xiàn)模型的點擊切換功能、外部模型加載的功能、幾何體模型拖拽加載功能、支持多類型(.glb,.obj,.gltf,.fbx)格式的模型文件加載,模型加載進度條功能
- 導出模塊:實現(xiàn)模型場景封面下載、模型文件導出功能
- 數(shù)據(jù)保存模塊:實現(xiàn)模塊編輯數(shù)據(jù)的預覽、模型編輯數(shù)據(jù)的保存
- 模型庫模塊:支持多個編輯模型數(shù)據(jù)的拖拽展示和保存
主要功能模塊實現(xiàn)的代碼
1.這里首先將three.js相關(guān)的API操作封裝在一個renderModel.js的class類函數(shù)中去方便在vue頁面中調(diào)用
2.將不同模塊的功能都寫入函數(shù)方法中去,將需要編輯操作的一些three.js的API屬性定義在constructor中去然后在通過this去修改
import * as THREE from 'three' //導入整個 three.js核心庫 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' //導入控制器模塊,軌道控制器 import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' //導入GLTF模塊,模型解析器,根據(jù)文件格式來定 import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js' import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js' import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js' import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js' import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js' import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader' import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader' import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js' import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter' import { DragControls } from 'three/examples/jsm/controls/DragControls'; import { ElMessage } from 'element-plus'; import { lightPosition, onlyKey } from '@/utils/utilityFunction' import store from '@/store' import TWEEN from "@tweenjs/tween.js"; import { vertexShader, fragmentShader, MODEL_DECOMPOSE } from '@/config/constant.js' // 定義一個 class類 class renderModel { constructor(selector) { this.container = document.querySelector(selector) // 相機 this.camera // 場景 this.scene //渲染器 this.renderer // 控制器 this.controls // 模型 this.model // 幾何體模型數(shù)組 this.geometryGroup = new THREE.Group() // 幾何體模型 this.geometryModel // 加載進度監(jiān)聽 this.loadingManager = new THREE.LoadingManager() //文件加載器類型 this.fileLoaderMap = { 'glb': new GLTFLoader(), 'fbx': new FBXLoader(this.loadingManager), 'gltf': new GLTFLoader(), 'obj': new OBJLoader(this.loadingManager), } //模型動畫列表 this.modelAnimation //模型動畫對象 this.animationMixer this.animationColock = new THREE.Clock() //動畫幀 this.animationFrame = null // 軸動畫幀 this.rotationAnimationFrame = null // 動畫構(gòu)造器 this.animateClipAction = null // 動畫循環(huán)方式枚舉 this.loopMap = { LoopOnce: THREE.LoopOnce, LoopRepeat: THREE.LoopRepeat, LoopPingPong: THREE.LoopPingPong } //模型材質(zhì)列表 this.modelMaterialList // 效果合成器 this.effectComposer this.outlinePass // 動畫渲染器 this.renderAnimation = null // 碰撞檢測 this.raycaster = new THREE.Raycaster() // 鼠標位置 this.mouse = new THREE.Vector2() // 模型自帶貼圖 this.modelTextureMap // 輝光效果合成器 this.glowComposer // 輝光渲染器 this.unrealBloomPass // 需要輝光的材質(zhì) this.glowMaterialList this.materials = {} // 拖拽對象控制器 this.dragControls // 是否開啟輝光 this.glowUnrealBloomPass = false // 窗口變化監(jiān)聽事件 this.onWindowResizesListener // 模型上傳進度條回調(diào)函數(shù) this.modelProgressCallback = (e) => e } init() { return new Promise(async (reslove, reject) => { //初始化渲染器 this.initRender() //初始化相機 this.initCamera() //初始化場景 this.initScene() //初始化控制器,控制攝像頭,控制器一定要在渲染器后 this.initControls() this.addEvenListMouseLisatener() // 添加物體模型 TODO:初始化時需要默認一個 const load = await this.setModel({ filePath: 'threeFile/glb/glb-9.glb', fileType: 'glb', decomposeName: 'transformers_3' }) // 創(chuàng)建效果合成器 this.createEffectComposer() //場景渲染 this.sceneAnimation() reslove(load) }) } // 創(chuàng)建場景 initScene() { this.scene = new THREE.Scene() const texture = new THREE.TextureLoader().load(require('@/assets/image/view-4.png')) texture.mapping = THREE.EquirectangularReflectionMapping this.scene.background = texture this.scene.environment = texture } // 創(chuàng)建相機 initCamera() { const { clientHeight, clientWidth } = this.container this.camera = new THREE.PerspectiveCamera(50, clientWidth / clientHeight, 0.25, 2000) } // 創(chuàng)建渲染器 initRender() { this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true }) //設(shè)置抗鋸齒 //設(shè)置屏幕像素比 this.renderer.setPixelRatio(window.devicePixelRatio) //渲染的尺寸大小 const { clientHeight, clientWidth } = this.container this.renderer.setSize(clientWidth, clientHeight) //色調(diào)映射 this.renderer.toneMapping = THREE.ReinhardToneMapping this.renderer.autoClear = true this.renderer.outputColorSpace = THREE.SRGBColorSpace //曝光 this.renderer.toneMappingExposure = 3 this.renderer.shadowMap.enabled = true this.renderer.shadowMap.type = THREE.PCFSoftShadowMap this.container.appendChild(this.renderer.domElement) } // 更新場景 sceneAnimation() { this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation()) // 將不需要處理輝光的材質(zhì)進行存儲備份 this.scene.traverse((v) => { if (v instanceof THREE.Scene) { this.materials.scene = v.background v.background = null } if (!this.glowMaterialList.includes(v.name) && v.isMesh) { this.materials[v.uuid] = v.material v.material = new THREE.MeshStandardMaterial({ color: 'black' }) } }) this.glowComposer.render() // 在輝光渲染器執(zhí)行完之后在恢復材質(zhì)原效果 this.scene.traverse((v) => { if (this.materials[v.uuid]) { v.material = this.materials[v.uuid] delete this.materials[v.uuid] } if (v instanceof THREE.Scene) { v.background = this.materials.scene delete this.materials.scene } }) this.controls.update() TWEEN.update(); this.effectComposer.render() } // 監(jiān)聽事件 addEvenListMouseLisatener() { //監(jiān)聽場景大小改變,跳轉(zhuǎn)渲染尺寸 this.onWindowResizesListener = this.onWindowResizes.bind(this) window.addEventListener("resize", this.onWindowResizesListener) // 鼠標點擊 this.onMouseClickListener = this.onMouseClickModel.bind(this) this.container.addEventListener('click', this.onMouseClickListener) } // 創(chuàng)建控制器 initControls() { this.controls = new OrbitControls(this.camera, this.renderer.domElement) this.controls.enablePan = false } // 加載模型 setModel({ filePath, fileType, scale, map, position, decomposeName }) { return new Promise((resolve, reject) => { const loader = this.fileLoaderMap[fileType] if (['glb', 'gltf'].includes(fileType)) { const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath('./threeFile/gltf/') loader.setDRACOLoader(dracoLoader) } loader.load(filePath, (result) => { switch (fileType) { case 'glb': this.model = result.scene this.skeletonHelper = new THREE.SkeletonHelper(result.scene) this.modelAnimation = result.animations || [] break; case 'fbx': this.model = result this.skeletonHelper = new THREE.SkeletonHelper(result) this.modelAnimation = result.animations || [] break; case 'gltf': this.model = result.scene this.skeletonHelper = new THREE.SkeletonHelper(result.scene) this.modelAnimation = result.animations || [] break; case 'obj': this.model = result this.skeletonHelper = new THREE.SkeletonHelper(result) this.modelAnimation = result.animations || [] break; default: break; } this.model.decomposeName = decomposeName this.getModelMeaterialList(map) this.setModelPositionSize() // 設(shè)置模型大小 if (scale) { this.model.scale.set(scale, scale, scale); } //設(shè)置模型位置 this.model.position.set(0, -.5, 0) if (position) { const { x, y, z } = position this.model.position.set(x, y, z) } this.skeletonHelper.visible = false this.scene.add(this.skeletonHelper) // 需要輝光的材質(zhì) this.glowMaterialList = this.modelMaterialList.map(v => v.name) this.scene.add(this.model) resolve(true) }, (xhr) => { this.modelProgressCallback(xhr.loaded) }, (err) => { ElMessage.error('文件錯誤') console.log(err) reject() }) }) } // 加載幾何體模型 setGeometryModel(model) { return new Promise((reslove, reject) => { const { clientHeight, clientWidth, offsetLeft, offsetTop } = this.container // 計算鼠標在屏幕上的坐標 this.mouse.x = ((model.clientX - offsetLeft) / clientWidth) * 2 - 1 this.mouse.y = -((model.clientY - offsetTop) / clientHeight) * 2 + 1 this.raycaster.setFromCamera(this.mouse, this.camera); const intersects = this.raycaster.intersectObjects(this.scene.children); if (intersects.length > 0) { // 在控制臺輸出鼠標在場景中的位置 const { type } = model // 不需要賦值的key const notGeometrykey = ['id', 'name', 'modelType', 'type'] const geometryData = Object.keys(model).filter(key => !notGeometrykey.includes(key)).map(v => model[v]) // 創(chuàng)建幾何體 const geometry = new THREE[type](...geometryData) const colors = ['#FF4500', '#90EE90', '#00CED1', '#1E90FF', '#C71585', '#FF4500', '#FAD400', '#1F93FF', '#90F090', '#C71585'] // 隨機顏色 const meshColor = colors[Math.ceil(Math.random() * 10)] const material = new THREE.MeshStandardMaterial({ color: new THREE.Color(meshColor),side: THREE.DoubleSide }) const mesh = new THREE.Mesh(geometry, material) const { x, y, z } = intersects[0].point mesh.position.set(x, y, z) mesh.name = type + '_' + onlyKey(4, 5) mesh.userData.geometry = true this.geometryGroup.add(mesh) this.model = this.geometryGroup this.onSetGeometryMeshList(mesh) this.skeletonHelper.visible = false this.skeletonHelper.dispose() this.glowMaterialList = this.modelMaterialList.map(v => v.name) this.setModelMeshDrag({ modelDrag: true }) this.scene.add(this.model) } reslove(true) }) } // 模型加載進度條回調(diào)函數(shù) onProgress(callback) { if (typeof callback == 'function') { this.modelProgressCallback = callback } } // 創(chuàng)建效果合成器 createEffectComposer() { const { clientHeight, clientWidth } = this.container this.effectComposer = new EffectComposer(this.renderer) const renderPass = new RenderPass(this.scene, this.camera) this.effectComposer.addPass(renderPass) this.outlinePass = new OutlinePass(new THREE.Vector2(clientWidth, clientHeight), this.scene, this.camera) this.outlinePass.visibleEdgeColor = new THREE.Color('#FF8C00') // 可見邊緣的顏色 this.outlinePass.hiddenEdgeColor = new THREE.Color('#8a90f3') // 不可見邊緣的顏色 this.outlinePass.edgeGlow = 2.0 // 發(fā)光強度 this.outlinePass.edgeThickness = 1 // 邊緣濃度 this.outlinePass.edgeStrength = 4 // 邊緣的強度,值越高邊框范圍越大 this.outlinePass.pulsePeriod = 100 // 閃爍頻率,值越大頻率越低 this.effectComposer.addPass(this.outlinePass) let effectFXAA = new ShaderPass(FXAAShader) const pixelRatio = this.renderer.getPixelRatio() effectFXAA.uniforms.resolution.value.set(1 / (clientWidth * pixelRatio), 1 / (clientHeight * pixelRatio)) effectFXAA.renderToScreen = true effectFXAA.needsSwap = true this.effectComposer.addPass(effectFXAA) //創(chuàng)建輝光效果 this.unrealBloomPass = new UnrealBloomPass(new THREE.Vector2(clientWidth, clientHeight),1.5, 0.4, 0.85) // 輝光合成器 const renderTargetParameters = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, stencilBuffer: false, }; const glowRender = new THREE.WebGLRenderTarget(clientWidth * 2, clientHeight * 2, renderTargetParameters) this.glowComposer = new EffectComposer(this.renderer,glowRender) this.glowComposer.renderToScreen = false this.glowComposer.addPass(new RenderPass(this.scene, this.camera)) this.glowComposer.addPass(this.unrealBloomPass) // 著色器 let shaderPass = new ShaderPass(new THREE.ShaderMaterial({ uniforms: { baseTexture: { value: null }, bloomTexture: { value: this.glowComposer.renderTarget2.texture }, tDiffuse: { value: null } }, vertexShader, fragmentShader, defines: {} }), 'baseTexture') shaderPass.renderToScreen = true shaderPass.needsSwap = true this.effectComposer.addPass(shaderPass) } // 切換模型 onSwitchModel(model) { return new Promise(async (reslove, reject) => { try { this.clearSceneModel() // 加載幾何模型 if (model.modelType && model.modelType == 'geometry') { // 重置"燈光"模塊數(shù)據(jù) this.onResettingLight({ ambientLight: false }) this.modelAnimation = [] this.camera.fov = 80 this.camera.updateProjectionMatrix() const load = await this.setGeometryModel(model) reslove() } else { // 重置"燈光"模塊數(shù)據(jù) this.onResettingLight({ ambientLight: true }) this.camera.fov = 50 this.geometryGroup.clear() // 加載模型 const load = await this.setModel(model) // 模型加載成功返回 true reslove({ load, filePath: model.filePath }) } } catch { reject() } }) } // 監(jiān)聽窗口變化 onWindowResizes() { if (!this.container) return false const { clientHeight, clientWidth } = this.container //調(diào)整屏幕大小 this.camera.aspect = clientWidth / clientHeight //攝像機寬高比例 this.camera.updateProjectionMatrix() //相機更新矩陣,將3d內(nèi)容投射到2d面上轉(zhuǎn)換 this.renderer.setSize(clientWidth, clientHeight) this.effectComposer.setSize(clientWidth * 2, clientHeight * 2) this.glowComposer.setSize(clientWidth, clientHeight) } }
2.在vue頁面中去使用
<template> <div id="model" ref="model"></div> </template> <script setup> import { onMounted} from "vue"; import renderModel from "./renderModel"; const store = useStore(); const state = reactive({ modelApi: computed(() => { return store.state.modelApi; }) }); const loading = ref(false); const progress = ref(0); // 初始化場景方法 onMounted(async () => { loading.value = true; const modelApi = new renderModel("#model"); //將當前場景函數(shù)存儲在vuex中 store.commit("SET_MODEL_API", modelApi); // 模型加載進度條 state.modelApi.onProgress((progressNum) => { progress.value = Number((progressNum / 1024 / 1024).toFixed(2)); // console.log('模型已加載' + progress.value + 'M') }); const load = await modelApi.init(); // load=true 表示模型加載完成(主要針對大模型文件) if (load) { loading.value = false; progress.value = 0; } });
3.ok這樣一個模型編輯器的初始化場景功能就完成了
如何將編輯的模型數(shù)據(jù)進行存儲和回顯???
- 模型數(shù)據(jù)的存儲和回顯應(yīng)該是這個編輯器最核心的東西了吧,我想你也不希望自己編輯操作了半天的模型數(shù)據(jù)被瀏覽器的F5一鍵重置了吧。
- 這里我的思路是將模型的背景、燈光、材質(zhì)、動畫、輔助線、位置等屬性值存儲在localStorage ,在頁面刷新或者進入頁面時候獲取到這些保存的數(shù)據(jù)值,然后將這些值進行數(shù)據(jù)回填。這種思路同樣也適用于將數(shù)據(jù)存儲在服務(wù)端然后在通過調(diào)用接口獲取。
- 新建一個initThreeTemplate.js 文件 用于專門處理模型數(shù)據(jù)回填 (renderModel) 方法 和創(chuàng)建模型渲染 (createThreeDComponent) 方法。
- renderModel 方法內(nèi)容和上面的基本一致,只是在傳遞和接收參數(shù)時新增一個模型數(shù)據(jù)的參數(shù) config,這里只列舉部分不同處的代碼作為解釋
/** * @describe three.js 組件數(shù)據(jù)初始化方法 * @param config 組件參數(shù)配置信息 * @param elementId 元素ID */ class renderModel { constructor(config, elementId) { this.config = config } // 獲取到創(chuàng)建相機位置 initCamera() { const { clientHeight, clientWidth } = this.container this.camera = new THREE.PerspectiveCamera(45, clientWidth / clientHeight, 0.25, 1000) this.camera.near = 0.1 const { camera } = this.config if (!camera) return false const { x, y, z } = camera this.camera.position.set(x, y, z) this.camera.updateProjectionMatrix() } // 設(shè)置輝光和模型操作數(shù)據(jù)回填 setModelLaterStage() { const { stage } = this.config if (!stage) return false const { threshold, strength, radius, toneMappingExposure, meshPositonList } = stage // 設(shè)置輝光效果 if (stage.glow) { this.unrealBloomPass.threshold = threshold this.unrealBloomPass.strength = strength this.unrealBloomPass.radius = radius this.renderer.toneMappingExposure = toneMappingExposure } else { this.unrealBloomPass.threshold = 0 this.unrealBloomPass.strength = 0 this.unrealBloomPass.radius = 0 this.renderer.toneMappingExposure = toneMappingExposure } // 模型材質(zhì)位置 meshPositonList.forEach(v => { const mesh = this.model.getObjectByProperty('name', v.name) const { x, y, z } = v mesh.position.set(x, y, z) }) } // 處理模型動畫數(shù)據(jù)回填 setModelAnimation() { const { animation } = this.config if (!animation) return false if (this.modelAnimation.length && animation && animation.visible) { this.animationMixer = new THREE.AnimationMixer(this.model) const { animationName, timeScale, weight, loop } = animation // 模型動畫 const clip = THREE.AnimationClip.findByName(this.modelAnimation, animationName) if (clip) { this.animateClipAction = this.animationMixer.clipAction(clip) this.animateClipAction.setEffectiveTimeScale(timeScale) this.animateClipAction.setEffectiveWeight(weight) this.animateClipAction.setLoop(this.loopMap[loop]) this.animateClipAction.play() } this.animationFrameFun() } // 軸動畫 if (animation.rotationVisible) { const { rotationType, rotationSpeed } = animation this.rotationAnimationFun(rotationType, rotationSpeed) } } // 模型動畫幀 animationFrameFun() { this.animationFrame = requestAnimationFrame(() => this.animationFrameFun()) if (this.animationMixer) { this.animationMixer.update(this.animationColock.getDelta()) } } // 軸動畫幀 rotationAnimationFun(rotationType, rotationSpeed) { this.rotationAnimationFrame = requestAnimationFrame(() => this.rotationAnimationFun(rotationType, rotationSpeed)) this.model.rotation[rotationType] += rotationSpeed / 50 } // 模型軸輔助線配置 setModelAxleLine() { const { attribute } = this.config if (!attribute) return false const { axesHelper, axesSize, color, divisions, gridHelper, positionX, positionY, positionZ, size, skeletonHelper, visible, x, y, z, rotationX, rotationY, rotationZ } = attribute if (!visible) return false //網(wǎng)格輔助線 this.gridHelper = new THREE.GridHelper(size, divisions, color, color); this.gridHelper.position.set(x, y, z) this.gridHelper.visible = gridHelper this.gridHelper.material.linewidth = 0.1 this.scene.add(this.gridHelper) // 坐標軸輔助線 this.axesHelper = new THREE.AxesHelper(axesSize); this.axesHelper.visible = axesHelper this.axesHelper.position.set(0, -.50, 0) this.scene.add(this.axesHelper); // 設(shè)置模型位置 this.model.position.set(positionX, positionY, positionZ) // 設(shè)置模型軸位置 this.model.rotation.set(rotationX, rotationY, rotationZ) // 開啟陰影 this.renderer.shadowMap.enabled = true; // 骨骼輔助線 this.skeletonHelper = new THREE.SkeletonHelper(this.model) this.skeletonHelper = skeletonHelper } }
6 在頁面中調(diào)用方法,獲取到 localStorage 然后傳入 createThreeDComponent 方法中去這樣一個模型渲染和數(shù)據(jù)回填的功能就實現(xiàn)了。沒錯就是這么簡單
<template> <div id="preview"> <tree-component /> </div> </template> <script setup lang="jsx" name="modelBase"> import { local } from "@/utils/storage"; import createThreeDComponent from "@/utils/initThreeTemplate"; import { MODEL_PRIVEW_CONFIG } from "@/config/constant"; // 獲取 localStorage 的模型編輯數(shù)據(jù) const config = local.get(MODEL_PRIVEW_CONFIG); const treeComponent = createThreeDComponent(config); </script> <style lang="less" scoped> #preview { width: 100%; height: 100vh; } </style>
模型編輯的數(shù)據(jù) MODEL_PRIVEW_CONFI 的結(jié)構(gòu)
數(shù)據(jù)回顯效果
如何實現(xiàn)多模型的數(shù)據(jù)回顯展示
1 這里通過列表循環(huán)渲染和 vue3-draggable-resizable 插件實現(xiàn) 可拖拽的多模型展示功能
<template> <div id="drag-content"> <div class="content" @drop="onDrop" @dragover.prevent> <draggable-container :adsorbParent="true" :disabled="true"> <draggable-resizable-item @onDragActived="onDragActived" @onDragDeactivated="onDragDeactivated" v-for="drag in dragModelList" :key="drag.modelKey" :config="drag" ></draggable-resizable-item> </draggable-container> </right-context-menu> </div> </div> </template> <script setup name="modelBase"> import DraggableResizableItem from "@/components/DraggableResizableItem/index"; const dragModelList = ref([]); // 當前選中的內(nèi)容 const dragActive = ref(null); const onDrop = (event) => { event.preventDefault(); // 設(shè)置模型拖放位置 const container = document.querySelector("#drag-content").getBoundingClientRect(); const x = event.clientX - container.left - 520 / 2; const y = event.clientY - container.top - 360 / 2; dragActive.value.x = x; dragActive.value.y = y; }; // 選中拖拽元素 const onDragActived = (drag) => { dragActive.value = drag; }; // 取消選中拖拽元素 const onDragDeactivated = (modelKey) => { if (modelKey == dragActive.value.modelKey) { dragActive.value = null; } }; // </script>
DraggableResizableItem.vue 代碼
<template> <draggable-resizable class="draggable-resizable" classNameDragging="dragging" classNameActive="active" :initW="props.config.width" :initH="props.config.height" v-model:x="props.config.x" v-model:y="props.config.y" v-model:w="props.config.width" v-model:h="props.config.height" :parent="false" :resizable="true" :draggable="true" @drag-end="dragEndHandle" @dragging="dragHandle" @activated="activatedHandle" @deactivated="deactivatedHandle" > <tree-component :width="props.config.width" :height="props.config.height" ></tree-component> <div :class="dragMask" class="mask"></div> </draggable-resizable> </template> <script setup> import DraggableResizable from "vue3-draggable-resizable"; import createThreeDComponent from "@/utils/initThreeTemplate"; import { ref } from "vue"; const props = defineProps({ config: { type: Object, default: {}, }, }); const emit = defineEmits(["onDragActived", "onDragDeactivated"]); const dragMask = ref(""); // 開始拖拽 const dragHandle = (e) => { dragMask.value = "mask-dragging"; }; // 拖拽結(jié)束 const dragEndHandle = (e) => { dragMask.value = "mask-dragactive"; }; // 選中 const activatedHandle = (e) => { dragMask.value = "mask-dragactive"; emit("onDragActived", props.config); }; // 取消選中 const deactivatedHandle = (e) => { dragMask.value = ""; emit("onDragDeactivated", props.config.modelKey); }; const treeComponent = createThreeDComponent(props.config); </script>
數(shù)據(jù)回顯效果
結(jié)語
1.好了這樣一個基于Three.js開發(fā)的3d模型可視化編輯系統(tǒng)就開發(fā)完成了
2.完整的項目代碼可訪問:
gitee:https://gitee.com/ZHANG_6666/Three.js3D
github:https://github.com/zhangbo126/Three3d-view
以上就是基于Vue3+Three.js實現(xiàn)一個3D模型可視化編輯系統(tǒng)的詳細內(nèi)容,更多關(guān)于Vue3+Three.js實現(xiàn)3D模型可視化系統(tǒng)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue3 reactive定義的引用類型直接賦值導致數(shù)據(jù)失去響應(yīng)式問題
這篇文章主要介紹了vue3 reactive定義的引用類型直接賦值導致數(shù)據(jù)失去響應(yīng)式問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06Vue實現(xiàn)登錄保存token并校驗實現(xiàn)保存登錄狀態(tài)的操作代碼
這篇文章主要介紹了Vue實現(xiàn)登錄保存token并校驗實現(xiàn)保存登錄狀態(tài),本文通過示例代碼給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-02-02