要用cesium做个三维热力图的效果,但在网上没找到现成的方案,摸索了很久,最终效果如下:
三维热力图
实现思路如下:
1、通过heatmapjs库生成二维热力图,拿到canvas;
addHeatMap() {
let width = 500
let height = 500
this.heatMap = h337.create(this.getDefaultData(width, height))
let bounds = [113.5, 22, 114.5, 23]
this.bounds = {
west: 113.5,
south: 22,
east: 114.5,
north: 23,
}
let data = [];
let max = 0
for (let i = 0; i < 200; i++) {
let value = Math.floor(Math.random() * 100)
max = Math.max(max, value)
let x = Math.floor(Math.random() * width)
let y = Math.floor(Math.random() * height)
data.push({ x: x, y: y, value: value });
}
this.heatMap.setData({
// min: 0,
max: max,
data: data
})
this.addcanvas(bounds, true)
}
2、canvas的rgb像素值转hsl,将h分量作为该像素点的高度值的参考(即越红高度越高);
先转灰度图
// 图片转灰度图
getGrayMap(img) {
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
const c = ctx.getImageData(0, 0, img.width, img.height)
for (let i = 0; i < img.width; i++) {
for (let j = 0; j < img.height; j++) {
const x = (j * 4) * c.width + (i * 4)
const r = c.data[x]
const g = c.data[x + 1]
const b = c.data[x + 2]
this.colors.push([r / 255, g / 255, b / 255, 1], [r / 255, g / 255, b / 255, 1])
const gray = 240 - this.rgbtohsl(r, g, b)[0]
c.data[x] = c.data[x + 1] = c.data[x + 2] = gray
}
}
ctx.putImageData(c, 0, 0, 0, 0, c.width, c.height)
return canvas.toDataURL()
}
灰度转出来是一维数组,转为rgba四通道
getGrayValues(img) {
const ch = new CanvasHeightmap()
ch.use(img).then(() => {
ch.draw()
return ch.getRgbaArray()
}).then((data) => { this.createTerrain(data) })
}
3、将整个canvas划分,获取每个顶点的坐标值(经纬度+通过2中得到的高度);
getPos(rgbaArray, bounds) {
const arr = []
const row = rgbaArray.length
const col = rgbaArray[0].length
const everyLon = (bounds.east - bounds.west) / col
const everyLat = (bounds.north - bounds.south) / row
for (let i = 0; i <= row ; i++) {
for (let j = 0; j <= col ; j++) {
if ((i * col + j + 1) % (col + 1) !== 0 && (i * col + j) < (col + 1) * (row)) {
this.indices.push(i * col + j, i * col + j + 1, i * col + j + 1 + col + 1, i * col + j + 1 + col + 1, i * col + j, i * col + j + col + 1)
}
const lon1 = bounds.west + j * everyLon
const lat1 = bounds.north - i * everyLat
// let height1 = rgbaArray[i][j][0]
let height1 = this.getHeight(i, j, col, row, rgbaArray)
const position = Cesium.Cartesian3.fromDegrees(
lon1,
lat1, height1 * 10
)
arr.push(position.x, position.y, position.z)
this.sts.push((j / rgbaArray[0].length), (1 - i / rgbaArray.length))
}
}
return arr
}
getHeight(i, j, col, row ,rgbaArray) {
let height1
if (i === 0 && j === 0) {
height1 = rgbaArray[i][j][0] }
if (i === 0 && j !== 0 && j !== col) { height1 = (rgbaArray[i][j][0] + rgbaArray[i][j - 1][0]) / 2 }
if (i === 0 && j === col) { height1 = rgbaArray[i][j - 1][0] }
if (i !== 0 && i !== row && j === 0) { height1 = (rgbaArray[i][j][0] + rgbaArray[i - 1][j][0]) / 2 }
if (i !== 0 && i !== row && j !== 0 && j !== col) { height1 = (rgbaArray[i][j][0] + rgbaArray[i - 1][j][0] + rgbaArray[i - 1][j - 1][0] + rgbaArray[i][j - 1][0]) / 4 }
if (i !== 0 && i !== row && j === col) { height1 = (rgbaArray[i - 1][j - 1][0] + rgbaArray[i][j - 1][0]) / 2 }
if (i !== 0 && i !== row && j !== 0 && j !== col) { height1 = (rgbaArray[i][j][0] + rgbaArray[i - 1][j][0] + rgbaArray[i - 1][j - 1][0] + rgbaArray[i][j - 1][0]) / 4 }
if (i !== 0 && i !== row && j === col) { height1 = (rgbaArray[i - 1][j - 1][0] + rgbaArray[i][j - 1][0]) / 2 }
if (i === row && j === 0) { height1 = rgbaArray[i - 1][j][0] }
if (i === row && j !== 0 && j !== col) { height1 = (rgbaArray[i - 1][j][0] + rgbaArray[i - 1][j - 1][0]) / 2 }
if (i === row && j === col) { height1 = rgbaArray[i - 1][j - 1][0] }
return height1
}
4、通过3中的坐标创建三角网,参考:
cesium 绘制自定义geometry、三角面_liuqing0.0的博客-优快云博客_cesium geometry
createGeometry(realPos) {
const attributes = new Cesium.GeometryAttributes({
position: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.DOUBLE,
componentsPerAttribute: 3,
values: new Float64Array(realPos)
}),
st: new Cesium.GeometryAttribute({
componentDatatype: Cesium.ComponentDatatype.FLOAT,
componentsPerAttribute: 2,
values: new Float32Array(this.sts)
})
})
const boundingSphere = Cesium.BoundingSphere.fromVertices(realPos,
new Cesium.Cartesian3(0.0, 0.0, 0.0), 3)
const geometry = new Cesium.Geometry({
attributes: attributes,
indices: this.indices,
primitiveType: Cesium.PrimitiveType.TRIANGLES,
boundingSphere: boundingSphere
})
return new Cesium.GeometryInstance({ geometry })
}
addGeometry(instance) {
this.primitive = this.viewer.scene.primitives.add(
new Cesium.Primitive({
geometryInstances: instance,
appearance: new Cesium.Appearance({
material: new Cesium.Material({
fabric: {
uniforms: {
image: this.heatMap._renderer.canvas.toDataURL()
},
source: this.getMS()
}
}),
aboveGround: true,
faceForward: true,
flat: true,
translucent: false,
renderState: {
blending: Cesium.BlendingState.PRE_MULTIPLIED_ALPHA_BLEND,
depthTest: {
enabled: true,
},
depthMask: true,
},
fragmentShaderSource: this.getFS(),
vertexShaderSource: this.getVS()
}),
asynchronous: false
}))
}
主要需要处理position的values、st的values、和indices这三个属性的值。
我的思路是求出每个像素块的四个顶点的经纬度坐标,高度为周围的像素的h分量值的平均值;
每个像素块绘制两个三角形,如:左下--左上--右上 + 右上--左下--右下,当然顺序可以自己定义,只要保持每个三角形的起点与上一个三角形的终点连续就行;
5、将二维热力图作为三角网的材质贴图,并将三角网添加进场景;
贴图参考:
Cesium学习笔记-工具篇20-PrimitiveTexture自定义渲染-贴图【转】 | PNG