three.js 机泵模型展示

最近做一个机泵项目,要求导入一个已有的机泵3D模型在浏览器中显示。前端使用vue来完成。现在将模型封装成一个组件pump.vue. 首先确保项目已经安装了three. (npm install three).

其中label是标签,可以在模型中完成对象的标注,这一功能使用three.js中的sprite来完成。有一个问题暂时还没有解决,就是在函数updateLabel以及函数genCanvas中存在内存泄漏的问题,即在更新label的时候并没有释放掉上一个label的空间,导致浏览器的GPU内存占用持续上涨。初步认为是genCanvas中的生成canvas画布产生的泄露,但这个功能被弃用所以暂时没有解决。

有一个高亮的需求使用 RenderPass, EffectComposer, OutlinePass 来完成,引入时需要安装 three-outlinepass (当然可以直接引入three中的这些包,但是我懒得找)。测试时在添加FBX模型那个函数中把需要高亮显示的对象添加到 selectedLists 列表中。然后在outlineObj函数中处理。最后在animate函数中对composer进行渲染。这里有个坑,我后面注释了一行 renderer.render(scene, camera); 如果没有讲这句话去掉,高亮的composer就不会显示。
图1  组件效果图

<template>
  <div ref="screen" style="flex: 1">
    <div id="container"></div>
  </div>

</template>

