这篇文章用来记录我对cesium实现散点生成曲面热力图的实现
简单说下原思考过程,最开始准备使用三角网生成曲面,但是这个方案还要去做顶点细分,实在太麻烦,干脆直接使用heatmapjs生成灰度纹理贴图,根据灰度和最大最小值计算顶点的Y轴偏移水平,并通过顶点着色器将偏移设置到polygon上,得到曲面
效果图
看起来还不错~如果您的需求也差不多,那么可以开抄了
首先你需要cesium和heatmapjs,不需多说
然后你需要通过heatmapjs创建灰度图,这一步唯一要注意的就是heatmapjs的容器要和地图散点真实距离的包围盒还有点的位置的映射计算
ini
let self = this;
let radius = this.radius;
let geoData = this.geoData;
let options = this.options;
let scale = this.scale;
// 获取包围盒
let extremum = getExtremum(geoData);
this.rectExtremum = extremum.rectExtremum;
this.minValue = extremum.min;
this.maxValue = extremum.max;
// 获取包围盒真实宽度
this.width = Cesium.Cartesian3.distance(
Cesium.Cartesian3.fromDegrees(...self.rectExtremum[0]),
Cesium.Cartesian3.fromDegrees(
self.rectExtremum[1][0],
self.rectExtremum[0][1]
)
);
if (this.minValue == this.maxValue) {
this.maxValue = this.minValue + 1;
}
// 获取包围盒真实高度
this.height = Cesium.Cartesian3.distance(
Cesium.Cartesian3.fromDegrees(...self.rectExtremum[0]),
Cesium.Cartesian3.fromDegrees(
self.rectExtremum[0][0],
self.rectExtremum[1][1]
)
);
let area = this.height * this.width;
let _w = Math.sqrt(area);
// 计算缩放比例
this.scale = (this.scale || 500) / _w;
// 计算点映射在heatmap容器的坐标,返回映射数据
const data = this.getDataPoints(geoData);
// 创建并加载heatmap实例
const container = document.createElement("div");
container.style.width = `${self.width * this.scale}px`;
container.style.height = `${self.height * this.scale}px`;
document.body.appendChild(container);
const instance = h337.create({
container,
radius,
...options, // 这里可以传入调色板数据,使灰度图映射出渐变色,详细可以看heatmap官方文档
});
this.heatmapInstance = instance
container.style.position = "fixed";
document.body.removeChild(container);
// 向heatmap实例设置映射后的散点
instance.setData({ max: this.maxValue, min: this.minValue, data: data });
// 使用的计算函数
// 计算包围盒
const getExtremum = (geoData) => {
let lonMax = -1000,
lonMin = 1000,
latMax = -1000,
latMin = 1000,
valueMax = 0,
valueMin = 0;
if (!geoData || geoData.length == 0) return [];
geoData.map((item) => {
lonMax = lonMax > parseFloat(item.lon) ? lonMax : parseFloat(item.lon);
lonMin = lonMin < parseFloat(item.lon) ? lonMin : parseFloat(item.lon);
latMax = latMax > parseFloat(item.lat) ? latMax : parseFloat(item.lat);
latMin = latMin < parseFloat(item.lat) ? latMin : parseFloat(item.lat);
valueMax = valueMax > item.value ? valueMax : item.value;
valueMin = valueMin < item.value ? valueMin : item.value;
});
return {
rectExtremum: [
[lonMin, latMin],
[lonMax, latMax],
],
min: valueMin,
max: valueMax,
};
};
// 映射散点数据
getDataPoints(data) {
let self = this;
const west = self.rectExtremum[0][0];
const east = self.rectExtremum[1][0];
const north = self.rectExtremum[1][1];
const south = self.rectExtremum[0][1];
return data.map(({ lon, lat, value }) => {
let leftLon = lon - west;
let topLat = north - lat;
let left = (leftLon / (east - west)) * self.width * this.scale;
let top = (topLat / (north - south)) * self.height * this.scale;
return {
x: Math.ceil(left),
y: Math.ceil(top),
value,
};
});
}
然后根据获取的灰度贴图创建材质,顺便一提,Cesium.Material的文档我看的不是很明白,好像是你配置的fabric必须要有一个type,和对应的uniform(试了一段时间)
php
const material = new Cesium.Material({
fabric: {
type: "Image",
uniforms: {
image: instance.getDataURL(), // 传入根据调色板映射后的热力图图片数据
},
},
});
给刚刚创建的材质配置着色器并创建几何实例,如果你看过我之前写的着色器入门教程,你就完全可以理解这里的着色代码
ini
// 根据材质和着色器创建材质外观(我的理解,这里的材质外观因该是更细致的材质贴图)
const appearance = new Cesium.MaterialAppearance({
flat: true,
material: material,
vertexShaderSource: `
#extension GL_OES_standard_derivatives : enable
attribute vec3 position3DHigh;
attribute vec3 position3DLow;
attribute vec3 normal;
attribute vec2 st;
attribute float batchId;
varying vec3 v_positionEC;
varying vec3 v_normalEC;
varying vec2 v_st;
// 这里的image_0就是上面传入的image,只是cesium内部加了个_0
uniform sampler2D image_0;
void main(){
// czm_computePosition是cesium自带的获取顶点位置的函数
vec4 p = czm_computePosition();
vec4 color = texture2D(image_0, st);
// 根据透明度(heatmap默认会直接将灰度转为透明度)计算位置的x,y,z偏移,
// 参数你可以调整到适合你的项目需求,比如下面的5.0, heightMultiplier,都可以调,多试试
p = vec4(p.xyz + normal * color.a * 5.0 * ${this.heightMultiplier.toFixed(1)}, 0.5);
v_positionEC = (czm_modelViewRelativeToEye * p).xyz;
v_normalEC = czm_normal * normal;
v_st = st;
gl_Position = czm_modelViewProjectionRelativeToEye * p;
}
`,
fragmentShaderSource: `
varying vec2 v_st;
void main() {
// 根据项目设计图设置极值颜色和透明度
vec4 color = texture2D(image_0, v_st);
if(color.r == 0.0 && color.g == 0.0 && color.b < 1.0) {
gl_FragColor = vec4(0.0, 0.0, 1.0, 0.5);
}
else {
gl_FragColor = vec4(color.r, color.g, color.b, color.a * 0.4 + 0.5);
}
}
`
});
let geometryInstances
// 曲面边界设置,如果没有边界,则直接使用包围盒作为边界
if(!this.boundary && Array.isArray(this.boundary) && this.boundary.length) {
geometryInstances = new Cesium.GeometryInstance({
geometry: new Cesium.RectangleGeometry({
rectangle: Cesium.Rectangle.fromDegrees(
...self.rectExtremum[0],
...self.rectExtremum[1]
),
granularity: Cesium.Math.toRadians(0.001),
vertexFormat: Cesium.VertexFormat.POSITION_NORMAL_AND_ST,
}),
})
} else {
geometryInstances = this.boundary.map(item => {
return new Cesium.GeometryInstance({
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(item),
granularity: Cesium.Math.toRadians(0.001),
vertexFormat: Cesium.VertexFormat.POSITION_NORMAL_AND_ST,
})
})
})
}
// 加载到cesium图元
this.primitive = this.viewer.scene.primitives.add(
new Cesium.Primitive({
geometryInstances,
appearance: appearance,
})
);
最后贴上完整代码,写的比较杂乱,因为是边学边写
ini
import h337 from '@zouyaoji/heatmap.js'
/**
* 数据可视化--2维热力图
*/
class heatMapBend {
constructor(
viewer,
geoData = [],
boundary,
radius = 25,
scale = 500,
heightMultiplier = 200,
options = {
maxOpacity: 1,
minOpacity: 0,
blur: 0.75,
gradient: {
0.05: "rgb(0,0,255)",
0.35: "rgb(0,255,0)",
0.65: "yellow",
1: "rgb(255,0,0)",
},
}
) {
let self = this;
this.geoData = geoData;
this.boundary = boundary;
this.viewer = viewer;
this.radius = radius;
this.scale = scale;
this.heightMultiplier = heightMultiplier;
this.options = options;
this.heatmapInstance = null
console.log(this.heightMultiplier);
self.init();
}
init() {
let self = this;
let radius = this.radius;
let geoData = this.geoData;
let options = this.options;
let scale = this.scale;
let extremum = getExtremum(geoData);
this.rectExtremum = extremum.rectExtremum;
this.minValue = extremum.min;
this.maxValue = extremum.max;
this.width = Cesium.Cartesian3.distance(
Cesium.Cartesian3.fromDegrees(...self.rectExtremum[0]),
Cesium.Cartesian3.fromDegrees(
self.rectExtremum[1][0],
self.rectExtremum[0][1]
)
);
if (this.minValue == this.maxValue) {
this.maxValue = this.minValue + 1;
}
this.height = Cesium.Cartesian3.distance(
Cesium.Cartesian3.fromDegrees(...self.rectExtremum[0]),
Cesium.Cartesian3.fromDegrees(
self.rectExtremum[0][0],
self.rectExtremum[1][1]
)
);
let area = this.height * this.width;
let _w = Math.sqrt(area);
this.scale = (this.scale || 500) / _w;
const data = this.getDataPoints(geoData);
const container = document.createElement("div");
container.style.width = `${self.width * this.scale}px`;
container.style.height = `${self.height * this.scale}px`;
document.body.appendChild(container);
const instance = h337.create({
container,
radius,
...options,
});
this.heatmapInstance = instance
container.style.position = "fixed";
document.body.removeChild(container);
instance.setData({ max: this.maxValue, min: this.minValue, data: data });
const material = new Cesium.Material({
fabric: {
type: "Image",
uniforms: {
image: instance.getDataURL(),
},
},
});
const appearance = new Cesium.MaterialAppearance({
flat: true,
material: material,
vertexShaderSource: `
#extension GL_OES_standard_derivatives : enable
attribute vec3 position3DHigh;
attribute vec3 position3DLow;
attribute vec3 normal;
attribute vec2 st;
attribute float batchId;
varying vec3 v_positionEC;
varying vec3 v_normalEC;
varying vec2 v_st;
uniform sampler2D image_0;
void main(){
vec4 p = czm_computePosition();
vec4 color = texture2D(image_0, st);
p = vec4(p.xyz + normal * color.a * 5.0 * ${this.heightMultiplier.toFixed(1)}, 0.5);
v_positionEC = (czm_modelViewRelativeToEye * p).xyz;
v_normalEC = czm_normal * normal;
v_st = st;
gl_Position = czm_modelViewProjectionRelativeToEye * p;
}
`,
fragmentShaderSource: `
varying vec2 v_st;
void main() {
vec4 color = texture2D(image_0, v_st);
if(color.r == 0.0 && color.g == 0.0 && color.b < 1.0) {
gl_FragColor = vec4(0.0, 0.0, 1.0, 0.5);
}
else {
gl_FragColor = vec4(color.r, color.g, color.b, 0.8);
}
}
`
});
let geometryInstances
// 边界
if(!this.boundary && Array.isArray(this.boundary) && this.boundary.length) {
geometryInstances = new Cesium.GeometryInstance({
geometry: new Cesium.RectangleGeometry({
rectangle: Cesium.Rectangle.fromDegrees(
...self.rectExtremum[0],
...self.rectExtremum[1]
),
granularity: Cesium.Math.toRadians(0.001),
vertexFormat: Cesium.VertexFormat.POSITION_NORMAL_AND_ST,
}),
})
} else {
geometryInstances = this.boundary.map(item => {
return new Cesium.GeometryInstance({
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(item),
granularity: Cesium.Math.toRadians(0.001),
vertexFormat: Cesium.VertexFormat.POSITION_NORMAL_AND_ST,
})
})
})
}
this.primitive = this.viewer.scene.primitives.add(
new Cesium.Primitive({
geometryInstances,
appearance: appearance,
})
);
}
// 计算点映射在heatmap容器的坐标
getDataPoints(data) {
let self = this;
const west = self.rectExtremum[0][0];
const east = self.rectExtremum[1][0];
const north = self.rectExtremum[1][1];
const south = self.rectExtremum[0][1];
return data.map(({ lon, lat, value }) => {
let leftLon = lon - west;
let topLat = north - lat;
let left = (leftLon / (east - west)) * self.width * this.scale;
let top = (topLat / (north - south)) * self.height * this.scale;
return {
x: Math.ceil(left),
y: Math.ceil(top),
value,
};
});
}
destroy() {
this.primitive && this.viewer.scene.primitives.remove(this.primitive);
this.viewer = undefined;
this.primitive = null;
}
remove() {
this.destroy();
}
getValueAt(longitude, latitude) {
const minLon = this.rectExtremum[0][0] ; // 最小经度
const maxLon = this.rectExtremum[1][0]; // 最大经度
const minLat = this.rectExtremum[0][1]; // 最小纬度
const maxLat = this.rectExtremum[1][1]; // 最大纬度
const renderer = this.heatmapInstance._renderer
const canvasWidth = renderer.canvas.width;
const canvasHeight = renderer.canvas.height;
const x = ((longitude - minLon) / (maxLon - minLon)) * canvasWidth;
const y = canvasHeight - ((latitude - minLat) / (maxLat - minLat)) * canvasHeight;
var img = renderer.shadowCtx.getImageData(x, y, 1, 1);
var data = img.data[3];
const min = renderer._min, max = renderer._max
return Math.abs(max-min) * (data/255)
}
}
const getExtremum = (geoData) => {
let lonMax = -1000,
lonMin = 1000,
latMax = -1000,
latMin = 1000,
valueMax = 0,
valueMin = 0;
if (!geoData || geoData.length == 0) return [];
geoData.map((item) => {
lonMax = lonMax > parseFloat(item.lon) ? lonMax : parseFloat(item.lon);
lonMin = lonMin < parseFloat(item.lon) ? lonMin : parseFloat(item.lon);
latMax = latMax > parseFloat(item.lat) ? latMax : parseFloat(item.lat);
latMin = latMin < parseFloat(item.lat) ? latMin : parseFloat(item.lat);
valueMax = valueMax > item.value ? valueMax : item.value;
valueMin = valueMin < item.value ? valueMin : item.value;
});
return {
rectExtremum: [
[lonMin, latMin],
[lonMax, latMax],
],
min: valueMin,
max: valueMax,
};
};
export default heatMapBend;