vue3+ts+three.js实现温度海洋3D热力图
·
3D热力图实现,写了个demo简单记录一下,话不多说先上效果图

注意:在更改渲染区域大小时,需要保持container盒子的宽高与渲染大小的宽高一致,这样射线器才能检测到准确检测到热力点(目前本人没有想到别的方法解决)。代码如下:
<template>
<div>
<el-switch v-model="colorRendering"></el-switch>
<el-button @click="resetRender">重置</el-button>
<div
id="container"
ref="container"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
>
<div
v-if="isShowInfo"
class="alter_info"
:style="{ position: 'absolute', top: `${clickMouseY}px`, left: `${clickMouseX}px` }"
>
<div>嗨,初音未来!</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import * as THREE from "three";
import { onMounted, ref, watch ,onBeforeUnmount } from "vue";
type Position = { x: number; y: number; z: number };
type Rotation = { x: number; y: number };
const container = ref<HTMLDivElement | null>(null);
const colorRendering = ref<boolean>(false);
const width = ref<number>(1200);
const height = ref<number>(800);
const isShowInfo = ref(false);
const clickMouseX = ref<number>(0);
const clickMouseY = ref<number>(0);
const initialPosition: Position = { x: 0, y: 4, z: 2.5 }; // 立方体初始摆放位置
const initialRotation: Rotation = { x: 0, y: 0 }; // 立方体初始旋转角度
const isDragging = ref<boolean>(false); // 是否在拖动
const mouseStart: THREE.Vector2 = new THREE.Vector2(); //记录鼠标起始位置
const rotationStart: THREE.Vector3 = new THREE.Vector3(); //旋转初始角度
const raycaster: THREE.Raycaster = new THREE.Raycaster(); //创建投射器
const mouse: THREE.Vector2 = new THREE.Vector2();
const hotPointMeshes: THREE.Mesh[] = []; // 记录热力点,每个热力点记录为一个mesh对象
const hotPointPositions: THREE.Vector3[] = [];
const hotPointColors: THREE.Color[] = [];
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let mesh: THREE.Mesh;
let bottomMaterial: THREE.MeshBasicMaterial;
let animationFrameId: number;
// 创建组,将立方体、棱线和点位添加到组中,整个渲染体可以一起旋转
const cubeGroup: THREE.Group = new THREE.Group();
const hotPoint = [
{
"X-coordinate": 0,
"Y-coordinate": 8,
"Z-coordinate": 0,
Health: "Ok",
},
{
"X-coordinate": 0,
"Y-coordinate": 9,
"Z-coordinate": 8,
Health: "Warning",
},
{
"X-coordinate": 0,
"Y-coordinate": 10,
"Z-coordinate": 16,
Health: "Critical",
},
{
"X-coordinate": 8,
"Y-coordinate": 8,
"Z-coordinate": 16,
Health: "Ok",
},
{
"X-coordinate": 8,
"Y-coordinate": 10,
"Z-coordinate": 8,
Health: "Critical",
},
{
"X-coordinate": 8,
"Y-coordinate": 9,
"Z-coordinate": 0,
Health: "Warning",
},
{
"X-coordinate": 16,
"Y-coordinate": 10,
"Z-coordinate": 0,
Health: "Critical",
},
{
"X-coordinate": 16,
"Y-coordinate": 8,
"Z-coordinate": 8,
Health: "Ok",
},
{
"X-coordinate": 16,
"Y-coordinate": 9,
"Z-coordinate": 16,
Health: "Warning",
},
];
const transparentMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.1, // 透明
depthWrite: false,
side: THREE.DoubleSide,
});
// 监听 `colorRendering` 的变化并执行对应的渲染逻辑
watch(colorRendering, (newValue) => {
if (newValue) {
applyGradientColor(); // 启用颜色渐变渲染
} else {
applyTransparentWithImage();
}
});
onMounted(() => {
initThreeJS();
});
const initThreeJS = () => {
// 创建相机对象
camera = new THREE.PerspectiveCamera(25, width.value / height.value, 0.1, 1000
);
// 创建场景
scene = new THREE.Scene();
// 创建渲染器
renderer = new THREE.WebGLRenderer();
//设置three.js渲染区域的尺寸(像素px)
renderer.setSize(width.value, height.value);
renderer.setClearColor(0x2e3b4c, 1);
if (container.value) {
container.value.appendChild(renderer.domElement);
}
//创建一个长方体几何对象Geometry
const geometry = new THREE.BoxGeometry(1.6, 0.2, 0.96);
// 加载底面图片纹理
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/src/assets/chuyin.png");
texture.flipY = false; // 取消 图片反转
//调整纹理过滤模式
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
//底面使用图片
bottomMaterial = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
});
const cubeMaterials = [
transparentMaterial, // 前面
transparentMaterial, // 背面
transparentMaterial, // 顶面
bottomMaterial, // 底面
transparentMaterial, // 右面
transparentMaterial, // 左面
];
// 网格模型对象Mesh,两个参数分别为几何体geometry、材质material
mesh = new THREE.Mesh(geometry, cubeMaterials);
// 10条棱添加颜色
const edges = new THREE.EdgesGeometry(geometry);
const line = new THREE.LineSegments(
edges,
new THREE.LineBasicMaterial({ color: 0xa19583 }) // 灰色
);
createHotPoints();
createPyramidLines();
creatGrid();
cubeGroup.add(mesh); // 添加立方体到组
cubeGroup.add(line); // 添加棱线到组
scene.add(cubeGroup); // 将组添加到场景中
//相机位置
camera.position.set(initialPosition.x, initialPosition.y, initialPosition.z);
camera.lookAt(0, -0.1, 0);
// 渲染场景
animate();
};
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
renderer.render(scene, camera);
// 自动旋转整个组
// cubeGroup.rotation.x = mousePosition.value.y * 1;
// cubeGroup.rotation.y = mousePosition.value.x * 2;
};
// 创建球体几何体来表示热力点
const createHotPoints = ()=>{
hotPoint.forEach((point) => {
const gradientColors = []; // 渐变颜色材质
// 根据健康状态设置颜色
const pointColor =
point.Health === "Ok"
? new THREE.Color(0x16b81e)
: point.Health === "Warning"
? new THREE.Color(0xf2a430)
: new THREE.Color(0xff0000);
const sphereGeometry = new THREE.SphereGeometry(0.02, 32, 32); // 调整球体大小
const coreSphereMaterial = new THREE.MeshBasicMaterial({
color: pointColor,
});
const sphere = new THREE.Mesh(sphereGeometry, coreSphereMaterial);
// 适当缩放坐标,匹配长方体的坐标系
const x = (point["X-coordinate"] - 8) * 0.09; // 根据立方体宽度平移并缩放
const y = (point["Y-coordinate"] - 8) * 0.03; // 根据立方体高度平移并缩放
const z = (point["Z-coordinate"] - 8) * 0.05; // 根据立方体深度平移并缩放
sphere.position.set(x, y, z);
hotPointPositions.push(new THREE.Vector3(x, y, z));
hotPointColors.push(pointColor);
// 添加热力点到场景
cubeGroup.add(sphere);
// 存储该热力点 mesh,以便于点击检测
hotPointMeshes.push(sphere);
// 记录颜色用于渐变
gradientColors.push(pointColor);
});
}
// 创建热力点与底面对应位置的金字塔状线条
const createPyramidLines = ()=>{
hotPointPositions.forEach((pointPosition) => {
// 底面对应位置
const bottomPosition1 = new THREE.Vector3(
pointPosition.x - 0.085,
-0.1,
pointPosition.z - 0.05
);
const bottomPosition2 = new THREE.Vector3(
pointPosition.x + 0.085,
-0.1,
pointPosition.z + 0.05
);
const bottomPosition3 = new THREE.Vector3(
pointPosition.x + 0.085,
-0.1,
pointPosition.z - 0.05
);
const bottomPosition4 = new THREE.Vector3(
pointPosition.x - 0.085,
-0.1,
pointPosition.z + 0.05
);
const lineGeometry1 = new THREE.BufferGeometry().setFromPoints([
pointPosition,
bottomPosition1,
]);
const lineGeometry2 = new THREE.BufferGeometry().setFromPoints([
pointPosition,
bottomPosition2,
]);
const lineGeometry3 = new THREE.BufferGeometry().setFromPoints([
pointPosition,
bottomPosition3,
]);
const lineGeometry4 = new THREE.BufferGeometry().setFromPoints([
pointPosition,
bottomPosition4,
]);
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff }); // 白色线条
const pyramidLine1 = new THREE.Line(lineGeometry1, lineMaterial);
const pyramidLine2 = new THREE.Line(lineGeometry2, lineMaterial);
const pyramidLine3 = new THREE.Line(lineGeometry3, lineMaterial);
const pyramidLine4 = new THREE.Line(lineGeometry4, lineMaterial);
// 添加线条到场景
cubeGroup.add(pyramidLine1,pyramidLine2,pyramidLine3, pyramidLine4);
});
}
// 创建网格
const creatGrid = () => {
// 创建一个网格并覆盖长方体的底部
const divisionsX = 16;
const divisionsZ = 16;
const gridSizeX = 1.6; // 与长方体的长度一致
const gridSizeZ = 0.96; // 与长方体的宽度一致
// 网格的材质
const gridMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });
// gridMaterial.position.y = -0.08; // 调整网格位置
// 创建 X 轴上的线条
for (let i = 0; i <= divisionsX; i++) {
const x = -gridSizeX / 2 + (gridSizeX / divisionsX) * i;
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(x, -0.1, -gridSizeZ / 2),
new THREE.Vector3(x, -0.1, gridSizeZ / 2),
]);
const line = new THREE.Line(geometry, gridMaterial);
line.position.y = 0.01;
cubeGroup.add(line);
}
// 创建 Z 轴上的线条
for (let i = 0; i <= divisionsZ; i++) {
const z = -gridSizeZ / 2 + (gridSizeZ / divisionsZ) * i;
const geometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-gridSizeX / 2, -0.1, z),
new THREE.Vector3(gridSizeX / 2, -0.1, z),
]);
const line = new THREE.Line(geometry, gridMaterial);
line.position.y = 0.01;
cubeGroup.add(line);
}
};
// 渲染热力图
const applyGradientColor = () => {
// uniforms 是一种在 WebGL 中用于向着色器传递全局变量的机制。这些变量不会随每个顶点或片段(片元)而改变,它们可以用于控制整个着色过程。
const cubeMaterial = new THREE.ShaderMaterial({
uniforms: {
// hotPoints:这是一组传递到片段着色器中的三维坐标,表示热力点的位置。其值来源于 hotPointPositions,这些点定义了立方体中颜色变化的源点。
hotPoints: { value: hotPointPositions },
// hotColors:这是一组与 hotPoints 一一对应的颜色,表示每个热力点的颜色。其值来源于 hotPointColors。
hotColors: { value: hotPointColors },
// numPoints:热力点的数量,即 hotPointPositions.length。虽然这个值没有直接在着色器中使用,但它提供了热力点的总数,用于控制循环次数等
numPoints: { value: hotPointPositions.length },
},
//vertexShader定点着色器
// vPosition:这是一个顶点着色器传递给片段着色器的变量,表示当前片段在三维空间中的位置。这个位置将用于计算该片段到热力点的距离。
// gl_Position:这是标准的顶点着色器输出,它使用模型视图矩阵和投影矩阵来计算每个顶点在屏幕上的最终位置。
vertexShader: `
varying vec3 vPosition;
void main() {
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
// fragmentShader 片段着色器
fragmentShader: `
uniform vec3 hotPoints[${hotPointPositions.length}]; //hotPoints是三维向量数组,代表热力点的位置
uniform vec3 hotColors[${hotPointColors.length}]; //hotColors 是颜色数组,代表每个点的颜色
uniform int numPoints;
varying vec3 vPosition;
void main() {
float totalWeight = 0.0;
vec3 mixedColor = vec3(0.0);
for (int i = 0; i < ${hotPointPositions.length}; i++) {
float dist = distance(vPosition, hotPoints[i]);
float weight = 1.0 / (pow(dist + 0.04, 1.5)); // 调整距离权重,pow函数加深权重效果
// 判断是否为 Critical 点,如果是则增加权重
if (hotColors[i] == vec3(1.0, 0.0, 0.0)) { // 红色判断
weight *= 2.0; // 增加 Critical 点的权重
}
totalWeight += weight;
mixedColor += weight * hotColors[i]; // 根据权重混合颜色
}
mixedColor /= totalWeight; // 根据总权重进行归一化
// 进一步加深颜色强度,将颜色乘以一个放大系数
mixedColor = pow(mixedColor, vec3(0.8)); // 加深颜色对比度
gl_FragColor = vec4(mixedColor, 0.5);
}
`,
transparent: true,
side: THREE.DoubleSide,
});
const cubeMaterials = [
cubeMaterial, // 前面
cubeMaterial, // 背面
cubeMaterial, // 顶面
bottomMaterial, // 底面
cubeMaterial, // 右面
cubeMaterial, // 左面
];
mesh.material = cubeMaterials;
};
// 渲染透明效果
const applyTransparentWithImage = () => {
const cubeMaterials = [
transparentMaterial, // 前面
transparentMaterial, // 背面
transparentMaterial, // 顶面
bottomMaterial, // 底面
transparentMaterial, // 右面
transparentMaterial, // 左面
];
mesh.material = cubeMaterials; // 应用透明材质并设置底面图片
};
// 鼠标按下事件
function onMouseDown(event: MouseEvent) {
event.preventDefault();
mouseStart.set(event.clientX, event.clientY);
// 获取鼠标在div中的相对位置,检测鼠标是否点击了小球
const rect = container.value!.getBoundingClientRect();
const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const mouseY = (-(event.clientY - rect.top) / rect.height) * 2 + 1;
// 更新鼠标位置
mouse.set(mouseX, mouseY);
// 使用相机和鼠标位置更新射线投射器
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(hotPointMeshes);
// 如果有相交的对象,获取该对象的信息
if (intersects.length > 0) {
//获取当前点位在div中的位置,然后进行偏移,确定弹窗位置
clickMouseX.value = event.clientX - rect.left + 30;
clickMouseY.value = event.clientY - rect.top - 30;
const intersectedObject = intersects[0].object as THREE.Mesh;
const index = hotPointMeshes.indexOf(intersectedObject);
if (index !== -1) {
// 获取热力点的信息
isShowInfo.value = true;
console.log(hotPoint[index], "hotPoint[index]");
}
} else {
isShowInfo.value = false;
// 没有点击到热力点,可能是旋转场景的操作
isDragging.value = true;
mouseStart.set(event.clientX, event.clientY);
rotationStart.copy(cubeGroup.rotation);
}
}
// 鼠标移动事件
function onMouseMove(event: MouseEvent) {
// 获取鼠标在div中的相对位置
const rect = container.value!.getBoundingClientRect();
const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const mouseY = (-(event.clientY - rect.top) / rect.height) * 2 + 1;
// 更新鼠标位置
mouse.set(mouseX, mouseY);
// 使用相机和鼠标位置更新射线投射器
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(hotPointMeshes);
// 如果有相交的对象,表示鼠标悬停在热力点上,改变鼠标样式
if (intersects.length > 0) {
container.value!.style.cursor = "pointer"; // 改变鼠标为手指样式
} else {
container.value!.style.cursor = "default"; // 恢复默认鼠标样式
}
if (isDragging.value) {
//左右旋转
const deltaX = event.clientX - mouseStart.x;
cubeGroup.rotation.y = rotationStart.y + deltaX * 0.01;
const deltaY = event.clientY - mouseStart.y;
cubeGroup.rotation.x = rotationStart.x + deltaY * 0.01;
}
}
// 鼠标松开事件
function onMouseUp() {
isDragging.value = false;
}
const resetRender = () => {
isShowInfo.value = false;
colorRendering.value = false;
camera.position.set(initialPosition.x, initialPosition.y, initialPosition.z);
camera.lookAt(0, -0.1, 0);
cubeGroup.rotation.set(initialRotation.x, initialRotation.y, 0);
};
onBeforeUnmount(()=>{
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
scene.remove(mesh);
scene.remove(cubeGroup);
mesh.geometry.dispose();
renderer.dispose();
})
</script>
<style scoped lang="scss">
#container {
width: 1200px;
height: 800px;
background-color: #2e3b4c;
position: relative;
.alter_info {
width: 150px;
height: 25px;
padding: 15px 10px;
background-color: #fff;
border-radius: 8px;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
border-top: 15px solid transparent;
border-bottom: 15px solid transparent;
border-right: 15px solid white;
margin-left: -15px;
}
}
}
</style>
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)