threejs 给3d模型添加标注 实例

js yekong

要求

项目要求,在3d模型中显示标注,并且可以点击标注进行弹窗。

实现

threejs 给3d模型添加标注,首先需要让建模师在模型中添加网格模型并按照指定要求命名,然后请求接口,获取名称,查询模型名称获取坐标,并渲染在指定位置。

threejs 给3d模型添加标注

代码

主组件

<template>
  <div class="canvasGLTFBody">
    <div ref='canvasGLTF' class="canvasGLTF" id="canvasGLTF">
    </div>
  </div>
</template>

<script>
import { renderer } from './RenderLoop.js'
import {labelRenderer} from '@/components/environment/scene/tag'

export default {
  name: 'GLTFCanvas',
  data () {
    return {
      list: []
    }
  },
  created: function () {

  },
  mounted: function () {
    var that = this
    that.$refs.canvasGLTF.appendChild(renderer.domElement)
    that.$refs.canvasGLTF.appendChild(labelRenderer.domElement)
  },
  methods: {}
}
</script>

<style lang="scss" scoped>
.canvasGLTFBody {
  position: absolute;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: nowrap;
  flex-direction: row;
  align-content: flex-start;
}

.canvasGLTF {
  position: relative;
  width: 2000px;
  height: 822px;
}

.pa {
  position: absolute;
  top: 0;
  left: 0;
}

.label {
  color: red;
}
</style>

标注组件

/**
* @Author: 858834013@qq.com
* @Name: item
* @Date: 2022-11-10
* @Desc:
*/
<template>
  <div class="point" id="point1" style="display: none">
    <div class="environment2">
      <div class="inforight cur">
        <i id="el-icon-circle-close" class="el-icon-circle-close"></i>
        <p>实时状态</p>
        <span id="wendu">温度:23.6℃</span>
        <span id="shidu">湿度:63%</span>
        <div class="echartsEnvironment" id="echartsEnvironment">
        </div>
      </div>
    </div>
  </div>
</template>

<script>

export default {
  name: 'item',
  components: {},
  props: {
    type: {
      type: String,
      default () {
        return '0'
      }
    },
    left: {
      type: String,
      default () {
        return '0'
      }
    },
    top: {
      type: String,
      default () {
        return '0'
      }
    },
    index: {
      type: String | Number,
      default () {
        return '0'
      }
    },
  },
  data () {
    return {
      show: false
    }
  },
  watch: {},
  mounted () {
  },
  methods: {}
}
</script>

