Three.js 3D 热力云图效果实现

一、效果展示及原理概述

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 云图效果的关键

  1. 高度图(HeightMap) :在顶点着色器中采样热力图纹理的 红色通道 (R)作为高度值,乘以 heightRatio 控制位移幅度
  2. 密集网格PlaneBufferGeometry(290, 80, 1000, 1000) 提供 100 万个顶点,实现平滑的地形起伏
  3. 透明云感:片元着色器中根据高度偏差计算透明度,高度越高偏差越大,透明度越高,形成云雾消散的视觉效果

二、完整 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)+ 增大 heightRatio(510)
  • 更平缓的丘陵 :增大 blur(0.80.9)+ 减小 heightRatio(13)
  • 更大范围的热力 :增大 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 参数即可动态调整高度、模糊度等
  • 易于扩展:可叠加地形、建筑模型,支持多图层
相关推荐
爱勇宝3 小时前
人到了一定年纪,才会看懂这些人性真相
前端·后端
梓䈑3 小时前
【用 Vibe Coding 实现的 C++17 在线判题系统】前端开发 + Web 自动化测试
前端·c++·ai编程
程序员黑豆4 小时前
鸿蒙应用开发 @BuilderParam 使用教程:实现灵活的 UI 插槽
前端·harmonyos
万少4 小时前
用 TraeWork 给小孩做一个家庭工作台
前端·人工智能·后端
孙启超4 小时前
【AI应用开发】ReAct 原理是什么?和普通直接提问 LLM 差别在哪?
前端·人工智能·llm·agent·react·rag·ai应用开发
其美杰布-富贵-李4 小时前
第 10 篇:灯光与阴影
javascript·three.js
SynthWriter4 小时前
下班前接到急活?我用 TRAE Work 20 分钟搞定了一套完整页面原型
前端
minimoon_jojo4 小时前
Antd Table虚拟滚动详解
前端
颜进强4 小时前
Calude Code - 23 用 MCP 把 Jenkins 变成 AI 队友:一次对话完成自动化发布
前端·后端