用Three.js搞个炫酷3D等高线(等值线)和断层阶梯热力图

1,773 阅读6分钟

等高线指的是地形图上高度相等的相邻各点所连成的闭合曲线。把地面上海拔高度相同的点连成的闭合曲线,并垂直投影到一个水平面上,并按比例缩绘在图纸上,就得到等高线。等高线也可以看作是不同海拔高度的水平面与实际地面的交线,所以等高线是闭合曲线。在等高线上标注的数字为该等高线的海拔。

image.png

下面就跟着我实现一个炫酷的3D等高线图(等值线图)吧!

20241230_120306.gif

1.绘制2D热力图Canvas

黑白热力图

【黑白热力图】

  • 黑白2D热力图根据颜色索引表转换成彩色2D热力图 彩色热力图

【彩色热力图】

2.黑白等高线

  • 顶点着色器,利用热力图透明度(热力值的映射)计算热力山丘高度
//热力贴图
uniform sampler2D map;
//山丘高度
uniform float uHeight;
varying vec4 vColor;
void main(void) {
//热力贴图颜色               
    vColor = texture2D(map, uv);
//热力高度
    float h = vColor.a * uHeight;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x, position.y, h, 1.0);
}
  • 片元着色器,利用等高线高度间隔取模,在uMinLne范围内绘制等高线
//等高线宽度
uniform float uMinLne;
//热力值信息
uniform vec4 uInfo;
//等高线颜色
uniform vec3 uLineColor;
varying vec4 vColor;
void main(void) { 
//还原热力值
    float v = vColor.a * uInfo.z + uInfo.x; 
//利用等高线高度间隔取模,在uMinLne范围内绘制等高线
    if(mod(v, uInfo.w) <= uMinLne) {
        //等高线颜色
        gl_FragColor.rgb = uLineColor;
        //透明度默认是1
        gl_FragColor.a = 1.;
    }
}
  • 使用【黑白热力图】绘制纯色等高线
//黑白热力贴图
const map = new THREE.CanvasTexture(heatmapCanvas);
          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: 50 },
              //热力信息:最小值,最大值,值范围,等高线高度间隔
              uInfo: { value: new THREE.Vector4(option.min, option.max, option.size, 10) },
              //等高线宽度
              uMinLne: { value: 0.3 },
              //等高线颜色
              uLineColor: { value: new THREE.Color('#ffffff') }
            },
            vertexShader:  ``,
            fragmentShader:  ``
          });
          const plane = new THREE.Mesh(geometry, material);
          plane.rotateX(-Math.PI * 0.5);
          this.scene.add(plane);
  • uInfo热力信息对应是:最小值,最大值,值范围,等高线高度间隔(值越小,等高线越多越密)

20241230_172426.gif

其中等高线线宽uMinLne的范围是(-等高线高度间隔,+等高线高度间隔),等高线线宽值越高,算出的等高线越粗,上图uMinLne=0.3等高线比较细,下图uMinLne=1.0等高线比较粗。

20241230_205958.gif

3.绘制彩色等高线

彩色等高线与黑白等高线相似,只不过将【黑白热力图】换成【彩色热力图】,等高线颜色换成【彩色热力图】的颜色。

  • 片元着色器
//等高线宽度
uniform float uMinLne;
//热力值信息
uniform vec4 uInfo;
//等高线颜色
uniform vec3 uLineColor;
varying vec4 vColor;
void main(void) { 
//还原热力值
    float v = vColor.a * uInfo.z + uInfo.x; 
//利用等高线高度间隔取模,在uMinLne范围内绘制等高线
    if(mod(v, uInfo.w) <= uMinLne) {
    //彩色热力图颜色
        gl_FragColor.rgb = vColor.rgb;
        gl_FragColor.a = 1.;
    }
}

20241230_120306.gif

4.彩色热力与纯色等高线结合

修改一下片元着色器,利用等高线高度间隔取模,在uMinLne范围内绘制纯色等高线,否则为彩色热力颜色

  • 片元着色器
//等高线宽度
uniform float uMinLne;
//热力值信息
uniform vec4 uInfo;
//等高线颜色
uniform vec3 uLineColor;
varying vec4 vColor;
void main(void) { 
//还原热力值
    float v = vColor.a * uInfo.z + uInfo.x; 
//利用等高线高度间隔取模,在uMinLne范围内绘制等高线
    if(mod(v, uInfo.w) <= uMinLne) {
    //纯色等高线颜色
        gl_FragColor.rgb = uLineColor;
        gl_FragColor.a = 1.;
    } else {
    //彩色热力颜色
        gl_FragColor.rgb = vColor.rgb;
        gl_FragColor.a = clamp(vColor.a * 2., 0., 1.);
    }
}

