• 确保全局只有一个 Three.js 管理器

  • 减少重复加载,提升性能

  • 主动清理资源,防止内存泄漏

  • 包含占位模型和详细的错误日志

  • 自动适应容器尺寸变化

  • 便于监控和优化

npm install three@0.128.0
我这里使用的是0.128.0版本的threejs

// 在main.js文件中导入ThreeManager管理器
import ThreeManager from '@/views/Blg-xjty/utils/ThreeManager'
// 全局挂载ThreeManager
Vue.prototype.$threeManager = ThreeManager

// 在组件中使用this.$threeManager

// 在非组件中没有vue上下文,不能使用this.$threeManager,需要import ThreeManager from '@/views/Blg-xjty/utils/ThreeManager'之后,使用ThreeManager,比如ThreeManager.init()
// ThreeManager.js全局Three.js管理器 单例模式

import * as THREE from 'three'

// 导入示例组件(注意:这些不再是THREE的一部分,需要单独导入)
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

class ThreeManager {
  constructor() {
    if (ThreeManager.instance) {
      console.log('♻️ 复用ThreeManager单例')
      return ThreeManager.instance
    }

    console.log('🆕 创建新的ThreeManager实例')
    console.log('THREE版本:', THREE.REVISION)
    console.log(
      'THREE.GLTFLoader:',
      '已从three/examples/jsm/loaders/GLTFLoader导入'
    )

    // 检查导入是否正确
    console.log('导入检查:', {
      GLTFLoader: !!GLTFLoader,
      DRACOLoader: !!DRACOLoader,
      OrbitControls: !!OrbitControls,
      THREE: !!THREE,
    })

    // 核心Three.js对象
    this.renderer = null
    this.camera = null
    this.scene = null
    this.controls = null
    this.isInitialized = false

    // 加载器(单例,复用)
    this.loaders = {
      gltf: null,
      texture: null,
      cubeTexture: null,
    }

    // 缓存系统
    this.cache = {
      models: new Map(), // 模型缓存 key: url/modelId -> gltf/obj
      textures: new Map(), // 纹理缓存 key: textureUrl -> texture
      materials: new Map(), // 材质缓存 key: materialType -> material
      geometries: new Map(), // 几何体缓存 key: geometryHash -> geometry
    }

    // 渲染状态
    this.isRendering = false
    this.animationFrameId = null

    // 性能统计
    this.stats = {
      totalModelsLoaded: 0,
      cacheHits: 0,
      cacheMisses: 0,
    }

    // 开发环境
    this.processEnvIsDev = process.env.NODE_ENV === 'development'
    // 生产环境
    this.processEnvIsPro = process.env.NODE_ENV === 'production'

    // 添加:跟踪当前活动模型
    this.activeModel = null
    this.temporaryObjects = new Set()

    ThreeManager.instance = this
  }

  /**
   * 初始化Three.js基础环境(只执行一次)
   * @param {HTMLElement} container - DOM容器
   */
  async init(container) {
    if (this.isInitialized) {
      console.log('Three.js已初始化,直接复用')
      return
    }

    console.log('🚀 初始化Three.js基础环境...')

    try {
      // 1. 创建渲染器(核心单例)
      this.renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
        preserveDrawingBuffer: true, //方便截图,允许保留透明背景
      })
      this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
      this.renderer.setSize(container.clientWidth, container.clientHeight)
      this.renderer.outputEncoding = THREE.LinearEncoding //使用线性 tonemapping(更接近原始颜色)
      this.renderer.toneMapping = THREE.NoToneMapping
      this.renderer.toneMappingExposure = 1
      container.appendChild(this.renderer.domElement)
      console.log('✅ 渲染器创建完成')

      // 2. 创建场景
      this.scene = new THREE.Scene()
      this.scene.background = new THREE.Color(0x0a192f)
      console.log('✅ 场景创建完成')

      // 3. 创建相机
      this.camera = new THREE.PerspectiveCamera(
        45,
        container.clientWidth / container.clientHeight,
        0.1,
        1000
      )
      this.camera.position.set(5, 5, 5)
      this.camera.lookAt(0, 0, 0)
      console.log('✅ 相机创建完成')

      // 4. 创建控制器
      this.controls = new OrbitControls(this.camera, this.renderer.domElement)
      this.controls.enableDamping = true
      this.controls.dampingFactor = 0.05
      this.controls.screenSpacePanning = true
      console.log('✅ 控制器创建完成')