<style lang="scss">
.environment2 {
  position: absolute;
  left: 0px;
  top: 0px;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: nowrap;
  flex-direction: column;
  align-content: flex-start;
  pointer-events: initial;

  .typebg {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: nowrap;
    flex-direction: row;
    align-content: flex-start;
    background: url("../../assets/environment/itembg1.png");
    width: 50px;
    height: 50px;
  }

  .imgdown {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: nowrap;
    flex-direction: row;
    align-content: flex-start;
    margin-top: 4px;
    background: url("../../assets/environment/icon_down.png");
    width: 18px;
    height: 18px;
  }

  .info {
    width: 130px;
    height: 84px;
    background: #000000;
    border: 1px solid #FFFFFF;
    box-shadow: 0px 6px 14px 0px rgba(10, 11, 11, 0.6);
    //opacity: 0.76;
    border-radius: 10px;
    display: flex;
    justify-content: flex-start;
    align-items: flex-start;
    flex-wrap: nowrap;
    flex-direction: column;
    align-content: flex-start;
    margin-bottom: 12px;

    p {
      font-size: 14px;
      font-family: MiSans;
      font-weight: 400;
      padding-left: 15px;
      color: #FFFFFF;
      padding-top: 5px;
      padding-bottom: 5px;
    }

    span {
      font-size: 14px;
      font-family: MiSans;
      font-weight: 400;
      color: #A9DDEE;
      padding-bottom: 5px;
      padding-left: 15px;
    }
  }

  .inforight {
    width: 592px;
    height: 310px;
    position: relative;
    background: rgba(#0E2B47, 0.76);
    border: 1px solid rgba(#FFFFFF, 0.76);
    border-top: 3px solid rgba(248, 130, 76, 1);
    display: flex;
    justify-content: flex-start;
    align-items: flex-start;
    flex-wrap: nowrap;
    margin-left: 150px;
    margin-top: -80px;
    flex-direction: column;
    align-content: flex-start;

    p {
      font-size: 18px;
      font-family: MiSans;
      font-weight: 400;
      padding-left: 15px;
      padding-top: 10px;
      color: #E2EBF1;
      margin-bottom: 10px;
    }

    i {
      color: #fff;
      font-size: 24px;
    }

    span {
      font-size: 18px;
      font-family: MiSans;
      font-weight: 400;
      color: #A9DDEE;
      padding-bottom: 5px;
      padding-left: 15px;
    }
  }

  .infos {
    width: 170px;
    height: 106px;
    margin-bottom: 12px;
  }

  .info0 {
    //border-top: 3px solid rgba(66, 194, 151, 1);
  }
}

.environment {
  width: 160px;
  position: absolute;
  left: -80px;
  top: -170px;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: nowrap;
  flex-direction: column;
  align-content: flex-start;
  pointer-events: initial;

  .typebg {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: nowrap;
    flex-direction: row;
    align-content: flex-start;
    background: url("../../assets/environment/itembg1.png");
    width: 50px;
    height: 50px;
  }

  .imgdown {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: nowrap;
    flex-direction: row;
    align-content: flex-start;
    margin-top: 4px;
    background: url("../../assets/environment/icon_down.png");
    width: 18px;
    height: 18px;
  }

  .info {
    width: 100%;
    height: 84px;
    background: #000000;
    border: 1px solid #FFFFFF;
    box-shadow: 0px 6px 14px 0px rgba(10, 11, 11, 0.6);
    //opacity: 0.76;
    border-radius: 10px;
    display: flex;
    justify-content: flex-start;
    align-items: flex-start;
    flex-wrap: nowrap;
    flex-direction: column;
    align-content: flex-start;
    margin-bottom: 12px;

    p {
      font-size: 14px;
      font-family: MiSans;
      font-weight: 400;
      padding-left: 15px;
      color: #FFFFFF;
      padding-top: 5px;
      padding-bottom: 5px;
    }

    span {
      font-size: 14px;
      font-family: MiSans;
      font-weight: 400;
      color: #A9DDEE;
      padding-bottom: 5px;
      padding-left: 15px;
    }
  }

  .inforight {
    width: 592px;
    height: 310px;
    background: rgba(#0E2B47, 0.76);
    border: 1px solid rgba(#FFFFFF, 0.76);
    border-top: 3px solid rgba(248, 130, 76, 1);
    display: flex;
    justify-content: flex-start;
    align-items: flex-start;
    flex-wrap: nowrap;
    margin-left: 150px;
    margin-top: -80px;
    flex-direction: column;
    align-content: flex-start;

    p {
      font-size: 18px;
      font-family: MiSans;
      font-weight: 400;
      padding-left: 15px;
      padding-top: 10px;
      color: #E2EBF1;
      margin-bottom: 10px;
    }

    span {
      font-size: 18px;
      font-family: MiSans;
      font-weight: 400;
      color: #A9DDEE;
      padding-bottom: 5px;
      padding-left: 15px;
    }
  }

  .infos {
    width: 170px;
    height: 106px;
    margin-bottom: 12px;
  }

  .info0 {
    //border-top: 3px solid rgba(66, 194, 151, 1);
  }
}

.echartsx {
  margin-top: -30px;
}

.point {
  //position: absolute;
  //left: 50%;
  //top: 50%;
  width: 0;
  height: 0;
  position: absolute;

  .el-icon-circle-close {
    position: absolute;
    right: 10px;
    top: 10px;
  }
}

.point.visible {
  //display: none !important;
}

.echartsEnvironment {
  position: relative;
  width: 100%;
  height: calc(100% - 0px);
}
</style>

场景文件

// 场景总文件
// 引入Three.js
import * as THREE from 'three/build/three.module.js';
import { model } from './model.js';
/**
 * 创建场景对象Scene
 */
var scene = new THREE.Scene();
scene.add(model);//粮仓基地三维模型添加到场景中

// 设置雾化效果,雾的颜色和背景颜色相近,这样远处网格线和背景颜色融为一体
// scene.fog = new THREE.Fog(0x005577, -100, 1000);
/**
* 光源设置
*/
// 平行光1
var directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(400, 200, 300);
scene.add(directionalLight);
// 平行光2
var directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight2.position.set(-400, -200, -300);
scene.add(directionalLight2);
//环境光
var ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);

// Three.js三维坐标轴 三个坐标轴颜色RGB分别对应xyz轴
var axesHelper = new THREE.AxesHelper(250);
// scene.add(axesHelper);

export { scene };

模型文件

// 引入Three.js
import * as THREE from 'three/build/three.module.js'
// 引入gltf模型加载库GLTFLoader.js
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import axios from 'axios'
import { devIp } from '@/api/ipConfig'
import { tag } from './tag.js'//HTML标签相关代码

var model = new THREE.Group()//声明一个组对象,用来添加加载成功的三维场景
var loader = new GLTFLoader() //创建一个GLTF加载器
loader.load('assets/立库d.gltf', function (gltf) {//gltf加载成功后返回一个对象
  // console.log('gltf对象场景属性', gltf.scene);
  var scale = 0.09
  gltf.scene.position.set(0, 8, 0)
  gltf.scene.rotation.set(-0.1, 0, 0)
  gltf.scene.scale.set(scale, scale, scale)
  // 递归遍历gltf.scene
  gltf.scene.traverse(function (object) {
    if (object.type === 'Mesh') {
      // 批量更改所有Mesh的材质
      object.material = new THREE.MeshLambertMaterial({
        map: object.material.map, //获取原来材质的颜色贴图属性值
        color: object.material.color, //读取原来材质的颜色
      })
    }
  })

// 请求的 URL 地址
  var url = devIp + '/jeecg-boot/haitian/hjjk/haiHjjkHumidity'
  var url2 = devIp + '/jeecg-boot/haitian/hjjk/haiHjjkTemperature'
// 获取温度
  axios.get(url2).then(function (res2) {
    // res.data 是服务器返回的数据
    var result2 = res2.data
    axios.get(url).then(function (res) {
      // res.data 是服务器返回的数据
      var result = res.data
      console.log(result)
      result.forEach((type) => {
        var group = gltf.scene.getObjectByName(type.name)
        if (group.type === 'Mesh') {
          var label = tag(group.name, type, result2)//把粮仓名称obj.name作为标签
          var pos = new THREE.Vector3()
          group.getWorldPosition(pos)//获取obj世界坐标、
          label.position.copy(pos)//标签标注在obj世界坐标
          model.add(label)//标签插入model组对象中
        }
      })
    })
  })
  //把gltf.scene中的所有模型添加到model组对象中
  model.add(gltf.scene)
})
export { model }

标注文件

import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
import echarts from 'echarts'

// 创建一个HTML标签
function tag (name, data, list) {
  // 创建div元素(作为标签)
  var div = document.createElement('div')
  div.innerHTML = `
   <div class="point">
    <div class="environment">
      <div class="info cur">
        <p>实时状态</p>
        <span>温度:${data.wdValue}℃</span>
        <span>湿度:${data.sdValue}%</span>
      </div>
      <div class="typebg cur">
      </div>
      <div class="imgdown">
      </div>
    </div>
  </div>`
  var elicon = document.getElementById('el-icon-circle-close')
  elicon.addEventListener('click', function (e) {
    var point1 = document.getElementById('point1')
    point1.style.display = 'none'
  })
  div.addEventListener('click', function (e) {
    // 点击标注进行弹窗
    var xdata = []
    var ydata1 = []
    var ydata2 = []
    list.forEach((type, index) => {
      if (type.wdName == data.name) {
        // xdata.push(index)
        ydata1.push(type.wdValue)
        ydata2.push(type.sdValue)
      }
    })
    for (var i = 0; i < 24; i++) {
      xdata.push(i)
    }
    var point1 = document.getElementById('point1')
    point1.style = div.style.cssText
    point1.style.display = 'block'
    point1.style.zIndex = '10000'
    document.getElementById('wendu').innerText = '温度:' + data.wdValue + '℃'
    document.getElementById('shidu').innerText = '湿度:' + data.sdValue + '%'
    setTimeout(() => {
      let myChart = echarts.init(document.getElementById('echartsEnvironment'))
      var option = {
        grid: {
          left: '5%',
          right: '5%',
          top: '25%',
          bottom: '10%',
          containLabel: true
        },
        tooltip: {
          show: true,
          trigger: 'axis',
        },
        legend: {
          icon: 'circle',
          top: 0,
          right: '1.8%',
          itemWidth: 14,
          itemHeight: 14,
          itemGap: 10,
          textStyle: {
            color: 'rgba(169, 221, 238, 1)',
            fontSize: '16'
          },
          data: ['温度', '湿度'],
        },
        xAxis: {
          data: xdata,
          axisLine: {
            lineStyle: {
              color: 'rgba(94, 115, 152, 1)',
            }
          },
          axisTick: {
            show: false
          },
          axisLabel: {
            color: 'rgba(146, 178, 215, 1)',
            fontSize: 14,
            interval: 0,
          }
        },
        yAxis: [
          {
            name: '温度',
            nameTextStyle: {
              color: 'rgba(255, 255, 255, 0.8)',
              fontSize: 16,
              padding: [20, 0, 0, -40]
            },
            splitNumber: 3,
            axisLine: {
              show: false,
              lineStyle: {
                color: '#708494'
              }
            },
            axisTick: {
              show: false
            },
            axisLabel: {
              color: 'rgba(146, 178, 215, 1)',
              fontSize: 14
            },
            splitLine: {
              show: true,
              lineStyle: {
                color: '#3E536C'
              }
            },
            yAxisIndex: 0
          },
          {
            splitNumber: 3,
            name: '湿度',
            nameTextStyle: {
              color: 'rgba(255, 255, 255, 0.8)',
              fontSize: 16,
              padding: [20, 0, 0, 40]
            },
            axisLine: {
              show: true,
              lineStyle: {
                color: '#3d5269'
              }
            },
            axisTick: {
              show: false
            },
            axisLabel: {
              color: 'rgba(146, 178, 215, 1)',
              fontSize: 14
            },
            splitLine: {
              show: false,
              lineStyle: {
                color: '#3E536C'
              }
            },
            yAxisIndex: 1
          }],
        series: [
          {
            name: '温度',
            type: 'line',
            smooth: true, //是否平滑
            showAllSymbol: true,
            symbol: 'circle',
            yAxisIndex: 0,
            symbolSize: 0,
            lineStyle: {
              normal: {
                color: 'rgba(0, 255, 126, 1)',
              },
            },
            zlevel: 1,
            label: {
              show: false,
              position: 'top',
              textStyle: {
                color: 'rgba(0, 255, 126, 1)',
              }
            },
            itemStyle: {
              color: 'rgba(0, 255, 126, 1)',
              borderColor: '#fff',
              borderWidth: 3,
            },
            data: ydata1
          },
          {
            name: '湿度',
            type: 'line',
            smooth: true,
            showAllSymbol: true,
            symbol: 'circle',
            yAxisIndex: 1,
            symbolSize: 0,
            lineStyle: {
              normal: {
                color: 'rgba(0, 204, 255, 1)',
              },
            },
            zlevel: 1,
            label: {
              show: false,
              position: 'top',
              textStyle: {
                color: 'rgba(0, 204, 255, 1)',
              }
            },
            itemStyle: {
              color: 'rgba(0, 204, 255, 1)',
              borderColor: '#fff',
              borderWidth: 3,
            },
            data: ydata2
          },
        ]
      }
      myChart.clear()
      myChart.resize()
      myChart.setOption(option)
    }, 800)
  })
  div.classList.add('tag')
  //div元素包装为CSS2模型对象CSS2DObject
  var label = new CSS2DObject(div)
  div.style.pointerEvents = 'none'//避免HTML标签遮挡三维场景的鼠标事件
  // 设置HTML元素标签在three.js世界坐标中位置
  // label.position.set(x, y, z);
  return label//返回CSS2模型标签
}

// 创建一个CSS2渲染器CSS2DRenderer
var labelRenderer = new CSS2DRenderer()
labelRenderer.setSize(1900, 822)
labelRenderer.domElement.style.position = 'absolute'
// 相对标签原位置位置偏移大小
labelRenderer.domElement.style.top = '0px'
labelRenderer.domElement.style.left = '0px'
// //设置.pointerEvents=none,以免模型标签HTML元素遮挡鼠标选择场景模型
labelRenderer.domElement.style.pointerEvents = 'none'
document.body.appendChild(labelRenderer.domElement)
// document.getElementById('canvasGLTF').appendChild(labelRenderer.domElement)
export { tag, labelRenderer }

效果演示地址

threejs 给3d模型添加标注演示

喜欢