cesium热力图曲面简单实现,使用heatmap

这篇文章用来记录我对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;
相关推荐
天宇&嘘月2 小时前
web第三次作业
前端·javascript·css
小王不会写code3 小时前
axios
前端·javascript·axios
发呆的薇薇°4 小时前
vue3 配置@根路径
前端·vue.js
luckyext4 小时前
HBuilderX中,VUE生成随机数字,vue调用随机数函数
前端·javascript·vue.js·微信小程序·小程序
小小码农(找工作版)4 小时前
JavaScript 前端面试 4(作用域链、this)
前端·javascript·面试
前端没钱4 小时前
前端需要学习 Docker 吗?
前端·学习·docker
前端郭德纲5 小时前
前端自动化部署的极简方案
运维·前端·自动化
海绵宝宝_5 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
码农土豆5 小时前
chrome V3插件开发,调用 chrome.action.setIcon,提示路径找不到
前端·chrome
鱼樱前端5 小时前
深入JavaScript引擎与模块加载机制:从V8原理到模块化实战
前端·javascript