      // 5. 初始化加载器(单例复用)
      this.initLoaders()

      // 6. 添加基础光源(可复用)
      this.initLights()

      // 7. 添加坐标轴辅助(开发时使用)
      if (this.processEnvIsDev) {
        // const axesHelper = new THREE.AxesHelper(5)
        // this.scene.add(axesHelper)
        // const gridHelper = new THREE.GridHelper(10, 10)
        // this.scene.add(gridHelper)
        // console.log('✅ 辅助工具已添加')
      }

      // 8. 窗口大小变化监听
      this.initResizeHandler(container)

      this.isInitialized = true
      console.log('🎉 Three.js初始化完成')
    } catch (error) {
      console.error('❌ Three.js初始化失败:', error)
      throw error
    }
  }

  /**
   * 初始化加载器(单例)
   */
  initLoaders() {
    console.log('🔧 初始化加载器...')

    try {
      // GLTF加载器(带DRACO压缩支持)
      const dracoLoader = new DRACOLoader()
      // 注意:这里使用了在线CDN,您可能需要下载到本地或使用其他方式
      dracoLoader.setDecoderPath(
        'https://www.gstatic.com/draco/versioned/decoders/1.5.6/'
      )
      dracoLoader.setDecoderConfig({ type: 'js' })
      console.log('✅ DRACOLoader初始化完成')

      this.loaders.gltf = new GLTFLoader()
      this.loaders.gltf.setDRACOLoader(dracoLoader)
      console.log('✅ GLTFLoader初始化完成')

      // 纹理加载器
      this.loaders.texture = new THREE.TextureLoader()
      this.loaders.cubeTexture = new THREE.CubeTextureLoader()
      console.log('✅ 纹理加载器初始化完成')
    } catch (error) {
      console.error('❌ 加载器初始化失败:', error)
      throw error
    }
  }

  /**
   * 初始化基础光源(永久存在)
   */
  initLights() {
    console.log('💡 初始化基础光源...')

    // 环境光
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
    ambientLight.name = 'ambientLight'
    ambientLight.userData = { isPermanent: true }
    this.scene.add(ambientLight)

    // 平行光/方向光(模拟太阳/主光源)
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
    directionalLight.name = 'directionalLight'
    directionalLight.userData = { isPermanent: true }
    directionalLight.position.set(5, 10, 7)
    directionalLight.castShadow = true

    // 优化阴影
    directionalLight.shadow.mapSize.width = 2048
    directionalLight.shadow.mapSize.height = 2048
    directionalLight.shadow.camera.near = 0.5
    directionalLight.shadow.camera.far = 500
    this.scene.add(directionalLight)

    // 点光源 - 从一点向所有方向发射
    const pointLight1 = new THREE.PointLight(0xffffff, 1, 100)
    pointLight1.position.set(0, 10, 0)
    const pointLight2 = new THREE.PointLight(0xffffff, 1, 100)
    pointLight2.position.set(0, -10, 0)
    this.scene.add(pointLight1, pointLight2)

    // 聚光灯 - 锥形光束(模拟手电筒、舞台灯)
    // const spotLight = new THREE.SpotLight(0xffffff, 1)
    // spotLight.position.set(0, 10, 0)
    // spotLight.angle = Math.PI / 6 // 30度锥角
    // spotLight.penumbra = 0.5 // 边缘柔和度
    // spotLight.castShadow = true
    // this.scene.add(spotLight)

    // 矩形区域光
    const rectAreaLight = new THREE.RectAreaLight(0xffffff, 2, 5, 5)
    rectAreaLight.position.set(0, 5, 0)
    rectAreaLight.lookAt(0, 0, 0)
    this.scene.add(rectAreaLight)

    // 辅助光、半球光 - 天空和地面颜色不同(模拟室外环境)
    const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.5)
    hemisphereLight.name = 'hemisphereLight'
    hemisphereLight.userData = { isPermanent: true }
    this.scene.add(hemisphereLight)

    console.log('✅ 光源初始化完成')
  }

  /**
   * 加载设备模型(带缓存)- 修复版
   * @param {string} deviceId - 设备ID
   * @param {string} modelUrl - 模型URL(可选)
   * @returns {Promise<THREE.Group>} 模型组
   */
  async loadDeviceModel(deviceId, modelUrl = null) {
    console.log(`📤 开始加载设备模型: ${deviceId}`)

    const cacheKey = deviceId

    // 1. 如果有活动的模型,先清理
    if (this.activeModel) {
      console.log(
        `🗑️ 清理前一个活动模型: ${this.activeModel.userData?.deviceId}`
      )
      this.removeModelFromScene(this.activeModel)
      this.activeModel = null
    }

    // 2. 检查缓存
    if (this.cache.models.has(cacheKey)) {
      this.stats.cacheHits++
      console.log(`✅ 缓存命中: ${deviceId}`)
      const cachedModel = this.cache.models.get(cacheKey)
      const clonedModel = this.cloneModel(cachedModel)
      this.activeModel = clonedModel
      return clonedModel
    }

    this.stats.cacheMisses++
    console.log(`🔄 从源加载模型: ${deviceId}`)

    try {
      // 3. 实际加载模型
      const model = await this.loadModelFromSource(deviceId, modelUrl)

      // 4. 预处理模型
      this.prepareModel(model)

      // 5. 缓存模型
      this.cache.models.set(cacheKey, model)
      this.stats.totalModelsLoaded++

      console.log(`🎉 模型加载完成: ${deviceId}`)
      const clonedModel = this.cloneModel(model)
      this.activeModel = clonedModel
      return clonedModel
    } catch (error) {
      console.error(`❌ 模型加载失败: ${deviceId}`, error)
      // 返回一个占位模型
      const placeholder = this.createPlaceholderModel(deviceId)
      this.activeModel = placeholder
      return placeholder
    }
  }

  /**
   * 缓存中没有,就从源url加载模型
   */
  async loadModelFromSource(deviceId, modelUrl) {
    const url = modelUrl || this.getModelUrlByDeviceId(deviceId)
    console.log('📥 加载模型URL:', url)

    return new Promise((resolve, reject) => {
      this.loaders.gltf.load(
        url,
        (gltf) => {
          console.log('✅ GLTF加载回调触发')

          // 详细检查gltf对象
          if (!gltf) {
            console.error('❌ GLTF对象为空')
            reject(new Error('GLTF对象为空'))
            return
          }

          console.log('🔍 GLTF对象检查:', {
            hasScene: !!gltf.scene,
            sceneType: gltf.scene?.constructor?.name,
            scenesCount: gltf.scenes?.length || 0,
            animationsCount: gltf.animations?.length || 0,
            camerasCount: gltf.cameras?.length || 0,
            asset: gltf.asset,
          })

          let model = null

          // 方法1:使用gltf.scene
          if (gltf.scene && gltf.scene.isObject3D) {
            console.log('✅ 使用 gltf.scene')
            model = gltf.scene
          }
          // 方法2:使用gltf.scenes数组
          else if (
            gltf.scenes &&
            gltf.scenes.length > 0 &&
            gltf.scenes[0].isObject3D
          ) {
            console.log('✅ 使用 gltf.scenes[0]')
            model = gltf.scenes[0]
          }
          // 方法3:创建新组并添加所有对象
          else {
            console.warn('⚠️ 创建新场景组')
            model = new THREE.Group()

            // 添加所有场景
            if (gltf.scenes) {
              gltf.scenes.forEach((scene, index) => {
                if (scene.isObject3D) {
                  model.add(scene)
                  console.log(`✅ 添加场景 ${index}`)
                }
              })
            }

            // 添加所有mesh
            gltf.scene.traverse((child) => {
              if (child.isMesh && !child.parent) {
                model.add(child)
                console.log('✅ 添加孤立mesh')
              }
            })
          }

          // 最终检查
          if (!model || !model.isObject3D) {
            console.error('❌ 最终model无效')
            reject(new Error('加载的模型不是有效的Object3D'))
            return
          }

          console.log('🎯 成功获取模型:', {
            name: model.name || '未命名',
            type: model.constructor.name,
            childrenCount: model.children.length,
            uuid: model.uuid,
          })

          model.userData = {
            deviceId,
            sourceUrl: url,
            loadedAt: Date.now(),
            isFromCache: false,
          }

          resolve(model)
        },
        (progress) => {
          if (progress.total > 0) {
            const percent = ((progress.loaded / progress.total) * 100).toFixed(
              2
            )
            console.log(`📊 加载进度: ${percent}%`)
          } else {
            console.log(`📊 已加载: ${progress.loaded} bytes`)
          }
        },
        (error) => {
          console.error('❌ GLTF加载失败:', error)
          console.error('错误详情:', error.message)
          console.error('错误堆栈:', error.stack)
          reject(error)
        }
      )
    })
  }

  /**
   * 预处理模型
   */
  prepareModel(model) {
    console.log('🔧 预处理模型...')

    model.traverse((child) => {
      // 启用阴影
      if (child.isMesh) {
        child.castShadow = true
        child.receiveShadow = true

        // 优化材质
        if (child.material) {
          this.optimizeMaterial(child.material)
        }
      }
    })

    // 统一缩放和位置
    model.scale.set(1, 1, 1)

    // 统一位置归零
    model.position.set(0, 0, 0)

    // 自动居中
    const box = new THREE.Box3().setFromObject(model)
    const center = box.getCenter(new THREE.Vector3())
    model.position.sub(center)

    console.log('✅ 模型预处理完成')
  }

  /**
   * 优化材质
   */
  optimizeMaterial(material) {
    const materialKey = `${material.type}_${material.uuid}`

    if (!this.cache.materials.has(materialKey)) {
      // 优化材质设置
      material.side = THREE.DoubleSide
      material.transparent = true
      material.opacity = 1.0
      // 确保材质有颜色
      if (!material.color) {
        material.color = new THREE.Color(0xffffff)
      }
      this.cache.materials.set(materialKey, material)
    } else {
      // 复用已缓存的材质
      return this.cache.materials.get(materialKey)
    }
  }

  /**
   * 克隆模型
   */
  cloneModel(originalModel) {
    console.log('🌀 克隆模型...')

    if (!originalModel || !originalModel.isObject3D) {
      console.error('❌ 无法克隆无效的模型')
      return this.createPlaceholderModel('clone-error')
    }

    try {
      const clone = originalModel.clone(true)

      // 深度克隆材质
      clone.traverse((child) => {
        if (child.isMesh && child.material) {
          if (Array.isArray(child.material)) {
            child.material = child.material.map((mat) => mat.clone())
          } else {
            child.material = child.material.clone()
          }
        }
      })

      // 标记为克隆
      clone.userData = {
        ...originalModel.userData,
        isClone: true,
        clonedAt: Date.now(),
        originalUuid: originalModel.uuid,
      }

      console.log('✅ 模型克隆完成')
      return clone
    } catch (error) {
      console.error('❌ 克隆失败:', error)
      return this.createPlaceholderModel('clone-failed')
    }
  }

  /**
   * 创建占位模型
   */
  createPlaceholderModel(deviceId) {
    console.log('⚠️ 创建占位模型:', deviceId)

    const group = new THREE.Group()
    group.name = `Placeholder_${deviceId}`
    group.userData = {
      deviceId,
      isPlaceholder: true,
      createdAt: Date.now(),
    }

    const geometry = new THREE.BoxGeometry(1, 1, 1)
    const material = new THREE.MeshStandardMaterial({
      color: 0xff0000,
      transparent: true,
      opacity: 0.5,
      wireframe: true,
    })
    const cube = new THREE.Mesh(geometry, material)
    cube.name = 'PlaceholderCube'

    group.add(cube)
    console.log('✅ 占位模型创建完成')
    return group
  }

  /**
   * 从场景中移除模型(并清理资源)
   */
  removeModelFromScene(model) {
    if (!model || !this.scene) return

    console.log('移除模型:', model.name || model.uuid)

    // 从场景中移除
    if (model.parent) {
      model.parent.remove(model)
    }

    // 从临时对象集合中移除
    this.temporaryObjects.delete(model)

    // 清理资源
    this.disposeObject3D(model)
  }

  /**
   * 清理Object3D及其子对象的资源
   */
  disposeObject3D(object) {
    if (!object) return

    object.traverse((child) => {
      if (child.isMesh) {
        // 清理几何体
        if (child.geometry) {
          child.geometry.dispose()
        }

        // 清理材质
        if (child.material) {
          if (Array.isArray(child.material)) {
            child.material.forEach((material) => this.disposeMaterial(material))
          } else {
            this.disposeMaterial(child.material)
          }
        }
      }
    })
  }

  /**
   * 清理材质资源
   */
  disposeMaterial(material) {
    if (!material) return

    // 清理纹理
    const textureProperties = [
      'map',
      'lightMap',
      'bumpMap',
      'normalMap',
      'specularMap',
      'envMap',
      'alphaMap',
      'aoMap',
      'displacementMap',
      'emissiveMap',
      'metalnessMap',
      'roughnessMap',
      'gradientMap',
    ]

    textureProperties.forEach((prop) => {
      if (material[prop]) {
        material[prop].dispose()
      }
    })

    // 清理材质本身
    material.dispose()
  }

  /**
   * 移除所有非永久对象 - 新增方法
   */
  removeAllNonPermanentObjects() {
    if (!this.scene) return

    console.log('🧹 移除所有非永久对象...')

    const objectsToRemove = []

    // 收集所有非永久对象
    this.scene.traverse((child) => {
      // 保留:光源、控制器、永久标记的对象
      if (
        !child.userData?.isPermanent &&
        !child.name?.includes('Light') &&
        !child.name?.includes('light') &&
        !child.name?.includes('Helper') &&
        !child.name?.includes('Camera') &&
        !child.name?.includes('control')
      ) {
        objectsToRemove.push(child)
      }
    })

    // 移除对象
    objectsToRemove.forEach((obj) => {
      try {
        // 从父级移除
        if (obj.parent) {
          obj.parent.remove(obj)
        }

        // 清理资源
        this.disposeObject3D(obj)

        console.log(`移除对象: ${obj.name || obj.uuid}`)
      } catch (e) {
        console.warn('移除对象失败:', e)
      }
    })

    // 清空临时对象集合
    this.temporaryObjects.clear()

    // 重置活动模型
    this.activeModel = null

    console.log(`✅ 移除了 ${objectsToRemove.length} 个对象`)

    // 强制垃圾回收(如果可用)
    if (typeof window.gc === 'function') {
      window.gc()
    }
  }

  /**
   * 开始渲染循环
   */
  startRenderLoop() {
    if (this.isRendering) {
      console.log('⏯️ 渲染循环已在运行中')
      return
    }

    this.isRendering = true
    console.log('▶️ 启动渲染循环...')

    const animate = () => {
      if (!this.isRendering) return

      this.animationFrameId = requestAnimationFrame(animate)

      // 更新控制器
      if (this.controls) {
        this.controls.update()
      }

      // 渲染场景
      if (this.renderer && this.scene && this.camera) {
        this.renderer.render(this.scene, this.camera)
      }
    }

    animate()
    console.log('✅ 渲染循环已启动')
  }

  /**
   * 停止渲染循环
   */
  stopRenderLoop() {
    if (this.animationFrameId) {
      cancelAnimationFrame(this.animationFrameId)
      this.animationFrameId = null
    }
    this.isRendering = false
    console.log('⏸️ 渲染循环已停止')
  }

  /**
   * 窗口大小变化处理
   */
  initResizeHandler(container) {
    this.handleResize = () => {
      if (!this.camera || !this.renderer) return

      const width = container.clientWidth
      const height = container.clientHeight

      this.camera.aspect = width / height
      this.camera.updateProjectionMatrix()
      this.renderer.setSize(width, height)
    }

    // 使用ResizeObserver或传统方式
    if ('ResizeObserver' in window) {
      const resizeObserver = new ResizeObserver(() => {
        this.handleResize()
      })
      resizeObserver.observe(container)
      this.resizeObserver = resizeObserver
    } else {
      window.addEventListener('resize', this.handleResize)
    }

    console.log('✅ 窗口大小监听器已设置')
  }

  /**
   * 完全销毁 - 增强版
   */
  destroy() {
    console.log('💥 销毁Three.js管理器...')

    // 停止渲染
    this.stopRenderLoop()

    // 清理所有非永久对象
    this.removeAllNonPermanentObjects()

    // 清理缓存
    this.cache.models.forEach((model) => {
      this.disposeObject3D(model)
    })

    this.cache.models.clear()
    this.cache.materials.clear()
    this.cache.textures.clear()
    this.cache.geometries.clear()

    // 移除DOM元素
    if (this.renderer && this.renderer.domElement.parentNode) {
      this.renderer.domElement.parentNode.removeChild(this.renderer.domElement)
      this.renderer.dispose()
    }

    // 移除事件监听
    if (this.resizeObserver) {
      this.resizeObserver.disconnect()
    } else {
      window.removeEventListener('resize', this.handleResize)
    }

    // 重置状态
    this.renderer = null
    this.camera = null
    this.scene = null
    this.controls = null
    this.activeModel = null
    this.temporaryObjects.clear()
    this.isInitialized = false

    console.log('✅ Three.js管理器已销毁')
  }

  /**
   * 获取性能统计 - 增强版
   */
  getStats() {
    return {
      ...this.stats,
      cacheSize: this.cache.models.size,
      activeModel: this.activeModel
        ? {
            name: this.activeModel.name,
            deviceId: this.activeModel.userData?.deviceId,
            uuid: this.activeModel.uuid,
          }
        : '无',
      temporaryObjects: this.temporaryObjects.size,
      memory: performance.memory
        ? {
            used:
              Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) +
              'MB',
            total:
              Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) +
              'MB',
          }
        : 'N/A',
      isRendering: this.isRendering,
      sceneObjects: this.scene?.children.length || 0,
    }
  }

  /**
   * 如果地图页面没有传过来模型url,就根据设备ID获取模型url
   */
  getModelUrlByDeviceId(deviceId) {
    console.log('🔍 获取设备模型URL:', deviceId)
    // 根据实际情况修改这里
    if (deviceId === 'test-official') {
      return 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf'
    }
    // 默认返回本地模型
    return '/qf.glb'
  }

  /**
   * 重新绑定 Three 到新的容器
   */
  rebindContainer(container) {
    if (!this.renderer || !this.camera) {
      console.error('❌ 无法重新绑定容器')
      return
    }

    const canvas = this.renderer.domElement
    console.log('🔄 重新绑定容器...')

    // 移动canvas
    if (canvas.parentNode !== container) {
      if (canvas.parentNode) {
        canvas.parentNode.removeChild(canvas)
      }
      container.appendChild(canvas)
    }

    // 2. 断开旧 ResizeObserver
    if (this.resizeObserver) {
      this.resizeObserver.disconnect()
      this.resizeObserver = null
    }

    // 3. 重新绑定 ResizeObserver(🔥核心)
    this.initResizeHandler(container)
    console.log(container.clientWidth, container.clientHeight)

    // 4. 强制同步一次尺寸
    const width = container.clientWidth
    const height = container.clientHeight

    this.renderer.setSize(width, height)
    this.camera.aspect = width / height
    this.camera.updateProjectionMatrix()

    if (this.controls) {
      this.controls.update()
    }

    console.log('✅ Three.js 已重新绑定容器')
  }
}

