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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 因为之前工作过的可视化大屏项目开发3d大屏组件模块需要用到Three.js来完成,其主功能是实现对3d模型的材质,灯光,背景,动画。等属性进行可视化的编辑操作以及模型编辑数据的存储和模型在大屏上面的拖拽显示

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

前言

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.png

实现的主要功能模块

  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 的结构
2.png

数据回显效果
3.png

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

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>

数据回显效果
4.png

结语

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

目录
相关文章
|
前端开发 JavaScript Java
Springboot2.x+Websocket+js实现实时在线文本协同编辑,并展示协同用户
Springboot2.x+Websocket+js实现实时在线文本协同编辑,并展示协同用户
Springboot2.x+Websocket+js实现实时在线文本协同编辑,并展示协同用户
|
1月前
|
前端开发 JavaScript 机器人
详解《基于 javascript 的流程图编辑框架LogicFlow
详解《基于 javascript 的流程图编辑框架LogicFlow
75 0
|
2月前
|
JavaScript 前端开发 开发者
JavaScript——周技能检测——菜单编辑——2022年11月22日(考完)
JavaScript——周技能检测——菜单编辑——2022年11月22日(考完)
25 0
|
8月前
|
JavaScript 前端开发
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件10
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件10
30 0
|
8月前
|
JavaScript 前端开发
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件1
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件1
32 0
|
4月前
|
前端开发 JavaScript
前端用原生js编辑文件内容→创建生成文件(格式可以自定义)→下载文件
前端用原生js编辑文件内容→创建生成文件(格式可以自定义)→下载文件
|
5月前
|
JavaScript 前端开发 Java
Eclipse编辑HTML,JSP,JS等时的卡顿问题,非常有效!!!
Eclipse编辑HTML,JSP,JS等时的卡顿问题,非常有效!!!
|
8月前
|
JavaScript 前端开发
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件9
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件9
37 0
|
8月前
|
JavaScript 前端开发
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件8
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件8
34 0
|
8月前
|
JavaScript 前端开发
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件6
前端学习笔记202306学习笔记第五十四天-react.js & material-ui之编辑表单 封装form组件6
31 0

热门文章

最新文章

  • 1
    Serverless 应用引擎产品使用之在函数计算中,数据库访问失败如何解决
    5
  • 2
    Serverless 应用引擎产品使用之在阿里云函数计算中发现没有NAC(Native Application Component)选项,且无法自己上传MOD(模块)如何解决
    5
  • 3
    Serverless 应用引擎操作报错合集之在阿里云函数计算中,调用了FC函数但是没有执行或者报错,并且在FC函数后台也看不到调用记录日志如何解决
    7
  • 4
    Serverless 应用引擎操作报错合集之在阿里函数计算中,sd部署启动报错CAExited 报错信息“operation not permitted”如何解决
    5
  • 5
    Serverless 应用引擎操作报错合集之在阿里函数计算中,SD Controlnet Depth 运行过程中出现错误“urllib3 v2.0 only supports OpenSSL 1.1.1+”如何解决
    6
  • 6
    Serverless 应用引擎操作报错合集之在阿里云函数计算中,laravel zip包使用示例的start.sh脚本启动时出现错误代码如何解决
    7
  • 7
    Serverless 应用引擎操作报错合集之在阿里云函数计算中,服务器调用FC函数时出现 "[Errno -3] Temporary failure in name resolution)" 错误如何解决
    5
  • 8
    Serverless 应用引擎操作报错合集之在Serverless 应用引擎中,部署过程中遇到错误代码如何解决
    9
  • 9
    Serverless 应用引擎操作报错合集之在 Serverless 应用引擎中,遇到“没法通过 head 传递灰度标识”如何解决
    5
  • 10
    Serverless 应用引擎操作报错合集之在阿里函数计算中,函数执行超时,报错Function time out after如何解决
    11