一、简介
地理数据与obj、gltf等模型数据亦或是canvas等图形数据在本质上其实并没什么区别,只是因为地理数据是在地球背景下地图数据,坐标投影系统是以真实地球为基准,地理数据坐标具有特殊的单位,经纬度坐标单位是度,墨卡托坐标单位是米,因此,地理数据被赋予了特殊的含义。
在web可视化中,按照真实比例绘制地球或区域地图显然是不合适的,因此,将地理数据合理的转换为场景数据是必要的。下面介绍一下坐标转换。
二、坐标系介绍
1、经纬度
经纬度坐标具体含义和由来这里不细说,地球坐标。在应用时,请注意最重要的一点:在一张世界地图上,左上角点坐标是(-180, 90),右下角点坐标是(180, -90)。
2、墨卡托
墨卡托投影是指等角圆柱投影,能保证形状不变性,投影之后能保证经线纬线各自平行互相垂直。适合做平面地图,Google就是用的墨卡托投影。墨卡托投影单位是米。左上角点坐标为(-20037508.3427892,20037508.3427892)。
3、球极坐标
以坐标原点为参考点,由方位角、仰角、半径构成。空间上的x、y、z可以用这三个参数表示。
三、坐标转换
地理数据坐标系主要分为经纬度和墨卡托,我们往往不能要求这种源数据的坐标信息,所以只能自己处理。
本项目采用的方案是:在地球可视化中,采用的经纬度 + 球极坐标。平面地图采用的是墨卡托投影坐标。下面给出坐标转换核心代码。
1、经纬度转球极坐标
三角函数即可完成,可以画个图推导一下。
const lonlatToSphere = (lon: number, lat: number, radius: number) => {
const phi = angleToRad(90 - lat);
const theta = angleToRad(180 + lon);
const x = -(radius * Math.sin(phi) * Math.cos(theta));
const z = radius * Math.sin(phi) * Math.sin(theta);
const y = radius * Math.cos(phi);
return { x, y, z };
};
2、经纬度转地球贴图坐标
地球采用threejs中的SphereGeometry,贴图需要按经纬度坐标利用canvas绘制。简单的线性变化,1080、540代表贴图大小。
const lonlatToFlat = (lon: number, lat: number) => {
const x = ((lon - -180) / 360) * 1080;
const y = ((90 - lat) / 180) * 540;
return { x, y };
};
3、经纬度转墨卡托
固定算法。
const lonlatToMocart = (lon: number, lat: number) => {
const x = (lon * 20037508.34) / 180;
let y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180);
y = (y * 20037508.34) / 180;
return { x, y };
};
4、墨卡托转经纬度
固定算法。
const mocartToLonlat = (x: number, y: number) => {
const lon = (x / 20037508.34) * 180;
let lat = (y / 20037508.34) * 180;
lat = (180 / Math.PI) * (2 * Math.atan(Math.exp((y * Math.PI) / 180)) - Math.PI / 2);
return { lon, lat };
};
四、绘制地球
源数据为geojson,geojson中记录了世界地图几何形状、几何坐标信息以及几何对应的属性。
生成地球时需要4张贴图:
利用canvas API 以及坐标转换绘制一张地球国家轮廓线、一张地球国家行政区划和一张海洋深度图。可以将绘制结果保存为png文件,这样可以避免每次都绘制一遍,造成不必要的损耗。绘制区划时,注意将每个几何颜色赋值为(1, 1, 1)、(2, 2, 2)、(3, 3, 3),依次类推。海洋设置为0。
利用canvas绘制一个255×1的索引图,第0个像素设置为(0, 0, 0)代表海洋,其余设置为(1, 1, 1),代表对应的国家。
使用THREE.SphereGeometry方法生成一个圆球当作地球,使用THREE.ShaderMaterial生成材质,ShaderMaterial需要自己写着色器代码实现,这里列出着色器的代码。
片元着色器
uniform sampler2D mapIndex;
uniform sampler2D lookup;
uniform sampler2D outline;
uniform sampler2D depthTexture;
uniform vec3 surfaceColor;
uniform vec3 lineColor;
uniform vec3 oceanColor;
varying vec2 vUv;
void main() {
vec4 mapColor = texture2D(mapIndex, vUv);
float indexedColor = mapColor.x;
vec4 lookupColor = texture2D(lookup, vec2(indexedColor, 0.0));
float outlineColor = texture2D(outline, vUv).x;
vec4 depth = texture2D(depthTexture, vUv);
vec4 earthColor = vec4(0.0);
if (lookupColor.x == 1.0) {
if (outlineColor > 0.2) {
earthColor = vec4(lineColor, 0.6);
} else {
earthColor = vec4(mix(surfaceColor, vec3(indexedColor), 0.0), 1.0);
}
} else if (lookupColor.x == 0.0) {
if(depth.x > 0.4) {
earthColor = vec4(mix(surfaceColor, depth.xyz, 0.1), 1.0);
} else {
earthColor = vec4(mix(surfaceColor, depth.xyz, 0.3), 1.0);
}
}
gl_FragColor = earthColor;
}
顶点着色器
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
五、地球大气
地球是有一层大气层的,在屏幕上显示为一层朦朦的光圈。
实现思路:做一个半径比地球稍大一点的SphereGeometry,编写着色器使用ShaderMaterial做材质,将做成的Mesh加入到Scene中即可。这里列出着色器代码。
片元着色器:
uniform float c;
uniform float p;
uniform vec3 color;
varying vec3 vNormal;
void main() {
float intensity = pow(c - dot(vNormal, vec3(0.0, 0.0, 0.7)), p);
gl_FragColor = vec4(color, 1.0) * intensity;
}
顶点着色器:
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}