// 导出单例
const threeManager = new ThreeManager()

export default threeManager

/**
 * 在vue组件中,比如地图点击点位事件中,使用如下方式初始化threejs
async handleMarkerClick() {
  // 添加loading状态
  this.threeLoading = true;
  try {
    // 1. 初始化或复用Three.js环境
    if (!this.$threeManager.isInitialized) {
      await this.$threeManager.init(this.$refs.container); //threejs容器
    } else {
      // 直接DOM操作
      const canvas = this.$threeManager.renderer.domElement;
      if (canvas.parentNode !== this.$refs.container) {
        // 先移除原位置(避免DOM错误)
        if (canvas.parentNode) {
          canvas.parentNode.removeChild(canvas);
        }
        this.$refs.container.appendChild(canvas);
        // 更新渲染器尺寸(重要!)
        this.$threeManager.renderer.setSize(
          this.$refs.container.clientWidth,
          this.$refs.container.clientHeight
        );
        this.$threeManager.camera.aspect =
          this.$refs.container.clientWidth / this.$refs.container.clientHeight;
        this.$threeManager.camera.updateProjectionMatrix();
      }
    }
    // 2. 加载设备模型
    const model = await this.$threeManager.loadDeviceModel(deviceId);
    // 3. 开始渲染
    this.$threeManager.startRenderLoop();
  } catch (error) {
    console.error('Three.js操作失败:', error);
    this.$message.error('3D视图加载失败');
  } finally {
    this.threeLoading = false;
  }
}

 */

GLTFLoader 主要就是为 .gltf 和 .glb 设计的。我这里用的都是.glb格式的模型文件。

.gltf - JSON 格式的 GLTF 文件
.glb - 二进制格式的 GLTF 文件 ,文件更小,加载更快

在这里插入图片描述

更多推荐