threejs如何实现已编辑好的3D模型数据回填,并嵌入到多个不同项目代码中去展示

前言

相信大家在使用three.js的时候,一定会用其加载外部的三维模型吧,将3d建模师做好的模型直接展示在自己的前端页面中去,并且还会对模型的灯光,材质,贴图,位置,旋转,缩放等属性进行二次处理。

three.js官网也提供了很多的代码deom示例,但是这样会产生一个问题就是一套代码,只适配一个模型的最佳效果。 但在实际的项目开发中,我们需要做到的是一套代码适配多个模型的最佳效果,那么模型编辑数据的格式保存和数据显示回填将是今天最主要和大家分享的。

同时在很多项目需求中我们可能只需要三维模型在某个页面进行一个效果的展示,在这样的需求背景下如果我们再去开发一套模型编辑的的代码,那么也很有可能造成很大的成本损失,因此今天也给大家分享一下如果将一个已经编辑好的三维模型嵌入到多个不同的项目代码中去。

首先实现对模型数据的编辑

这里可以参数我前面的文章:Three.js实现一个3D模型可视化编辑系统

模型编辑的在线网站:zhang_6666.gitee.io/three.js3d

模型数据的回填

  1. 当我们加载好一个模型时它是这样的
  1. 但这时候我希望改变模型的一些内容,使模型的展示效果更加炫酷一点点,比如:更换背景,修改贴图,添加光效等
  1. 当你觉得你修改和配置的一些效果都Ok时,你肯定希望这些效果都能够保存下来吧,并且在下次加载这个模型时也是这样的效果吧。

  2. 关于数据格式的如何保存? 这里我思路是将每个场景的数据配置模块化,如:背景,材质,灯光,动画,辅助线,后期,文件信息等,将每个模块用一个对象的字段保存

每个字段对象下的数据就是当前模型操作后的数据

  1. ok既然已经确认了模型数据的保存格式,这里我们就只需要将数据保存下来,然后在模型加载的时候获取到数据,最后在根据获取到的数据对模型进行数据回显就完成了(听见起来是不是很简单)。
  2. 考虑一个页面可能展示多个模型 效果展示的场景,这里我将模型创建数据回显封装成两个方法。

模型创建和数据回显代码

1.模型创建方法(createThreeDComponent):这里使用的是vue3的jsx语法来动态创建一个3d模型容器,它接收一个参数 config 也就是模型配置保存的数据

js 复制代码
/**
 * @describe 动态创建3d模型组件的方法
 * @param config 组件参数配置信息
*/

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()
		}
	})
}

2.模型数据回显渲染方法(renderModel):这里使用的是 class构造函数来实现的,主要分为六个模块功能:

  1. 模型材质数据回填功能:setModelMeaterial
  2. 背景数据回填功能:setSceneBackground
  3. 辉光和模型操作数据回填功能:setModelLaterStage
  4. 灯光数据回填功能:setSceneLight
  5. 模型动画数据回填功能:setModelAnimation
  6. 模型轴辅助线配置回填功能:setModelAxleLine

3.renderModel完整的代码如下:

js 复制代码
/**
 * @describe three.js 组件数据初始化方法
 * @param config 组件参数配置信息
*/

