一、效果展示及原理概述

1.1 核心思想
3D 热力云图的核心思路是 将 2D 热力图数据转化为 3D 立体效果。整体流程分为三个阶段:
scss
原始数据 → 2D 热力图(heatmap.js) → 纹理贴图 → 3D 几何体顶点位移(Three.js Shader)
1.2 技术架构
| 技术 | 作用 |
|---|---|
| heatmap.js | 生成 2D 热力图画布(颜色映射热度值) |
| Three.js Texture | 将热力图画布作为纹理注入 GPU |
| PlaneBufferGeometry | 高分辨率平面(1000×1000 分段),提供密集顶点供位移 |
| ShaderMaterial | 自定义着色器,读取纹理像素值驱动顶点 Y 方向位移 |
1.3 3D 云图效果的关键
- 高度图(HeightMap) :在顶点着色器中采样热力图纹理的 红色通道 (R)作为高度值,乘以
heightRatio控制位移幅度 - 密集网格 :
PlaneBufferGeometry(290, 80, 1000, 1000)提供 100 万个顶点,实现平滑的地形起伏 - 透明云感:片元着色器中根据高度偏差计算透明度,高度越高偏差越大,透明度越高,形成云雾消散的视觉效果
二、完整 HTML 实现
2.1 完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>3D 热力云图教程</title>
<style type="text/css">
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.btn {
position: absolute;
top: 20px;
left: 100px;
z-index: 100;
}
</style>
<!-- 顶点着色器:根据高度图位移顶点 -->
<script id="vertexShader" type="x-shader/x-vertex">
uniform sampler2D heightMap;
uniform float heightRatio;
varying vec2 vUv;
varying float hValue;
varying vec3 cl;
void main() {
vUv = uv;
vec3 pos = position;
// 采样热力图颜色(用于片元着色器显示)
cl = texture2D(heightMap, vUv).rgb;
// 采样热力图红色通道作为高度值
hValue = texture2D(heightMap, vUv).r;
// 根据高度值位移顶点 Y 坐标
pos.y = hValue * heightRatio;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
</script>
<!-- 片元着色器:根据高度偏差计算透明度 -->
<script id="fragmentShader" type="x-shader/x-fragment">
varying float hValue;
varying vec3 cl;
void main() {
// 计算与最大高度(1.0)的偏差
float v = abs(hValue - 1.0);
// 透明度随偏差增大而降低,形成云雾消散效果
gl_FragColor = vec4(cl, 0.8 - v * v);
}
</script>
</head>
<body>
<div id="heatmap"></div>
<div id="heatmap-canvas" style="display: none;"></div>
<div class="btn">
<button onclick="toggleHeatMap()">显隐热力图</button>
<button onclick="setHeatMap()">随机热力图</button>
</div>
<!-- Three.js r120 -->
<script type="text/javascript" src="https://unpkg.com/three@0.120.1/build/three.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/three@0.120.1/examples/js/controls/OrbitControls.js"></script>
<script type="text/javascript" src="https://unpkg.com/three@0.120.1/examples/js/libs/stats.min.js"></script>
<!-- dat.GUI -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/dat.gui@0.7.9/build/dat.gui.min.js"></script>
<!-- heatmap.js -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/heatmap.js@2.0.2/heatmap.min.js"></script>
<script type="text/javascript">
// ============ 场景初始化 ============
let container = document.getElementById("heatmap");
// 场景
let scene = new THREE.Scene();
// 相机
let camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
1,
1000
);
camera.position.set(21, 34, 55);
// 光源
let ambientLight = new THREE.AmbientLight(0xcccccc, 0.4);
scene.add(ambientLight);
let pointLight = new THREE.PointLight(0xffffff, 0.8);
camera.add(pointLight);
scene.add(camera);
// 渲染器
let renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
// 轨道控制器
let controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.maxDistance = 200;
// 性能监视器
let stats = new Stats();
container.appendChild(stats.dom);
// ============ 热力图数据生成 ============
function getRandom(max, min) {
return Math.round((Math.random() * (max - min + 1) + min) * 10) / 10;
}
function setHeatMat() {
let heatData = [];
// 生成 600 个随机热力点
for (let i = 0; i < 600; i++) {
heatData.push({
x: getRandom(1, 256),
y: getRandom(1, 256),
value: getRandom(1, 6),
});
}
return {
width: 256,
height: 256,
blur: 0.7,
radius: 10,
valueMax: 10,
data: heatData,
position: [0, 0, 0],
heightRatio: 3,
geoWidth: 290,
geoHeight: 80,
};
}
// ============ 核心:3D 热力云图生成 ============
let heatMesh = null;
function initHeatmap(option) {
// 移除旧热力图
if (heatMesh) {
scene.remove(heatMesh);
}
// 第一步:用 heatmap.js 生成 2D 热力图画布
let heatmap = h337.create({
container: document.getElementById("heatmap-canvas"),
width: option.width,
height: option.height,
blur: option.blur,
radius: option.radius,
});
heatmap.setData({
max: option.valueMax,
data: option.data,
});
// 第二步:将画布转为 Three.js 纹理
let texture = new THREE.Texture(heatmap._renderer.canvas);
// 第三步:创建高分辨率平面几何体
let geometry = new THREE.PlaneBufferGeometry(
option.geoWidth, // 宽度
option.geoHeight, // 高度
1000, // 宽度分段数
1000 // 高度分段数
);
// 旋转 -90° 使平面水平放置
geometry.rotateX(-Math.PI * 0.5);
// 第四步:创建自定义着色器材质
let material = new THREE.ShaderMaterial({
uniforms: {
heightMap: { value: texture },
heightRatio: { value: option.heightRatio },
},
vertexShader: document.getElementById("vertexShader").textContent,
fragmentShader: document.getElementById("fragmentShader").textContent,
transparent: true,
});
// 第五步:组装网格并添加到场景
heatMesh = new THREE.Mesh(geometry, material);
heatMesh.position.set(...option.position);
scene.add(heatMesh);
return heatMesh;
}
// ============ 初始化 ============
let config = setHeatMat();
let heatMapMesh = initHeatmap(config);
// ============ GUI 控制 ============
let gui = new dat.GUI();
gui.add(heatMesh.material.uniforms.heightRatio, "value", 1, 15)
.name("高度倍率");
gui.add(config, "geoWidth", 0, 400).step(10)
.name("平面宽度").onChange(_ => initHeatmap(config));
gui.add(config, "geoHeight", 0, 400).step(10)
.name("平面高度").onChange(_ => initHeatmap(config));
gui.add(config, "radius", 0, 100)
.name("热力半径").onChange(_ => initHeatmap(config));
gui.add(config, "valueMax", 0, 100)
.name("最大值").onChange(_ => initHeatmap(config));
gui.add(config, "blur", 0, 0.98).step(0.01)
.name("模糊度").onChange(_ => initHeatmap(config));
// ============ 交互按钮 ============
function toggleHeatMap() {
heatMapMesh.visible = !heatMapMesh.visible;
}
function setHeatMap() {
config = setHeatMat();
heatMapMesh = initHeatmap(config);
}
// ============ 动画循环 ============
function animate() {
requestAnimationFrame(animate);
// 更新纹理(必须,否则热力图变化不会实时反映)
if (heatMesh) {
heatMesh.material.uniforms.heightMap.value.needsUpdate = true;
}
renderer.render(scene, camera);
stats.update();
}
animate();
// ============ 响应窗口变化 ============
window.addEventListener("resize", function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
</script>
</body>
</html>
三、关键代码深度解析
3.1 顶点着色器:高度位移的核心
glsl
uniform sampler2D heightMap; // 热力图纹理(高度数据来源)
uniform float heightRatio; // 高度缩放因子
void main() {
vec3 pos = position; // 原始顶点位置
hValue = texture2D(heightMap, vUv).r; // 采样红色通道作为高度
pos.y = hValue * heightRatio; // 位移 Y 坐标
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
原理解析:
texture2D(heightMap, vUv).r--- 每个顶点根据自身 UV 坐标从纹理中采样,红色通道值(0~1)代表该位置的热力强度pos.y = hValue * heightRatio--- 将 0~1 的归一化高度值乘以倍率,得到实际的位移高度- 这就是为什么红色区域(热力图中心)越高,蓝色区域(边缘)越低
3.2 片元着色器:云雾消散效果
glsl
void main() {
float v = abs(hValue - 1.0); // 距离最大高度的偏差
gl_FragColor = vec4(cl, 0.8 - v * v); // 透明度二次衰减
}
原理解析:
- 高度值越接近 1.0(热力中心),
v越小,透明度越高(几乎不透明) - 高度值偏离 1.0 越远,
v越大,透明度越低(逐渐透明消散) v * v二次函数让边缘过渡更自然,形成云雾飘动的感觉
3.3 高分辨率平面:密集顶点的必要性
javascript
new THREE.PlaneBufferGeometry(290, 80, 1000, 1000)
| 参数 | 值 | 说明 |
|---|---|---|
| 参数 1 | 290 | 平面宽度 |
| 参数 2 | 80 | 平面高度 |
| 参数 3 | 1000 | 宽度方向分段数 |
| 参数 4 | 1000 | 高度方向分段数 |
为什么需要 1000×1000?
- 热力图的分辨率是 256×256,如果几何分段数太少(比如 10×10),每个三角面会覆盖多个热力像素,导致位移呈现阶梯状
- 1000×1000 = 100 万个顶点,平均每个热力像素有 ~4 个顶点对应,保证位移平滑
3.4 坐标旋转:平面水平放置
javascript
geometry.rotateX(-Math.PI * 0.5);
Three.js 中 PlaneGeometry 默认在 XY 平面上,而我们需要它水平放置(XZ 平面)。绕 X 轴旋转 -90° 即可。
3.5 纹理实时更新
javascript
texture.needsUpdate = true;
在 animate() 中每帧设置此标志,确保当 heatmap.js 更新画布时,Three.js 同步更新 GPU 纹理。
四、热力图参数说明
4.1 数据参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
width |
number | 256 | 热力图画布宽度 |
height |
number | 256 | 热力图画布高度 |
valueMax |
number | 10 | 单个数据点最大值 |
data |
array | 600 点 | 热力点数据 {x, y, value} |
4.2 视觉参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
blur |
number | 0.7 | 模糊程度(0 锐利 ~ 1 极模糊) |
radius |
number | 10 | 单个热力点的影响半径 |
heightRatio |
number | 3 | 3D 高度缩放倍率 |
geoWidth |
number | 290 | 3D 平面宽度 |
geoHeight |
number | 80 | 3D 平面高度 |
4.3 参数调优建议
- 更尖锐的山峰 :减小
blur(0.30.5)+ 增大10)heightRatio(5 - 更平缓的丘陵 :增大
blur(0.80.9)+ 减小3)heightRatio(1 - 更大范围的热力 :增大
radius(20~50)+ 增加数据点数量 - 更细腻的纹理 :增大
width/height(512~1024)+ 保持高PlaneBufferGeometry分段
五、扩展方向
5.1 加载真实数据
将 setHeatMat() 中的随机数据替换为实际业务数据:
javascript
// 从接口获取真实数据
fetch('/api/heatdata').then(res => res.json()).then(data => {
let heatData = data.map(item => ({
x: item.x,
y: item.y,
value: item.intensity
}));
config.data = heatData;
heatMapMesh = initHeatmap(config);
});
5.2 地形叠加
在热力云图下方叠加真实地形模型:
javascript
// 加载地形 DEM 数据作为底层模型
miniThree.addGlbModel({
url: "./terrain.glb",
modelSize: [1, 1, 1]
});
5.3 动画增强
让热力点动态闪烁,增强视觉表现力:
javascript
// 在顶点着色器中添加时间动画
uniform float uTime;
void main() {
hValue = texture2D(heightMap, vUv).r;
float wave = sin(uTime * 2.0 + vUv.x * 10.0) * 0.5 + 0.5;
pos.y = hValue * heightRatio * (0.8 + wave * 0.2);
}
5.4 多热力图层叠加
创建多个热力层,分别控制不同区域的效果:
javascript
let layers = [
{ data: officeZone, heightRatio: 5, color: 'warm' },
{ data: productionZone, heightRatio: 3, color: 'cool' }
];
layers.forEach(layer => {
initHeatmap({ ...config, data: layer.data, heightRatio: layer.heightRatio });
});
六、总结
3D 热力云图的实现核心是 "2D 数据 → 3D 几何"的映射思想:
markdown
热力图数据点
↓
heatmap.js 生成 2D 彩色热力画布
↓
Three.js 将画布作为纹理注入 GPU
↓
ShaderMaterial 采样纹理高度值位移顶点
↓
高分辨率网格 + 透明云雾着色 = 3D 热力云图
这种实现方式的优势:
- ✅ 性能优秀:GPU 顶点着色器并行处理百万顶点,不阻塞主线程
- ✅ 实时更新:修改热力数据后只需更新纹理即可实时反映
- ✅ 效果可调:通过 uniform 参数即可动态调整高度、模糊度等
- ✅ 易于扩展:可叠加地形、建筑模型,支持多图层