vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > vue2 Three.js预览3D文件

vue2引入Three.js预览3D文件的实现方法

作者:weixin_45435220

three.js是一个基于WebGL的JavaScript库,它允许开发者在网页上创建和显示3D图形,这篇文章主要介绍了vue2引入Three.js预览3D文件的实现方法,文中通过代码介绍的非常详细,需要的朋友可以参考下

实现方案

使用occt-import-js将.stp或者.STEP文件解析成可以渲染的模型对象,使用Three.js创建3D场景,渲染模型。我使用的node版本是v18.20.0

引入Three.js:

npm install three

这一步一般很少出错,如果版本冲突可以加上–legacy-peer-deps参数解决。

引入occt-import-js解析模型文件

npm install occt-import-js

这里有个坑,依赖安装之后运行报错

Module parse failed: Unexpected token (3:76)
You may need an appropriate loader to handle this file type.
|
| var occtimportjs = (() => {
>   var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined;
|   if (typeof __filename != 'undefined') _scriptName = _scriptName || __filename;
|   return (

 @ ./node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/views/emsTool/info/preview.vue?vue&type=script&lang=js 207:63-88

这里是说Babel 未正确转译现代 JavaScript 语法,检查了很久咱也不知道到底哪个语法不支持,ai建议我配置 Babel 支持可选链操作符然后强制 Webpack 转译特定依赖,一顿操作发现没啥用,而且问题越整越复杂了。后来灵机一动想着是不是occt-import-js版本太新了,然后就把occt-import-js卸载装了一个更低的版本:

npm install occt-import-js@0.0.20

问题完美解决!

加载occt解析器

这一步需要将occt-import-js脚本加载进来,这样才能使用里面的解析方法。官方推荐了两种方式1、手动加载(如果需要自定义解析器能力时可选);2、使用官方胶水代码加载。
一开始傻傻的跟着资料一步步手动加载,发现问题特别多,这一步也是卡我最久的地方,**这里非常不推荐使用手动加载,直接用官方的胶水代码省时省力还省心。**步骤如下:

  1. 将occt-import-js.js(胶水脚本)和occt-import-js.wasm(核心!!!WASM 二进制文件)复制到public目录下。方便后面在代码中导入。这一步很有必要,因为public目录在Vue项目中是一个特殊目录,这个目录下的文件在构建时会被直接复制到输出目录,不会被Webpack处理。
  2. vue代码中动态注入script标签,即引入该胶水脚本
const script = document.createElement('script');
script.src = `${process.env.BASE_URL}/occt-import-js.js`;
document.head.appendChild(script);

// 然后使用脚本中的occtimportjs()方法去初始化occt实例

初始化3D场景

导入Three.js

import * as THREE from 'three'

这一步就没啥说的,官方文档里写的很清楚,按照流程走就行,详见完整代码。

  1. 创建场景
  2. 初始化相机
  3. 创建渲染器
  4. 添加光源
  5. 添加轨道控制器

创建3D模型方法

  1. 设置顶点数据
  2. 设置法线数据
  3. 设置索引
  4. 创建材质
  5. 创建网格并添加到场景

到这一步基本上准备工作就完成了,接下来就是使用occt实例解析stp文件内容,创建3D模型,然后使用Three.js渲染即可。
这里放出完整代码供各位参考:

<template>
  <div class="stp-renderer">
    <!-- 渲染容器 -->
    <div ref="canvasContainer" class="canvas-container"></div>
  </div>
</template>

<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import api from '@/utils/api'

export default {
  name: 'STPRenderer',
  props: {
    fileId: {  // 从父组件接收文件ID
      type: String,
      required: true
    }
  },
  data() {
    return {
      scene: null,
      camera: null,
      renderer: null,
      controls: null,
      animationId: null,
      occt: null,
    }
  },
  async mounted() {
    console.log('mounted');
    try {
      await this.initOCCT();
      this.initThreeScene();
      if (this.fileId) {
        this.renderModel();
      } 
    } catch (error) {
      console.error(error);
    }
  },
  watch: {
    fileId(newVal) {
      if (newVal) {
        this.renderModel();
      } 
    }
  },

  beforeDestroy() {
    this.cleanupResources()
  },
  methods: {
    async initOCCT() {
      // 动态注入 script 标签
      const script = document.createElement('script');
      script.src = `${process.env.BASE_URL}wasm/occt-import-js.js`;
      document.head.appendChild(script);
      
      // 等待脚本加载完成
      await new Promise((resolve, reject) => {
        script.onload = resolve;
        script.onerror = () => reject(new Error('脚本加载失败'));
      });
      
      return occtimportjs();
    },
    
    // 初始化Three.js场景
    initThreeScene() {
      console.log('initThreeScene');
      const container = this.$refs.canvasContainer
      
      // 1. 创建场景
      this.scene = new THREE.Scene()
      this.scene.background = new THREE.Color(0xf0f0f0);
      
      
      // 2. 初始化相机
      this.camera = new THREE.PerspectiveCamera(
        75,
        container.clientWidth / container.clientHeight,
        0.1,
        1000
      )
      this.camera.position.set(10, 10, 10)
      this.camera.lookAt(0, 0, 0)
      
      // 3. 创建渲染器
      this.renderer = new THREE.WebGLRenderer({ antialias: true })
      this.renderer.setSize(container.clientWidth, container.clientHeight)
      this.renderer.setPixelRatio(window.devicePixelRatio)
      container.appendChild(this.renderer.domElement)
      
      // 4. 添加光源
      const ambientLight = new THREE.AmbientLight(0xffffff, 2.0)
      this.scene.add(ambientLight)
      
      const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5)
      directionalLight.position.set(10, 20, 15)
      this.scene.add(directionalLight);

      // 增加左上角光源
      const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight2.position.set(-10, 5, -10); // 左前方向,增加立体感
      this.scene.add(directionalLight2);

      this.scene.add(new THREE.AxesHelper(10));
      const gridHelper = new THREE.GridHelper(50, 20);
      this.scene.add(gridHelper);
      
      // 5. 添加轨道控制器
      this.controls = new OrbitControls(this.camera, this.renderer.domElement)
      this.controls.enableDamping = true
      this.controls.dampingFactor = 0.05
      
      // 6. 窗口大小自适应
      window.addEventListener('resize', this.handleResize)
    },

    // 核心渲染流程
    async renderModel() {
      console.log('renderModel');
      let loadingInstance = null;
      
      try {
        // 1. 下载文件
        loadingInstance = this.$loading({
          lock: true,
          text: '正在下载文件...',
          spinner: 'el-icon-loading',
          background: 'rgba(0, 0, 0, 0.7)'
        });
        const { buffer } = await api.downloadSTP(this.fileId);
        console.log(buffer);
        
        // 2. 加载OCCT解析器
        loadingInstance.setText('正在解析模型数据...');
        const occt = await occtimportjs();
        console.log(occt)
        
        // 3. 解析STEP文件
        loadingInstance.setText('正在解析模型数据...');
        const fileBuffer = new Uint8Array(buffer)
        // console.log(this.occt);
        const result = occt.ReadStepFile(fileBuffer, null)
        
        if (!result.success) {
          throw new Error(`解析失败: ${result.error || '未知错误'}`)
        }
        
        // 4. 清除旧模型
        this.clearScene()
        
        // 5. 创建3D模型
        this.createModelFromData(result.meshes)
        
      } catch (err) {
        this.$message.error(`渲染失败: ${err.message}`);
        this.cleanupResources();
        this.initThreeScene();
      } finally {
        loadingInstance.close();
        this.startAnimationLoop()
      }
    },

    // 创建3D模型
    createModelFromData(meshes) {
      meshes.forEach((meshData, index) => {
        const geometry = new THREE.BufferGeometry()
        
        // 设置顶点数据
        const positionArray = Array.isArray(meshData.attributes.position.array)
        ? new Float32Array(meshData.attributes.position.array) // 普通数组转TypedArray
        : meshData.attributes.position.array;
        geometry.setAttribute(
          'position',
          new THREE.BufferAttribute(positionArray, 3)
        )
        
        // 设置法线数据
        if (meshData.attributes.normal) {
          const normalArray = Array.isArray(meshData.attributes.normal.array)
          ? new Float32Array(meshData.attributes.normal.array)
          : meshData.attributes.normal.array;
          geometry.setAttribute(
            'normal',
            new THREE.BufferAttribute(normalArray, 3)
          )
        }
        
        // 设置索引
        const indexArray = meshData.index.array;
        let indexTypedArray;
        
        if (Array.isArray(indexArray)) {
          // 根据索引数量选择合适类型
          indexTypedArray = indexArray.length > 65535 
            ? new Uint32Array(indexArray)  // 超过65535用32位
            : new Uint16Array(indexArray); // 否则用16位
        } else {
          indexTypedArray = indexArray;
        }
        
        // 创建索引BufferAttribute
        geometry.setIndex(
          new THREE.BufferAttribute(indexTypedArray, 1)
        )
        // geometry.setIndex(
        //   new THREE.BufferAttribute(meshData.index.array, 1)
        // )
        
        // 计算法线(如果缺失)
        if (!meshData.attributes.normal) {
          geometry.computeVertexNormals()
        }
        
        // 创建材质
        const material = new THREE.MeshPhongMaterial({
          color: meshData.color 
            ? new THREE.Color(...meshData.color) 
            : 0x42a5f5,
          emissive: 0x333333, // 添加自发光避免全黑
          emissiveIntensity: 0.3,
          specular: 0x111111,
          shininess: 50,
          side: THREE.DoubleSide
        })
        
        // 创建网格并添加到场景
        const mesh = new THREE.Mesh(geometry, material)
        this.scene.add(mesh)
      })
      
      // 自动调整视角
      this.autoCenterModel()
    },

    // 自动居中模型
    autoCenterModel() {
      const bbox = new THREE.Box3().setFromObject(this.scene);
      if (bbox.isEmpty()) { // 检查包围盒是否为空
        console.log("模型包围盒为空,未添加任何物体");
        return;
      }
      const center = bbox.getCenter(new THREE.Vector3())
      const size = bbox.getSize(new THREE.Vector3());
      console.log(size.length());
      
      this.camera.position.copy(center)
      this.camera.position.x += size.length() * 1.2
      this.camera.position.y += size.length() * 0.8
      this.camera.position.z += size.length() * 1.2
      
      this.camera.lookAt(center)
      this.controls.target.copy(center)
      this.controls.update()
    },

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

    // 窗口大小调整
    handleResize() {
      if (!this.camera || !this.renderer) {
        return;
      } 
      
      const container = this.$refs.canvasContainer
      this.camera.aspect = container.clientWidth / container.clientHeight
      this.camera.updateProjectionMatrix()
      this.renderer.setSize(container.clientWidth, container.clientHeight)
    },

    // 清理场景
    clearScene() {
      // 保留光源和相机
      const preserveObjects = this.scene.children.filter(
        obj => obj instanceof THREE.Light || obj === this.camera
      )
      
      // 销毁几何体和材质
      this.scene.children.forEach(child => {
        if (child instanceof THREE.Mesh) {
          child.geometry.dispose()
          if (Array.isArray(child.material)) {
            child.material.forEach(mat => mat.dispose())
          } else {
            child.material.dispose()
          }
        }
      })
      
      // 重置场景
      this.scene.children = preserveObjects
    },

    // 释放资源
    cleanupResources() {
      if (this.animationId) {
        cancelAnimationFrame(this.animationId)
      };
      window.removeEventListener('resize', this.handleResize)
      
      if (this.renderer) {
        const container = this.$refs.canvasContainer;
        if (container.contains(this.renderer.domElement)) {
          container.removeChild(this.renderer.domElement);
        }
        this.renderer.dispose();
        this.renderer.forceContextLoss();
        this.renderer = null;
      }
      
      if (this.controls) {
        this.controls.dispose()
      } 
      this.clearScene()
    },

    // 重置组件
    reset() {
      this.cleanupResources()
      this.initThreeScene()
      this.renderModel()
    }
  }
}
</script>

<style scoped>
.canvas-container {
  width: 100%;
  height: 80vh;
  position: relative;
}

.status-overlay, .error-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  padding: 15px;
  text-align: center;
  background: rgba(19, 18, 18, 0.7);
  color: white;
  font-size: 16px;
}

.error-overlay {
  background: rgba(255, 0, 0, 0.7);
}

.error-overlay button {
  margin-left: 10px;
  padding: 5px 10px;
  background: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

补充说明

import axios from 'axios'

export default {
  /**
   * 下载STP文件
   * @param {string} fileId - 文件唯一标识
   * @returns {Promise<ArrayBuffer>} - 文件二进制数据
   */
  async downloadSTP(fileId) {
    try {
      const response = await axios.get('http://' + xxx + fileId, {
        responseType: 'arraybuffer',
        onDownloadProgress: (progressEvent) => {
          const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100)
          console.log(`下载进度: ${percent}%`)
        }
      })
      
      // 从响应头提取文件名
      const contentDisposition = response.headers['content-disposition']
      const filename = contentDisposition 
        ? contentDisposition.split('filename=')[1].replace(/"/g, '') 
        : 'model.stp'
      
      return {
        buffer: response.data,
        filename
      }
    } catch (error) {
      throw new Error(`文件下载失败: ${error.message}`)
    }
  }
}

总结 

到此这篇关于vue2引入Three.js预览3D文件的文章就介绍到这了,更多相关vue2 Three.js预览3D文件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文