class renderModel {
	constructor(config, elementId) {
		this.config = config
		this.container = document.querySelector('#' + elementId)
		// 相机
		this.camera
		// 场景
		this.scene
		//渲染器
		this.renderer
		// 控制器
		this.controls
		// 模型
		this.model
		//文件加载器类型
		this.fileLoaderMap = {
			'glb': new GLTFLoader(),
			'fbx': new FBXLoader(),
			'gltf': new GLTFLoader(),
			'obj': new OBJLoader(),
		}
		//模型动画列表
		this.modelAnimation
		//模型动画对象
		this.animationMixer
		this.animationColock = new THREE.Clock()
		// 动画帧
		this.animationFrame
		// 轴动画帧
		this.rotationAnimationFrame
		// 动画构造器
		this.animateClipAction = null
		// 动画循环方式枚举
		this.loopMap = {
			LoopOnce: THREE.LoopOnce,
			LoopRepeat: THREE.LoopRepeat,
			LoopPingPong: THREE.LoopPingPong
		}
		// 模型骨架
		this.skeletonHelper
		// 网格辅助线
		this.gridHelper
		// 坐标轴辅助线
		this.axesHelper
		//模型平面
		this.planeGeometry
		//模型材质列表
		this.modelMaterialList
		// 效果合成器
		this.effectComposer
		this.outlinePass
		// 动画渲染器
		this.renderAnimation
		// 碰撞检测
		this.raycaster = new THREE.Raycaster()
		// 鼠标位置
		this.mouse = new THREE.Vector2()
		// 模型自带贴图
		this.modelTextureMap
		// 辉光效果合成器
		this.glowComposer
		// 辉光渲染器
		this.unrealBloomPass
		// 辉光着色器
		this.shaderPass
		// 需要辉光的材质
		this.glowMaterialList
		this.materials = {}
		// 窗口变化监听事件
		this.onWindowResizesListener
		// 鼠标移动
		this.onMouseMoveListener
	}
	init() {
		return new Promise(async (reslove, reject) => {
			//初始化渲染器
			this.initRender()
			// //初始化相机
			this.initCamera()
			//初始化场景
			this.initScene()
			//初始化控制器,控制摄像头,控制器一定要在渲染器后
			this.initControls()

			const load = await this.loadModel(this.config.fileInfo)
			// 创建效果合成器
			this.createEffectComposer()
			// 设置背景信息
			this.setSceneBackground()
			// 设置模型材质信息
			this.setModelMeaterial()
			// 设置后期/操作信息
			this.setModelLaterStage()
			// 设置灯光信息
			this.setSceneLight()
			// 设置模型动画信息
			this.setModelAnimation()
			// 设置模型轴/辅助线信息
			this.setModelAxleLine()
			//场景渲染
			this.sceneAnimation()
			this.addEvenListMouseLisatener()
			reslove(load)
		})
	}
	// 创建渲染器
	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 = 2
		this.renderer.shadowMap.enabled = true
		this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
		this.container.appendChild(this.renderer.domElement)
	}
	// 创建相机
	initCamera() {
		const { clientHeight, clientWidth } = this.container
		this.camera = new THREE.PerspectiveCamera(45, clientWidth / clientHeight, 0.25, 2000)
		// 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()
	}
	// 创建场景
	initScene() {
		this.scene = new THREE.Scene()
	}
	addEvenListMouseLisatener() {
		// 监听场景大小改变,跳转渲染尺寸
		this.onWindowResizesListener = this.onWindowResize.bind(this)
		window.addEventListener("resize", this.onWindowResizesListener)
	}
	// 创建控制器
	initControls() {
		this.controls = new OrbitControls(this.camera, this.renderer.domElement)
		this.controls.enablePan = true
		this.controls.enabled = true
	}
	// 更新场景
	sceneAnimation() {
		this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation())
		const { stage } = this.config
		//辉光效果开关开启时执行
		if (stage && stage.glow) {
			// 将不需要处理辉光的材质进行存储备份
			this.setMeshFlow()
		} else {
			this.effectComposer.render()
			this.controls.update()
		}
	}
	// 设置材质辉光
	setMeshFlow() {
		this.scene.traverse((v) => {
			if (v instanceof THREE.GridHelper) {
				this.materials.gridHelper = v.material
				v.material = new THREE.MeshStandardMaterial({ color: '#000' })
			}
			if (v instanceof THREE.Scene) {
				this.materials.scene = v.background
				this.materials.environment = v.environment
				v.background = null
				v.environment = null
			}
			if (!this.glowMaterialList.includes(v.name) && v.isMesh) {
				this.materials[v.uuid] = v.material
				v.material = new THREE.MeshStandardMaterial({ color: '#000' })
			}
		})
		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.GridHelper) {
				v.material = this.materials.gridHelper
				delete this.materials.gridHelper
			}
			if (v instanceof THREE.Scene) {
				v.background = this.materials.scene
				v.environment = this.materials.environment
				delete this.materials.scene
				delete this.materials.environment
			}
		})
		this.effectComposer.render()
		this.controls.update()

	}
	// 创建效果合成器
	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 outputPass = new OutputPass()
		this.effectComposer.addPass(outputPass)

		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), 0, 0, 0)
		// 辉光合成器
		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)
		// 着色器
		this.shaderPass = new ShaderPass(new THREE.ShaderMaterial({
			uniforms: {
				baseTexture: { value: null },
				bloomTexture: { value: this.glowComposer.renderTarget2.texture },
				tDiffuse: { value: null },
				glowColor: { value: null }
			},
			vertexShader,
			fragmentShader,
			defines: {}
		}), 'baseTexture')

		this.shaderPass.material.uniforms.glowColor.value = new THREE.Color();
		this.shaderPass.renderToScreen = true
		this.shaderPass.needsSwap = true
		this.effectComposer.addPass(this.shaderPass)

	}
	// 加载模型
	loadModel({ filePath, fileType, map }) {
		return new Promise((resolve, reject) => {
			const loader = this.fileLoaderMap[fileType]
			loader.load(filePath, (result) => {
				switch (fileType) {
					case 'glb':
						this.model = result.scene
						this.skeletonHelper = new THREE.SkeletonHelper(result.scene)
						break;
					case 'fbx':
						this.model = result
						this.skeletonHelper = new THREE.SkeletonHelper(result)
						break;
					case 'gltf':
						this.model = result.scene
						this.skeletonHelper = new THREE.SkeletonHelper(result.scene)
						break;
					case 'obj':
						this.model = result
						this.skeletonHelper = new THREE.SkeletonHelper(result)
						break;
					default:
						break;
				}
				this.getModelMeaterialList(map)
				this.modelAnimation = result.animations || []
				this.setModelPositionSize()
				this.skeletonHelper.visible = false
				this.scene.add(this.skeletonHelper)
				this.glowMaterialList = this.modelMaterialList.map(v => v.name)
				this.scene.add(this.model)
				resolve(true)
			}, () => {

			}, (err) => {
				ElMessage.error('文件错误')
				console.log(err)
				reject()
			})
		})
	}
	onWindowResize() {
		const { clientHeight, clientWidth } = this.container
		//调整屏幕大小
		this.camera.aspect = clientWidth / clientHeight //摄像机宽高比例
		this.camera.updateProjectionMatrix() //相机更新矩阵,将3d内容投射到2d面上转换
		this.renderer.setSize(clientWidth, clientHeight)
		if (this.effectComposer) this.effectComposer.setSize(clientWidth, clientHeight)
		if (this.glowComposer) this.glowComposer.setSize(clientWidth, clientHeight)
	}
	// 清除模型数据
	onClearModelData() {
		cancelAnimationFrame(this.rotationAnimationFrame)
		cancelAnimationFrame(this.renderAnimation)
		cancelAnimationFrame(this.animationFrame)
		this.scene.traverse((v) => {
			if (v.type === 'Mesh') {
				v.geometry.dispose();
				v.material.dispose();
			}
		})
		this.scene.clear()
		this.renderer.clear()
		this.renderer.dispose()
		this.camera.clear()
		if (this.gridHelper) {
			this.gridHelper.clear()
			this.gridHelper.dispose()
		}
		if (this.axesHelper) {
			this.axesHelper.clear()
			this.axesHelper.dispose()
		}
		this.effectComposer.dispose()
		this.glowComposer.dispose()
		this.container.removeEventListener('mousemove', this.onMouseMoveListener)
		window.removeEventListener("resize", this.onWindowResizesListener)
		this.config = null
		this.container = null
		// 相机
		this.camera = null
		// 场景
		this.scene = null
		//渲染器
		this.renderer = null
		// 控制器
		this.controls = null
		// 模型
		this.model = null
		//文件加载器类型
		this.fileLoaderMap = null
		//模型动画列表
		this.modelAnimation = null
		//模型动画对象
		this.animationMixer = null
		this.animationColock = null
		// 动画帧
		this.animationFrame = null
		// 轴动画帧
		this.rotationAnimationFrame = null
		// 动画构造器
		this.animateClipAction = null
		// 动画循环方式枚举
		this.loopMap = null
		// 模型骨架
		this.skeletonHelper = null
		// 网格辅助线
		this.gridHelper = null
		// 坐标轴辅助线
		this.axesHelper = null
		//模型平面
		this.planeGeometry = null
		//模型材质列表
		this.modelMaterialList = null
		// 效果合成器
		this.effectComposer = null
		this.outlinePass = null
		// 动画渲染器
		this.renderAnimation = null
		// 碰撞检测
		this.raycaster == null
		// 鼠标位置
		this.mouse = null
		// 模型自带贴图
		this.modelTextureMap = null
		// 辉光效果合成器
		this.glowComposer = null
		// 辉光渲染器
		this.unrealBloomPass = null
		// 辉光合成器
		this.shaderPass = null
		// 需要辉光的材质
		this.glowMaterialList = null
		this.materials = null
	}

	// 设置模型定位缩放大小
	setModelPositionSize() {
		//设置模型位置
		this.model.updateMatrixWorld()
		const box = new THREE.Box3().setFromObject(this.model);
		const size = box.getSize(new THREE.Vector3());
		const center = box.getCenter(new THREE.Vector3());
		// 计算缩放比例
		const maxSize = Math.max(size.x, size.y, size.z);
		const targetSize = 2.5; // 目标大小
		const scale = targetSize / (maxSize > 1 ? maxSize : .5);
		this.model.scale.set(scale, scale, scale)
		// 设置模型位置
		this.model.position.sub(center.multiplyScalar(scale))
		// 设置控制器最小缩放值
		this.controls.maxDistance = size.length() * 10
		// 设置相机坐标系
		this.camera.updateProjectionMatrix();

	}
	// 获取当前模型材质
	getModelMeaterialList() {
		this.modelMaterialList = []
		this.model.traverse((v) => {
			if (v.isMesh) {
				v.castShadow = true
				v.frustumCulled = false
				if (v.material) {
					const newMaterial = v.material.clone()
					v.material = newMaterial
					this.modelMaterialList.push(v)
				}
			}
		})
	}

	// 处理背景数据回填
	setSceneBackground() {
		const { background } = this.config
		if (!background) return false
		// 设置背景
		if (background.visible) {
			const { color, image, viewImg, intensity, blurriness } = background
			switch (background.type) {
				case 1:
					this.scene.background = new THREE.Color(color)
					break;
				case 2:
					const bgTexture = new THREE.TextureLoader().load(image);
					this.scene.background = bgTexture
					bgTexture.dispose()
					break;
				case 3:
					const texture = new THREE.TextureLoader().load(viewImg);
					texture.mapping = THREE.EquirectangularReflectionMapping
					this.scene.background = texture
					this.scene.environment = texture
					this.scene.backgroundIntensity = intensity
					this.scene.backgroundBlurriness = blurriness
					texture.dispose()
					break;
				default:
					break;
			}
		} else {
			this.scene.background = new THREE.Color('#000')
		}
	}
	// 处理模型材质数据回填
	setModelMeaterial() {
		const { material } = this.config
		if (!material || !material.meshList) return false
		const mapIdList = mapImageList.map(v => v.id)
		material.meshList.forEach(v => {
			const mesh = this.model.getObjectByProperty('name', v.meshName)
			const { color, opacity, depthWrite, wireframe, visible, type } = v
			const { map } = mesh.material
			if (material.materialType) {
				mesh.material = new THREE[type]({
					map,
				})
			} else {
				mesh.material.map = map
			}
			// 处理修改了贴图的材质
			if (v.meshFrom) {
				// 如果使用的是系统贴图
				if (mapIdList.includes(v.meshFrom)) {
					// 找到当前的系统材质
					const mapInfo = mapImageList.find(m => m.id == v.meshFrom) || {}
					// 加载系统材质贴图
					const mapTexture = new THREE.TextureLoader().load(mapInfo.url)
					mapTexture.wrapS = THREE.MirroredRepeatWrapping;
					mapTexture.wrapT = THREE.MirroredRepeatWrapping;
					mapTexture.flipY = false
					mapTexture.colorSpace = THREE.SRGBColorSpace
					mapTexture.minFilter = THREE.LinearFilter;
					mapTexture.magFilter = THREE.LinearFilter;
					// 如果当前模型的材质类型被修改了,则使用用新的材质type
					if (material.materialType) {
						mesh.material = new THREE[type]({
							map: mapTexture,
						})

					} else {
						mesh.material.map = mapTexture
					}
					mapTexture.dispose()
				} else {
					// 如果是当前模型材质自身贴图
					const meshFrom = this.model.getObjectByProperty('name', v.meshFrom)
					const { map } = meshFrom.material
					// 如果当前模型的材质类型被修改了,则使用用新的材质type
					if (material.materialType) {
						mesh.material = new THREE[type]({
							map,
						})
					} else {
						mesh.material.map = map
					}
				}
			}
			// 设置材质显隐
			mesh.material.visible = visible
			//设置材质颜色
			mesh.material.color.set(new THREE.Color(color))
			//设置网格
			mesh.material.wireframe = wireframe
			// 设置深度写入
			mesh.material.depthWrite = depthWrite
			//设置透明度
			mesh.material.transparent = true
			mesh.material.opacity = opacity
		})

	}
	// 设置辉光和模型操作数据回填
	setModelLaterStage() {
		const { stage } = this.config
		if (!stage) return false
		const { threshold, strength, radius, toneMappingExposure, meshPositonList, color } = stage
		// 设置辉光效果
		if (stage.glow) {
			this.unrealBloomPass.threshold = threshold
			this.unrealBloomPass.strength = strength
			this.unrealBloomPass.radius = radius
			this.renderer.toneMappingExposure = toneMappingExposure
			this.shaderPass.material.uniforms.glowColor.value = new THREE.Color(color)

		} else {
			this.unrealBloomPass.threshold = 0
			this.unrealBloomPass.strength = 0
			this.unrealBloomPass.radius = 0
			this.renderer.toneMappingExposure = toneMappingExposure
			this.shaderPass.material.uniforms.glowColor.value = new THREE.Color()
		}
		// 模型材质位置
		meshPositonList.forEach(v => {
			const mesh = this.model.getObjectByProperty('name', v.name)
			const { rotation, scale, position } = v
			mesh.rotation.set(rotation.x, rotation.y, rotation.z)
			mesh.scale.set(scale.x, scale.y, scale.z)
			mesh.position.set(position.x, position.y, position.z)
		})
	}
	// 处理灯光数据回填
	setSceneLight() {
		const { light } = this.config
		if (!light) return false
		// 环境光
		if (light.ambientLight) {
			// 创建环境光
			const ambientLight = new THREE.AmbientLight(light.ambientLightColor, light.ambientLightIntensity)
			ambientLight.visible = light.ambientLight
			this.scene.add(ambientLight)
		}
		// 平行光
		if (light.directionalLight) {
			const directionalLight = new THREE.DirectionalLight(light.directionalLightColor, light.directionalLightIntensity)
			const { x, y, z } = lightPosition(light.directionalHorizontal, light.directionalVertical, light.directionalSistance)
			directionalLight.position.set(x, y, z)
			directionalLight.castShadow = light.directionaShadow
			directionalLight.visible = light.directionalLight
			this.scene.add(directionalLight)
			const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, .5)
			directionalLightHelper.visible = light.directionalLightHelper
			this.scene.add(directionalLightHelper)
		}
		// 点光源
		if (light.pointLight) {
			const pointLight = new THREE.PointLight(light.pointLightColor, light.pointLightIntensity, 100)
			pointLight.visible = light.pointLight
			const { x, y, z } = lightPosition(light.pointHorizontal, light.pointVertical, light.pointSistance)
			pointLight.position.set(x, y, z)
			this.scene.add(pointLight)
			// 创建点光源辅助线
			const pointLightHelper = new THREE.PointLightHelper(pointLight, .5)
			pointLightHelper.visible = light.pointLightHelper
			this.scene.add(pointLightHelper)
		}
		// 聚光灯
		if (light.spotLight) {
			const spotLight = new THREE.SpotLight(light.spotLightColor, 900);
			spotLight.visible = light.spotLight
			const texture = new THREE.TextureLoader().load(require('@/assets/image/model-bg-1.jpg'));
			texture.dispose()
			spotLight.map = texture
			spotLight.decay = 2;
			spotLight.shadow.mapSize.width = 1920;
			spotLight.shadow.mapSize.height = 1080;
			spotLight.shadow.camera.near = 1;
			spotLight.shadow.camera.far = 10;
			spotLight.intensity = light.spotLightIntensity
			spotLight.angle = light.spotAngle
			spotLight.penumbra = light.spotPenumbra
			spotLight.shadow.focus = light.spotFocus
			spotLight.castShadow = light.spotCastShadow
			spotLight.distance = light.spotDistance
			const { x, y, z } = lightPosition(light.spotHorizontal, light.spotVertical, light.spotSistance)
			spotLight.position.set(x, y, z)
			this.scene.add(spotLight);
			//创建聚光灯辅助线
			const spotLightHelper = new THREE.SpotLightHelper(spotLight);
			spotLightHelper.visible = light.spotLightHelper && light.spotLight
			this.scene.add(spotLightHelper)
		}
		// 模型平面
		if (light.planeGeometry) {
			const geometry = new THREE.PlaneGeometry(light.planeWidth, light.planeHeight);
			var groundMaterial = new THREE.MeshStandardMaterial({ color: light.planeColor });
			const planeGeometry = new THREE.Mesh(geometry, groundMaterial);
			planeGeometry.name = 'planeGeometry'
			planeGeometry.rotation.x = -Math.PI / 2
			planeGeometry.position.set(0, -1.2, 0)
			planeGeometry.visible = light.planeGeometry
			planeGeometry.material.side = THREE.DoubleSide
			planeGeometry.geometry.verticesNeedUpdate = true
			// 让地面接收阴影
			planeGeometry.receiveShadow = true;
			this.scene.add(planeGeometry);
		}
	}
	// 处理模型动画数据回填
	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.visible = skeletonHelper
	}
}

