当前位置: 首页 > news >正文

Three.js 模型热力图教程

模型热力图 ·Model Heatmap· ▶ 在线运行案例

  • 案例合集:三维可视化功能案例(threehub.cn)
  • 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
  • 400个案例代码:网盘链接

你将学到什么

  • ShaderMaterial 自定义着色器实现核心视觉效果
  • OrbitControls 相机轨道交互
  • glTF/Draco 模型加载与优化
  • BufferGeometry 自定义顶点/索引数据
  • requestAnimationFrame渲染循环与resize自适应

效果说明

本案例演示模型热力图效果:基于 WebGL 实现「模型热力图」可视化效果,附完整可运行源码;核心用到 ShaderMaterial、OrbitControls、glTF/Draco。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。

核心概念

  • Scene / Camera / WebGLRenderer构成最小渲染闭环;大场景可开logarithmicDepthBuffer缓解 Z-fighting。
  • ShaderMaterial通过uniforms+ 自定义 GLSL 控制逐像素/逐点效果;透明粒子常配合depthTest: false
  • OrbitControls提供轨道旋转/缩放;开启enableDamping后需在 animate 中controls.update()

实现步骤

  • 搭建 Scene、PerspectiveCamera、WebGLRenderer,挂载 canvas 并处理resize
  • 异步加载模型 / 3D Tiles / GeoJSON 等资源并加入 scene 或 entities
  • 定义 uniforms / onBeforeCompile 或 ShaderMaterial,编写 GLSL 与材质参数
  • 创建 OrbitControls(及 Raycaster 等交互控件,若源码包含)
  • requestAnimationFrame循环中更新状态并 render(Cesium 为viewer.render或自动渲染)
  • 代码要点

    import * as THREE from 'three'

    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'

    const box = document.getElementById('box')

    const scene = new THREE.Scene()

    const camera = new THREE.PerspectiveCamera(75, box.clientWidth / box.clientHeight, 0.1, 1000)

    camera.position.set(5, 5, 5)

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true })

    renderer.setSize(box.clientWidth, box.clientHeight)

    box.appendChild(renderer.domElement)

    new OrbitControls(camera, renderer.domElement)

    window.onresize = () => {

    renderer.setSize(box.clientWidth, box.clientHeight)

    camera.aspect = box.clientWidth / box.clientHeight

    camera.updateProjectionMatrix()

    }

    animate()

    function animate() {

    requestAnimationFrame(animate)

    renderer.render(scene, camera)

    }

    scene.add(new THREE.AmbientLight(0xffffff, 3))

    new GLTFLoader().load(

    'https://z2586300277.github.io/three-editor/dist/files/resource/datacenter.glb',

    gltf => {

    scene.add(gltf.scene)

    callModel(gltf.scene)

    }

    )

    let model = null

    function callModel(e) { model = e const box3 = new THREE.Box3().setFromObject(model) const { min, max } = box3 // 根据模型的包围盒 固定y 生成一个平面 const p1 = new THREE.Vector3(min.x, 0, min.z) const p2 = new THREE.Vector3(min.x, 0, max.z) const p3 = new THREE.Vector3(max.x, 0, max.z) const p4 = new THREE.Vector3(max.x, 0, min.z) const geometry = new THREE.BufferGeometry() const vertices = new Float32Array([ p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z, p4.x, p4.y, p4.z, ]) geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3)) geometry.setIndex([0, 1, 2, 0, 2, 3]) geometry.attributes.uv = new THREE.Float32BufferAttribute([ 0, 0, 0, 1, 1, 1, 1, 0 ], 2) geometry.computeVertexNormals()

    let list = []

    model.traverse(i => { if (i.isMesh) { i.material.transparent = true i.material.opacity = 0.5 i.isMesh && list.push(i.name) } })

    // list 随机获取 5 - 10 个名字形成新的数组 const randomNum = Math.floor(Math.random() * (10 - 5 + 1)) + 5 list = list.sort(() => Math.random() - 0.5).slice(0, randomNum)

    let w = max.x - min.x let h = max.z - min.z

    /热力图实现/ const arr = list.map(i => { const obj = model.getObjectByName(i) const worldPosition = new THREE.Vector3() obj.getWorldPosition(worldPosition) return [(worldPosition.x - min.x) / w, (worldPosition.z - min.z) / h, Math.random() * 10] }).flat() const uniforms1 = { HEAT_MAX: { value: 10, type: 'number', unit: 'float' }, PointRadius: { value: 0.2, type: 'number', unit: 'float' }, intensity: { value: 3, type: 'number', unit: 'float' }, PointsCount: { value: arr.length, type: 'number-array', unit: 'int' }, c1: { value: new THREE.Color('green'), type: 'color', unit: 'vec3' }, // 蓝色 c2: { value: new THREE.Color('red'), type: 'color', unit: 'vec3' }, // 红色 uvY: { value: 1, type: 'number', unit: 'float' }, uvX: { value: 1, type: 'number', unit: 'float' }, opacity: { value: 0.6, type: 'number', unit: 'float' }, // 稍微降低整体不透明度 edgeFalloff: { value: 2.0, type: 'number', unit: 'float' } // 边缘衰减参数 }

    const gui = new GUI() gui.add(uniforms1.HEAT_MAX, 'value', 0, 10).name('HEAT_MAX') gui.add(uniforms1.PointRadius, 'value', 0, 1).name('PointRadius') gui.add(uniforms1.intensity, 'value', 0, 10).name('intensity') gui.add(uniforms1.uvY, 'value', 0, 1).name('uvY') gui.add(uniforms1.uvX, 'value', 0, 1).name('uvX') gui.add(uniforms1.opacity, 'value', 0, 1).name('opacity') gui.add(uniforms1.edgeFalloff, 'value', 0.1, 5).name('边缘衰减') gui.addColor(uniforms1.c1, 'value').name('冷色') gui.addColor(uniforms1.c2, 'value').name('热色')

    const uniforms2 = { Points: { value: arr, type: 'vec3-array', unit: 'vec3' } }

    const uniforms = { ...uniforms1, ...uniforms2 } const vertexShader =varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrixmodelViewMatrixvec4(position, 1.0); }const getFragmentShader = () => 'precision highp float;\n' + 'varying vec2 vUv; \n' +

    Object.keys(uniforms1).map(i => 'uniform ' + uniforms1[i].unit + ' ' + i + ';') .join('\n')

    • '\nuniform vec3 Points['
    • uniforms1.PointsCount.value + '];'
    vec3 gradient(float w, vec2 uv) { // 平滑过渡的热力图颜色 w = pow(clamp(w, 0., 1.), 0.8); return mix(c1, c2, w); } void main() { vec2 uv = vUv; uv.xy *= vec2(uvX, uvY); float d = 0.; // 计算热度值 for (int i = 0; i < PointsCount; i++) { vec3 v = Points[i]; float intensity = v.z / HEAT_MAX; float dist = length(uv - v.xy); float pd = (1. - dist / PointRadius) * intensity; d += pow(max(0., pd), 1.5); } // 计算边缘衰减因子 float edgeFactor = 1.0; vec2 center = vec2(0.5, 0.5); float distFromCenter = length(uv - center); // 在UV坐标的边缘部分应用透明度衰减 float edgeStart = 0.4; if (distFromCenter > edgeStart) { edgeFactor = 1.0 - pow((distFromCenter - edgeStart) / (0.5 - edgeStart), edgeFalloff); } // 确保边缘的透明度为0 edgeFactor = clamp(edgeFactor, 0.0, 1.0); // 应用热力颜色和透明度 vec3 heatColor = gradient(d, uv); float alpha = min(opacityedgeFactor, d > 0.05 ? 1.0 : d20.0); gl_FragColor = vec4(heatColor * vec3(intensity,intensity,intensity), alpha); }const shaderMaterial = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader: getFragmentShader(), side: THREE.DoubleSide, depthWrite: false, depthTest: false, transparent: true, blending: THREE.AdditiveBlending // 使用加法混合使热力图更具光晕效果 }) const mesh = new THREE.Mesh(geometry, shaderMaterial) scene.add(mesh) }

    完整源码:GitHub

    小结

    • 本文提供模型热力图完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
    • 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库
http://www.jsqmd.com/news/1088518/

相关文章:

  • LVGL实战指南:打造个性化嵌入式日历界面
  • 浮空全域透视动向·自愈专网直抵指挥 穿云夜视广域感知与立体管控融合指挥系统技术方案
  • Web文件上传安全:从漏洞原理到纵深防御实战指南
  • 基于STM32F407ZGT6与蓝牙的简易机械臂控制系统设计与实现
  • NCMDump:三步解锁网易云音乐加密文件,让音乐真正属于你
  • Java国密SM2集成:解决BouncyCastle“未知曲线”报错全攻略
  • Chromatic:如何像专业安全研究员一样调试和修改任意Chromium应用?
  • Blender3mfFormat插件:3D打印工作流的终极解决方案
  • 揭秘QQ聊天记录隐藏的密钥:全平台数据库解密技术深度解析
  • 从原理到代码:深入理解RSA加密算法及其Python实现
  • 盲波束成形技术与BORN算法在无线通信中的应用
  • 如何用DDrawCompat让Windows 10/11上的DirectX老游戏重获新生:技术原理与实战指南
  • [ 实战篇 ] 手把手教你激活谷歌HackBar (附疑难排查)
  • 3步打造极简高效Windows右键菜单:ContextMenuManager终极管理指南
  • Lenovo Legion Toolkit:拯救者笔记本性能调校终极指南
  • ENVI实战:从QuickBird数据到精准正射影像的完整流程
  • [特殊字符] 从零搭一个淘宝商品价格监控系统:TOP API + 定时任务 + 微信推送(附Python源码)
  • 5分钟快速上手:B站视频语音转文字工具Bili2text完整指南
  • AI模型受限发布机制与技术可信度验证
  • BetterGI安装前检查清单
  • 文件上传漏洞实战:从基础绕过到二次渲染与解析漏洞利用
  • 如何快速下载网页视频资源:猫抓浏览器扩展完整使用指南
  • 3分钟解锁网易云音乐新玩法:BetterNCM安装器终极指南
  • QMCDecode:三分钟解锁QQ音乐加密文件,让音乐真正属于你
  • 零代码UI自动化测试录制工具:原理、实现与实战指南
  • Python自动化NVD漏洞监控:从API抓取到钉钉/飞书实时告警
  • 从Excel到DOORS:需求管理工具如何应对复杂项目中的变更与协同挑战
  • N_m3u8DL-RE:跨平台流媒体下载工具的完整使用指南
  • IDM激活脚本终极指南:永久免费解锁Internet Download Manager完整功能
  • 从投稿到录用:揭秘Transactions on Industrial Electronics (TIE) 期刊的完整实战指南