ArcGIS JSAPI 高级教程 - 加载北斗网格码 - 大量几何体数据(Mesh - Graphic)性能优化(使用完整公共样式MeshSymbol3D)

ArcGIS JSAPI 高级教程 - 加载北斗网格码 - 大量几何体数据(Mesh - Graphic)性能优化(使用完整公共样式MeshSymbol3D)

在 arcgis js 中,一般不推荐在客户端创建、加载大量几何数据(Graphic 和 GraphicLayer)。

具体见:ArcGIS JSAPI 高级教程 - 异步(async - Promise)添加大量北斗网格码(Mesh - Graphic)

在加载大量几何体数据时,加载数量相近的情况下,出现帧率相差非常大。

经过研究测试,找到原因,这里记录一下,也算是进行一次性能优化。

本文包括 核心代码、完整代码以及在线示例。


核心代码

具体原因:

由于没有正确使用 MeshSymbol3D 创建样式对象,而是使用 {type: 'mesh-3d'} 创建。

这样会导致,在加载数据时,自动创建大量 MeshSymbol3D 对象,进而导致渲染帧率下降。

因此,建议进行优化:

1. 尽量使用原始样式类(MeshSymbol3D、FillSymbol3DLayer、SolidEdges3D)创建样式,避免使用 {type: 'mesh-3d'} 。

2. 尽量不添加边框:edges;若添加 edges,会导致每帧循环更新 edges,导致卡顿。

以下是部分核心代码:

javascript 复制代码
// 获取网格样式
function getMeshSymbol3D(materialColor, edgesColor) {
    return new MeshSymbol3D({
        symbolLayers: [
            new FillSymbol3DLayer({
                material: {color: materialColor || colorFill.value},
                edges: new SolidEdges3D({
                    color: edgesColor || colorOutline.value,
                    size: 1,
                }),
            })
        ]
    })
}

// 获取网格样式
function getMesh3d(materialColor, edgesColor) {
    return {
        type: 'mesh-3d',
        symbolLayers: [
            {
                type: 'fill',
                material: {color: materialColor || colorFill.value},
                edges: {
                    type: 'solid',
                    color: edgesColor || colorOutline.value,
                    size: 0,
                },
            },
        ],
    };
}

完整代码