如何在页面中去使用

在vue3项目中通过引入 createThreeDComponent 传入之前保存的config模型数据值(这里我是将数据保存在本地localStorage中的,这样的方法同样也适用于存储在后端),最后将返回结果显示在template模型页面中去

js 复制代码
<template>
  <div id="preview">
    <tree-component />
  </div>
</template>
<script setup lang="jsx" name="modelBase">
import { local } from "@/utils/storage";
import { MODEL_PRIVEW_CONFIG } from "@/config/constant";
import { ref } from 'vue'
import createThreeDComponent from "@/utils/initThreeTemplate";
import { ElMessageBox } from 'element-plus'
const config = ref(null)
config.value = local.get(MODEL_PRIVEW_CONFIG)
const treeComponent = createThreeDComponent(config.value);
</script>
<style lang="scss" scoped>
#preview {
  width: 100%;
  height: 100vh;
}
</style>

新的需求场景:如将编辑好的模型数据展示在别的项目中去?

1.很多时候在一些项目中,其实我们只需要根据需求在项目中去展现一下模型的效果,因此如果只是仅仅展示一下模型的编辑效果就去开发一套模型数据回显的代码,那可能就有点大材小用了。

2.如何实现模型编辑效果在别的项目中零成本的去实现了?

3.这里我首先想到的是使用 iframe 嵌入到需要使用的项目中去,外部项目只需要添加一个 iframe 标签就能展示我们的模型了

