Three.js:打造独一无二的3D模型可视化编辑神器!

news2024/11/27 16:43:38

前言

1.因为之前工作过的可视化大屏项目开发3d大屏组件模块需要用到Three.js来完成,其主功能是实现对3d模型的材质,灯光,背景,动画。等属性进行可视化的编辑操作以及模型编辑数据的存储和模型在大屏上面的拖拽显示

2.因为是第一次使用Three.js开发实际的项目,在开发这些功能的过程中也遇到了许多Three.js的坑(好在最终都解决了)

3.同时在开发这个项目模块的过程中也发现github能够搜索到的Three.js3d模型可视化编辑相关的开源项目非常的少,许多的three.js相关问题和功能的实现在网上也很难搜索到答案

4.因此在参考了之前工作项目中做过的可视化大屏项目的3D模型编辑模块的功能和three.js 官方编辑器 https://threejs.org/editor/ 的部分功能的基础之上开发了一款基于Three.js+Vue3的3d模块可视化编辑器系统,其主要目的是尽可能更多的将three.js提供的API结合在实际的项目中去使用,作为自己个人学习three.js的记录,也供大家学习和参考

项目的在线访问地址:https://zhang_6666.gitee.io/three.js3d/

系统界面图:在这里插入图片描述

实现的主要功能模块

  1. 背景模块:实现背景图、全景图、背景颜色的编辑功能
  2. 材质模块:实现模型材质颜色、透明度、网格、材质显示/隐藏、材质贴图、模型材质类型切换等编辑功能
  3. 后期处理模块:实现模型材质的辉光效果强度、半径、阈值、色调曝光度、模型的拖拽和分解等编辑功能
  4. 灯光模块:实现环境光、点光源、半球光、聚光灯等参数的编辑功能
  5. 动画模块:实现模型自带动画的播放、播放速度、播放类型、动作幅度和模型x,y,z轴动画等编辑功能
  6. 辅助线/轴配置模块:实现模型的轴坐标、轴位置、网格辅助线、模型骨架、模型坐标轴辅助线等编辑功能
  7. 几何体模型配置模块:实现对Three.js中的几何体API函数的参数编辑功能
  8. 模型加载模块:实现模型的点击切换功能、外部模型加载的功能、几何体模型拖拽加载功能、支持多类型(.glb,.obj,.gltf,.fbx)格式的模型文件加载,模型加载进度条功能
  9. 导出模块:实现模型场景封面下载、模型文件导出功能
  10. 数据保存模块:实现模块编辑数据的预览、模型编辑数据的保存
  11. 模型库模块:支持多个编辑模型数据的拖拽展示和保存

主要功能模块实现的代码

1.这里首先将three.js相关的API操作封装在一个renderModel.jsclass类函数中去方便在vue页面中调用

