模型热力图 ·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 开源仓库