代码嵌入功能

1.在原有的编辑器系统中添加一个代码嵌入功能

2. 原理其实很简单,在编辑器项目中新增一个专门展示 iframe 嵌入功能的页面,将模型数据跟在url地址栏之后,然后在页面通过获取 url 的路径参数值来进行数据回显(这种方法也同样适用于将数据存储在后端,然后通过传入在url后面的id来获取)。

3.代码嵌入功能代码:

js 复制代码
<template>
	<el-dialog v-model="visible" title="将当前编辑效果嵌入你的项目" width="600px" :close-on-click-modal="false">
		<el-scrollbar max-height="400px">
			<code class="code">{{ codeIframe }}</code>
		</el-scrollbar>
		<el-row :style="{ marginTop: '10px' }">
			<el-col :span="12">
				<el-text type="primary">容器宽度</el-text>
				<el-input-number class="number-style" :precision="0" v-model="iframeConfig.width" />
			</el-col>
			<el-col :span="12">
				<el-text type="primary">容器高度</el-text>
				<el-input-number class="number-style" :precision="0" v-model="iframeConfig.height" />
			</el-col>
		</el-row>
		<template #footer>
			<span class="dialog-footer">
				<el-button type="primary" class="copy-button" @click="onCopyCode">
					复制代码
				</el-button>
			</span>
		</template>
	</el-dialog>