2.将不同模块的功能都写入函数方法中去,将需要编辑操作的一些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模块,模型解析器,根据文件格式来定
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
		// 几何体模型数组
		this.geometryGroup = new THREE.Group()
		// 几何体模型
		this.geometryModel
		// 加载进度监听
		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
		// 动画构造器
		this.animateClipAction = null
		// 动画循环方式枚举
		this.loopMap = {
			LoopOnce: THREE.LoopOnce,
			LoopRepeat: THREE.LoopRepeat,
			LoopPingPong: THREE.LoopPingPong
		}

		//模型材质列表
		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
		// 需要辉光的材质
		this.glowMaterialList
		this.materials = {}
		// 拖拽对象控制器
		this.dragControls
		// 是否开启辉光
		this.glowUnrealBloomPass = false
		// 窗口变化监听事件
		this.onWindowResizesListener
		// 模型上传进度条回调函数
		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' })
			// 创建效果合成器
			this.createEffectComposer()
			//场景渲染
			this.sceneAnimation()
			reslove(load)
		})
	}
	// 创建场景
	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
	}
	// 创建相机
	initCamera() {
		const { clientHeight, clientWidth } = this.container
		this.camera = new THREE.PerspectiveCamera(50, clientWidth / clientHeight, 0.25, 2000)
	}
	// 创建渲染器
	initRender() {
		this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true }) //设置抗锯齿
		//设置屏幕像素比
		this.renderer.setPixelRatio(window.devicePixelRatio)
		//渲染的尺寸大小
		const { clientHeight, clientWidth } = this.container
		this.renderer.setSize(clientWidth, clientHeight)
		//色调映射
		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())
		// 将不需要处理辉光的材质进行存储备份
		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()
		// 在辉光渲染器执行完之后在恢复材质原效果
		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()
	}
	// 监听事件
	addEvenListMouseLisatener() {
		//监听场景大小改变,跳转渲染尺寸
		this.onWindowResizesListener = this.onWindowResizes.bind(this)
		window.addEventListener("resize", this.onWindowResizesListener)
		// 鼠标点击
		this.onMouseClickListener = this.onMouseClickModel.bind(this)
		this.container.addEventListener('click', this.onMouseClickListener)
	}
	// 创建控制器
	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()
				//	设置模型大小
				if (scale) {
					this.model.scale.set(scale, scale, scale);
				}
				//设置模型位置 
				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)
				// 需要辉光的材质
				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])
				// 创建几何体
				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)

		})

	}
	// 模型加载进度条回调函数
	onProgress(callback) {
		if (typeof callback == 'function') {
			this.modelProgressCallback = callback
		}
	}

	// 创建效果合成器
	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 // 发光强度
		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)

		//创建辉光效果
		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') {
					// 重置"灯光"模块数据
					this.onResettingLight({ ambientLight: false })
					this.modelAnimation = []
					this.camera.fov = 80
					this.camera.updateProjectionMatrix()
					const load = await this.setGeometryModel(model)
					reslove()
				} else {
					// 重置"灯光"模块数据
					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()
			}
		})
	}

	// 监听窗口变化
	onWindowResizes() {
		if (!this.container) return false
		const { clientHeight, clientWidth } = this.container
		//调整屏幕大小
		this.camera.aspect = clientWidth / clientHeight //摄像机宽高比例
		this.camera.updateProjectionMatrix() //相机更新矩阵,将3d内容投射到2d面上转换
		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");
  //将当前场景函数存储在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这样一个模型编辑器的初始化场景功能就完成了

如何将编辑的模型数据进行存储和回显???

  1. 模型数据的存储和回显应该是这个编辑器最核心的东西了吧,我想你也不希望自己编辑操作了半天的模型数据被浏览器的F5一键重置了吧。
  2. 这里我的思路是将模型的背景、灯光、材质、动画、辅助线、位置等属性值存储在localStorage ,在页面刷新或者进入页面时候获取到这些保存的数据值,然后将这些值进行数据回填。这种思路同样也适用于将数据存储在服务端然后在通过调用接口获取。
  3. 新建一个initThreeTemplate.js 文件 用于专门处理模型数据回填 (renderModel) 方法 和创建模型渲染
    (createThreeDComponent) 方法。
  4. renderModel 方法内容和上面的基本一致,只是在传递和接收参数时新增一个模型数据的参数 config,这里只列举部分不同处的代码作为解释
/**
 * @describe three.js 组件数据初始化方法
 * @param config 组件参数配置信息
 * @param elementId 元素ID
 
*/
class renderModel {
    constructor(config, elementId) {
         this.config = config  
     }
  // 获取到创建相机位置
	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()
	}
    // 设置辉光和模型操作数据回填
	setModelLaterStage() {
		const { stage } = this.config
		if (!stage) return false
		const { threshold, strength, radius, toneMappingExposure, meshPositonList } = stage
		// 设置辉光效果
		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
		}
		// 模型材质位置
		meshPositonList.forEach(v => {
			const mesh = this.model.getObjectByProperty('name', v.name)
			const { x, y, z } = v
			mesh.position.set(x, y, z)
		})
	}

	// 处理模型动画数据回填
	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
		//网格辅助线
		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);
		// 设置模型位置
		this.model.position.set(positionX, positionY, positionZ)
		// 设置模型轴位置
		this.model.rotation.set(rotationX, rotationY, rotationZ)
		// 开启阴影
		this.renderer.shadowMap.enabled = true;
		// 骨骼辅助线
		this.skeletonHelper = new THREE.SkeletonHelper(this.model)
		this.skeletonHelper = skeletonHelper
	}
}  

5 createThreeDComponent方法用于动态创建3d模型组件的方法,这种方法的优势就是在于将three.js的逻辑抽离出来进行单独处理提高了代码的可读性和可复用性,这里的内容渲染使用了vue jsx语法render函数

/**
 * @describe 动态创建3d模型组件的方法
 * @param config 组件参数配置信息
*/
import { lightPosition, onlyKey, debounce } from '@/utils/utilityFunction'
import { defineComponent, h } from 'vue'
function createThreeDComponent(config) {
	// 创建一个元素ID 
	const elementId = 'answer' + onlyKey(5, 10)
	let modelApi = null
	return defineComponent({
		data() {
			return {
				loading: false,
			}
		},
		props: ['width', 'height'],
		watch: {
			$props: {
				handler(val) {
					if (modelApi) {
					    debounce(modelApi.onWindowResize(), 200)
					}
				},
				immediate: false,
				deep: true
			}
		},
		render() {
			if (this.width && this.height) {
				return h(<div v-zLoading={this.loading} style={{ width: this.width - 10 + 'px', height: this.height - 10 + 'px', pointerEvents: 'none', }} id={elementId} ></div>)

			} else {
				return h(<div v-zLoading={this.loading} style={{ width: '100%', height: '100%' }} id={elementId} ></div>)
			}
		},
		async mounted() {
			this.loading = true
			modelApi = new renderModel(config, elementId);
			const load = await modelApi.init()
			if (load) {
				this.loading = false
			}
		},
		beforeUnmount() {
			modelApi.onClearModelData()
		}
	})
}

