实现3D热力图
实现思路
- 首先是需要用canvas绘制一个2D的热力图,如果你还不会,请看json绘制热力图。
- 使用Threejs中的canvas贴图,将贴图贴在PlaneGeometry平面上。
- 使用着色器材质,更具json中的数据让平面模型 拔地而起。
- 使用Threejs内置的TWEEN,加上一个动画。
效果
效果如下:
具体代码
具体实现效果代码:
import * as THREE from 'three';
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import * as d3geo from 'd3-geo'
import trafficJSON from '../assets/json/traffic.json'
export default (domId) => {
/* ------------------------------初始化三件套--------------------------------- */
const dom = document.getElementById(domId);
const { innerHeight, innerWidth } = window
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 1, 2000);
camera.position.set(-25, 288, 342);
camera.lookAt(scene.position);
const renderer = new THREE.WebGLRenderer({
antialias: true,// 抗锯齿
alpha: false,// 透明度
powerPreference: 'high-performance',// 性能
logarithmicDepthBuffer: true,// 深度缓冲
})
// renderer.setClearColor(0x000000, 0);// 设置背景色
// renderer.clear();// 清除渲染器
renderer.shadowMap.enabled = true;// 开启阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap;// 阴影类型
renderer.outputEncoding = THREE.sRGBEncoding;// 输出编码
renderer.toneMapping = THREE.ACESFilmicToneMapping;// 色调映射
renderer.toneMappingExposure = 1;// 色调映射曝光
renderer.physicallyCorrectLights = true;// 物理正确灯光
renderer.setPixelRatio(devicePixelRatio);// 设置像素比
renderer.setSize(innerWidth, innerHeight);// 设置渲染器大小
dom.appendChild(renderer.domElement);
// 重置大小
window.addEventListener('resize', () => {
const { innerHeight, innerWidth } = window
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
})
/* ------------------------------初始化工具--------------------------------- */
const controls = new OrbitControls(camera, renderer.domElement) // 相机轨道控制器
controls.enableDamping = true // 是否开启阻尼
controls.dampingFactor = 0.05// 阻尼系数
controls.panSpeed = -1// 平移速度
const axesHelper = new THREE.AxesHelper(10);
scene.add(axesHelper);
/* ------------------------------正题--------------------------------- */
let geoFun;// 地理投影函数
let info = {
max: Number.MIN_SAFE_INTEGER,
min: Number.MAX_SAFE_INTEGER,
maxlng: Number.MIN_SAFE_INTEGER,
minlng: Number.MAX_SAFE_INTEGER,
maxlat: Number.MIN_SAFE_INTEGER,
minlat: Number.MAX_SAFE_INTEGER,
data: []
};
// 初始化地理投影
const initGeo = (size) => {
geoFun = d3geo.geoMercator().scale(size || 100)
}
// 经纬度转像素坐标
const latlng2px = (pos) => {
if (pos[0] >= -180 && pos[0] <= 180 && pos[1] >= -90 && pos[1] <= 90) {
return geoFun(pos);
}
return pos;
};
// 创建颜色
const createColors = (option) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 256;
canvas.height = 1;
const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
for (let k in option.colors) {
grad.addColorStop(k, option.colors[k]);
}
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return ctx.getImageData(0, 0, canvas.width, 1).data;
}
// 绘制圆
const drawCircle = (ctx, option, item) => {
let { lng, lat, value } = item;
let x = lng - option.minlng + option.radius;
let y = lat - option.minlat + option.radius;
const grad = ctx.createRadialGradient(x, y, 0, x, y, option.radius);
grad.addColorStop(0.0, 'rgba(0,0,0,1)');
grad.addColorStop(1.0, 'rgba(0,0,0,0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(x, y, option.radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.globalAlpha = (value - option.min) / option.size;
ctx.fill();
}
// 创建热力图
const createHeatmap = (option) => {
const canvas = document.createElement('canvas');
canvas.width = option.width;
canvas.height = option.height;
const ctx = canvas.getContext('2d');
option.size = option.max - option.min;
option.data.forEach((item) => {
drawCircle(ctx, option, item);
});
const colorData = createColors(option);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (let i = 3; i < imageData.data.length; i = i + 4) {
let opacity = imageData.data[i];
let offset = opacity * 4;
//red
imageData.data[i - 3] = colorData[offset];
//green
imageData.data[i - 2] = colorData[offset + 1];
//blue
imageData.data[i - 1] = colorData[offset + 2];
}
ctx.putImageData(imageData, 0, 0);
return { canvas, option };
}
// 初始化
const init = () => {
initGeo(1000)
// 处理数据
trafficJSON.features.forEach((item) => {
let pos = latlng2px(item.geometry.coordinates);// 经纬度转像素坐标
let newitem = {
lng: pos[0],
lat: pos[1],
value: item.properties.avg
}
info.max = Math.max(newitem.value, info.max);
info.maxlng = Math.max(newitem.lng, info.maxlng);
info.maxlat = Math.max(newitem.lat, info.maxlat);
info.min = Math.min(newitem.value, info.min);
info.minlng = Math.min(newitem.lng, info.minlng);
info.minlat = Math.min(newitem.lat, info.minlat);
info.data.push(newitem);
})
info.size = info.max - info.min;
info.sizelng = info.maxlng - info.minlng;
info.sizelat = info.maxlat - info.minlat;
const radius = 50;
const { canvas, option } = createHeatmap({
width: info.sizelng + radius * 2,
height: info.sizelng + radius * 2,
colors: {
0.1: '#2A85B8',
0.2: '#16B0A9',
0.3: '#29CF6F',
0.4: '#5CE182',
0.5: '#7DF675',
0.6: '#FFF100',
0.7: '#FAA53F',
1: '#D04343'
},
radius,
...info
})
const map = new THREE.CanvasTexture(canvas);
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
// 创建平面
const geometry = new THREE.PlaneGeometry(
option.width * 0.5,
option.height * 0.5,
500,
500
);
const material = new THREE.ShaderMaterial({
transparent: true,
side: THREE.DoubleSide,
uniforms: {
map: { value: map },
uHeight: { value: 100 },
uOpacity: { value: 2.0 }
},
vertexShader: `
uniform sampler2D map;
uniform float uHeight;
varying vec2 v_texcoord;
void main(void)
{
v_texcoord = uv;
float h=texture2D(map, v_texcoord).a*uHeight;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position.x,position.y,h, 1.0 );
}`,
fragmentShader: `
precision mediump float;
uniform sampler2D map;
uniform float uOpacity;
varying vec2 v_texcoord;
void main (void)
{
vec4 color= texture2D(map, v_texcoord);
float a=color.a*uOpacity;
gl_FragColor.rgb =color.rgb;
gl_FragColor.a=a>1.0?1.0:a;
}`
});
const plane = new THREE.Mesh(geometry, material);
plane.rotateX(-Math.PI * 0.5);
scene.add(plane);
const tween = new TWEEN
.Tween({ v: 0 })
.to({ v: 100 }, 2000)
.onUpdate(obj => {
material.uniforms.uHeight.value = obj.v;
})
.easing(TWEEN.Easing.Quadratic.InOut)
.start();
TWEEN.add(tween);
}
init();
/* ------------------------------动画函数--------------------------------- */
const animation = () => {
TWEEN.update();
controls.update();// 如果不调用,就会很卡
renderer.render(scene, camera);
requestAnimationFrame(animation);
}
animation();
}