</template>
<script setup>
import { ElMessage } from "element-plus";
import { defineExpose, ref, reactive, computed } from 'vue'
import Clipboard from 'clipboard';
import { IFRAME_PREVIEW } from '@/config/constant'
const visible = ref(false)
const codeString = ref(null)
const iframeConfig = reactive({
	width: 400,
	height: 300,
})
const codeIframe = computed(() => {
	const codeConfig = codeString.value.replace(/"([^"\\]*(\\.[^"\\]*)*)"/g, "'$1'");
	const src = `${IFRAME_PREVIEW}?` + 'modelConfig=' + codeConfig
	const iframe = `<iframe  width="${iframeConfig.width}" height="${iframeConfig.height}" src="${src}" allowfullscreen></iframe>`;
	return iframe
})

const showDialog = (code) => {
	visible.value = true
	codeString.value = JSON.stringify(code)
}
const onCopyCode = () => {
	const clipboard = new Clipboard('.copy-button', { text: () => codeIframe.value });
	clipboard.on('success', (val) => {
		ElMessage.success('复制成功,将代码粘贴至你的项目中即可')
		clipboard.destroy();
		visible.value=false
	});
	clipboard.on('error', () => {
		console.log('复制失败');
		clipboard.destroy();
	});
}
defineExpose({ showDialog })
</script>
  1. 展示 iframe 页面:因为数据是通过url地址栏传入过来的,在数据转换过程中会出现乱码,这里需要通过 decodeURIComponent 进行转码,然后在通过JSON.parse() 转为对象
js 复制代码
<template>
	<div class="iframe-box"> <tree-component /></div>
</template>
<script setup lang="jsx" name="modelBase">
import { useRouter } from "vue-router";
import { ref } from 'vue'
import createThreeDComponent from "@/utils/initThreeTemplate";
import { ElMessageBox } from 'element-plus'
const router = useRouter();
const config = ref(null)
//获取URL参数
const modelConfig = window.location.href.split('?modelConfig=')[1]
if (modelConfig) {
	const configStr = decodeURIComponent(modelConfig).replace(/'/g, '"')
	config.value = JSON.parse(configStr)
} else {
	ElMessageBox.alert(`当前页面出错,返回首页`, '提示', {
		confirmButtonText: '确认',
		type: 'warning'
	}).then(() => {
		router.push({ path: '/' })
	})
}
const treeComponent = createThreeDComponent(config.value);
</script>
<style lang="scss" scoped>
.iframe-box {
	width: 100%;
	height: 100vh;
}
</style>

5.ok这样一个模型代码嵌入功能就实现了,下面让我们看看如何在项目中去使用。

在别的项目中去使用嵌入功能

这里就简单的做一个演示效果:

1.首先新建一个 1.html文件

2.然后在编辑器中对模型进行一些简单的编辑操作

3.点击复制代码

4.最后将代码粘贴在 1.html 文件中去

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <iframe  width="400" height="300" src="https://zhang_6666.gitee.io/three.js3d/modelIframe?modelConfig={'background':{'visible':true,'type':3,'image':'/three.js3d/static/img/model-bg-3.6c273dac.jpg','viewImg':'/three.js3d/static/img/view-4.46878642.png','color':'#000','blurriness':1,'intensity':1},'material':{'materialType':'','meshList':[{'meshName':'Object_2','meshFrom':222,'color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_3','color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_4','color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_5','color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_6','color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_7','color':'rgb(255,255,255)','opacity':1,'depthWrite':false,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'}]},'animation':{'visible':false,'animationName':null,'loop':'LoopRepeat','timeScale':1,'weight':1,'rotationVisible':true,'rotationType':'y','rotationSpeed':1},'attribute':{'visible':true,'skeletonHelper':false,'gridHelper':true,'x':0,'y':-1.04,'z':-0.1,'positionX':0,'positionY':-1,'positionZ':0,'divisions':18,'size':6,'color':'rgb(193,193,193)','axesHelper':false,'axesSize':1.8,'rotationX':0,'rotationY':54.50000000000292,'rotationZ':0},'light':{'planeGeometry':true,'planeColor':'#000000','planeWidth':7,'planeHeight':7,'ambientLight':true,'ambientLightColor':'#fff','ambientLightIntensity':0.8,'directionalLight':false,'directionalLightHelper':true,'directionalLightColor':'#fff','directionalLightIntensity':5,'directionalHorizontal':-1.26,'directionalVertical':-3.85,'directionalSistance':2.98,'directionaShadow':true,'pointLight':false,'pointLightHelper':true,'pointLightColor':'#1E90FF','pointLightIntensity':10,'pointHorizontal':-4.21,'pointVertical':-4.1,'pointSistance':2.53,'spotLight':true,'spotLightColor':'#898B8B','spotLightIntensity':1406.7,'spotHorizontal':-3.49,'spotVertical':-4.37,'spotSistance':4.09,'spotAngle':0.5,'spotPenumbra':1,'spotFocus':1,'spotCastShadow':true,'spotLightHelper':true,'spotDistance':20},'stage':{'meshPositonList':[{'name':'Object_2','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_3','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_4','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_5','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_6','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_7','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}}],'glow':true,'threshold':0.05,'strength':0.41,'radius':1,'decompose':0,'transformType':'translate','manageFlage':false,'toneMappingExposure':2,'color':'#FFFFFF'},'camera':{'x':0,'y':2,'z':5.999999999999999},'fileInfo':{'name':'人物(男)','key':'characterMan','fileType':'glb','id':8,'animation':false,'filePath':'threeFile/glb/glb-8.glb','icon':'/three.js3d/static/img/3.ebc2b040.png'}}" allowfullscreen></iframe>
</body>
</html>

编辑器中的效果:

1.html文件中的效果

结语

好了这样一个:threejs实现将已编辑好的模型数据回填,并嵌入到多个不同项目代码中去展示的功能就实现了

如果你有更好的开发思路 ,欢迎留言交流~

完整的代码示例可参考:

gitee: gitee.com/ZHANG_6666/...

github: github.com/zhangbo126/...

相关推荐
崔庆才丨静觅2 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅23 分钟前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 小时前
jwt介绍
前端
爱敲代码的小鱼1 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
麦聪聊数据2 小时前
Web 原生架构如何重塑企业级数据库协作流?
数据库·sql·低代码·架构
NEXT062 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法