6 在页面中调用方法,获取到 localStorage 然后传入 createThreeDComponent 方法中去这样一个模型渲染和数据回填的功能就实现了。没错就是这么简单

<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 的模型编辑数据
const config = local.get(MODEL_PRIVEW_CONFIG);
const treeComponent = createThreeDComponent(config);

</script>
<style lang="less" scoped>
#preview {
  width: 100%;
  height: 100vh;
}
</style>

模型编辑的数据 MODEL_PRIVEW_CONFI 的结构
在这里插入图片描述

数据回显效果
在这里插入图片描述

如何实现多模型的数据回显展示

1 这里通过列表循环渲染和 vue3-draggable-resizable 插件实现 可拖拽的多模型展示功能

<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([]);
 // 当前选中的内容
 const dragActive = ref(null);

 const onDrop = (event) => {
  event.preventDefault();
  // 设置模型拖放位置
  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";
};
// 拖拽结束
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>

数据回显效果
在这里插入图片描述

结语

好了这样一个基于Three.js开发的3d模型可视化编辑系统就开发完成了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1055942.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

不能显式拦截ajax请求的302响应?

记录工作中早该加深印象的一个小小小case&#xff1a;ajax请求不能显式拦截 302响应。 我们先来看一个常规的登录case: 1. 浏览器请求资源&#xff0c;服务器发现该请求未携带相关凭据&#xff08;cookie或者token&#xff09;2. 服务器响应302&#xff0c;并在响应头Location写…

2021-06-15 51单片机c语言秒表的仿真ISIS7 professional

缘由51单片机c语言秒表的仿真ISIS7 professional_嵌入式-CSDN问答 #include "REG52.h" sbit K1 P1^5; sbit K2 P1^6; sbit K3 P1^7; sbit K4 P1^4; sbit LED1P1^0; sbit LED2P1^1; sbit LED3P1^2; sbit LED4P1^3; bit k0; unsigned char code SmZiFu[]{63,6,91,…

IDT 一款自动化挖掘未授权访问漏洞的信息收集工具

IDT v1.0 IDT 意为 Interface detection&#xff08;接口探测) 项目地址: https://github.com/cikeroot/IDT/该工具主要的功能是对批量url或者接口进行存活探测&#xff0c;支持浏览器自动打开指定的url&#xff0c;避免手动重复打开网址。只需输入存在批量的url文件即可。 …

EM聚类(上):数据分析 | 数据挖掘 | 十大算法之一

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ &#x1f434;作者&#xff1a;秋无之地 &#x1f434;简介&#xff1a;CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作&#xff0c;主要擅长领域有&#xff1a;爬虫、后端、大数据…

Polygon Miden:扩展以太坊功能集的ZK-optimized rollup

1. 引言 Polygon Miden定位为zkVM&#xff0c;定于2023年Q4上公开测试网。 zk、zkVM、zkEVM及其未来中指出&#xff0c;当前主要有3种类型的zkVM&#xff0c;括号内为其相应的指令集&#xff1a; mainstream&#xff08;WASM, RISC-V&#xff09;EVM&#xff08;EVM bytecod…

1.3 互联网的组成

思维导图&#xff1a; 前言&#xff1a; 我的笔记&#xff1a; #### 一、总览 - **互联网的结构**&#xff1a; - 具有全球覆盖和复杂的拓扑结构。 - 即便结构复杂&#xff0c;还是可以从工作方式上简化为两大部分&#xff1a;边缘部分和核心部分。 #### 二、边缘部分 -…

免费、丰富、便捷的资源论坛——Yiove论坛,包括但不限于阿里云盘、夸克云盘、迅雷云盘等等

引言 目前资源的数量达到了60000&#xff0c;六万多的资源意味着在这里几乎可以找到任何你想要的资源。 当然&#xff0c;资源并不是论坛的全部&#xff0c;其中还包括了技术交流、福利分享、最新资讯等等。 传送门&#xff1a;YiOVE论坛 - 一个有资源有交流&#xff0c;有一…