<script>

  import * as THREE from 'three';
  import {GLTFLoader} from 'three/examples/jsm/loaders/GLTFLoader';
  // import {MTLLoader, OBJLoader} from 'three-obj-mtl-loader';
  import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader';
  import elementResizeDetectorMaker from 'element-resize-detector';

  import { RenderPass, EffectComposer, OutlinePass } from 'three-outlinepass'

  let camera = '',
    scene = '',
    renderer = '';
  let container = '';
  let controls = '';
  let gridHelper = '';
  let axisHelper = '';
  let modelGroup = '';
  let mixer = '';
  let clock = '';
  let ctxes = [];
  let composer = '';
  let outlinePass = '';
  let renderPass = '';

  export default {
    name: "pump",

    props: {
      'modelFilename': {
        type: String,
        default: 'static/models/mx/mx.FBX'
      },
      'metFilename': {
        type: String,
        default: 'static/models/mx/mx.mtl'
      },
      'configs': {
        type: Object,
        default: () => {
          return {
            bgColor: '#000000',
            bgAlpha: 0.8
          }
        }
      }
    },

    data() {
      return {
        models: [],
        lights: [],
        labels: [],
        labelFont: '120px "华文彩体"',
        aniPlay: true,       // 控制动画是否播放
        labelScale: 0.12,      // 标签模型缩放比例
        laCvsWidth: 1024,
        laCvsHeight: 512,
        selectedList: [],      //选中的物体列表
      }
    },

    methods: {

      init(modelFilename, metFilename, modelType) {

        if (modelType === null || modelType === 'fbx') {

          this.sceneInit()

          // console.log(scene)
          // this.addObjModel(modelFilename, 1, metFilename);
          // this.addGltfModel('static/models/Bee.glb', 0.05, '')

          return this.addFBXModel(modelFilename, 0)
        } else {
          if (modelType === 'obj') {
            this.addObjModel(modelFilename, metFilename, 1)
          }
        }


      },

      sceneInit() {
        container = document.getElementById('container');
        scene = new THREE.Scene();      //创建场景
        camera = new THREE.PerspectiveCamera      //创建相机
          (65, container.clientWidth / container.clientHeight, 0.1, 100000);
        camera.position.set(-120, 0, 400);

        renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});     //创建渲染器,antialias抗锯齿
        renderer.setPixelRatio(window.devicePixelRatio)
        renderer.setSize(container.clientWidth, container.clientHeight);      //设置渲染器的尺寸
        // renderer.setClearColor(this.configs.bgColor, this.configs.bgAlpha);      //设置背景颜色
        container.appendChild(renderer.domElement);     //渲染器加入到dom中,这个元素是一个canvas
        // renderer.render(scene, camera)

        let ambient = this.addLight('AmbientLight', {color: 0x555555})
        let pointLight = this.addLight('PointLight', {color: 0xaaaaaa})
        let pointLight2 = this.addLight('PointLight', {color: 0xaaaaaa})
        pointLight.position.set(600, 800, 1000);
        pointLight2.position.set(-600, 800, 1000)
      },

      openGrid(isOpen) {
        if (isOpen === true) {
          gridHelper = new THREE.GridHelper(1000, 100, 0x888888, 0x444444);
          gridHelper.position.y = 0;
          gridHelper.position.x = 0;
          gridHelper.name = "Grid";
          scene.add(gridHelper);
        } else
          scene.remove(gridHelper)
      },

      openAxis(isOpen) {
        if (isOpen === true) {
          axisHelper = new THREE.AxisHelper(1000);
          scene.add(axisHelper);
        } else
          scene.remove(axisHelper)
      },

      addLight(type, params) {
        let light = new THREE[type](params.color)
        this.lights.push(light)
        scene.add(light)
        return light
      },

      // 添加Obj模型
      addObjModel(modelFilename, metFilename, scale) {

        let mtlLoader = new MTLLoader();
        let objLoader = new OBJLoader();
        mtlLoader.load(metFilename, (materials) => {
          materials.preload()
          objLoader.setMaterials(materials)
          objLoader.load(modelFilename, (object) => {
            // object.position.set(-13.2, -0.3, 0)
            // object.scale.set(scale, scale, scale);
            // group.add(object)
            scene.add(object)
            console.log('obj模型加载成功');
            console.log(object)
            // camera.lookAt(new THREE.Vector3(10, 0, 0))
          })
        }, () => {
          console.log('obj正在加载');
        })


        // let loader = new OBJLoader()
        // // obj.setMaterials(new THREE.MeshNormalMaterial().clone())
        // loader.load(modelFilename , obj => {
        //   obj.position.set(-6.7, 0, 0.2);
        //   obj.scale.set(scale, scale, scale);
        //   let material = new THREE.MeshLambertMaterial({color: '#ffffff'});
        //   // console.log(material)
        //   // 加载完obj文件是一个场景组Group,遍历它的子元素,每一个元素是一个Mesh
        //   obj.children.forEach(child => {
        //     child.material = material;
        //     child.geometry.computeFaceNormals();
        //     child.geometry.computeVertexNormals();
        //   });
        //   scene.add(obj);
        //
        //   console.log('模型添加完成');
        //
        //   this.models.push(obj)
        // })

      },

      // 添加FBX模型
      addFBXModel(modelFilename, scale) {

        let _this = this

        return new Promise((resolve, reject) => {
          let fLoader = new FBXLoader()
          fLoader.load(modelFilename, object => {
            mixer = new THREE.AnimationMixer(object);
            let action = mixer.clipAction(object.animations[0]);
            action.play();

            object.traverse(function (child) {

              if (child instanceof THREE.Mesh) {

                child.castShadow = true;
                child.receiveShadow = true;

                // 添加高亮的位置
                if (Math.random() < 0.5)
                  _this.selectedList.push(child)

              }

            });

            scene.add(object);

            // console.log(object)

            this.models.push(object);

            // this.getPosition();

            // // 添加标签
            // let label1 = this.addLabel({x: -190, y: 170, z: 0},
            //     {
            //       title: '电机非驱动端',
            //       hori: '未知',
            //       vert: '未知',
            //       axis: '未知'
            //     }, this.labelFont)
            // let label2 = this.addLabel({x: -80, y: 200, z: 0},
            //     {
            //       title: '电机驱动端',
            //       hori: '未知',
            //       vert: '未知',
            //       axis: '未知'
            //     }, this.labelFont)
            // let label3 = this.addLabel({x: 130, y: 200, z: 0},
            //     {
            //       title: '水泵驱动端',
            //       hori: '未知',
            //       vert: '未知',
            //       axis: '未知'
            //     }, this.labelFont)
            // let label4 = this.addLabel({x: 240, y: 160, z: 0},
            //     {
            //       title: '水泵非驱动端',
            //       hori: '未知',
            //       vert: '未知',
            //       axis: '未知'
            //     }, this.labelFont)

            resolve();          // 加载完成后的Promise要调用resolve函数


          })
        })

      },

      // 添加Gltf模型
      addGltfModel(modelFilename, metFilename, scale) {

        let loader = new GLTFLoader();
        loader.load(modelFilename, obj => {
          // console.log(obj);
          obj.scene.scale.set(scale, scale, scale)
          obj.scene.position.set(-5, 0, -1);
          scene.add(obj.scene);
        });
      },

      addLabel(pos, text) {

        // 这个scale参数暂时没有用,函数内部已经写死了
        // console.log(this.models)


        let canvas = this.genCanvas(text)

        let texture = new THREE.CanvasTexture(canvas);
        texture.needsUpdate = true
        let spriteMaterial = new THREE.SpriteMaterial(
          {map: texture, sizeAttenuation: false});
        let label = new THREE.Sprite(spriteMaterial);
        let scaleY = this.labelScale;
        let scaleX = scaleY * canvas.width / canvas.height;
        label.position.set(pos.x, pos.y, pos.z);
        // sprite默认会令canvas变形 需要通过scale调整比例
        label.scale.set(scaleX, scaleY, 1);
        scene.add(label);
        this.labels.push(label);

        // label.materialObj = spriteMaterial;
        // label.textureObj = texture;
        label.textCanvas = canvas;

        texture.dispose();
        spriteMaterial.dispose();

        return label;

      },

      rmLabel(label) {
        // console.log(label)
        scene.remove(label)
        console.log(label)
        label.materialObj.dispose()
        label.textureObj.dispose()
        label.geometry.dispose()
        label.textCanvas = null
        label = null
        // console.log(label)
        // label.dispose()
        // console.log(label)
      },

      updateLabel(label, newText) {

        if (newText) {
          // let pos = label.position
          // let newLabel = this.addLabel(pos, newText, this.labelFont)
          //将原来的列表中对应的标签替换掉
          // let index = this.labels.indexOf(label)
          // this.labels.splice(index, 1, newLabel)
          // this.rmLabel(label)
          console.log(label)
          // let newCanvas = this.genCanvas(newText, this.labelFont)
          let newCanvas =

            // label.textureObj.dispose()
            // label.materialObj.dispose()
            // label.textCanvas = null
            // window.gc()
            // console.log(label.material.map)
            label.textCanvas = null

          // label.material.map.dispose()
          // label.material.map.image.dispose()
          // label.material.map = null
          // window.gc()
          label.material.map.image = newCanvas
          label.material.map.dispose()

          newCanvas.remove()
          newCanvas = null
          // label.textureObj = texture
          // label.materialObj.map = texture
          // label.textCanvas = newCanvas

          // console.log(new Date())
          //
          // console.log(this.labels)
          //
          // console.log(this.labels)
        }
      },

      genCanvas(text) {
        // 绘制canvas作为sprite的贴图
        let canvas = document.createElement("canvas");
        let ctx = canvas.getContext("2d");
        ctxes.push(ctx)       // 将当前的ctx保存
        let title = text.title;
        if (ctx !== null) {
          let textWidth =
            title.length * Number(this.labelFont.split(' ')[0].replace('px', ''))
          let textHeight =
            Number(this.labelFont.split(' ')[0].replace('px', ''))
          // console.log(textWidth + '  ' + textHeight)
          canvas.width = this.laCvsWidth;
          canvas.height = this.laCvsHeight;

          // console.log(canvas.width + '    ' + canvas.height)

          // ctx.fillStyle = "#333";
          // ctx.fillRect(0, 0, canvas.width, canvas.height)


          ctx.font = this.labelFont;
          ctx.fillStyle = "#478dc6";

          ctx.fillText(title, 50, textHeight);

          ctx.fillStyle = "#fff";

          ctx.font = '90px Arial'
          ctx.fillText('垂直:' + text.vert, 50, 2 * textHeight + 10);
          ctx.fillText('水平:' + text.hori, 50, 3 * textHeight + 10);

          if (text.axis !== '')
            ctx.fillText('轴向:' + text.axis, 50, 4 * textHeight + 10);

        }

        return canvas
      },

      testLabel() {
        let newText1 = {
          title: '电机非驱动端',
          hori: Math.random().toFixed(2) + '',
          vert: Math.random().toFixed(2) + '',
          axis: Math.random().toFixed(2) + '',
        }
        let newText2 = {
          title: '电机驱动端',
          hori: Math.random().toFixed(2) + '',
          vert: Math.random().toFixed(2) + '',
          axis: Math.random().toFixed(2) + '',
        }
        let newText3 = {
          title: '水泵驱动端',
          hori: Math.random().toFixed(2) + '',
          vert: Math.random().toFixed(2) + '',
          axis: Math.random().toFixed(2) + '',
        }
        let newText4 = {
          title: '水泵非驱动端',
          hori: Math.random().toFixed(2) + '',
          vert: Math.random().toFixed(2) + '',
          axis: Math.random().toFixed(2) + '',
        }
        this.updateLabel(this.labels[0], newText1)
        this.updateLabel(this.labels[1], newText2)
        this.updateLabel(this.labels[2], newText3)
        this.updateLabel(this.labels[3], newText4)
      },

      getPosition() {

        let meshSet = this.models[0].children
        let posSet = meshSet.map(item => item.position)

        // for (let item of meshSet) {
        //   posSet.push(item.position)
        // }

        console.log('--------获取的模型坐标集合-------')
        console.log(posSet)
        console.log('--------------------------------')


      },

      aniControl(flag) {
        if (flag !== null) {
          this.aniPlay = flag
        } else {
          this.aniPlay = !this.aniPlay
          // let tip = '动画' + (this.aniPlay === true ? '开始播放' : '停止播放')
          // console.log(tip)
        }
      },

      outlineObj(selectedObjects) {

        composer = new EffectComposer(renderer); // 特效组件

        renderPass = new RenderPass(scene, camera);
        composer.addPass(renderPass); // 特效渲染

        outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
        composer.addPass(outlinePass); // 加入高光特效

        outlinePass.pulsePeriod = 2; //数值越大,律动越慢
        outlinePass.visibleEdgeColor.set('#c413e8'); // 高光颜色
        outlinePass.hiddenEdgeColor.set('#585353');// 阴影颜色
        outlinePass.usePatternTexture = false; // 使用纹理覆盖?
        outlinePass.edgeStrength = 5; // 高光边缘强度
        outlinePass.edgeGlow = 2; // 边缘微光强度
        outlinePass.edgeThickness = 2; // 高光厚度

        outlinePass.selectedObjects = selectedObjects; // 需要高光的obj
      },

      animate() {
        requestAnimationFrame(this.animate);
        // this.Mesh.rotation.x += 0.01;
        // this.Mesh.rotation.y += 0.01;

        let delta = clock.getDelta();
        if (this.aniPlay) {
          if (mixer !== '')   mixer.update(delta);
        }

        if (composer !== '')
          composer.render(delta)

        // renderer.render(scene, camera);
      },

    },

    mounted() {

      let div = this.$refs.screen

      this.init(this.modelFilename, this.metFilename, null).then(() => {

        clock = new THREE.Clock();

        camera.position.x = -250
        camera.position.y = 50
        camera.lookAt(-100, 0, 0)
        // this.openGrid(true);
        // this.openAxis(true);
        // controls = new OrbitControls(camera, renderer.domElement)     //加入轨道控制

        for (let item of this.models) {
          item.position.x = -90
          item.position.y = -20
          item.position.z = 20
        }

        console.log('当前的已选择列表')
        console.log(this.selectedList)

        this.outlineObj(this.selectedList)

        this.animate();

        setInterval(this.aniControl, 2000, null)

        // setInterval(this.testLabel, 1000)

        // resolve()

      })

      let erd = elementResizeDetectorMaker()
      erd.listenTo(div, (element) => {
        // console.log(element.clientHeight + ' ' + element.clientWidth)
        camera.aspect = (element.clientWidth) / (element.clientHeight);
        camera.updateProjectionMatrix();
        renderer.setSize((element.clientWidth), (element.clientHeight));
      });

      // window.addEventListener('resize', () => {
      //   camera.aspect = (window.innerWidth - 30) / (window.innerHeight - 100);
      //   camera.updateProjectionMatrix();
      //   renderer.setSize((window.innerWidth - 30), (window.innerHeight - 100));
      // });

    },

  }
</script>

<style lang="scss" scoped>


  #container {
    margin: 0;
    overflow: hidden;
    background: url("../../../src/assets/comImgs/bigBg.png") center no-repeat;
    background-size: cover;
  }

  /*.tap {*/
  /*  position: absolute;*/
  /*  background-color: MidnightBlue;*/
  /*  background-color: rgba(0, 10, 40);*/
  /*  border-top-left-radius: 10px;*/
  /*  border-bottom-right-radius: 10px;*/
  /*  opacity: 0.5;*/
  /*  font-size: 4px;*/
  /*  !*color: aqua;*!*/
  /*  !*width: 36px;*!*/
  /*  !*height: 44px;*!*/
  /*  padding: 1px 1px 1px;*/
  /*}*/

</style>

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值