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/addons/postprocessing/EffectComposer.js' import { RenderPass } from 'three/addons/postprocessing/RenderPass.js' import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js' import { OutputPass } from 'three/addons/postprocessing/OutputPass.js' import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js' import { FXAAShader } from 'three/addons/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 { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js' import { DragControls } from 'three/examples/jsm/controls/DragControls'; import { ElMessage ,ElMessageBox} 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 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.skeletonHelper // 网格辅助线 this.gridHelper // 坐标轴辅助线 this.axesHelper // 环境光 this.ambientLight //平行光 this.directionalLight // 平行光辅助线 this.directionalLightHelper // 点光源 this.pointLight //点光源辅助线 this.pointLightHelper //聚光灯 this.spotLight //聚光灯辅助线 this.spotLightHelper //模型平面 this.planeGeometry //模型材质列表 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.onMouseClickListener // 模型上传进度条回调函数 this.modelProgressCallback = (e) => e // 当前拖拽的几何模型 this.dragGeometryModel = {} } init() { return new Promise(async (reslove, reject) => { //初始化渲染器 this.initRender() //初始化相机 this.initCamera() //初始化场景 this.initScene() //初始化控制器,控制摄像头,控制器一定要在渲染器后 this.initControls() // 创建辅助线 this.createHelper() // 创建灯光 this.createLight() this.addEvenListMouseLisatener() // 添加物体模型 TODO:初始化时需要默认一个 // https://images.wanjunshijie.com/demo/threeDemo2/glb/city.glb // https://threejs.org/examples/models/gltf/LittlestTokyo.glb 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.ACESFilmicToneMapping 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) } // 更新场景 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 THREE_PATH = `https://unpkg.com/three@0.${THREE.REVISION}.x`; let loader if (['glb', 'gltf'].includes(fileType)) { const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath(`draco/gltf/`) dracoLoader.setDecoderConfig({ type: 'js' }) dracoLoader.preload() loader = new GLTFLoader().setDRACOLoader(dracoLoader) } else { loader = this.fileLoaderMap[fileType] } 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() 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) // 获取模型材质贴图 this.getModelMeaterialMaps(map) resolve(true) }, (xhr) => { this.modelProgressCallback(xhr.loaded) }, (err) => { ElMessage.error('文件错误') console.log(err) resolve(true) }) }) } // 加载几何体模型 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, true); 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) //计算控制器缩放大小 const box = new THREE.Box3().setFromObject(this.model); const size = box.getSize(new THREE.Vector3()); this.controls.maxDistance = size.length() * 10 reslove(true) } else { ElMessage.warning('当前角度无法获取鼠标位置请调整“相机角度”在添加') } }) } // 模型加载进度条回调函数 onProgress(callback) { if (typeof callback == 'function') { this.modelProgressCallback = callback } } // 创建辅助线 createHelper() { //网格辅助线 this.gridHelper = new THREE.GridHelper(4, 10, 'rgb(193,193,193)', 'rgb(193,193,193)'); this.gridHelper.position.set(0, -.59, -.1) this.gridHelper.visible = false this.scene.add(this.gridHelper) // 坐标轴辅助线 this.axesHelper = new THREE.AxesHelper(2); this.axesHelper.visible = false this.scene.add(this.axesHelper); // 开启阴影 this.renderer.shadowMap.enabled = true; } // 创建光源 createLight() { // 创建环境光 this.ambientLight = new THREE.AmbientLight('#fff', .8) this.scene.add(this.ambientLight) // 创建平行光 this.directionalLight = new THREE.DirectionalLight('#fff', 5) this.directionalLight.position.set(-1.44, 2.2, 1) this.directionalLight.castShadow = true this.directionalLight.visible = false this.scene.add(this.directionalLight) // 创建平行光辅助线 this.directionalLightHelper = new THREE.DirectionalLightHelper(this.directionalLight, .3) this.directionalLightHelper.visible = false this.scene.add(this.directionalLightHelper) // 创建点光源 this.pointLight = new THREE.PointLight(0xff0000, 5, 100) this.pointLight.visible = false this.scene.add(this.pointLight) // 创建点光源辅助线 this.pointLightHelper = new THREE.PointLightHelper(this.pointLight, .5) this.pointLightHelper.visible = false this.scene.add(this.pointLightHelper) // 创建聚光灯 this.spotLight = new THREE.SpotLight('#00BABD', 900); this.spotLight.visible = false this.spotLight.map = new THREE.TextureLoader().load(require('@/assets/image/model-bg-1.jpg')); this.spotLight.decay = 2; this.spotLight.shadow.mapSize.width = 1920; this.spotLight.shadow.mapSize.height = 1080; this.spotLight.shadow.camera.near = 1; this.spotLight.shadow.camera.far = 10; this.scene.add(this.spotLight); //创建聚光灯辅助线 this.spotLightHelper = new THREE.SpotLightHelper(this.spotLight); this.spotLightHelper.visible = false this.scene.add(this.spotLightHelper) // 模型平面 const geometry = new THREE.PlaneGeometry(4, 4); var groundMaterial = new THREE.MeshStandardMaterial({ color: '#000000' }); this.planeGeometry = new THREE.Mesh(geometry, groundMaterial); this.planeGeometry.name = 'planeGeometry' this.planeGeometry.rotation.x = -Math.PI / 2 this.planeGeometry.position.set(0, -.5, 0) // 让地面接收阴影 this.planeGeometry.receiveShadow = true; this.planeGeometry.visible = false this.scene.add(this.planeGeometry); } // 创建效果合成器 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.usePatternTexture = false // 是否使用纹理图案 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), 1.5, 0.4, 0.85) // 辉光合成器 const renderTargetParameters = { minFilter: 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 { // 加载几何模型 if (model.modelType && model.modelType == 'geometry') { // 重置"灯光"模块数据 this.modelAnimation = [] this.camera.fov = 80 this.camera.updateProjectionMatrix() const load = await this.setGeometryModel(model) reslove() } else { this.clearSceneModel() // 重置"灯光"模块数据 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) } // 下载场景封面 onDownloadScenCover() { var link = document.createElement('a'); var canvas = this.renderer.domElement; link.href = canvas.toDataURL("image/png"); link.download = `${new Date().toLocaleString()}.png` link.click(); ElMessage.success('下载成功') } // 导出模型 onExporterModel(type) { const exporter = new GLTFExporter(); const options = { trs: true, // 是否保留位置、旋转、缩放信息 animations: this.modelAnimation, // 导出的动画 binary: type == 'glb' ? true : false, // 是否以二进制格式输出 embedImages: true,//是否嵌入贴图 onlyVisible: true, //是否只导出可见物体 includeCustomExtensions: true, } exporter.parse(this.scene, function (result) { if (result instanceof ArrayBuffer) { // 将结果保存为GLB二进制文件 saveArrayBuffer(result, `${new Date().toLocaleString()}.glb`); } else { // 将结果保存为GLTF JSON文件 saveString(JSON.stringify(result), `${new Date().toLocaleString()}.gltf`); } function saveArrayBuffer(buffer, filename) { // 将二进制数据保存为文件 const blob = new Blob([buffer], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); ElMessage.success('导出成功') } function saveString(text, filename) { // 将字符串数据保存为文件 const blob = new Blob([text], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); ElMessage.success('导出成功') } }, (err) => { ElMessage.error(err) }, options); } // 清除模型数据 onClearModelData() { cancelAnimationFrame(this.rotationAnimationFrame) cancelAnimationFrame(this.renderAnimation) cancelAnimationFrame(this.animationFrame) this.container.removeEventListener('click', this.onMouseClickListener) window.removeEventListener("resize", this.onWindowResizesListener) this.scene.traverse((v) => { if (v.type === 'Mesh') { v.geometry.dispose(); v.material.dispose(); } }) this.scene.clear() this.renderer.dispose() this.renderer.clear() this.container = null // 相机 this.camera = null // 场景 this.scene = null //渲染器 this.renderer = null // 控制器 this.controls = null // 模型 this.model = null //几何体模型 this.geometryModel = 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.ambientLight = null //平行光 this.directionalLight = null // 平行光辅助线 this.directionalLightHelper = null // 点光源 this.pointLight = null //点光源辅助线 this.pointLightHelper = null //聚光灯 this.spotLight = null //聚光灯辅助线 this.spotLightHelper = 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.glowMaterialList = null this.materials = null // 拖拽对象控制器 this.dragControls = null this.dragGeometryModel = null } /** * @describe 左侧面板操作方法 * @function clearSceneModel 清除场景模型数据 * @function setDragGeometryModel 设置当前被拖拽的几何模型 */ // 清除场景模型数据 clearSceneModel() { //先移除模型 材质释放内存 this.scene.traverse((v) => { if (v.type === 'Mesh') { v.geometry.dispose(); v.material.dispose(); } }) this.dragGeometryModel = {} //取消动画帧 cancelAnimationFrame(this.animationFrame) cancelAnimationFrame(this.rotationAnimationFrame) this.scene.remove(this.model) this.model = null this.modelTextureMap = [] this.glowMaterialList = [] this.modelMaterialList = [] this.materials = {} if (this.dragControls) { this.dragControls.dispose() } this.renderer.toneMappingExposure = 2 Object.assign(this.unrealBloomPass, { threshold: 0, strength: 0, radius: 0, }) // 重置"辅助线/轴配置"模块数据 this.skeletonHelper.visible = false const config = { gridHelper: false, x: 0, y: -0.59, z: -0.1, positionX: 0, positionY: -0.5, positionZ: 0, divisions: 10, size: 4, color: "rgb(193,193,193)", axesHelper: false, axesSize: 1.8, } this.onResettingLight({ ambientLight: false }) this.onSetModelGridHelper(config) this.onSetModelGridHelperSize(config) this.onSetModelAxesHelper(config) } // 设置当前被拖拽的几何模型 setDragGeometryModel(model) { this.dragGeometryModel = model } /** * @describe 背景模块方法 * @function onSetSceneColor 设置场景颜色 * @function onSetSceneImage 设置场景图片 * @function onSetSceneViewImage 设置全景图 */ // 设置场景颜色 onSetSceneColor(color) { this.scene.background = new THREE.Color(color) } // 设置场景图片 onSetSceneImage(url) { this.scene.background = new THREE.TextureLoader().load(url); } // 设置全景图 onSetSceneViewImage(url) { const texture = new THREE.TextureLoader().load(url); texture.mapping = THREE.EquirectangularReflectionMapping this.scene.background = texture this.scene.environment = texture } /** * @describe 材质模块方法 * @function getModelMeaterialList 获取当前模型材质 * @function setModelPositionSize 设置模型定位缩放大小 * @function getModelMaps 获取模型自带贴图 * @function onSetModelMaterial 设置材质属性(网格,透明度,颜色,深度写入) * @function onSetModelMap 设置模型贴图(模型自带) * @function onSetSystemModelMap 设置模型贴图(系统贴图) * @function onChangeModelMeaterial 选择材质 * @function onGetEditMeshList 获取最新材质信息列表 * @function onChangeModelMeshType 切换材质类型 * @function onSetGeometryMeshList 设置几何体模型材质 */ // 获取当前模型材质 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) } } }) } // 获取当前模型材质贴图 getModelMeaterialMaps(map) { this.modelTextureMap = [] // TODO 获取当前模型材质数量如果超过100个 则不加载模型自带贴图 const materials = new Set(); this.model.traverse((node) => { if (node.isMesh) { const meshMaterials = Array.isArray(node.material) ? node.material : [node.material]; meshMaterials.forEach((material) => materials.add(material)); } }); const numMaterials = materials.size; if (numMaterials > 100) { ElMessageBox.alert(`当前模型材质数量过大“${numMaterials}个”,编辑器页面可能有卡顿`, '提示', { confirmButtonText: '确认', }) return this.modelTextureMap = null } const isMap = map ? true : false let i = 0; this.model.traverse((v) => { const { uuid } = v if (v.isMesh && v.material) { i++; const materials = Array.isArray(v.material) ? v.material : [v.material] const { url, mapId } = this.getModelMaps(materials, uuid) const mesh = { meshName: v.name, material: v.material, url, mapId: mapId + '_' + i } // 获取当前模型材质 v.mapId = mapId + '_' + i this.modelTextureMap.push(mesh) // 部分模型本身没有贴图需 要单独处理 if (isMap) { const mapTexture = new THREE.TextureLoader().load(map) const newMaterial = v.material.clone() v.material = newMaterial v.material.map = mapTexture v.mapId = uuid + '_' + i this.modelTextureMap = [{ meshName: v.name, material: v.material, url: map, mapId: uuid + '_' + i }] } } }) } // 设置模型定位缩放大小 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.position.set(0, 2, 6) // 设置相机坐标系 this.camera.lookAt(center) this.camera.updateProjectionMatrix(); } // 获取模型自带贴图 getModelMaps(materials, uuid) { let textureMap = {} materials.forEach(texture => { if (texture.map && texture.map.image) { const canvas = document.createElement('canvas') const { width, height } = texture.map.image canvas.width = 75 canvas.height = 75 const context = canvas.getContext('2d') context.drawImage(texture.map.image, 0, 0) textureMap = { url: canvas.toDataURL('image/png', .5), } canvas.remove() } }) return textureMap } // 设置材质属性 onSetModelMaterial(config) { const { color, wireframe, depthWrite, opacity } = JSON.parse(JSON.stringify(config)) const uuid = store.state.selectMesh.uuid const mesh = this.scene.getObjectByProperty('uuid', uuid) if (mesh && mesh.material) { const { name, map } = mesh.material Object.assign(mesh.material, { map, name, transparent: true, color: new THREE.Color(color), wireframe, depthWrite, opacity }) } } // 设置模型贴图(模型自带) onSetModelMap({ material, mapId, meshName }) { const uuid = store.state.selectMesh.uuid const mesh = this.scene.getObjectByProperty('uuid', uuid) mesh.material = material.clone() mesh.mapId = mapId // 设置当前材质来源唯一标记值key 用于预览处数据回填需要 mesh.meshFrom = meshName } // 设置模型贴图(系统贴图) onSetSystemModelMap({ id, url }) { const uuid = store.state.selectMesh.uuid const mesh = this.scene.getObjectByProperty('uuid', uuid) const mapTexture = new THREE.TextureLoader().load(url) const newMaterial = mesh.material.clone() newMaterial.map = mapTexture mesh.material = newMaterial mesh.mapId = id // 设置当前材质来源唯一标记值key 用于预览处数据回填需要 mesh.meshFrom = id } // 选择材质 onChangeModelMeaterial(name) { const mesh = this.model.getObjectByName(name) this.outlinePass.selectedObjects = [mesh] store.commit('SELECT_MESH', mesh) return mesh } // 模型点击事件 onMouseClickModel(event) { const { clientHeight, clientWidth, offsetLeft, offsetTop } = this.container this.mouse.x = ((event.clientX - offsetLeft) / clientWidth) * 2 - 1 this.mouse.y = -((event.clientY - offsetTop) / clientHeight) * 2 + 1 this.raycaster.setFromCamera(this.mouse, this.camera) const intersects = this.raycaster.intersectObjects(this.scene.children).filter(item => item.object.isMesh) if (intersects.length > 0) { const intersectedObject = intersects[0].object this.outlinePass.selectedObjects = [intersectedObject] store.commit('SELECT_MESH', intersectedObject) } else { this.outlinePass.selectedObjects = [] store.commit('SELECT_MESH', {}) } } // 获取最新材质信息列表 onGetEditMeshList() { const meshList = [] this.model.traverse((v) => { if (v.isMesh && v.material) { const { color, opacity, depthWrite, wireframe } = v.material const obj = { meshName: v.name, meshFrom: v.meshFrom, color: color.getStyle(), opacity, depthWrite, wireframe, visible: v.visible, type: v.material.type } meshList.push(obj) } }) return meshList } // 设置材质类型 onChangeModelMeshType(activeMesh) { this.model.traverse(v => { if (v.isMesh && v.material) { const { name, color, map, wireframe, depthWrite, opacity } = v.material v.material = new THREE[activeMesh.type]({ map, transparent: true, color, name, }) depthWrite ? v.material.depthWrite = depthWrite : '' opacity ? v.material.opacity = opacity : '' wireframe ? v.material.wireframe = wireframe : '' } }) } // 设置几何体材质 onSetGeometryMeshList(v) { this.modelMaterialList = [] this.modelTextureMap = [] this.model.traverse((v) => { const { uuid, name } = v v.castShadow = true v.frustumCulled = false if (v.isMesh && v.material) { const materials = Array.isArray(v.material) ? v.material : [v.material] // 统一将模型材质 设置为 MeshLambertMaterial 类型 this.modelMaterialList.push(v) // 获取模型自动材质贴图 const { url } = this.getModelMaps(materials, uuid) const mesh = { meshName: v.name, material: v.material, url, mapId: name } // 获取当前模型材质 v.mapId = name this.modelTextureMap.push(mesh) } }) } /** * @describe 后期/操作模块方法 * @function onSetUnrealBloomPass 设置辉光效果 * @function setModelMeshDecompose 模型拆分 * @function setModelMeshDrag 模型材质可拖拽 * @function getMeshDragPosition 获取模型材质位拖拽置 */ // 设置辉光效果 onSetUnrealBloomPass(config) { const { glow, threshold, strength, radius, toneMappingExposure } = config this.glowUnrealBloomPass = glow if (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 } } // 模型拆分 setModelMeshDecompose({ decompose }) { if (this.glowMaterialList.length <= 1) return false const modelDecomposeMove = (obj, position) => { const Tween = new TWEEN.Tween(obj.position) Tween.to(position, 500) Tween.onUpdate(function (val) { obj.position.set(val.x || 0, val.y || 0, val.z || 0); }) Tween.start() } const length = this.glowMaterialList.length const angleStep = (2 * Math.PI) / length; this.glowMaterialList.forEach((name, i) => { const mesh = this.model.getObjectByName(name) const { decomposeName } = this.model if (mesh.type == 'Mesh') { // 如果当前模型有设置模型分解的自定义参数 if (MODEL_DECOMPOSE[decomposeName] && MODEL_DECOMPOSE[decomposeName][name]) { const position = { x: 0, y: 0, z: 0 } const { x: modelX, y: modelY, z: modelZ } = MODEL_DECOMPOSE[decomposeName][name] if (modelX == 'straight') { position.x += decompose } else if (modelX == 'burden') { position.x -= decompose } if (modelY == 'straight') { position.y += decompose } else if (modelY == 'burden') { position.y -= decompose } if (modelZ == 'straight') { position.z += decompose } else if (modelZ == 'burden') { position.z -= decompose } modelDecomposeMove(mesh, position) } else { // 材质位置计算 const angle = i * angleStep; const x = (decompose) * Math.cos(angle); const y = (decompose) * Math.sin(angle); const position = { x, y, z: 0 } modelDecomposeMove(mesh, position) } } }) } // 模型材质可拖拽 setModelMeshDrag({ modelDrag }) { // 先把之前的拖拽信息给清除掉 if (this.dragControls) this.dragControls.dispose() if (modelDrag) { this.dragControls = new DragControls(this.modelMaterialList, this.camera, this.renderer.domElement); // 拖拽事件监听 this.dragControls.addEventListener('dragstart', () => { this.controls.enabled = false }) this.dragControls.addEventListener('dragend', () => { this.controls.enabled = true }) } } // 获取模型材质位拖拽置 getMeshDragPosition() { const positonList = [] this.modelMaterialList.forEach(v => { const mesh = this.model.getObjectByProperty('name', v.name) const obj = { name: v.name, ...mesh.position } positonList.push(obj) }) return positonList } /** * @describe 灯光模块方法 * @function onSetModelAmbientLight 设置环境光 * @function onSetModelDirectionalLight 设置平行光 * @function onSetModelPointLight 设置点光源 * @function onSetModelSpotLight 设置聚光灯 * @function onSetModelPlaneGeometry 设置模型平面 * @function onResettingLight 重置场景灯光 */ // 设置环境光 onSetModelAmbientLight({ ambientLight, ambientLightColor, ambientLightIntensity }) { this.ambientLight.visible = ambientLight this.ambientLight.intensity = ambientLightIntensity this.ambientLight.color.set(ambientLightColor) } // 设置平行光 onSetModelDirectionalLight(config) { const { directionaShadow, directionalHorizontal, directionalVertical, directionalSistance, directionalLight, directionalLightColor, directionalLightIntensity, directionalLightHelper } = config this.directionalLight.visible = directionalLight this.directionalLightHelper.visible = directionalLightHelper && directionalLight this.directionalLight.intensity = directionalLightIntensity this.directionalLight.castShadow = directionaShadow this.directionalLight.color.set(directionalLightColor) const { x, y, z } = lightPosition(directionalHorizontal, directionalVertical, directionalSistance) this.directionalLight.position.set(x, y, z) this.directionalLightHelper.update() } // 设置点光源 onSetModelPointLight(config) { const { pointHorizontal, pointVertical, pointSistance, pointLight, pointLightColor, pointLightIntensity, pointLightHelper } = config this.pointLight.visible = pointLight this.pointLightHelper.visible = pointLight && pointLightHelper this.pointLight.intensity = pointLightIntensity this.pointLight.color.set(pointLightColor) const { x, y, z } = lightPosition(pointHorizontal, pointVertical, pointSistance) this.pointLight.position.set(x, y, z) this.pointLightHelper.update() } // 设置聚光灯 onSetModelSpotLight(config) { const { spotDistance, spotCastShadow, spotLightHelper, spotFocus, spotPenumbra, spotAngle, spotLight, spotLightColor, spotLightIntensity, spotHorizontal, spotVertical, spotSistance } = config this.spotLight.visible = spotLight this.spotLightHelper.visible = spotLight && spotLightHelper this.spotLight.intensity = spotLightIntensity this.spotLight.angle = spotAngle this.spotLight.penumbra = spotPenumbra this.spotLight.shadow.focus = spotFocus this.spotLight.castShadow = spotCastShadow this.spotLight.distance = spotDistance this.spotLight.color.set(spotLightColor) const { x, y, z } = lightPosition(spotHorizontal, spotVertical, spotSistance) this.spotLight.position.set(x, y, z) this.spotLightHelper.update() } // 设置模型平面 onSetModelPlaneGeometry({ planeGeometry, planeColor, planeWidth, planeHeight }) { this.planeGeometry.visible = planeGeometry this.planeGeometry.geometry = new THREE.PlaneGeometry(planeWidth, planeHeight) this.planeGeometry.material.color.set(planeColor) this.planeGeometry.geometry.verticesNeedUpdate = true } // 重置场景灯光 onResettingLight({ ambientLight }) { const config = { planeGeometry: false, planeColor: "#939393", planeWidth: 7, planeHeight: 7, //环境光 ambientLight, ambientLightColor: "#fff", ambientLightIntensity: 0.8, //平行光 directionalLight: false, directionalLightHelper: true, directionalLightColor: "#1E90FF", directionalLightIntensity: 1, directionalHorizontal: -1.26, directionalVertical: -3.85, directionalSistance: 2.98, directionaShadow: true, //点光源 pointLight: false, pointLightHelper: true, pointLightColor: "#1E90FF", pointLightIntensity: 1, pointHorizontal: -4.21, pointVertical: -4.1, pointSistance: 2.53, //聚光灯 spotLight: false, spotLightColor: "#323636", spotLightIntensity: 400, spotHorizontal: -3.49, spotVertical: -4.37, spotSistance: 4.09, spotAngle: 0.5, spotPenumbra: 1, spotFocus: 1, spotCastShadow: true, spotLightHelper: true, spotDistance: 20 } this.onSetModelAmbientLight(config) this.onSetModelDirectionalLight(config) this.onSetModelPointLight(config) this.onSetModelSpotLight(config) this.onSetModelPlaneGeometry(config) } /** * @describe 模型动画模块方法 * @function onStartModelAnimaion 开始执行动画 * @function onSetModelAnimaion 设置模型动画 * @function animationFrameFun 动画帧 * @function onClearAnimation 清除动画 * @function onSetRotation 设置模型轴动画 * @function onSetRotationType 设置模型轴动画类型 */ // 开始执行动画 onStartModelAnimaion(config) { this.onSetModelAnimaion(config) cancelAnimationFrame(this.animationFrame) this.animationFrameFun() } // 设置模型动画 onSetModelAnimaion({ animationName, loop, timeScale, weight }) { this.animationMixer = new THREE.AnimationMixer(this.model) 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() } } // 动画帧 animationFrameFun() { this.animationFrame = requestAnimationFrame(() => this.animationFrameFun()) if (this.animationMixer) { this.animationMixer.update(this.animationColock.getDelta()) } } // 清除动画 onClearAnimation() { if (!this.animateClipAction) return this.animationMixer.stopAllAction(); this.animationMixer.update(0); cancelAnimationFrame(this.animationFrame) } // 设置模型轴动画 onSetRotation(config) { const { rotationVisible, rotationType, rotationSpeed } = config if (rotationVisible) { cancelAnimationFrame(this.rotationAnimationFrame) this.rotationAnimationFun(rotationType, rotationSpeed) } else { cancelAnimationFrame(this.rotationAnimationFrame) this.model.rotation.set(0, 0, 0) } } // 设置轴动画类型 onSetRotationType(config) { const { rotationType, rotationSpeed } = config this.model.rotation.set(0, 0, 0) cancelAnimationFrame(this.rotationAnimationFrame) this.rotationAnimationFun(rotationType, rotationSpeed) } // 轴动画帧 rotationAnimationFun(rotationType, rotationSpeed) { this.rotationAnimationFrame = requestAnimationFrame(() => this.rotationAnimationFun(rotationType, rotationSpeed)) this.model.rotation[rotationType] += rotationSpeed / 50 } /** * @describe 辅助线/轴配置模块方法 * @function onSetModelHelper 设置模型骨架 * @function onSetModelRotateOnAxis 设置模型轴旋转 * @function onResultModelRotateOnAxis 重置模型轴位置 * @function onSetModelPosition 设置模型位置 * @function onResultModelPosition 重置模型位置 * @function onResetModelCamera 重置相机位置 * @function onGetModelCamera 获取相机位置 * @function onSetModelGridHelper 设置网格辅助线位置和颜色 * @function onSetModelGridHelperSize 设置网格数量和大小 * @function onSetModelAxesHelper 设置坐标轴辅助线 */ // 设置模型骨架 onSetModelHelper(visible) { this.skeletonHelper.visible = visible } // 设置模型轴旋转 onSetModelRotateOnAxis(type, flag) { const maxAxis = Math.PI / 2 const { x, y, z } = this.model.rotation const endPosition = { x, y, z } endPosition[type] += flag ? maxAxis : -maxAxis const Tween = new TWEEN.Tween({ x, y, z }) Tween.to(endPosition, 500) Tween.onUpdate((val) => { this.model.rotation[type] = val[type] }) Tween.start(); } // 重置模型轴位置 onResultModelRotateOnAxis() { this.model.rotation.x = 0 this.model.rotation.y = 0 this.model.rotation.z = 0 } // 设置模型位置 onSetModelPosition({ positionX, positionY, positionZ }) { const Tween = new TWEEN.Tween(this.model.position) const endPosition = { x: positionX, y: positionY, z: positionZ } Tween.to(endPosition, 500) Tween.onUpdate((val) => { this.model.position.set(val.x || 0, val.y || 0, val.z || 0) }) Tween.start(); } // 重置模型位置 onResultModelPosition({ positionX, positionY, positionZ }) { this.model.position.set(positionX, positionY, positionZ) } // 重置相机位置 onResetModelCamera() { // 设置相机位置 this.camera.position.set(0, 2, 6) // 设置相机坐标系 this.camera.lookAt(0, 0, 0) } // 获取相机位置 onGetModelCamera() { return this.camera.position } // 设置网格辅助线位置和颜色 onSetModelGridHelper({ x, y, z, gridHelper, color }) { this.gridHelper.visible = gridHelper this.gridHelper.material.color.set(color); const Tween = new TWEEN.Tween(this.gridHelper.position) const endPosition = { x, y, z } Tween.to(endPosition, 500) Tween.onUpdate((val) => { this.gridHelper.position.set(val.x || 0, val.y || 0, val.z || 0) }) Tween.start(); } // 设置网格数量和大小 onSetModelGridHelperSize({ x, y, z, size, divisions, color, gridHelper }) { // 需要先把辅助线移除然后在重新创建 this.scene.remove(this.gridHelper) this.gridHelper.geometry.dispose() this.gridHelper.material.dispose() this.gridHelper = new THREE.GridHelper(size, divisions, color, color); this.gridHelper.position.set(x, y, z) this.gridHelper.material.linewidth = 0.1 this.gridHelper.material.color.set(color); this.gridHelper.visible = gridHelper this.scene.add(this.gridHelper) } // 设置坐标轴辅助线 onSetModelAxesHelper({ axesHelper, axesSize }) { // 需要先把辅助线移除然后在重新创建 this.scene.remove(this.axesHelper) this.axesHelper.geometry.dispose() this.axesHelper.material.dispose() this.axesHelper = new THREE.AxesHelper(axesSize); this.axesHelper.position.set(0, -.50, 0) this.axesHelper.visible = axesHelper this.scene.add(this.axesHelper); } /** * @describe 辅助线/轴配置模块方法 * @function onDeleteGeometryMesh 删除几何体材质 * @function onSetGeometryMesh 修改几何体材质信息 */ onDeleteGeometryMesh(uuid) { // 找到需要删除的材质 const mesh = this.scene.getObjectByProperty('uuid', uuid) this.modelMaterialList = this.modelMaterialList.filter(v => v.uuid != uuid) this.glowMaterialList = this.modelMaterialList.map(v => v.name) mesh.clear() this.geometryGroup.remove(mesh) this.dragControls.dispose() // 更新拖拽函数的材质对象 if (this.modelMaterialList.length == 0) { this.setModelMeshDrag({ modelDrag: false }) } else { this.setModelMeshDrag({ modelDrag: true }) } } onSetGeometryMesh(activeGeometry, type) { const uuid = store.state.selectMesh.uuid const mesh = this.scene.getObjectByProperty('uuid', uuid) const geometryData = Object.keys(activeGeometry).map(v => activeGeometry[v]) // 创建几何体 const newGeometry = new THREE[type](...geometryData) mesh.geometry = newGeometry } } export default renderModel