Cesium 大数据量优化加载方案

1. 使用 Primitive API 替代 Entity API

Entity API 虽然简单,但性能较差。对于大量数据,应使用 Primitive API。

// ❌ 不推荐:Entity API(性能差)
entities.forEach(data => {
    viewer.entities.add({
        polygon: {
            hierarchy: Cesium.Cartesian3.fromDegreesArray(data.coordinates),
            material: Cesium.Color.RED
        }
    });
});

// ✅ 推荐:Primitive API(性能好)
const instances = [];
entities.forEach(data => {
    instances.push(new Cesium.GeometryInstance({
        geometry: new Cesium.PolygonGeometry({
            polygonHierarchy: new Cesium.PolygonHierarchy(
                Cesium.Cartesian3.fromDegreesArray(data.coordinates)
            )
        }),
        id: data.id,
        attributes: {
            color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.RED)
        }
    }));
});

viewer.scene.primitives.add(new Cesium.Primitive({
    geometryInstances: instances,
    appearance: new Cesium.PerInstanceColorAppearance({
        closed: true
    })
}));

2. 数据分批加载(异步加载)

async function loadDataInBatches(allData, batchSize = 1000) {
    const totalBatches = Math.ceil(allData.length / batchSize);
  
    for (let i = 0; i < totalBatches; i++) {
        const start = i * batchSize;
        const end = Math.min(start + batchSize, allData.length);
        const batch = allData.slice(start, end);
      
        // 加载当前批次
        loadBatch(batch);
      
        // 显示进度
        console.log(`加载进度: ${((i + 1) / totalBatches * 100).toFixed(1)}%`);
      
        // 让出主线程,避免UI卡顿
        await new Promise(resolve => setTimeout(resolve, 0));
    }
}

function loadBatch(batchData) {
    const instances = batchData.map(data => 
        new Cesium.GeometryInstance({
            geometry: createGeometry(data),
            id: data.id,
            attributes: {
                color: Cesium.ColorGeometryInstanceAttribute.fromColor(data.color)
            }
        })
    );
  
    viewer.scene.primitives.add(new Cesium.Primitive({
        geometryInstances: instances,
        appearance: new Cesium.PerInstanceColorAppearance()
    }));
}

3. 使用聚合(Clustering)处理大量点数据

// 对于大量点数据,使用聚合显示
const dataSource = await Cesium.GeoJsonDataSource.load(url, {
    clampToGround: true
});

viewer.dataSources.add(dataSource);

// 启用聚合
dataSource.clustering.enabled = true;
dataSource.clustering.pixelRange = 50; // 聚合像素范围
dataSource.clustering.minimumClusterSize = 3; // 最小聚合数量

// 自定义聚合样式
dataSource.clustering.clusterEvent.addEventListener((entities, cluster) => {
    cluster.label.show = true;
    cluster.label.text = entities.length.toString();
    cluster.label.font = '16px sans-serif';
    cluster.label.fillColor = Cesium.Color.WHITE;
    cluster.label.outlineColor = Cesium.Color.BLACK;
    cluster.label.outlineWidth = 2;
  
    cluster.billboard.show = true;
    cluster.billboard.image = createClusterIcon(entities.length);
    cluster.billboard.width = 40;
    cluster.billboard.height = 40;
});

4. LOD(Level of Detail)按距离加载

class LODDataManager {
    constructor(viewer) {
        this.viewer = viewer;
        this.loadedTiles = new Map();
        this.allData = []; // 所有数据
      
        // 监听相机变化
        this.viewer.camera.changed.addEventListener(() => {
            this.updateVisibleData();
        });
    }
  
    updateVisibleData() {
        const cameraHeight = this.viewer.camera.positionCartographic.height;
      
        // 根据相机高度决定显示精度
        let lodLevel;
        if (cameraHeight > 1000000) {
            lodLevel = 'low'; // 只显示少量数据
        } else if (cameraHeight > 100000) {
            lodLevel = 'medium';
        } else {
            lodLevel = 'high'; // 显示所有细节
        }
      
        this.loadDataByLOD(lodLevel);
    }
  
    loadDataByLOD(level) {
        // 根据LOD级别加载不同精度的数据
        const dataToLoad = this.filterDataByLOD(this.allData, level);
        this.renderData(dataToLoad);
    }
}

5. 使用 3D Tiles 格式

对于超大数据量,转换为 3D Tiles 格式是最优方案:

// 加载 3D Tiles
const tileset = await Cesium.Cesium3DTileset.fromUrl('path/to/tileset.json');
viewer.scene.primitives.add(tileset);

// 3D Tiles 自动处理LOD和裁剪
tileset.maximumScreenSpaceError = 16; // 控制精度,值越小越精细
tileset.maximumMemoryUsage = 512; // 最大内存使用(MB)

6. 视口裁剪(Frustum Culling)

// 只加载视口内的数据
function loadVisibleData(allData) {
    const frustum = viewer.camera.frustum;
    const cullingVolume = frustum.computeCullingVolume(
        viewer.camera.position,
        viewer.camera.direction,
        viewer.camera.up
    );
  
    const visibleData = allData.filter(data => {
        const position = Cesium.Cartesian3.fromDegrees(data.lon, data.lat);
        const boundingSphere = new Cesium.BoundingSphere(position, 1000);
      
        // 检查是否在视口内
        return cullingVolume.computeVisibility(boundingSphere) !== Cesium.Intersect.OUTSIDE;
    });
  
    renderData(visibleData);
}

