成果图
开发过程
- 工具插件three.js
- 先加载模型
- 做水体衔接
- 水位测量标尺
- 水位标记
- 断面标记
- 大坝监测点打点
上代码,技术交流+V: bloxed
<template>
<div class="box w100 h100">
<el-row :gutter="20" v-loading="loading"
element-loading-background="rgba(122, 122, 122, 0.8)"
element-loading-text="模型加载中...">
<el-col :span="24" class="h100" ref="boxRef">
<div id="container" ref="container"></div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import {OrbitControls} from "three/addons/controls/OrbitControls";
import { Water } from 'three/examples/jsm/objects/Water.js';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry';
import TWEEN from '@tweenjs/tween.js';
import { CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
import { instrumentList,waterData,getTooltipData} from "../index.api"
const props = defineProps({
damId:{
type:Number,
default:1
},
epcId:{
type:String,
default:""
}
})
const boxRef = ref<any>(null);
const loading = ref(false);
let scene:any;
let textMesh:any;
let textMeshs:any =[];
let rectMeshs:any =[];
const camera = ref<any>(null);
let water:any;
const renderer = ref<any>(null);
const container = ref<any>(null);
const controls = ref<any>(null);
const loaderT = new FontLoader();
import { useAppStore} from "@/store";
import { set } from '@vueuse/core';
const appStore = useAppStore();
//label ref
const dam_top_heightRef = ref<any>(null);
const tooltipData = ref<any>([])
//刻度尺
const acalesText = [
{id:0,value:"校核洪水位:80.6m",x:-175,y:0,z:-130,color:0xFF0000},
{id:0,value:"设计洪水位:80.6m",x:-175,y:-10,z:-130,color:0xFFFF00},
{id:0,value:"正常水位:80.6m",x:-175,y:-20,z:-130,color:0x00FF00},
{id:0,value:"当前水位:80.6m",x:-175,y:-49,z:-130,color:0xfffffff},
{id:0,value:"死水位:80.6m",x:-175,y:-39,z:-130,color:0xfffffff},
{id:1,value:"50",x:-224,y:-100,z:-130,color:0xfffffff},
{id:2,value:"60",x:-224,y:-80,z:-130,color:0xfffffff},
{id:3,value:"70",x:-224,y:-60,z:-130,color:0xfffffff},
{id:4,value:"80",x:-224,y:-40,z:-130,color:0xfffffff},
{id:5,value:"90",x:-224,y:-20,z:-130,color:0xfffffff},
{id:6,value:"100m",x:-224,y:-0,z:-130,color:0xfffffff},
];
const rectangles = [
//坝顶高程
{id:0,width:48,height:16,x:-20,y:40,z:175,color:0x367DF9 },
//B09
{id:0,width:30,height:10,x:-30,y:0,z:80,color:0x367DF9 },
{id:0,width:30,height:10,x:-30,y:0,z:-30,color:0x367DF9 },
{id:0,width:30,height:10,x:-30,y:0,z:-100,color:0x367DF9 },
//B10
{id:0,width:30,height:10,x:20,y:-20,z:80,color:0x367DF9 },
{id:0,width:30,height:10,x:20,y:-20,z:-30,color:0x367DF9 },
{id:0,width:30,height:10,x:20,y:-20,z:-100,color:0x367DF9 },
//B11
{id:0,width:30,height:10,x:80,y:-30,z:80,color:0x367DF9 },
{id:0,width:30,height:10,x:80,y:-30,z:-30,color:0x367DF9 },
{id:0,width:30,height:10,x:80,y:-30,z:-100,color:0x367DF9 },
];
//label标签
const labels = [
{id:"damTop",value:"坝顶高程:98.6m",x:-20,y:40,z:175,color:0xfffffff},
{id:"B09",value:"B09:0mm",x:-30,y:0,z:80,color:0xfffffff},
{id:"B05",value:"B05:0mm",x:-30,y:0,z:-30,color:0xfffffff},
{id:"B01",value:"B01:0mm",x:-30,y:0,z:-100,color:0xfffffff},
{id:"B10",value:"B10:0mm",x:20,y:-20,z:80,color:0xfffffff},
{id:"B06",value:"B06:0mm",x:20,y:-20,z:-30,color:0xfffffff},
{id:"B02",value:"B02:0mm",x:20,y:-20,z:-100,color:0xfffffff},
{id:"B11",value:"B11:0mm",x:80,y:-30,z:80,color:0xfffffff},
{id:"B07",value:"B7:0mm",x:80,y:-30,z:-30,color:0xfffffff},
{id:"B03",value:"B3:0mm",x:80,y:-30,z:-100,color:0xfffffff},
//断面
{id:3,value:"断面:B0+027",x:-80,y:-30,z:-100,color:0xfffffff},
{id:3,value:"断面:B0+050",x:-80,y:-30,z:-30,color:0xfffffff},
{id:3,value:"断面:B0+073",x:-80,y:-30,z:80,color:0xfffffff},
]
//label标签线
const labelsLine = [
{id:0,x:-10,y:35,z:175, x1:-10,y1:-50,z1:175, color:0x00FFff},
// B09
{id:1,x:-30,y:0,z:80, x1:-30,y1:-50,z1:80, color:0x00FFff},
{id:2,x:-30,y:0,z:-30, x1:-30,y1:-50,z1:-30, color:0x00FFff},
{id:3,x:-22,y:0,z:-100, x1:-22,y1:-50,z1:-100, color:0x00FFff},
//B10
{id:1,x:20,y:-20,z:80, x1:20,y1:-50,z1:80, color:0x00FFff},
{id:2,x:20,y:-20,z:-30, x1:20,y1:-50,z1:-30, color:0x00FFff},
{id:3,x:20,y:-20,z:-100, x1:20,y1:-50,z1:-100, color:0x00FFff},
//B11
{id:4,x:80,y:-30,z:80, x1:80,y1:-80,z1:80, color:0x00FFff},
{id:5,x:80,y:-30,z:-30, x1:80,y1:-80,z1:-30, color:0x00FFff},
{id:6,x:80,y:-30,z:-100, x1:80,y1:-80,z1:-100, color:0x00FFff},
//断面
{id:7,x:-100,y:-34,z:-100, x1:-22,y1:-34,z1:-100, color:0x00FFff},
{id:7,x:-100,y:-34,z:-30, x1:-32,y1:-34,z1:-30, color:0x00FFff},
{id:7,x:-100,y:-34,z:80, x1:-32,y1:-34,z1:80, color:0x00FFff},
]
const labelTwoLines =[
{ id:0,
x:-153,y:-3,z:-130,
x1:-200,y1:-5,z1:-130,
x2:-215,y2:-9,z2:-130,
color:0xFF0000
},
{ id:0,
x:-153,y:-13,z:-130,
x1:-200,y1:-14,z1:-130,
x2:-215,y2:-11,z2:-130,
color:0xFFFF00
},
]
onMounted(() => {
})
const initPage = async ()=>{
loading.value = true;
initScene();
initCamera();
initRenderer();
initControl();
initWater();
await initModel();
await initLine();
await addText();
await addLabelLine();
await addLabelTwoLine();
initRectangle();
flyToCamera();
initAnimate();
initAddClick();
}
// Create the scene
const initScene = ()=>{
scene = new THREE.Scene();
};
// Create the camera
const initCamera= ()=>{
camera.value = new THREE.PerspectiveCamera(45,1153 / 819, 0.1, 1000);
camera.value.position.set(-163,143,-508);
};
// Create the renderer
const initRenderer =()=>{
renderer.value = new THREE.WebGLRenderer({ antialias: true });
renderer.value.setSize(container.value.clientWidth, container.value.clientHeight);
renderer.value.setClearColor('#03243f', 0.1);
renderer.value.domElement.style.position = "absolute";
renderer.value.domElement.style.top = "-160px";
renderer.value.domElement.style.left = "-60px";
container.value.appendChild(renderer.value.domElement);
};
const initControl = ()=>{
controls.value = new OrbitControls(camera.value, renderer.value.domElement);
controls.value.enableDamping = true;
// // 最大角度
controls.value.maxPolarAngle = Math.PI / 2.2;
};
// 创建Raycaster实例
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const initAnimate = ()=> {
water.material.uniforms["time"].value += 1.0 / 60.0;
requestAnimationFrame(initAnimate);
renderer.value.render(scene, camera.value);
controls.value.update();
textMeshs.forEach((mesh:any)=>{
mesh.lookAt(camera.value.position)
})
rectMeshs.forEach((mesh:any)=>{
mesh.lookAt(camera.value.position)
})
TWEEN.update();
// console.log('Camera Position:', camera.value.position);
};
const initWater = ()=>{
const waterGeometry = new THREE.BoxGeometry(200, 280,35);
water = new Water(
waterGeometry,
{
textureWidth: 512,
textureHeight: 512,
waterNormals: new THREE.TextureLoader().load('/3D/water.jpeg', function (texture:any) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
}),
sunDirection: new THREE.Vector3(),
sunColor: 0x007FFF,
waterColor: 0x007FFF,
distortionScale: 3.7,
}
);
water.rotation.x = - Math.PI / 2;
water.position.x = -114;
water.position.y = -81.5;
water.position.z = 10;
scene.add(water)
};
const initLine = ()=>{
const axisMaterial = new THREE.LineBasicMaterial({ color: 0xffffff }); // Blue for X
const yAxisMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 }); // Red for Y
const zAxisMaterial = new THREE.LineBasicMaterial({ color: 0xffff }); // Green for Z
// Create the geometry for the axes
const axisGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-110, -140, -182.2),
new THREE.Vector3(-11, -140, -182.2)
]);
const yAxisGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-215, 0, -130),
new THREE.Vector3(-215, -100, -130)
]);
const zAxisGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(19, 0, -22.2),
new THREE.Vector3(19, 0, 5)
]);
//begin 这里用作刻度线
//水位使用
const zAxisGeometry0 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-160, -60, -130),
new THREE.Vector3(-214, -60, -130)
]);
const zAxisGeometry1 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-210, -100, -130),
new THREE.Vector3(-215, -100, -130)
]);
const zAxisGeometry11 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-210, -90, -130),
new THREE.Vector3(-215, -90, -130)
]);
const zAxisGeometry2 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-205, -80, -130),
new THREE.Vector3(-215, -80, -130)
]);
const zAxisGeometry22 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-210, -70, -130),
new THREE.Vector3(-215, -70, -130)
]);
const zAxisGeometry3 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-205, -50, -130),
new THREE.Vector3(-215, -50, -130)
]);
const zAxisGeometry33 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-210, -40, -130),
new THREE.Vector3(-215, -40, -130)
]);
const zAxisGeometry4 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-205, -30, -130),
new THREE.Vector3(-215, -30, -130)
]);
const zAxisGeometry44 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-210, -20, -130),
new THREE.Vector3(-215, -20, -130)
]);
const zAxisGeometry5 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-205, -10, -130),
new THREE.Vector3(-215, -10, -130)
]);
const zAxisGeometry55 = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-210, 0, -130),
new THREE.Vector3(-215, 0, -130)
]);
//end
// Create the line segments for the axes
const axis = new THREE.Line(axisGeometry, axisMaterial);
const yAxis = new THREE.Line(yAxisGeometry, yAxisMaterial);
const zAxis = new THREE.Line(zAxisGeometry, zAxisMaterial);
//begin 这里用作刻度线
const zAxis0 = new THREE.Line(zAxisGeometry0, axisMaterial);
const zAxis1 = new THREE.Line(zAxisGeometry1, axisMaterial);
const zAxis2 = new THREE.Line(zAxisGeometry2, axisMaterial);
const zAxis3 = new THREE.Line(zAxisGeometry3, axisMaterial);
const zAxis4 = new THREE.Line(zAxisGeometry4, axisMaterial);
const zAxis11 = new THREE.Line(zAxisGeometry11, axisMaterial);
const zAxis22 = new THREE.Line(zAxisGeometry22, axisMaterial);
const zAxis33 = new THREE.Line(zAxisGeometry33, axisMaterial);
const zAxis44 = new THREE.Line(zAxisGeometry44, axisMaterial);
const zAxis5 = new THREE.Line(zAxisGeometry5, axisMaterial);
const zAxis55 = new THREE.Line(zAxisGeometry55, axisMaterial);
//end
// scene.add(axis);
scene.add(yAxis);
// scene.add(zAxis);
//begin 这里用作刻度线
scene.add(zAxis0);
scene.add(zAxis1);
scene.add(zAxis2);
scene.add(zAxis3);
scene.add(zAxis4);
scene.add(zAxis5);
scene.add(zAxis11);
scene.add(zAxis22);
scene.add(zAxis33);
scene.add(zAxis44);
scene.add(zAxis55);
//end
}
const addText =()=> {
loaderT.load(
// font资源URL
'/3D/HONOR_Sans_CN_Regular.json',
// onLoad回调
function (font:any) {
acalesText.forEach(item=>{
const textGeometry = new TextGeometry(item.value, {
font: font,
size: 3.5, // 字体大小
height: 10, // 挤出文本的厚度
})
textGeometry.center() // 居中文本
const materials = new THREE.MeshBasicMaterial({
color: item.color || 0xfffffff,
transparent: true,
opacity: 0.9,
})
textMesh = new THREE.Mesh(textGeometry, materials)
textMesh.position.set(item.x,item.y, item.z)
textMeshs.push(textMesh)
scene.add(textMesh)
})
labels.forEach(item=>{
const textGeometry = new TextGeometry(item.value, {
font: font,
size: 4, // 字体大小
height: 10, // 挤出文本的厚度
})
textGeometry.center() // 居中文本
const materials = new THREE.MeshBasicMaterial({
color: item.color || 0xfffffff,
transparent: true,
opacity: 0.9,
})
textMesh = new THREE.Mesh(textGeometry, materials)
textMesh.position.set(item.x,item.y, item.z)
textMeshs.push(textMesh)
scene.add(textMesh)
})
}
)
}
const addLabelLine = ()=>{
labelsLine.forEach(item=>{
const lineGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(item.x,item.y, item.z),
new THREE.Vector3(item.x1,item.y1, item.z1)
]);
const lineMaterial = new THREE.LineBasicMaterial({ color: item.color || 0xfffffff });
const line = new THREE.Line(lineGeometry, lineMaterial);
scene.add(line)
})
}
//添加拐角线
const addLabelTwoLine = ()=>{
labelTwoLines.forEach(item=>{
const lineGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(item.x,item.y, item.z),
new THREE.Vector3(item.x1,item.y1, item.z1),
new THREE.Vector3(item.x2,item.y2, item.z2)
]);
const lineMaterial = new THREE.LineBasicMaterial({ color: item.color || 0xfffffff });
const line = new THREE.Line(lineGeometry, lineMaterial);
scene.add(line)
})
}
const initModel = ()=>{
// 加载.glb模型
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/'); // 指定Decoder的路径
loader.setDRACOLoader(dracoLoader);
loader.load('/3D/zhuba.glb', (gltf:any) => {
//解决加载进来为黑色的情况
const ambientLight = new THREE.AmbientLight(0xffffff, 1); // 白光,强度为1
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight('rgb(253,253,253)', 5);
dirLight.position.set(10, 10, 5); // 根据需要自行调整位置
scene.add(dirLight);
gltf.scene.name = 'dam_model'
scene.add(gltf.scene);
setTimeout(()=>{
loading.value = false;
},1100)
}, undefined, (error:any) => {
console.error(error);
});
}
const initRectangle =() =>{
rectangles.forEach(item=>{
const geometry = new THREE.PlaneGeometry( item.width, item.height );
const material = new THREE.MeshBasicMaterial( {color: item.color, transparent: true, opacity: 1} );
const plane = new THREE.Mesh( geometry, material );
plane.position.set(item.x, item.y, item.z)
rectMeshs.push(plane)
scene.add( plane );
})
}
const flyToCamera = ()=>{
}
const initAddClick = ()=>{
//点击事件
renderer.value.domElement.addEventListener('click', function (e:any) {
//获取点击位置
const mouse = new THREE.Vector2();
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
//创建射线
const raycaster = new THREE.Raycaster();
// 更新Raycaster的射线方向
raycaster.setFromCamera(mouse, camera.value);
// 假设你想要获取与场景中所有对象的交点
// 注意:intersectObjects会返回一个包含所有交点的数组
const intersects = raycaster.intersectObjects(scene.children, true); // 第二个参数为true表示递归检查所有子对象
if (intersects.length > 0) {
// 获取最近的交点(通常是数组中的第一个元素)
const intersect = intersects[0];
// intersect.point就是你在3D空间中点击的位置
console.log('Clicked point in 3D space:', intersect.point);
// 如果你想要获取被点击对象的详细信息,可以使用intersect.object
console.log('Clicked object:', intersect.object);
} else {
console.log('No objects intersected by the ray.');
}
})
}
const getwaterInfo = async()=>{
const res = await waterData({
epcId:appStore.epc.epcId
})
labels[0].value = "坝顶高程:" + res.damTop + 'm';
acalesText[0].value = "校核洪水位:" + res.checkZ + 'm';
acalesText[1].value = "设计洪水位:" + res.designZ + 'm';
acalesText[2].value = "正常水位:" + res.normalZ + 'm';
acalesText[3].value = "当前水位:" + (res.z ?? 0) + 'm';
acalesText[4].value = "死水位:" + res.deadZ + 'm';
}
const getItemData = async()=>{
const res = await getTooltipData({
damId:props.damId,
epcId:props.epcId
})
tooltipData.value = res;
}
onMounted(async() => {
await getwaterInfo();
initPage();
})
</script>
<style lang="scss" scoped>
#container{
width: 100%;
height: calc(100vh - 100px);
background-image: url("./bg.png");
background-size: cover;
}
.box{
overflow: hidden;
height: calc(100vh - 100px);
}
</style>