等高线指的是地形图上高度相等的相邻各点所连成的闭合曲线。把地面上海拔高度相同的点连成的闭合曲线,并垂直投影到一个水平面上,并按比例缩绘在图纸上,就得到等高线。等高线也可以看作是不同海拔高度的水平面与实际地面的交线,所以等高线是闭合曲线。在等高线上标注的数字为该等高线的海拔。
下面就跟着我实现一个炫酷的3D等高线图(等值线图)吧!
1.绘制2D热力图Canvas
-
根据热力数据绘制热力Canvas贴图,可以参考我之前写的文《用Three.js搞个炫酷热力山丘图》
-
数据来源于:高德地图-普通热力图-全国交通事故增长率
-
黑白2D热力图的透明度即热力值
【黑白热力图】
- 黑白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
热力信息对应是:最小值,最大值,值范围,等高线高度间隔(值越小,等高线越多越密)
其中等高线线宽uMinLne
的范围是(-等高线高度间隔,+等高线高度间隔)
,等高线线宽值越高,算出的等高线越粗,上图uMinLne=0.3
等高线比较细,下图uMinLne=1.0
等高线比较粗。
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.;
}
}
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.);
}
}
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.);
}
【断层阶梯热力图1】
看上去有点梦幻深林的感觉!
注意
- 如果不想断层,想要完整阶梯状,可以注释掉断层判断的shader
//断层,利用等高线高度间隔取模,在[-uMinLne,+uMinLne]范围内断开连接的点
float m = mod(v, uInfo.w);
if(m <= uMinLne || m >= uInfo.w - uMinLne)
return;
- 因为断层阶梯是基于热力贴图计算的,断层存在误差,会出现部分粘连的小片段,然而
uMinLne
断层值越大会导致热力面的大小则减少,所以uMinLne
断层值需要取舍热力面大小和粘连的问题,适量调整才能呈现良好的效果。
上面【断层阶梯热力图1】的断层粘连比较少的uMinLne=1.0
,下图断层粘连比较多uMinLne=0.5
当然增加平面的片段数来增加三角面数量,计算断层更加精准,也可以减少粘连。
上面【断层阶梯热力图1】的断层粘连比较少的平面长宽片段数都是1000
,下图断层粘连比较多的平面长宽片段数都是500
,可以看到面数对粘连的影响比较大。
const geometry = new THREE.PlaneGeometry(
option.width * 0.5,
option.height * 0.5,
500,
500
);
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.);
}
GitHub地址
https://github.com/xiaolidan00/my-earth
参考