7. 使用 Web Worker 处理数据

// main.js
const worker = new Worker('dataProcessor.worker.js');

worker.postMessage({
    type: 'processGeoJSON',
    data: largeGeoJSONData
});

worker.onmessage = (e) => {
    const processedData = e.data;
    renderDataToCesium(processedData);
};

// dataProcessor.worker.js
self.onmessage = (e) => {
    if (e.data.type === 'processGeoJSON') {
        const processed = processData(e.data.data);
        self.postMessage(processed);
    }
};

function processData(data) {
    // 在 worker 中处理数据转换、简化等
    return data.map(item => ({
        positions: simplifyCoordinates(item.coordinates),
        properties: item.properties
    }));
}

8. 几何简化

// 使用 Turf.js 简化几何体
import * as turf from '@turf/turf';

function simplifyGeometry(coordinates, tolerance = 0.01) {
    const line = turf.lineString(coordinates);
    const simplified = turf.simplify(line, { tolerance: tolerance });
    return simplified.geometry.coordinates;
}

// 或使用 Douglas-Peucker 算法
function douglasPeucker(points, epsilon) {
    // 实现 Douglas-Peucker 简化算法
    // ...
}

9. 使用 Billboard/Label Collection

对于大量点和标注:

// ❌ 不推荐
points.forEach(point => {
    viewer.entities.add({
        position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat),
        billboard: { image: 'pin.png' }
    });
});

// ✅ 推荐:使用 Collection
const billboards = viewer.scene.primitives.add(new Cesium.BillboardCollection());

points.forEach(point => {
    billboards.add({
        position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat),
        image: 'pin.png'
    });
});

10. 完整优化示例

class OptimizedDataLoader {
    constructor(viewer) {
        this.viewer = viewer;
        this.primitives = [];
        this.batchSize = 1000;
    }
  
    async loadLargeDataset(data) {
        // 1. 数据预处理(在 Worker 中)
        const processedData = await this.preprocessInWorker(data);
      
        // 2. 几何简化
        const simplifiedData = this.simplifyGeometries(processedData);
      
        // 3. 分批加载
        await this.loadInBatches(simplifiedData);
      
        // 4. 设置相机事件监听,实现LOD
        this.setupLOD();
    }
  
    async preprocessInWorker(data) {
        return new Promise((resolve) => {
            const worker = new Worker('processor.worker.js');
            worker.postMessage(data);
            worker.onmessage = (e) => resolve(e.data);
        });
    }
  
    simplifyGeometries(data) {
        const cameraHeight = this.viewer.camera.positionCartographic.height;
        const tolerance = cameraHeight / 1000000; // 动态容差
      
        return data.map(item => ({
            ...item,
            coordinates: this.simplify(item.coordinates, tolerance)
        }));
    }
  
    async loadInBatches(data) {
        const batches = Math.ceil(data.length / this.batchSize);
      
        for (let i = 0; i < batches; i++) {
            const batch = data.slice(
                i * this.batchSize,
                (i + 1) * this.batchSize
            );
          
            this.loadBatch(batch);
          
            // 显示进度
            this.updateProgress((i + 1) / batches * 100);
          
            // 避免阻塞UI
            await new Promise(resolve => requestAnimationFrame(resolve));
        }
    }
  
    loadBatch(batch) {
        const instances = batch.map(item => 
            new Cesium.GeometryInstance({
                geometry: this.createGeometry(item),
                id: item.id,
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                        Cesium.Color.fromCssColorString(item.color)
                    )
                }
            })
        );
      
        const primitive = this.viewer.scene.primitives.add(
            new Cesium.Primitive({
                geometryInstances: instances,
                appearance: new Cesium.PerInstanceColorAppearance({
                    closed: true
                }),
                asynchronous: true // 异步创建
            })
        );
      
        this.primitives.push(primitive);
    }
  
    setupLOD() {
        this.viewer.camera.changed.addEventListener(() => {
            const height = this.viewer.camera.positionCartographic.height;
          
            // 根据高度动态调整显示
            this.primitives.forEach((primitive, index) => {
                primitive.show = this.shouldShowAtHeight(index, height);
            });
        });
    }
  
    shouldShowAtHeight(index, height) {
        // LOD 逻辑
        if (height > 1000000) {
            return index % 10 === 0; // 只显示 10%
        } else if (height > 100000) {
            return index % 2 === 0; // 显示 50%
        }
        return true; // 显示全部
    }
}

// 使用
const loader = new OptimizedDataLoader(viewer);
await loader.loadLargeDataset(yourLargeDataset);

性能对比总结

方案 适用场景 性能提升
Entity → Primitive 所有场景 5-10倍
分批加载 初始加载 避免卡顿
Clustering 大量点数据(>10000) 10-100倍
LOD 多尺度查看 2-5倍
3D Tiles 超大数据(百万级) 100+倍
Collection API 大量Billboard/Label 3-5倍
几何简化 复杂几何 2-3倍
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