前言
相信大家在使用three.js的时候,一定会用其加载外部的三维模型吧,将3d建模师做好的模型直接展示在自己的前端页面中去,并且还会对模型的灯光,材质,贴图,位置,旋转,缩放等属性进行二次处理。
three.js官网也提供了很多的代码deom示例,但是这样会产生一个问题就是一套代码,只适配一个模型的最佳效果。 但在实际的项目开发中,我们需要做到的是一套代码适配多个模型的最佳效果,那么模型编辑数据的格式保存和数据显示回填将是今天最主要和大家分享的。
同时在很多项目需求中我们可能只需要三维模型在某个页面进行一个效果的展示,在这样的需求背景下如果我们再去开发一套模型编辑的的代码,那么也很有可能造成很大的成本损失,因此今天也给大家分享一下如果将一个已经编辑好的三维模型嵌入到多个不同的项目代码中去。
首先实现对模型数据的编辑
这里可以参数我前面的文章:Three.js实现一个3D模型可视化编辑系统
模型编辑的在线网站:zhang_6666.gitee.io/three.js3d
模型数据的回填
- 当我们加载好一个模型时它是这样的
- 但这时候我希望改变模型的一些内容,使模型的展示效果更加炫酷一点点,比如:更换背景,修改贴图,添加光效等
-
当你觉得你修改和配置的一些效果都Ok时,你肯定希望这些效果都能够保存下来吧,并且在下次加载这个模型时也是这样的效果吧。
-
关于数据格式的如何保存? 这里我思路是将每个场景的数据配置模块化,如:背景,材质,灯光,动画,辅助线,后期,文件信息等,将每个模块用一个对象的字段保存
每个字段对象下的数据就是当前模型操作后的数据
- ok既然已经确认了模型数据的保存格式,这里我们就只需要将数据保存下来,然后在模型加载的时候获取到数据,最后在根据获取到的数据对模型进行数据回显就完成了(听见起来是不是很简单)。
- 考虑一个页面可能展示多个模型 效果展示的场景,这里我将模型创建 和数据回显封装成两个方法。
模型创建和数据回显代码
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构造函数来实现的,主要分为六个模块功能:
- 模型材质数据回填功能:setModelMeaterial
- 背景数据回填功能:setSceneBackground
- 辉光和模型操作数据回填功能:setModelLaterStage
- 灯光数据回填功能:setSceneLight
- 模型动画数据回填功能:setModelAnimation
- 模型轴辅助线配置回填功能: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>
- 展示 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/...