正则表达式模块re

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 正则表达式模块re [太阳]选择题 下列程序输出的结果是&#xff1f; import re print("【执行】re.match(www, www.china.com)") print(re.match(www, www.china.com)) print(&quo…

环形链表[简单]

优质博文&#xff1a;IT-BLOG-CN 一、题目 给你一个链表的头节点head&#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪next指针再次到达&#xff0c;则链表中存在环。为了表示给定链表中的环&#xff0c;评测系统内部使用整数pos来表示链…

【VIM】VIm初步使用

玩转Vim-从放弃到入门_哔哩哔哩_bilibili

csgo搬砖怎么样?steam搬砖赚钱吗?需要怎么做?

CSGO搬砖目前怎么样&#xff1f; Steam-csgo游戏搬砖已经发展了好几年。经过几年的沉淀&#xff0c;任何行业都可以继续这样做&#xff0c;这必须适合市场发展。这就是所谓的适者生存&#xff01;没有不赚钱的行业&#xff0c;只有不赚钱的头脑。有很多人通过游戏移动砖块来致富…

【前段基础入门之】=> 吃透CSS元素盒模型

导语 在正式了解盒模型之前&#xff0c;我们应该&#xff0c;先了解&#xff0c;在CSS 中元素的几种不同显示模式。方能让我们后续更加透彻清晰的了解盒模型。 文章目录 元素的显示模式元素的显示模式划分修改元素的默认显示模式 盒模型的组成部分盒子内容区&#xff08;conten…

【STL】list常见用法及模拟实现(附完整源码)

目录 前言1. list介绍及使用1.1 list介绍1.2 list使用 2. list模拟实现2.1 迭代器功能分类2.2 list迭代器模拟实现2.2.1 普通迭代器2.2.2 const迭代器 3. list和vector区别4. 源码 前言 这篇文章我们继续STL中容器的学习&#xff0c;这篇文章要讲解的是list。 1. list介绍及使用…

拒绝水文!八大排序(二)【适合初学者】冒泡排序和选择排序

文章目录 冒泡排序选择排序 大家好&#xff0c;我是纪宁。 这篇文章介绍八大排序中思路最简单&#xff0c;但效率也是最低的两种排序算法&#xff01; 冒泡排序 冒泡排序&#xff0c;可以说是每个人在接触编程时最先学会的一种排序。 冒泡排序基本思想 冒泡排序&#xff08;…

前言技术 VScode + 其他插件-2

一、扩展插件 1.1 chinese 作用&#xff1a;使得软件变成中文显示 1.2 prettier 作用&#xff1a;格式化代码 需要多重配置&#xff1b;看视频 第一步&#xff1a;安装 第二步&#xff1a;软件设置 第三步&#xff1a;查询 save , 修改保存时自动对齐格式&#xff0c;打✔…

英国B2C数字钱包提供商【Hyperjar】完成2400万美元A轮融资

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;总部位于英国伦敦的B2C数字钱包提供商Hyperjar今日宣布已完成2400万美元A轮融资。 本轮轮融资由Susquehanna Private Equity Investments领投&#xff0c;安大略省医疗保险计划&#xff08;Health…

全网最全Python系列教程(非常详细)---Python注释讲解(学Python入门必收藏)

&#x1f9e1;&#x1f9e1;&#x1f9e1;这篇是关于Python中注释的讲解&#xff0c;涉及到以下内容&#xff0c;欢迎点赞和收藏&#xff0c;你点赞和收藏是我更新的动力&#x1f9e1;&#x1f9e1;&#x1f9e1; 1、什么是注释&#xff1f; 2、注释的特性&#xff1f; 3、注释…

lv6 嵌入式开发-Flappy bird项目

目录 1 项目功能总结 2 知识储备&#xff1a; 3 项目框图 4 Ncurses库介绍 做Flappy bird项目有什么用&#xff1f; 1. 复习、巩固c语言知识 2. 培养做项目的逻辑思维能力 3. 具备开发简单小游戏的能力 学会了Flappy bird项目&#xff0c;贪吃蛇和推房子两款小游戏也可…

前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— Web APIs(四)

思维导图 一、日期对象 1.1 实例化 实例化&#xff0c;默认得到当前时间&#xff0c;也可以指定时间 1.2 日期对象方法 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible&q…

Vue3.0跨端Web SDK访问微信小程序云储存,文件上传路径不存在/文件受损无法显示问题(已解决)

整理需求&#xff1a; 需要vue3.0作为pc端的后台管理来连接微信小程序客户端需要Web SDK的引入&#xff0c;实现vue3.0接入云开发环境需要以云环境作为线上服务器&#xff0c;将vue3.0上传的本地文件通过云环境进入云储存&#xff0c;并将文件在云端生成云端快捷访问路径及htt…