前言
在 坡度分析 文章中已经介绍了在三维地形中坡度分析的业务场景,数学逻辑和实现过程,但是在日后的使用中,发现在电脑配置不是很好的情况下,会出现显存过高,帧率下降的情况,计算的地形面积太大甚至会导致浏览器崩溃,所以才有了这次的网格版本的优化。
性能瓶颈分析
经过对实现代码的排查,这个版本的 坡度分析 主要问题就一个,创建的primitive实体太多,即使已经在单个分析的面积上做了扩大(每个独立的 primitive 是一个三角形,其边长约为 25 米)与对计算出坡度进行分类。批量加载,但是在分析范围扩大到一定程度之后,仍然会有性能瓶颈的问题!
坡地分析(网格)
针对上个版本的性能瓶颈的痛点进行定向优化,摒弃N个实体,采用单个实体进行坡度分析可视化效果的绘制,主要是在cesium中采用webGL相关技术来实现,学习过相关底层技术就知道,再复杂的图形可视化,在webGL中都是一个个三角形进行拼接出来的,cesium底层也是使用webGL来实现的,肯定也是支持,所以在查阅官方文档后 链接,采用每个三角形完全独立,不共享任何顶点的方式实现!主要是因为每个三角形的坡度不同,需要独立着色,因此不共享顶点可以保证每个三角形拥有独立的颜色。
实现过程
- 数据处理: 使用turf生成规则的三角形网格,通过对网格的处理,通过异步调用cesium中的
sampleTerrainMostDetailedAPI批量获取地形的高度,回填到每个三角形每个顶点中。 - 数据填充: 遍历处理好的三角形网格,对每个三角形执行以下操作:
- 根据三个顶点的经纬度和高程,使用
Cesium.Cartesian3.fromDegrees转换为地心笛卡尔坐标。 - 计算面法线,并与三角形中心点的椭球面法线点积,得到坡度角(具体数学过程可参考上一篇文章 链接)。
- 将坡度角映射为颜色(例如使用连续渐变色)。
- 关键:将每个顶点的坐标和颜色写入二进制数组。 这里使用
Float64Array存储顶点位置(每个顶点 3 个分量 x, y, z),使用Uint8Array存储顶点颜色(每个顶点 4 个分量 RGBA)。每个三角形完全独立,不共享顶点,因此总顶点数 = 三角形数量 × 3。颜色数组中的 Alpha 通道固定为 179(约 70% 不透明度),保证可视化效果柔和。最后返回这两个类型化数组。
- 构建实体: 将上一步得到的
positionValues和colorValues传入Cesium.Geometry的attributes中,其中位置属性对应position,颜色属性对应color。必须指定primitiveType为Cesium.PrimitiveType.TRIANGLES,这样 Cesium 才会按三角形图元进行渲染(每三个顶点组成一个三角形)。同时需要调用Cesium.BoundingSphere.fromVertices计算包围球,用于视锥体裁剪和渲染优化。最后通过Cesium.GeometryInstance和Cesium.Primitive将实体添加到场景中
代码如下
const createPrimitive = (positionValues, colorValues) => {
// 构建 Geometry(无索引缓冲,每个三角形3个独立顶点)
const geometry = new cesium.Geometry({
attributes: {
position: new cesium.GeometryAttribute({
componentDatatype: cesium.ComponentDatatype.DOUBLE,
componentsPerAttribute: 3,
values: positionValues
}),
color: new cesium.GeometryAttribute({
componentDatatype: cesium.ComponentDatatype.UNSIGNED_BYTE,
componentsPerAttribute: 4,
values: colorValues,
normalize: true
})
},
primitiveType: cesium.PrimitiveType.TRIANGLES,
boundingSphere: cesium.BoundingSphere.fromVertices(positionValues)
});
// 创建 GeometryInstance 和 Primitive
const instance = new cesium.GeometryInstance({
geometry: geometry
});
const primitive = new cesium.Primitive({
geometryInstances: instance,
appearance: new cesium.PerInstanceColorAppearance({
flat: true,
faceForward: true
}),
asynchronous: false
});
return primitive
}
const dataProcess = (points, gridFc) => {
// 建立 "lng,lat" -> [lng, lat, height] 的快速查找表
const coordToPoint = new Map();
points.forEach(p => {
const key = `${p[0]},${p[1]}`;
coordToPoint.set(key, p);
});
// 每个三角形使用独立顶点(不共享),以便每个三角形拥有独立的颜色
const triangleCount = gridFc.features.length;
const vertexCount = triangleCount * 3;
const positionValues = new Float64Array(vertexCount * 3);
const colorValues = new Uint8Array(vertexCount * 4);
gridFc.features.forEach((f, triIndex) => {
const coords = f.geometry.coordinates[0]; // 闭合环,取前三个点
// 获取三个顶点的 Cartesian3 坐标
const carts = [];
for (let i = 0; i < 3; i++) {
const [lng, lat] = coords[i];
const key = `${lng},${lat}`;
const point = coordToPoint.get(key);
const height = point ? point[2] : 0;
carts.push(cesium.Cartesian3.fromDegrees(lng, lat, height));
}
// --- 坡度计算 ---
// 面法线 = cross(v1-v0, v2-v0)
const edge1 = cesium.Cartesian3.subtract(carts[1], carts[0], new cesium.Cartesian3());
const edge2 = cesium.Cartesian3.subtract(carts[2], carts[0], new cesium.Cartesian3());
const faceNormal = cesium.Cartesian3.cross(edge1, edge2, new cesium.Cartesian3());
cesium.Cartesian3.normalize(faceNormal, faceNormal);
// 三角形中心点的椭球面法线(即"上"方向)
const center = new cesium.Cartesian3();
cesium.Cartesian3.add(carts[0], carts[1], center);
cesium.Cartesian3.add(center, carts[2], center);
cesium.Cartesian3.divideByScalar(center, 3, center);
const upNormal = cesium.Ellipsoid.WGS84.geodeticSurfaceNormal(center, new cesium.Cartesian3());
// 坡度角 = acos(|faceNormal · upNormal|)(取绝对值,因为面法线方向可能翻转)
const dot = Math.abs(cesium.Cartesian3.dot(faceNormal, upNormal));
const slopeRad = Math.acos(Math.min(1, dot));
const slopeDeg = slopeRad * 180 / Math.PI;
// 根据坡度获取颜色(连续渐变)
const [r, g, b] = slopeToColor(slopeDeg);
// --- 坡度计算结束 ---
// 填充三个顶点的位置和颜色
for (let i = 0; i < 3; i++) {
const cart = carts[i];
const vi = triIndex * 3 + i;
positionValues[vi * 3] = cart.x;
positionValues[vi * 3 + 1] = cart.y;
positionValues[vi * 3 + 2] = cart.z;
colorValues[vi * 4] = r;
colorValues[vi * 4 + 1] = g;
colorValues[vi * 4 + 2] = b;
colorValues[vi * 4 + 3] = 179; // alpha: 0.7 * 255
}
});
return {
positionValues,
colorValues
}
}
const sampleTriangleGridHeight = async (gridFc, viewer) => {
const terrainProvider = viewer.terrainProvider;
// 收集唯一顶点(保留顺序)
const coordList = [];
const keySet = new Set();
gridFc.features.forEach(f => {
f.geometry.coordinates[0].forEach(([lng, lat]) => {
const key = `${lng},${lat}`;
if (!keySet.has(key)) {
keySet.add(key);
coordList.push({
lng,
lat,
carto: cesium.Cartographic.fromDegrees(lng, lat)
});
}
});
});
// 批量地形采样
const cartographics = coordList.map(v => v.carto);
const sampled = await cesium.sampleTerrainMostDetailed(
terrainProvider,
cartographics
);
// 回填高度(按索引一一对应)
sampled.forEach((c, i) => {
coordList[i].height = c.height +2 ?? 0;
});
// 输出唯一顶点(无错误重复)
return coordList.map(v => [v.lng, v.lat, v.height]);
}
function generateTrianglesFromBbox(points) {
// 计算最小/最大经纬度
let minLon = Infinity, maxLon = -Infinity;
let minLat = Infinity, maxLat = -Infinity;
for (let i = 0; i < points.length; i += 2) {
const lon = points[i];
const lat = points[i + 1];
minLon = Math.min(minLon, lon);
maxLon = Math.max(maxLon, lon);
minLat = Math.min(minLat, lat);
maxLat = Math.max(maxLat, lat);
}
const finalBbox = [minLon, minLat, maxLon, maxLat];
console.log(finalBbox);
// 生成三角网格
return turf.triangleGrid(finalBbox, 10, { units: 'meters' });
}
实现效果
相比上一版,网格实现下的效果更好,分析的整体面积更大,单体面积更小,更准确。有什么问题欢迎评论区交流!