基于Vue3+Three.js實(shí)現(xiàn)一個(gè)3D模型可視化編輯系統(tǒng)
前言
1.因?yàn)橹肮ぷ鬟^(guò)的可視化大屏項(xiàng)目開(kāi)發(fā)3d大屏組件模塊需要用到Three.js來(lái)完成,其主功能是實(shí)現(xiàn)對(duì)3d模型的材質(zhì),燈光,背景,動(dòng)畫(huà)。等屬性進(jìn)行可視化的編輯操作以及模型編輯數(shù)據(jù)的存儲(chǔ)和模型在大屏上面的拖拽顯示
2.因?yàn)槭堑谝淮问褂肨hree.js開(kāi)發(fā)實(shí)際的項(xiàng)目,在開(kāi)發(fā)這些功能的過(guò)程中也遇到了許多Three.js的坑(好在最終都解決了)
3.同時(shí)在開(kāi)發(fā)這個(gè)項(xiàng)目模塊的過(guò)程中也發(fā)現(xiàn)github能夠搜索到的Three.js3d模型可視化編輯相關(guān)的開(kāi)源項(xiàng)目非常的少,許多的three.js相關(guān)問(wèn)題和功能的實(shí)現(xiàn)在網(wǎng)上也很難搜索到答案
4.因此在參考了之前工作項(xiàng)目中做過(guò)的可視化大屏項(xiàng)目的3D模型編輯模塊的功能和three.js 官方編輯器 https://threejs.org/editor/的部分功能的基礎(chǔ)之上開(kāi)發(fā)了一款基于Three.js+Vue3的3d模塊可視化編輯器系統(tǒng),其主要目的是盡可能更多的將three.js提供的API結(jié)合在實(shí)際的項(xiàng)目中去使用,作為自己個(gè)人學(xué)習(xí)three.js的記錄,也供大家學(xué)習(xí)和參考
項(xiàng)目的在線訪問(wèn)地址:https://zhang_6666.gitee.io/three.js3d/
系統(tǒng)界面圖:

實(shí)現(xiàn)的主要功能模塊
- 背景模塊:實(shí)現(xiàn)背景圖、全景圖、背景顏色的編輯功能
- 材質(zhì)模塊:實(shí)現(xiàn)模型材質(zhì)顏色、透明度、網(wǎng)格、材質(zhì)顯示/隱藏、材質(zhì)貼圖、模型材質(zhì)類型切換等編輯功能
- 后期處理模塊:實(shí)現(xiàn)模型材質(zhì)的輝光效果強(qiáng)度、半徑、閾值、色調(diào)曝光度、模型的拖拽和分解等編輯功能
- 燈光模塊:實(shí)現(xiàn)環(huán)境光、點(diǎn)光源、半球光、聚光燈等參數(shù)的編輯功能
- 動(dòng)畫(huà)模塊:實(shí)現(xiàn)模型自帶動(dòng)畫(huà)的播放、播放速度、播放類型、動(dòng)作幅度和模型x,y,z軸動(dòng)畫(huà)等編輯功能
- 輔助線/軸配置模塊:實(shí)現(xiàn)模型的軸坐標(biāo)、軸位置、網(wǎng)格輔助線、模型骨架、模型坐標(biāo)軸輔助線等編輯功能
- 幾何體模型配置模塊:實(shí)現(xiàn)對(duì)Three.js中的幾何體API函數(shù)的參數(shù)編輯功能
- 模型加載模塊:實(shí)現(xiàn)模型的點(diǎn)擊切換功能、外部模型加載的功能、幾何體模型拖拽加載功能、支持多類型(.glb,.obj,.gltf,.fbx)格式的模型文件加載,模型加載進(jìn)度條功能
- 導(dǎo)出模塊:實(shí)現(xiàn)模型場(chǎng)景封面下載、模型文件導(dǎo)出功能
- 數(shù)據(jù)保存模塊:實(shí)現(xiàn)模塊編輯數(shù)據(jù)的預(yù)覽、模型編輯數(shù)據(jù)的保存
- 模型庫(kù)模塊:支持多個(gè)編輯模型數(shù)據(jù)的拖拽展示和保存
主要功能模塊實(shí)現(xiàn)的代碼
1.這里首先將three.js相關(guān)的API操作封裝在一個(gè)renderModel.js的class類函數(shù)中去方便在vue頁(yè)面中調(diào)用
2.將不同模塊的功能都寫(xiě)入函數(shù)方法中去,將需要編輯操作的一些three.js的API屬性定義在constructor中去然后在通過(guò)this去修改
import * as THREE from 'three' //導(dǎo)入整個(gè) three.js核心庫(kù)
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' //導(dǎo)入控制器模塊,軌道控制器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' //導(dǎo)入GLTF模塊,模型解析器,根據(jù)文件格式來(lái)定
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'
// 定義一個(gè) class類
class renderModel {
constructor(selector) {
this.container = document.querySelector(selector)
// 相機(jī)
this.camera
// 場(chǎng)景
this.scene
//渲染器
this.renderer
// 控制器
this.controls
// 模型
this.model
// 幾何體模型數(shù)組
this.geometryGroup = new THREE.Group()
// 幾何體模型
this.geometryModel
// 加載進(jìn)度監(jiān)聽(tīng)
this.loadingManager = new THREE.LoadingManager()
//文件加載器類型
this.fileLoaderMap = {
'glb': new GLTFLoader(),
'fbx': new FBXLoader(this.loadingManager),
'gltf': new GLTFLoader(),
'obj': new OBJLoader(this.loadingManager),
}
//模型動(dòng)畫(huà)列表
this.modelAnimation
//模型動(dòng)畫(huà)對(duì)象
this.animationMixer
this.animationColock = new THREE.Clock()
//動(dòng)畫(huà)幀
this.animationFrame = null
// 軸動(dòng)畫(huà)幀
this.rotationAnimationFrame = null
// 動(dòng)畫(huà)構(gòu)造器
this.animateClipAction = null
// 動(dòng)畫(huà)循環(huán)方式枚舉
this.loopMap = {
LoopOnce: THREE.LoopOnce,
LoopRepeat: THREE.LoopRepeat,
LoopPingPong: THREE.LoopPingPong
}
//模型材質(zhì)列表
this.modelMaterialList
// 效果合成器
this.effectComposer
this.outlinePass
// 動(dòng)畫(huà)渲染器
this.renderAnimation = null
// 碰撞檢測(cè)
this.raycaster = new THREE.Raycaster()
// 鼠標(biāo)位置
this.mouse = new THREE.Vector2()
// 模型自帶貼圖
this.modelTextureMap
// 輝光效果合成器
this.glowComposer
// 輝光渲染器
this.unrealBloomPass
// 需要輝光的材質(zhì)
this.glowMaterialList
this.materials = {}
// 拖拽對(duì)象控制器
this.dragControls
// 是否開(kāi)啟輝光
this.glowUnrealBloomPass = false
// 窗口變化監(jiān)聽(tīng)事件
this.onWindowResizesListener
// 模型上傳進(jìn)度條回調(diào)函數(shù)
this.modelProgressCallback = (e) => e
}
init() {
return new Promise(async (reslove, reject) => {
//初始化渲染器
this.initRender()
//初始化相機(jī)
this.initCamera()
//初始化場(chǎng)景
this.initScene()
//初始化控制器,控制攝像頭,控制器一定要在渲染器后
this.initControls()
this.addEvenListMouseLisatener()
// 添加物體模型 TODO:初始化時(shí)需要默認(rèn)一個(gè)
const load = await this.setModel({ filePath: 'threeFile/glb/glb-9.glb', fileType: 'glb', decomposeName: 'transformers_3' })
// 創(chuàng)建效果合成器
this.createEffectComposer()
//場(chǎng)景渲染
this.sceneAnimation()
reslove(load)
})
}
// 創(chuàng)建場(chǎ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)建相機(jī)
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)
}
// 更新場(chǎng)景
sceneAnimation() {
this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation())
// 將不需要處理輝光的材質(zhì)進(jìn)行存儲(chǔ)備份
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í)行完之后在恢復(fù)材質(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)聽(tīng)事件
addEvenListMouseLisatener() {
//監(jiān)聽(tīng)場(chǎng)景大小改變,跳轉(zhuǎn)渲染尺寸
this.onWindowResizesListener = this.onWindowResizes.bind(this)
window.addEventListener("resize", this.onWindowResizesListener)
// 鼠標(biāo)點(diǎn)擊
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('文件錯(cuò)誤')
console.log(err)
reject()
})
})
}
// 加載幾何體模型
setGeometryModel(model) {
return new Promise((reslove, reject) => {
const { clientHeight, clientWidth, offsetLeft, offsetTop } = this.container
// 計(jì)算鼠標(biāo)在屏幕上的坐標(biāo)
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) {
// 在控制臺(tái)輸出鼠標(biāo)在場(chǎng)景中的位置
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']
// 隨機(jī)顏色
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)
})
}
// 模型加載進(jìn)度條回調(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') // 可見(jiàn)邊緣的顏色
this.outlinePass.hiddenEdgeColor = new THREE.Color('#8a90f3') // 不可見(jiàn)邊緣的顏色
this.outlinePass.edgeGlow = 2.0 // 發(fā)光強(qiáng)度
this.outlinePass.edgeThickness = 1 // 邊緣濃度
this.outlinePass.edgeStrength = 4 // 邊緣的強(qiáng)度,值越高邊框范圍越大
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)聽(tīng)窗口變化
onWindowResizes() {
if (!this.container) return false
const { clientHeight, clientWidth } = this.container
//調(diào)整屏幕大小
this.camera.aspect = clientWidth / clientHeight //攝像機(jī)寬高比例
this.camera.updateProjectionMatrix() //相機(jī)更新矩陣,將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頁(yè)面中去使用
<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);
// 初始化場(chǎng)景方法
onMounted(async () => {
loading.value = true;
const modelApi = new renderModel("#model");
//將當(dāng)前場(chǎng)景函數(shù)存儲(chǔ)在vuex中
store.commit("SET_MODEL_API", modelApi);
// 模型加載進(jìn)度條
state.modelApi.onProgress((progressNum) => {
progress.value = Number((progressNum / 1024 / 1024).toFixed(2));
// console.log('模型已加載' + progress.value + 'M')
});
const load = await modelApi.init();
// load=true 表示模型加載完成(主要針對(duì)大模型文件)
if (load) {
loading.value = false;
progress.value = 0;
}
});3.ok這樣一個(gè)模型編輯器的初始化場(chǎng)景功能就完成了
如何將編輯的模型數(shù)據(jù)進(jìn)行存儲(chǔ)和回顯???
- 模型數(shù)據(jù)的存儲(chǔ)和回顯應(yīng)該是這個(gè)編輯器最核心的東西了吧,我想你也不希望自己編輯操作了半天的模型數(shù)據(jù)被瀏覽器的F5一鍵重置了吧。
- 這里我的思路是將模型的背景、燈光、材質(zhì)、動(dòng)畫(huà)、輔助線、位置等屬性值存儲(chǔ)在localStorage ,在頁(yè)面刷新或者進(jìn)入頁(yè)面時(shí)候獲取到這些保存的數(shù)據(jù)值,然后將這些值進(jìn)行數(shù)據(jù)回填。這種思路同樣也適用于將數(shù)據(jù)存儲(chǔ)在服務(wù)端然后在通過(guò)調(diào)用接口獲取。
- 新建一個(gè)initThreeTemplate.js 文件 用于專門(mén)處理模型數(shù)據(jù)回填 (renderModel) 方法 和創(chuàng)建模型渲染 (createThreeDComponent) 方法。
- renderModel 方法內(nèi)容和上面的基本一致,只是在傳遞和接收參數(shù)時(shí)新增一個(gè)模型數(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)建相機(jī)位置
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)
})
}
// 處理模型動(dòng)畫(huà)數(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
// 模型動(dòng)畫(huà)
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()
}
// 軸動(dòng)畫(huà)
if (animation.rotationVisible) {
const { rotationType, rotationSpeed } = animation
this.rotationAnimationFun(rotationType, rotationSpeed)
}
}
// 模型動(dòng)畫(huà)幀
animationFrameFun() {
this.animationFrame = requestAnimationFrame(() => this.animationFrameFun())
if (this.animationMixer) {
this.animationMixer.update(this.animationColock.getDelta())
}
}
// 軸動(dòng)畫(huà)幀
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)
// 坐標(biāo)軸輔助線
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)
// 開(kāi)啟陰影
this.renderer.shadowMap.enabled = true;
// 骨骼輔助線
this.skeletonHelper = new THREE.SkeletonHelper(this.model)
this.skeletonHelper = skeletonHelper
}
} 6 在頁(yè)面中調(diào)用方法,獲取到 localStorage 然后傳入 createThreeDComponent 方法中去這樣一個(gè)模型渲染和數(shù)據(jù)回填的功能就實(shí)現(xiàn)了。沒(méi)錯(cuò)就是這么簡(jiǎ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ù)回顯效果