20241230_174317.gif

5.断层阶梯热力图

断层阶梯热力图就是将每个等高线高度间隔压平,向下取整,并根据uMinLne等高线线宽计算断层范围,形成一层层的热力面。

  • 顶点着色器
//热力贴图
uniform sampler2D map;
//山丘高度
uniform float uHeight;
//等高线宽度
uniform float uMinLne;
//热力信息
uniform vec4 uInfo;
varying vec4 vColor;
void main(void) {
//热力贴图颜色
    vec4 color = texture2D(map, uv);
    vColor = color;
//热力贴图透明度
    float a = color.a;
//还原热力值 
    float v = a * uInfo.z + uInfo.x;
//断层,利用等高线高度间隔取模,在[-uMinLne,+uMinLne]范围内断开连接的点
    float m = mod(v, uInfo.w);
    if(m <= uMinLne || m >= uInfo.w - uMinLne)
        return;
//热力值对等高线高度间隔向下取整
    float f = (floor(v / uInfo.w) * uInfo.w - uInfo.x) / uInfo.z;
//计算热力高度
    float h = f * uHeight;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x, position.y, h, 1.0);
}
  • 片元着色器
varying vec4 vColor;
void main(void) {
//热力贴图颜色
    gl_FragColor.rgb = vColor.rgb; 
//增加透明度,限制范围
    gl_FragColor.a = clamp(vColor.a * 10., 0., 1.);
}

20241230_180008.gif

【断层阶梯热力图1】

看上去有点梦幻深林的感觉!

注意

  1. 如果不想断层,想要完整阶梯状,可以注释掉断层判断的shader
//断层,利用等高线高度间隔取模,在[-uMinLne,+uMinLne]范围内断开连接的点
   float m = mod(v, uInfo.w);
 if(m <= uMinLne || m >= uInfo.w - uMinLne)
      return;

20241230_192358.gif

  1. 因为断层阶梯是基于热力贴图计算的,断层存在误差,会出现部分粘连的小片段,然而uMinLne断层值越大会导致热力面的大小则减少,所以uMinLne断层值需要取舍热力面大小和粘连的问题,适量调整才能呈现良好的效果。

上面【断层阶梯热力图1】的断层粘连比较少的uMinLne=1.0,下图断层粘连比较多uMinLne=0.5

20241230_193126.gif

当然增加平面的片段数来增加三角面数量,计算断层更加精准,也可以减少粘连。

上面【断层阶梯热力图1】的断层粘连比较少的平面长宽片段数都是1000,下图断层粘连比较多的平面长宽片段数都是500,可以看到面数对粘连的影响比较大。

const geometry = new THREE.PlaneGeometry(
            option.width * 0.5,
            option.height * 0.5,
            500,
            500
          );

image.png 3. 要形成一层层的阶梯,需要在顶点着色器里面计算出需要断开点的范围,返回空,不能在片元着色器里面计算,因为顶点着色器的数据经过栅格化后才传输到片元着色器进行处理,数据会有偏差,断层阶梯范围会出现锯齿三角形状。

下面是在片元着色器进行断层的shader代码和效果

  • 顶点着色器
//热力贴图
uniform sampler2D map;
//山丘高度
uniform float uHeight;
//等高线宽度
uniform float uMinLne;
//热力信息
uniform vec4 uInfo;
varying vec4 vColor;
varying float val;
void main(void) {
//热力贴图颜色
    vec4 color = texture2D(map, uv);
    vColor = color;
//热力贴图透明度
    float a = color.a;
//还原热力值 
    float v = a * uInfo.z + uInfo.x;
    //传递热力值
   val=v;
//热力值对等高线高度间隔向下取整
    float f = (floor(v / uInfo.w) * uInfo.w - uInfo.x) / uInfo.z;
//计算热力高度
    float h = f * uHeight;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x, position.y, h, 1.0);
}
  • 片元着色器
varying vec4 vColor;
uniform float uMinLne;
//热力信息
uniform vec4 uInfo;
 varying float val;
void main(void) {
//断层,利用等高线高度间隔取模,在[-uMinLne,+uMinLne]范围内断开连接的点
float m = mod(val, uInfo.w);
if(m <= uMinLne || m >= uInfo.w - uMinLne)return;
//热力贴图颜色
    gl_FragColor.rgb = vColor.rgb; 
//增加透明度,限制范围
    gl_FragColor.a = clamp(vColor.a * 10., 0., 1.);

}

image.png

GitHub地址

https://github.com/xiaolidan00/my-earth

参考