Skip to content

Vue实现3D工厂可视化大屏展示及交互

最近考虑做一个3D工厂的可视化大屏项目,现在大屏项目越来越成熟,之前的拖拉拽开发大屏的技术路线基本上已经被各家放弃的差不多了。

现在开发大屏更多的时候是依赖于公司以前的页面 / 框架进行基础的搭建,然后再由前端开发根据客户的需求进行定制化改造的方式进行。

据我了解,现在很多的中小公司做3D大屏都是这么玩儿的。当然像帆软、优诺这种貌似不是,除非是非常特殊的需求,常见的人家都有比较成熟的解决方案,所以用不着再开发了。

废话不多说了,简单用 Vue3 + TypeScript + Vite + Three.js 搭建了一个 3D 可视化工厂的Demo。

安装依赖

bash
// 安装 three.js
npm install three @types/three
// 推荐安装 draco3dgltf
npm install draco3dgltf

注意:这里安装的 draco3dgltf 插件是针对压缩过的模型进行解码的插件。如果直接把3D模型放在服务器上,拉取静态资源的过程非常之慢。

项目结构

这里我没有详述 Vite 的安装和使用,我想一个基础的前端开发也应该懂这些。真的不太清楚可以去 Vite 官网了解一下。

推荐模型文件放在 public 文件夹下,避免打包过程中出现找不到的问题。

digital-twin/
├── public/
│   └── models/
│       └── factory.glb      # 模型文件
├── src/
│   ├── components/
│   │   └── FactoryViewer.vue
│   └── App.vue
└── main.ts

代码实现

html
<template>
  <div ref="container" class="viewer-container"></div>
</template>

<script setup lang="ts">
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
// 支持 Draco 压缩模型
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { onMounted, onUnmounted, ref } from 'vue'

const container = ref<HTMLDivElement | null>(null)

let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let animationId: number | null = null

onMounted(() => {
  if (!container.value) return

  // === 场景 ===
  scene = new THREE.Scene()
  scene.background = new THREE.Color(0xf0f0f0)
  scene.fog = new THREE.Fog(0xf0f0f0, 20, 100)

  // === 相机 ===
  camera = new THREE.PerspectiveCamera(
    45,
    container.value.clientWidth / container.value.clientHeight,
    0.1,
    1000
  )
  camera.position.set(20, 15, 20)

  // === 渲染器 ===
  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(container.value.clientWidth, container.value.clientHeight)
  renderer.setPixelRatio(window.devicePixelRatio)
  container.value.appendChild(renderer.domElement)

  // === 控制器 ===
  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true
  controls.dampingFactor = 0.05

  // === 光源 ===
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
  scene.add(ambientLight)

  const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
  directionalLight.position.set(10, 20, 15)
  directionalLight.castShadow = true
  scene.add(directionalLight)

  // === 加载模型 ===
  const loader = new GLTFLoader()

  // 使用 Draco 压缩模型进行解码
  const dracoLoader = new DRACOLoader()
  dracoLoader.setDecoderPath('/draco/') // 需要把 node_modules/draco3dgltf/build/ 下的 wasm 文件复制到 public/draco/
  loader.setDRACOLoader(dracoLoader)

  loader.load(
    '/models/factory.glb', // 确保模型放在 public/models/ 下
    (gltf) => {
      const model = gltf.scene
      scene.add(model)

      // 自动计算模型包围盒并居中
      const box = new THREE.Box3().setFromObject(model)
      const center = box.getCenter(new THREE.Vector3())
      const size = box.getSize(new THREE.Vector3())

      // 重置模型位置到原点
      model.position.x -= center.x
      model.position.y -= center.y
      model.position.z -= center.z

      // 调整相机距离以完整显示模型
      const maxDim = Math.max(size.x, size.y, size.z)
      const fov = camera.fov * (Math.PI / 180)
      let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2))
      cameraZ *= 1.5 // 留点边距
      camera.position.z = cameraZ
      camera.position.y = maxDim / 2
      camera.lookAt(0, 0, 0)
      controls.update()
    },
    undefined,
    (error) => {
      console.error('加载模型失败:', error)
    }
  )

  // === 动画循环 ===
  const animate = () => {
    animationId = requestAnimationFrame(animate)
    controls.update()
    renderer.render(scene, camera)
  }
  animate()

  // === 窗口缩放适配 ===
  const handleResize = () => {
    if (!container.value) return
    camera.aspect = container.value.clientWidth / container.value.clientHeight
    camera.updateProjectionMatrix()
    renderer.setSize(container.value.clientWidth, container.value.clientHeight)
  }

  window.addEventListener('resize', handleResize)

  onUnmounted(() => {
    window.removeEventListener('resize', handleResize)
    if (animationId) cancelAnimationFrame(animationId)
    if (renderer) renderer.dispose()
    if (container.value) container.value.innerHTML = ''
  })
})
</script>

<style scoped>
.viewer-container {
  width: 100%;
  height: 100vh;
  background: #f0f0f0;
}
</style>

总结

这里只是简单实现了一下模型在web页面的渲染,并没有深入实现内部的点击、告警、视图切换、楼体炸开效果等等。

后面有时间了都会一一实现,希望能给大家带来帮助。