引言
在地理信息系统(GIS)和地球科学领域,3D可视化技术正在迅速发展。它不仅为研究人员提供了更直观的数据展示方式,还使得普通用户能够以全新的视角探索世界。本文将介绍如何使用CesiumJS库来创建一个基于Web的3D地球视图,并实现特定区域的等高线显示功能。我们将通过一段HTML代码实例,演示如何加载地形数据、添加自定义材质以及生成等高线。
项目设置
为了开始我们的项目,首先需要确保页面中包含了CesiumJS库及其样式文件。我们可以通过CDN链接引入这些资源:
<script src="https://cesium.com/downloads/cesiumjs/releases/1.121/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.121/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
初始化Cesium Viewer
接下来是初始化Cesium Viewer,这是CesiumJS的核心组件,负责渲染3D场景。在初始化时,我们可以配置各种参数,如是否启用光照效果、大气层显示等。此外,还需要指定地形数据源,这里我们选择了WorldTerrain作为默认地形提供商:
Cesium.Ion.defaultAccessToken = 'YOUR_ACCESS_TOKEN'; // 替换为您的访问令牌
let viewer = new Cesium.Viewer('cesiumContainer', {
terrain: Cesium.Terrain.fromWorldTerrain(),
enableLighting: true,
showGroundAtmosphere: true,
dynamicAtmosphereLightingFromSun: true,
});
添加等高线
要添加等高线,我们需要编写自定义材质,并将其应用于地球表面。在这个例子中,我们将根据给定的一系列坐标点计算矩形范围,并使用GLSL着色器语言定义等高线的绘制逻辑
1、编写shader
定义Uniform变量
uniform vec4 color;
uniform float spacing;
uniform float width;
uniform vec4 rect;
uniform vec4 m_0;
uniform vec4 m_1;
uniform vec4 m_2;
uniform vec4 m_3;
color
:设置等高线的颜色。spacing
:设定等高线之间的间隔,即每隔多少高度绘制一条等高线。width
:设定等高线的宽度。rect
:一个矩形范围,用来限定等高线显示的区域。m_0
到m_3
:这些是转换矩阵的四个行向量,用来将世界坐标转换为局部坐标系统,以便根据矩形范围来确定是否绘制等高线。
计算距离最近的等高线
float distanceToContour = mod(materialInput.height, spacing);
- 这一行计算了当前像素的高度与最接近的等高线之间的差值。
mod
函数返回的是当前高度除以spacing
后的余数,也就是到下一个等高线的距离。
检查是否应该绘制等高线
#if (__VERSION__ == 300 || defined(GL_OES_standard_derivatives))
float dxc = abs(dFdx(materialInput.height));
float dyc = abs(dFdy(materialInput.height));
float dF = max(dxc, dyc) * czm_pixelRatio * width;
float alpha = (distanceToContour < dF) ? 1.0 : 0.0;
#else
// If no derivatives available (IE 10?), use pixel ratio
float alpha = (distanceToContour < (czm_pixelRatio * width)) ? 1.0 : 0.0;
#endif
- 这一部分代码首先检查是否支持标准导数(
GL_OES_standard_derivatives
),如果支持,则使用dFdx
和dFdy
函数来计算相邻像素高度变化的最大值,并结合像素比例和等高线宽度来决定是否绘制等高线。如果不支持导数计算(例如在较老的浏览器或硬件上),则直接使用像素比例和宽度来判断。 alpha
值决定了该像素的透明度。当distanceToContour
小于等于dF
时,alpha
设为1.0,表示完全不透明;否则设为0.0,表示完全透明,这样就只有靠近等高线的像素会被绘制出来。
应用颜色和透明度
vec4 outColor = czm_gammaCorrect(vec4(color.rgb, alpha * color.a));
material.diffuse = outColor.rgb;
material.alpha =0.;
if(local.x>rect.x&&local.x<rect.z&&local.y<rect.w&&local.y>rect.y){
material.alpha = outColor.a;
}
定义Material函数
czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);
float distanceToContour = mod(materialInput.height, spacing);
#if (__VERSION__ == 300 || defined(GL_OES_standard_derivatives))
float dxc = abs(dFdx(materialInput.height));
float dyc = abs(dFdy(materialInput.height));
float dF = max(dxc, dyc) * czm_pixelRatio * width;
float alpha = (distanceToContour < dF) ? 1.0 : 0.0;
#else
// If no derivatives available (IE 10?), use pixel ratio
float alpha = (distanceToContour < (czm_pixelRatio * width)) ? 1.0 : 0.0;
#endif
vec4 outColor = czm_gammaCorrect(vec4(color.rgb, alpha * color.a));
material.diffuse = outColor.rgb;
mat4 m=mat4(m_0[0],m_0[1],m_0[2],m_0[3],m_1[0],m_1[1],m_1[2],m_1[3],m_2[0],m_2[1],m_2[2],m_2[3],m_3[0],m_3[1],m_3[2],m_3[3]);
vec4 eyeCoordinate =vec4(-materialInput.positionToEyeEC,1.0);
vec4 worldCoordinate4 = czm_inverseView * eyeCoordinate;
vec3 worldCoordinate = worldCoordinate4.xyz ;// worldCoordinate4.w;
vec4 local=m * vec4(worldCoordinate,1.);
material.alpha =0.;
if(local.x>rect.x&&local.x<rect.z&&local.y<rect.w&&local.y>rect.y){
material.alpha = outColor.a;
}
return material;
}
czm_getMaterial
是CesiumJS提供的一个钩子函数,允许我们自定义材质属性。在这个函数里,我们可以修改材质的颜色、透明度等属性。
2、处理传参数据
将地理坐标转换为笛卡尔坐标
let positionsHieght = Cesium.Cartesian3.fromDegreesArrayHeights(
[].concat.apply([], positions)
);
positions
是一个二维数组,其中每个子数组包含三个元素:经度、纬度和高度。fromDegreesArrayHeights
方法将这些地理坐标(以度为单位的经纬度和米为单位的高度)转换为笛卡尔坐标系中的三维点(Cartesian3
),这些点可以直接用于CesiumJS的3D场景中。[].concat.apply([], positions)
将二维数组展平为一维数组,以便符合fromDegreesArrayHeights
方法的参数要求。
创建东-北-上(ENU)固定框架
let m = Cesium.Transforms.eastNorthUpToFixedFrame(positionsHieght[0]);
-
eastNorthUpToFixedFrame
方法创建了一个从世界坐标系到东-北-上(ENU)局部坐标系的转换矩阵。这个矩阵基于positionsHieght[0]
(即第一个点的位置)来定义局部坐标系的原点和方向。 -
ENU坐标系是一种常用的局部坐标系,其中:
- 东方向为X轴正方向,
- 北方向为Y轴正方向,
- 上方向为Z轴正方向。
计算逆矩阵
let inverse = Cesium.Matrix4.inverse(m, new Cesium.Matrix4());
Matrix4.inverse
方法计算上述转换矩阵的逆矩阵。逆矩阵的作用是将局部坐标系中的点转换回世界坐标系。
将所有点转换到局部坐标系
let localPositions = [];
positionsHieght.forEach((position) => {
localPositions.push(
Cesium.Matrix4.multiplyByPoint(
inverse,
position,
new Cesium.Cartesian3()
)
);
});
- 这段代码遍历所有转换后的笛卡尔坐标点,并使用逆矩阵将它们从世界坐标系转换到局部坐标系。
multiplyByPoint
方法执行矩阵乘法,将点从世界坐标系转换到局部坐标系。
计算包围矩形
//计算矩形范围
rect = Cesium.BoundingRectangle.fromPoints(
localPositions,
new Cesium.BoundingRectangle()
);
rect = new Cesium.Cartesian4(
rect.x,
rect.y,
rect.x + rect.width,
rect.y + rect.height
);
BoundingRectangle.fromPoints
方法计算所有局部坐标点所围成的最小矩形范围。这个矩形是在局部坐标系中的。new Cesium.Cartesian4(...)
将这个矩形的四个角点(左下角和右上角)封装为一个Cartesian4
对象,方便后续在着色器中使用。
添加Material材质
let material = new Cesium.Material({
fabric: {
type: "ElevationContour",
uniforms: {
width: 1,
spacing: 50,
color: Cesium.Color.YELLOW,
rect: rect,
m_0: new Cesium.Cartesian4(
inverse[0],
inverse[1],
inverse[2],
inverse[3]
),
m_1: new Cesium.Cartesian4(
inverse[4],
inverse[5],
inverse[6],
inverse[7]
),
m_2: new Cesium.Cartesian4(
inverse[8],
inverse[9],
inverse[10],
inverse[11]
),
m_3: new Cesium.Cartesian4(
inverse[12],
inverse[13],
inverse[14],
inverse[15]
),
},
},
完整代码
// 设置访问令牌
Cesium.Ion.defaultAccessToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjMzU2ZTQyYy1iOTU5LTQ5MDQtOGNkNC0yYzcxMTI1ZDJiZGQiLCJpZCI6NzY1OTcsImlhdCI6MTYzOTU2MDcwOH0.kbWigipGD6l2OPBGpnkkN6dzp8NuNjoHNNM1NF4gaIo";
let viewer = "";
const init = () => {
viewer = new Cesium.Viewer("cesiumContainer", {
terrain: Cesium.Terrain.fromWorldTerrain(), // 地形数据
enableLighting: true,
showGroundAtmosphere: true,
dynamicAtmosphereLightingFromSun: true,
});
viewer.scene.skyAtmosphere.show = true;
viewer.shadows = true;
addLine([
[110.88106709929548, 30.10536702913351, 1187.5288999303148],
[110.8834848758976, 30.04803830643123, 923.651454152295],
[110.93732277846644, 30.048178667944597, 597.5883761989865],
[110.95818944973519, 30.08307354872316, 1011.2606325887339],
[110.92957034551483, 30.09950535924442, 1180.6223600558253],
]);
};
const addLine = (positions) => {
let positionsHieght = Cesium.Cartesian3.fromDegreesArrayHeights(
[].concat.apply([], positions)
);
let m = Cesium.Transforms.eastNorthUpToFixedFrame(positionsHieght[0]);
let inverse = Cesium.Matrix4.inverse(m, new Cesium.Matrix4());
let localPositions = [];
positionsHieght.forEach((position) => {
localPositions.push(
Cesium.Matrix4.multiplyByPoint(
inverse,
position,
new Cesium.Cartesian3()
)
);
});
//计算矩形范围
rect = Cesium.BoundingRectangle.fromPoints(
localPositions,
new Cesium.BoundingRectangle()
);
rect = new Cesium.Cartesian4(
rect.x,
rect.y,
rect.x + rect.width,
rect.y + rect.height
);
Cesium.Material._materialCache._materials.ElevationContour.fabric.source = `
uniform vec4 color;
uniform float spacing;
uniform float width;
uniform vec4 rect;
uniform vec4 m_0;
uniform vec4 m_1;
uniform vec4 m_2;
uniform vec4 m_3;
czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);
float distanceToContour = mod(materialInput.height, spacing);
#if (__VERSION__ == 300 || defined(GL_OES_standard_derivatives))
float dxc = abs(dFdx(materialInput.height));
float dyc = abs(dFdy(materialInput.height));
float dF = max(dxc, dyc) * czm_pixelRatio * width;
float alpha = (distanceToContour < dF) ? 1.0 : 0.0;
#else
// If no derivatives available (IE 10?), use pixel ratio
float alpha = (distanceToContour < (czm_pixelRatio * width)) ? 1.0 : 0.0;
#endif
vec4 outColor = czm_gammaCorrect(vec4(color.rgb, alpha * color.a));
material.diffuse = outColor.rgb;
mat4 m=mat4(m_0[0],m_0[1],m_0[2],m_0[3],m_1[0],m_1[1],m_1[2],m_1[3],m_2[0],m_2[1],m_2[2],m_2[3],m_3[0],m_3[1],m_3[2],m_3[3]);
vec4 eyeCoordinate =vec4(-materialInput.positionToEyeEC,1.0);
vec4 worldCoordinate4 = czm_inverseView * eyeCoordinate;
vec3 worldCoordinate = worldCoordinate4.xyz ;// worldCoordinate4.w;
vec4 local=m * vec4(worldCoordinate,1.);
material.alpha =0.;
if(local.x>rect.x&&local.x<rect.z&&local.y<rect.w&&local.y>rect.y){
material.alpha = outColor.a;
}
return material;
}
`;
let material = new Cesium.Material({
fabric: {
type: "ElevationContour",
uniforms: {
width: 1,
spacing: 50,
color: Cesium.Color.YELLOW,
rect: rect,
m_0: new Cesium.Cartesian4(
inverse[0],
inverse[1],
inverse[2],
inverse[3]
),
m_1: new Cesium.Cartesian4(
inverse[4],
inverse[5],
inverse[6],
inverse[7]
),
m_2: new Cesium.Cartesian4(
inverse[8],
inverse[9],
inverse[10],
inverse[11]
),
m_3: new Cesium.Cartesian4(
inverse[12],
inverse[13],
inverse[14],
inverse[15]
),
},
},
});
viewer.scene.globe.material = material;
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(
positions[0][0],
positions[0][1],
1000
),
orientation: {},
});
};
init();