如何實(shí)現(xiàn)多模型的數(shù)據(jù)回顯展示
1 這里通過(guò)列表循環(huán)渲染和 vue3-draggable-resizable 插件實(shí)現(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([]);
// 當(dāng)前選中的內(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("");
// 開(kāi)始拖拽
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é)語(yǔ)
1.好了這樣一個(gè)基于Three.js開(kāi)發(fā)的3d模型可視化編輯系統(tǒng)就開(kāi)發(fā)完成了
2.完整的項(xiàng)目代碼可訪問(wèn):
gitee:https://gitee.com/ZHANG_6666/Three.js3D
github:https://github.com/zhangbo126/Three3d-view
以上就是基于Vue3+Three.js實(shí)現(xiàn)一個(gè)3D模型可視化編輯系統(tǒng)的詳細(xì)內(nèi)容,更多關(guān)于Vue3+Three.js實(shí)現(xiàn)3D模型可視化系統(tǒng)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
element plus tree拖動(dòng)節(jié)點(diǎn)交換位置和改變層級(jí)問(wèn)題(解決方案)
圖層list里有各種組件,用element plus的tree來(lái)渲染,可以把圖片等組件到面板里,面板是容器,非容器組件,比如圖片、文本等,就不能讓其他組件拖進(jìn)來(lái),這篇文章主要介紹了element plus tree拖動(dòng)節(jié)點(diǎn)交換位置和改變層級(jí)問(wèn)題(解決方案),需要的朋友可以參考下2024-04-04
Vue3中對(duì)數(shù)組去重的方法總結(jié)
隨著開(kāi)發(fā)語(yǔ)言及人工智能工具的普及,使得越來(lái)越多的人會(huì)主動(dòng)學(xué)習(xí)使用一些開(kāi)發(fā)工具,本文主要介紹了Vue數(shù)組去重的幾種方法,結(jié)合 Vue 3 的響應(yīng)式特性實(shí)現(xiàn)數(shù)據(jù)更新,需要的朋友可以參考下2025-09-09
Vue.js組件使用開(kāi)發(fā)實(shí)例教程
Vue.js的組件可以理解為預(yù)先定義好了行為的ViewModel類。這篇文章主要介紹了Vue.js組件使用開(kāi)發(fā)實(shí)例教程的相關(guān)資料,需要的朋友可以參考下2016-11-11
vue自定義開(kāi)發(fā)滑動(dòng)圖片驗(yàn)證組件
這篇文章主要為大家詳細(xì)介紹了vue自定義開(kāi)發(fā)滑動(dòng)圖片驗(yàn)證組件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
如何利用SpringBoot與Vue3構(gòu)建前后端分離項(xiàng)目
在當(dāng)前的互聯(lián)網(wǎng)時(shí)代,前后端分離架構(gòu)已經(jīng)成為構(gòu)建應(yīng)用系統(tǒng)的主流方式,本文將詳細(xì)介紹如何利用 SpringBoot 與 Vue3 構(gòu)建一個(gè)前后端分離的項(xiàng)目,感興趣的小伙伴可以了解下2025-04-04
vue動(dòng)態(tài)添加背景圖簡(jiǎn)單示例
這篇文章主要給大家介紹了關(guān)于vue動(dòng)態(tài)添加背景圖的相關(guān)資料,在一些場(chǎng)景下我們需要使用戶可以進(jìn)行自定義背景圖片,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-07-07
Vue3+NodeJS+Soket.io實(shí)現(xiàn)實(shí)時(shí)聊天的示例代碼
本文主要介紹了Vue3+NodeJS+Soket.io實(shí)現(xiàn)實(shí)時(shí)聊天的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
el-menu遞歸實(shí)現(xiàn)多級(jí)菜單組件的示例
本文主要介紹了el-menu使用遞歸組件實(shí)現(xiàn)多級(jí)菜單組件,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04

