three.js 使用 heatmap 实现 3d 热力图
先上效果图:
闲来无事想随便做做3d的东西,正好想到之前有个3d热力图的需求,后面没做被砍了,今天正好来做一下。在网上搜了搜参看了一些其他人的成果,最后还是弄出来了。记录一下,避免以后找不到。
单独定义一个ts 文件 ,导出 TDHeatMap 方法,接收了参数,但是没用,可以自己封装一下参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | import * as THREE from 'three' ; export const TDHeatMap = (data: any) => { // 热力图 var heatmap = h337.create({ container: document.getElementById( 'heatmap' ) }); var len = 100; var width = 300; var height = 300; var points = []; var max = 0; while (len--) { var val = Math.floor(Math.random() * 100); max = Math.max(max, val); var point = { x: Math.floor(Math.random() * width), y: Math.floor(Math.random() * height), value: val }; points.push(point); } heatmap.setData({ max: max, data: points }); // 灰度图 var greymap = h337.create({ container: document.getElementById( 'greymap' ), gradient: { '0' : 'black' , '1.0' : 'white' } }); greymap.setData({ max: max, data: points }); let heatMapMaterial = new THREE.ShaderMaterial({ transparent: true , vertexShader: `varying vec2 vUv; uniform float Zscale; uniform sampler2D greyMap; void main() { vUv = uv; vec4 frgColor = texture2D(greyMap, uv); //获取灰度图点位信息 float height = Zscale * frgColor.a; //通过灰度图的rgb*需要设置的高度计算出热力图每个点位最终在z轴高度 vec3 transformed = vec3( position.x, position.y, height); //重新组装点坐标 gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0); //渲染点位 } `, fragmentShader: `varying vec2 vUv; uniform sampler2D heatMap; //热力图 uniform vec3 u_color; //基础颜色 uniform float u_opacity; // 透明度 void main() { //vec4 alphaColor = texture2D(heatMap, vUv); // gl_FragColor = alphaColor; gl_FragColor = vec4(u_color, u_opacity) * texture2D(heatMap, vUv); //把热力图颜色和透明度进行渲染 }`, uniforms: { 'heatMap' : { value: { value: undefined } }, 'greyMap' : { value: { value: undefined } }, Zscale: { value: 100.0 }, // 高度参数 u_color: { value: new THREE.Color( 'rgb(255, 255, 255)' ) }, u_opacity: { value: 1.0 } } }); let texture = new THREE.Texture(heatmap._config.container.children[0]); texture.needsUpdate = true ; let texture2 = new THREE.Texture(greymap._config.container.children[0]); texture2.needsUpdate = true ; heatMapMaterial.uniforms.heatMap.value = texture; heatMapMaterial.side = THREE.DoubleSide; // 双面渲染 heatMapMaterial.uniforms.greyMap.value = texture2 const heatMapModel = new THREE.PlaneBufferGeometry(800, 800, 300, 300) // 3d热力图大小,及分块数量 let heatMapPlane = new THREE.Mesh(heatMapModel, heatMapMaterial) heatMapPlane.rotation. set (-Math.PI / 2, 0, 0); heatMapPlane.position. set (200, 0, 0); // 3d热力图中心点位置 return heatMapPlane } |
使用
1 2 | const HeatMesh = TDHeatMap( '' ); Three.threeExample.lightEfficiencyGroup.add(HeatMesh); // 添加到场景中。 |
需要引入 heatmap.js 文件
文件如下,或者自己去网上找
1 2 3 4 5 6 7 8 9 | /* * heatmap.js v2.0.2 | JavaScript Heatmap Library * * Copyright 2008-2016 Patrick Wied <heatmapjs@patrick-wied.at> - All rights reserved. * Dual licensed under MIT and Beerware license * * :: 2016-02-04 21:25 */ (function(a,b,c){ if ( typeof module!== "undefined" &&module.exports){module.exports=c()} else if ( typeof define=== "function" &&define.amd){define(c)} else {b[a]=c()}})( "h337" , this ,function(){ var a={defaultRadius:40,defaultRenderer: "canvas2d" ,defaultGradient:{.25: "rgb(0,0,255)" ,.55: "rgb(0,255,0)" ,.85: "yellow" ,1: "rgb(255,0,0)" },defaultMaxOpacity:1,defaultMinOpacity:0,defaultBlur:.85,defaultXField: "x" ,defaultYField: "y" ,defaultValueField: "value" ,plugins:{}}; var b=function h(){ var b=function d(a){ this ._coordinator={}; this ._data=[]; this ._radi=[]; this ._min=0; this ._max=1; this ._xField=a[ "xField" ]||a.defaultXField; this ._yField=a[ "yField" ]||a.defaultYField; this ._valueField=a[ "valueField" ]||a.defaultValueField; if (a[ "radius" ]){ this ._cfgRadius=a[ "radius" ]}}; var c=a.defaultRadius;b.prototype={_organiseData:function(a,b){ var d=a[ this ._xField]; var e=a[ this ._yField]; var f= this ._radi; var g= this ._data; var h= this ._max; var i= this ._min; var j=a[ this ._valueField]||1; var k=a.radius|| this ._cfgRadius||c; if (!g[d]){g[d]=[];f[d]=[]} if (!g[d][e]){g[d][e]=j;f[d][e]=k} else {g[d][e]+=j} if (g[d][e]>h){ if (!b){ this ._max=g[d][e]} else { this .setDataMax(g[d][e])} return false } else { return {x:d,y:e,value:j,radius:k,min:i,max:h}}},_unOrganizeData:function(){ var a=[]; var b= this ._data; var c= this ._radi; for ( var d in b){ for ( var e in b[d]){a.push({x:d,y:e,radius:c[d][e],value:b[d][e]})}} return {min: this ._min,max: this ._max,data:a}},_onExtremaChange:function(){ this ._coordinator.emit( "extremachange" ,{min: this ._min,max: this ._max})},addData:function(){ if (arguments[0].length>0){ var a=arguments[0]; var b=a.length; while (b--){ this .addData.call( this ,a[b])}} else { var c= this ._organiseData(arguments[0], true ); if (c){ this ._coordinator.emit( "renderpartial" ,{min: this ._min,max: this ._max,data:[c]})}} return this },setData:function(a){ var b=a.data; var c=b.length; this ._data=[]; this ._radi=[]; for ( var d=0;d<c;d++){ this ._organiseData(b[d], false )} this ._max=a.max; this ._min=a.min||0; this ._onExtremaChange(); this ._coordinator.emit( "renderall" , this ._getInternalData()); return this },removeData:function(){},setDataMax:function(a){ this ._max=a; this ._onExtremaChange(); this ._coordinator.emit( "renderall" , this ._getInternalData()); return this },setDataMin:function(a){ this ._min=a; this ._onExtremaChange(); this ._coordinator.emit( "renderall" , this ._getInternalData()); return this },setCoordinator:function(a){ this ._coordinator=a},_getInternalData:function(){ return {max: this ._max,min: this ._min,data: this ._data,radi: this ._radi}},getData:function(){ return this ._unOrganizeData()}}; return b}(); var c=function i(){ var a=function(a){ var b=a.gradient||a.defaultGradient; var c=document.createElement( "canvas" ); var d=c.getContext( "2d" );c.width=256;c.height=1; var e=d.createLinearGradient(0,0,256,1); for ( var f in b){e.addColorStop(f,b[f])}d.fillStyle=e;d.fillRect(0,0,256,1); return d.getImageData(0,0,256,1).data}; var b=function(a,b){ var c=document.createElement( "canvas" ); var d=c.getContext( "2d" ); var e=a; var f=a;c.width=c.height=a*2; if (b==1){d.beginPath();d.arc(e,f,a,0,2*Math.PI, false );d.fillStyle= "rgba(0,0,0,1)" ;d.fill()} else { var g=d.createRadialGradient(e,f,a*b,e,f,a);g.addColorStop(0, "rgba(0,0,0,1)" );g.addColorStop(1, "rgba(0,0,0,0)" );d.fillStyle=g;d.fillRect(0,0,2*a,2*a)} return c}; var c=function(a){ var b=[]; var c=a.min; var d=a.max; var e=a.radi; var a=a.data; var f=Object.keys(a); var g=f.length; while (g--){ var h=f[g]; var i=Object.keys(a[h]); var j=i.length; while (j--){ var k=i[j]; var l=a[h][k]; var m=e[h][k];b.push({x:h,y:k,value:l,radius:m})}} return {min:c,max:d,data:b}};function d(b){ var c=b.container; var d= this .shadowCanvas=document.createElement( "canvas" ); var e= this .canvas=b.canvas||document.createElement( "canvas" ); var f= this ._renderBoundaries=[1e4,1e4,0,0]; var g=getComputedStyle(b.container)||{};e.className= "heatmap-canvas" ; this ._width=e.width=d.width=b.width||+g.width.replace(/px/, "" ); this ._height=e.height=d.height=b.height||+g.height.replace(/px/, "" ); this .shadowCtx=d.getContext( "2d" ); this .ctx=e.getContext( "2d" );e.style.cssText=d.style.cssText= "position:absolute;left:0;top:0;" ;c.style.position= "relative" ;c.appendChild(e); this ._palette=a(b); this ._templates={}; this ._setStyles(b)}d.prototype={renderPartial:function(a){ if (a.data.length>0){ this ._drawAlpha(a); this ._colorize()}},renderAll:function(a){ this ._clear(); if (a.data.length>0){ this ._drawAlpha(c(a)); this ._colorize()}},_updateGradient:function(b){ this ._palette=a(b)},updateConfig:function(a){ if (a[ "gradient" ]){ this ._updateGradient(a)} this ._setStyles(a)},setDimensions:function(a,b){ this ._width=a; this ._height=b; this .canvas.width= this .shadowCanvas.width=a; this .canvas.height= this .shadowCanvas.height=b},_clear:function(){ this .shadowCtx.clearRect(0,0, this ._width, this ._height); this .ctx.clearRect(0,0, this ._width, this ._height)},_setStyles:function(a){ this ._blur=a.blur==0?0:a.blur||a.defaultBlur; if (a.backgroundColor){ this .canvas.style.backgroundColor=a.backgroundColor} this ._width= this .canvas.width= this .shadowCanvas.width=a.width|| this ._width; this ._height= this .canvas.height= this .shadowCanvas.height=a.height|| this ._height; this ._opacity=(a.opacity||0)*255; this ._maxOpacity=(a.maxOpacity||a.defaultMaxOpacity)*255; this ._minOpacity=(a.minOpacity||a.defaultMinOpacity)*255; this ._useGradientOpacity=!!a.useGradientOpacity},_drawAlpha:function(a){ var c= this ._min=a.min; var d= this ._max=a.max; var a=a.data||[]; var e=a.length; var f=1- this ._blur; while (e--){ var g=a[e]; var h=g.x; var i=g.y; var j=g.radius; var k=Math.min(g.value,d); var l=h-j; var m=i-j; var n= this .shadowCtx; var o; if (! this ._templates[j]){ this ._templates[j]=o=b(j,f)} else {o= this ._templates[j]} var p=(k-c)/(d-c);n.globalAlpha=p<.01?.01:p;n.drawImage(o,l,m); if (l< this ._renderBoundaries[0]){ this ._renderBoundaries[0]=l} if (m< this ._renderBoundaries[1]){ this ._renderBoundaries[1]=m} if (l+2*j> this ._renderBoundaries[2]){ this ._renderBoundaries[2]=l+2*j} if (m+2*j> this ._renderBoundaries[3]){ this ._renderBoundaries[3]=m+2*j}}},_colorize:function(){ var a= this ._renderBoundaries[0]; var b= this ._renderBoundaries[1]; var c= this ._renderBoundaries[2]-a; var d= this ._renderBoundaries[3]-b; var e= this ._width; var f= this ._height; var g= this ._opacity; var h= this ._maxOpacity; var i= this ._minOpacity; var j= this ._useGradientOpacity; if (a<0){a=0} if (b<0){b=0} if (a+c>e){c=e-a} if (b+d>f){d=f-b} var k= this .shadowCtx.getImageData(a,b,c,d); var l=k.data; var m=l.length; var n= this ._palette; for ( var o=3;o<m;o+=4){ var p=l[o]; var q=p*4; if (!q){ continue } var r; if (g>0){r=g} else { if (p<h){ if (p<i){r=i} else {r=p}} else {r=h}}l[o-3]=n[q];l[o-2]=n[q+1];l[o-1]=n[q+2];l[o]=j?n[q+3]:r}k.data=l; this .ctx.putImageData(k,a,b); this ._renderBoundaries=[1e3,1e3,0,0]},getValueAt:function(a){ var b; var c= this .shadowCtx; var d=c.getImageData(a.x,a.y,1,1); var e=d.data[3]; var f= this ._max; var g= this ._min;b=Math.abs(f-g)*(e/255)>>0; return b},getDataURL:function(){ return this .canvas.toDataURL()}}; return d}(); var d=function j(){ var b= false ; if (a[ "defaultRenderer" ]=== "canvas2d" ){b=c} return b}(); var e={merge:function(){ var a={}; var b=arguments.length; for ( var c=0;c<b;c++){ var d=arguments[c]; for ( var e in d){a[e]=d[e]}} return a}}; var f=function k(){ var c=function h(){function a(){ this .cStore={}}a.prototype={ on :function(a,b,c){ var d= this .cStore; if (!d[a]){d[a]=[]}d[a].push(function(a){ return b.call(c,a)})},emit:function(a,b){ var c= this .cStore; if (c[a]){ var d=c[a].length; for ( var e=0;e<d;e++){ var f=c[a][e];f(b)}}}}; return a}(); var f=function(a){ var b=a._renderer; var c=a._coordinator; var d=a._store;c. on ( "renderpartial" ,b.renderPartial,b);c. on ( "renderall" ,b.renderAll,b);c. on ( "extremachange" ,function(b){a._config.onExtremaChange&&a._config.onExtremaChange({min:b.min,max:b.max,gradient:a._config[ "gradient" ]||a._config[ "defaultGradient" ]})});d.setCoordinator(c)};function g(){ var g= this ._config=e.merge(a,arguments[0]||{}); this ._coordinator= new c; if (g[ "plugin" ]){ var h=g[ "plugin" ]; if (!a.plugins[h]){ throw new Error( "Plugin '" +h+ "' not found. Maybe it was not registered." )} else { var i=a.plugins[h]; this ._renderer= new i.renderer(g); this ._store= new i.store(g)}} else { this ._renderer= new d(g); this ._store= new b(g)}f( this )}g.prototype={addData:function(){ this ._store.addData.apply( this ._store,arguments); return this },removeData:function(){ this ._store.removeData&& this ._store.removeData.apply( this ._store,arguments); return this },setData:function(){ this ._store.setData.apply( this ._store,arguments); return this },setDataMax:function(){ this ._store.setDataMax.apply( this ._store,arguments); return this },setDataMin:function(){ this ._store.setDataMin.apply( this ._store,arguments); return this },configure:function(a){ this ._config=e.merge( this ._config,a); this ._renderer.updateConfig( this ._config); this ._coordinator.emit( "renderall" , this ._store._getInternalData()); return this },repaint:function(){ this ._coordinator.emit( "renderall" , this ._store._getInternalData()); return this },getData:function(){ return this ._store.getData()},getDataURL:function(){ return this ._renderer.getDataURL()},getValueAt:function(a){ if ( this ._store.getValueAt){ return this ._store.getValueAt(a)} else if ( this ._renderer.getValueAt){ return this ._renderer.getValueAt(a)} else { return null }}}; return g}(); var g={create:function(a){ return new f(a)},register:function(b,c){a.plugins[b]=c}}; return g}); |
我一般不讲代码,只有部分注释,需要的同志可以自己看,源码也比较少,可以自己理解一下,很简单,
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了