Cesium应用(九):坡度分析的网格优化

35 阅读5分钟

前言

  在 坡度分析 文章中已经介绍了在三维地形中坡度分析的业务场景,数学逻辑和实现过程,但是在日后的使用中,发现在电脑配置不是很好的情况下,会出现显存过高,帧率下降的情况,计算的地形面积太大甚至会导致浏览器崩溃,所以才有了这次的网格版本的优化。

性能瓶颈分析

  经过对实现代码的排查,这个版本的 坡度分析 主要问题就一个,创建的primitive实体太多,即使已经在单个分析的面积上做了扩大(每个独立的 primitive 是一个三角形,其边长约为 25 米)与对计算出坡度进行分类。批量加载,但是在分析范围扩大到一定程度之后,仍然会有性能瓶颈的问题!

坡地分析(网格)

  针对上个版本的性能瓶颈的痛点进行定向优化,摒弃N个实体,采用单个实体进行坡度分析可视化效果的绘制,主要是在cesium中采用webGL相关技术来实现,学习过相关底层技术就知道,再复杂的图形可视化,在webGL中都是一个个三角形进行拼接出来的,cesium底层也是使用webGL来实现的,肯定也是支持,所以在查阅官方文档后 链接,采用每个三角形完全独立,不共享任何顶点的方式实现!主要是因为每个三角形的坡度不同,需要独立着色,因此不共享顶点可以保证每个三角形拥有独立的颜色。

实现过程

  • 数据处理: 使用turf生成规则的三角形网格,通过对网格的处理,通过异步调用cesium中的sampleTerrainMostDetailedAPI批量获取地形的高度,回填到每个三角形每个顶点中。
  • 数据填充: 遍历处理好的三角形网格,对每个三角形执行以下操作:
  1. 根据三个顶点的经纬度和高程,使用 Cesium.Cartesian3.fromDegrees 转换为地心笛卡尔坐标。
  2. 计算面法线,并与三角形中心点的椭球面法线点积,得到坡度角(具体数学过程可参考上一篇文章 链接)。
  3. 将坡度角映射为颜色(例如使用连续渐变色)。
  4. 关键:将每个顶点的坐标和颜色写入二进制数组。  这里使用 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' });
}

实现效果

  相比上一版,网格实现下的效果更好,分析的整体面积更大,单体面积更小,更准确。有什么问题欢迎评论区交流! de83d3b2-cab5-4b4a-b8f3-a80b6889ce15.png