示例中,加载数据时切换创建样式,用以展示不同样式下的帧率。

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>加载大量数据性能优化</title>


    <script type="module" src="https://openlayers.vip/arcgis_api/calcite-components/2.8.1/calcite.esm.js"></script>
    <link rel="stylesheet" type="text/css"
          href="https://openlayers.vip/arcgis_api/calcite-components/2.8.1/calcite.css"/>

    <link rel="stylesheet" href="https://openlayers.vip/arcgis_api/4.33/esri/themes/light/main.css"/>
    <script src="https://openlayers.vip/arcgis_api/4.33/init.js"></script>

    <script src="https://openlayers.vip/examples/resources/stats.min.js"></script>
    <script src="https://openlayers.vip/examples/resources/data_.js"></script>


    <style>
        html,
        body,
        #viewDiv {
            padding: 0;
            margin: 0;
            height: 100%;
            width: 100%;
        }

        /* 去掉地图点击蓝色边框 */
        .esri-view .esri-view-surface--inset-outline:focus::after {
            outline: auto 0px Highlight !important;
            outline: auto 0px -webkit-focus-ring-color !important;
        }

    </style>
    <script>


        require([
            "esri/config",
            "esri/views/SceneView",
            "esri/layers/GraphicsLayer",
            "esri/Graphic",
            "esri/geometry/Mesh",
            "esri/widgets/LayerList",
            "esri/Map",

            "esri/geometry/Point",
            "esri/geometry/SpatialReference",
            "esri/symbols/FillSymbol3DLayer",
            "esri/symbols/MeshSymbol3D",
            "esri/symbols/edges/SolidEdges3D",

        ], function (
            esriConfig, SceneView,
            GraphicsLayer, Graphic, Mesh, LayerList, Map,
            Point, SpatialReference, FillSymbol3DLayer, MeshSymbol3D, SolidEdges3D,
        ) {

            let loadMode = true;
            let batchSize = 50;

            activeState('left');

            const spatialReference = SpatialReference.WebMercator;

            // Add graphic when GraphicsLayer is constructed
            const layerGrid = new GraphicsLayer({
                spatialReference,
                popupTemplate: {
                    title: 'The grid',
                    content: 'PosX:{PosX},PosY:{PosY},PosZ:{PosZ},',
                },
                title: '网格图层',
            });

            const scene = new Map({
                layers: [layerGrid]
            })

            const view = new SceneView({
                map: scene,
                container: "viewDiv",
                qualityProfile: 'high',
                camera: {
                    position: {
                        x: 12957235.377120316,
                        y: 4863943.339089994,
                        z: 540.7203787067715,
                        spatialReference
                    },
                    tilt: 60
                }
            });

            // 默认边框颜色
            const colorOutline = {
                value: [255, 255, 255, 1]
            }
            // 默认填充颜色
            const colorFill = {
                value: [255, 255, 255, 0.5]
            }

            // 获取网格样式
            function getMeshSymbol3D(materialColor, edgesColor) {
                return new MeshSymbol3D({
                    symbolLayers: [
                        new FillSymbol3DLayer({
                            material: {color: materialColor || colorFill.value},
                            edges: new SolidEdges3D({
                                color: edgesColor || colorOutline.value,
                                size: 1,
                            }),
                        })
                    ]
                })
            }

            // 获取网格样式
            function getMesh3d(materialColor, edgesColor) {
                return {
                    type: 'mesh-3d',
                    symbolLayers: [
                        {
                            type: 'fill',
                            material: {color: materialColor || colorFill.value},
                            edges: {
                                type: 'solid',
                                color: edgesColor || colorOutline.value,
                                size: 1,
                            },
                        },
                    ],
                };
            }

            // @todo 定义全局变量,用于控制
            // 网格数组
            // 网格图层
            // 过滤对象,用于隐藏显示单个建筑
            // 网格样式
            let symbol = getMeshSymbol3D();

            async function addGridAsync(data, batchSize = 500) {
                // 验证输入参数
                if (!data || !data.BBox || !data.CenterPoint) {
                    throw new Error('无效的数据格式:缺少BBox或CenterPoint');
                }

                if (batchSize <= 0) {
                    throw new Error('batchSize必须大于0');
                }

                try {
                    // 清除现有图层
                    layerGrid.removeAll();

                    const {CenterPoint: centerWebMercator, BBox: bboxData} = data;
                    const totalLength = bboxData.length;

                    console.log('box 个数:', totalLength);

                    // 预计算总批次数用于进度跟踪
                    const totalBatches = Math.ceil(totalLength / batchSize);

                    for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
                        const startIndex = batchIndex * batchSize;
                        const endIndex = Math.min(startIndex + batchSize, totalLength);
                        const batch = bboxData.slice(startIndex, endIndex);

                        const graphics = batch.map(datum => {
                            const location = {
                                x: centerWebMercator.x + datum.posX,
                                y: centerWebMercator.y + datum.posZ,
                                z: centerWebMercator.z + datum.posY,
                                spatialReference: {wkid: 102100} // Web Mercator
                            };

                            const geometry = Mesh.createBox(
                                new Point(location),
                                {
                                    size: {
                                        width: datum.lenX,
                                        height: datum.lenY,
                                        depth: datum.lenZ
                                    },
                                    vertexSpace: 'georeferenced'
                                }
                            );

                            return new Graphic({
                                geometry: geometry,
                                symbol: symbol
                            });
                        });

                        // 批量添加图形
                        layerGrid.addMany(graphics);

                        const processedCount = Math.min(endIndex, totalLength);
                        const progress = ((processedCount / totalLength) * 100).toFixed(1);
                        console.log(`进度: ${processedCount}/${totalLength} (${progress}%)`);

                        // 让出主线程,但避免过短的延迟
                        if (batchIndex < totalBatches - 1) {
                            // 不是最后一批
                            await new Promise(resolve => requestAnimationFrame(resolve));
                        }
                    }

                    console.log('所有网格添加完成');
                    return {success: true, count: totalLength};

                } catch (error) {
                    console.error("添加网格时出错:", error);
                    // 可以根据错误类型提供更具体的错误信息
                    if (error.message.includes('Invalid')) {
                        throw new Error('数据格式无效,请检查输入数据');
                    }
                    throw error; // 重新抛出错误让调用者处理
                }
            }

            // 开启显示帧率
            function activeState(left = 'right', bottom = 'top') {

                let stats = initState();

                function initState(type) {
                    let panelType = (typeof type != 'undefined' && type) && (!isNaN(type)) ? parseInt(type) : 0;
                    let stats = new Stats();
                    stats.domElement.style.position = 'absolute'; //绝对坐标
                    stats.domElement.style.left = 'auto';
                    stats.domElement.style[left] = '30px';//
                    stats.domElement.style[bottom] = '20px';
                    stats.showPanel(panelType);
                    document.body.appendChild(stats.domElement);
                    return stats;
                }

                function renderer() {
                    stats.update();
                    requestAnimationFrame(renderer);
                }

                renderer();
            }

            var layerList = new LayerList({
                view: view
            });
            // Add widget to the top right corner of the view
            view.ui.add(layerList, "top-right");

            // 定义显示隐藏建筑控件
            const loadModeToggle = document.getElementById("loadModeToggle");
            loadModeToggle.addEventListener("calciteSwitchChange", () => {
                loadMode ? symbol = getMeshSymbol3D() : symbol = getMesh3d('rgba(106, 106, 255, 0.4)');
                addGridAsync(data_, batchSize);
                loadMode = !loadMode;
            });

            view.ui.add("renderNodeUI", "bottom-left");
        });
    </script>
</head>

<body>

<div id="viewDiv">
    <calcite-block open heading="" id="renderNodeUI">
        <calcite-label layout="inline">
            点击载入数据:
        </calcite-label>
        <calcite-label layout="inline">
            高帧率/低帧率
            <calcite-switch id="loadModeToggle" checked></calcite-switch>
        </calcite-label>
    </calcite-block>
</div>
</body>
</html>

在线示例

ArcGIS Maps SDK for JavaScript 在线示例:大量几何体数据(Mesh - Graphic)性能优化