要求
项目要求,在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 }