CesiumJS 拾取系统完全指南

目录

  1. 核心方法概览
  2. 方法详细解析
  3. 联合使用策略
  4. 3D Tiles 专项
  5. 实战代码库
  6. 性能优化
  7. 常见问题

一、核心方法概览

1.1 五大拾取方法对比

方法 输入 目标 原理 精度 性能 穿透性 典型场景
pickEllipsoid Cartesian2 数学椭球面 数学计算 ⭐⭐⭐ ⭐⭐⭐ 快速经纬度跟踪
globe.pick Ray 地形表面 射线求交 ⭐⭐⭐ ⭐⭐☆ 地形测量、地下拾取
scene.pick Cartesian2 最顶层对象 深度缓冲 ⭐⭐☆ ⭐⭐⭐ Entity 选择
scene.drillPick Cartesian2 所有重叠对象 深度缓冲 ⭐⭐☆ ⭐☆☆ 多图层分析
scene.pickPosition Cartesian2 精确三维坐标 深度缓冲 ⭐⭐☆ ⭐⭐☆ - 高程测量

1.2 坐标系统说明

屏幕坐标 (Cartesian2)          世界坐标 (Cartesian3)           地理坐标 (Cartographic)
     ↓                              ↓                              ↓
  (x: 像素, y: 像素)    ←→    (x, y, z: 地心米)    ←→    (longitude, latitude: 弧度, height: 米)
     ↓                              ↓                              ↓
  鼠标位置                    ECEF 地心坐标系                   经纬度高程

二、方法详细解析

2.1 camera.pickEllipsoid - 椭球面快速拾取

用途:无视地形和物体,快速获取椭球面经纬度

const cartesian = viewer.camera.pickEllipsoid(windowPosition, ellipsoid?);

// 典型使用
const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
handler.setInputAction((move) => {
    const cartesian = viewer.camera.pickEllipsoid(move.endPosition);
    if (cartesian) {
        const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
        const lon = Cesium.Math.toDegrees(cartographic.longitude).toFixed(4);
        const lat = Cesium.Math.toDegrees(cartographic.latitude).toFixed(4);
        console.log(`${lon}°, ${lat}°`); // 高度始终为 0
    }
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

特点

  • ✅ 性能最优,适合每帧调用
  • ❌ 忽略地形起伏和建筑物
  • ❌ 地下返回 undefined

2.2 scene.globe.pick - 地形射线求交

用途:精确获取地形表面坐标,支持穿透和地下拾取

const ray = viewer.camera.getPickRay(windowPosition);
const cartesian = viewer.scene.globe.pick(ray, viewer.scene);

// 完整示例
handler.setInputAction((click) => {
    const ray = viewer.camera.getPickRay(click.position);
    const position = viewer.scene.globe.pick(ray, viewer.scene);
    
    if (position) {
        const carto = Cesium.Cartographic.fromCartesian(position);
        console.log({
            lon: Cesium.Math.toDegrees(carto.longitude),
            lat: Cesium.Math.toDegrees(carto.latitude),
            height: carto.height // 真实地形高程
        });
    }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

特点

  • ✅ 包含真实地形高程
  • ✅ 射线可穿透建筑到达地面
  • ✅ 支持地下模式(射线入地)
  • ⚠️ 需要地形瓦片已加载

2.3 scene.pick - 场景顶层拾取

用途:获取最顶层的 Entity、Primitive 或 3D Tiles

const picked = viewer.scene.pick(windowPosition);

// 返回值结构
{
    primitive: Primitive,      // 底层图元对象
    id: Entity,                // 关联的 Entity(如果有)
    node: Object,              // 3D Tiles 节点信息
    content: Object,           // 瓦片内容
    batchId: Number            // 批量 ID
}

// 典型使用
handler.setInputAction((click) => {
    const picked = viewer.scene.pick(click.position);
    
    if (picked?.id instanceof Cesium.Entity) {
        const entity = picked.id;
        console.log('选中 Entity:', entity.name);
        
        // 高亮
        entity.polygon.material = Cesium.Color.YELLOW.withAlpha(0.6);
    }
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

特点

  • ✅ 返回对象引用,可获取属性
  • ✅ 包含 3D Tiles 要素级信息
  • ❌ 只能拾取最顶层(被遮挡的拾不到)
  • ⚠️ 需开启 depthTestAgainstTerrain 才能拾取地形

2.4 scene.drillPick - 穿透拾取

用途:获取屏幕位置下所有重叠的对象

const pickedObjects = viewer.scene.drillPick(windowPosition, limit?);

// 返回值:从顶层到底层的数组
[
    { primitive, id, ... },  // 最顶层
    { primitive, id, ... },
    ...
    { primitive, id, ... }   // 最底层
]

// 典型使用
handler.setInputAction((click) => {
    const objects = viewer.scene.drillPick(click.position, 10);
    
    console.log(`拾取到 ${objects.length} 个图层:`);
    objects.forEach((obj, i) => {
        const name = obj.id?.name || `图层-${i}`;
        const type = obj.primitive.constructor.name;
        console.log(`[${i}] ${name} (${type})`);
    });
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

特点

  • ✅ 穿透所有图层,获取完整堆栈
  • ✅ 适合地下管线、建筑内部查看
  • ⚠️ 性能较低,建议设置 limit
  • ⚠️ 对象过多时需优化

2.5 scene.pickPosition - 精确位置拾取

用途:获取屏幕坐标对应的精确三维世界坐标

// 必须先 scene.pick 成功,才能获取精确坐标
const picked = viewer.scene.pick(windowPosition);
if (picked) {
    const cartesian = viewer.scene.pickPosition(windowPosition);
    // cartesian 包含真实高程(建筑顶部、地形表面等)
}

// 独立使用(可能不准确)
const cartesian = viewer.scene.pickPosition(windowPosition);

特点

  • ✅ 包含真实高程(建筑、地形、模型)
  • ✅ 精度依赖深度缓冲
  • ❌ 需要可见对象才能成功
  • ⚠️ 需开启 depthTestAgainstTerrain

三、联合使用策略

3.1 决策流程图

开始拾取
    │
    ▼
是否有特定拾取目标?
    │
    ├── 是 → 需要对象属性?
    │           │
    │           ├── 是 → 需要穿透查看?
    │           │           │
    │           │           ├── 是 → drillPick() + 遍历
    │           │           │           │
    │           │           │           ▼
    │           │           │       显示分层选择器
    │           │           │
    │           │           └── 否 → pick() 快速选择
    │           │                       │
    │           │                       ▼
    │           │                   高亮 + 显示属性
    │           │
    │           └── 否 → 仅需坐标?
    │                       │
    │                       ▼
    │                   pickPosition() 精确坐标
    │
    └── 否 → 需要穿透建筑?
                │
                ├── 是 → globe.pick() 地面坐标
                │           │
                │           ▼
                │       地形高程测量
                │
                └── 否 → 性能优先?
                            │
                            ├── 是 → pickEllipsoid() 快速经纬度
                            │
                            └── 否 → globe.pick() 精确地形

3.2 智能拾取器完整实现

class CesiumSmartPicker {
    constructor(viewer) {
        this.viewer = viewer;
        this.handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
        this.selectedStack = [];
        this.currentLayer = 0;
        
        this.init();
    }
    
    init() {
        // 普通点击:快速拾取
        this.handler.setInputAction((e) => {
            this.quickPick(e.position);
        }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
        
        // Ctrl+点击:穿透拾取
        this.handler.setInputAction((e) => {
            this.deepPick(e.position);
        }, Cesium.ScreenSpaceEventType.LEFT_CLICK, Cesium.KeyboardEventModifier.CTRL);
        
        // Shift+点击:测量高差
        this.handler.setInputAction((e) => {
            this.measureHeight(e.position);
        }, Cesium.ScreenSpaceEventType.LEFT_CLICK, Cesium.KeyboardEventModifier.SHIFT);
        
        // 移动:实时预览
        this.handler.setInputAction((e) => {
            this.hoverPreview(e.endPosition);
        }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
    }
    
    /**
     * 快速拾取:使用 pick() 获取最顶层
     */
    quickPick(position) {
        const picked = this.viewer.scene.pick(position);
        
        if (!picked) {
            // 无场景对象,退化为地面拾取
            const ground = this.pickGround(position);
            this.showResult({
                type: 'ground',
                position: ground,
                description: '裸地/地形'
            });
            return;
        }
        
        // 获取精确坐标
        const surfacePos = this.viewer.scene.pickPosition(position);
        
        // 检查是否有隐藏层
        const allLayers = this.viewer.scene.drillPick(position, 3);
        
        const result = {
            type: 'object',
            target: picked,
            layer: 1,
            totalLayers: allLayers.length,
            position: surfacePos,
            properties: this.getProperties(picked),
            hasHiddenLayers: allLayers.length > 1
        };
        
        this.selectAndHighlight(result, picked);
        this.showResult(result);
        
        // 提示有隐藏层
        if (result.hasHiddenLayers) {
            console.log('💡 按住 Ctrl 查看被遮挡的图层');
        }
    }
    
    /**
     * 穿透拾取:使用 drillPick() 获取所有层
     */
    deepPick(position) {
        const layers = this.viewer.scene.drillPick(position, 10);
        
        if (layers.length === 0) return;
        
        this.selectedStack = layers.map((layer, index) => ({
            index,
            target: layer,
            name: layer.id?.name || `图层-${index}`,
            type: this.getType(layer),
            position: this.viewer.scene.pickPosition(position),
            color: this.getLayerColor(index)
        }));
        
        this.currentLayer = 0;
        
        // 高亮所有层
        this.highlightAllLayers();
        
        // 显示分层 UI
        this.showLayerPicker(this.selectedStack);
        
        // 计算穿透深度
        if (this.selectedStack.length > 1) {
            const top = this.selectedStack[0].position;
            const bottom = this.selectedStack[this.selectedStack.length - 1].position;
            if (top && bottom) {
                const depth = Cesium.Cartesian3.distance(top, bottom);
                console.log(`📏 穿透深度: ${depth.toFixed(2)}m`);
            }
        }
    }
    
    /**
     * 测量高度:surface vs ground
     */
    measureHeight(position) {
        // 表面高度(含建筑)
        const picked = this.viewer.scene.pick(position);
        let surfaceHeight = null;
        let surfacePos = null;
        
        if (picked) {
            surfacePos = this.viewer.scene.pickPosition(position);
            if (surfacePos) {
                surfaceHeight = Cesium.Cartographic.fromCartesian(surfacePos).height;
            }
        }
        
        // 地面高度(穿透)
        const groundPos = this.pickGround(position);
        const groundHeight = groundPos ? 
            Cesium.Cartographic.fromCartesian(groundPos).height : null;
        
        if (surfaceHeight !== null && groundHeight !== null) {
            const result = {
                type: 'measurement',
                surfaceHeight,
                groundHeight,
                objectHeight: surfaceHeight - groundHeight,
                surfacePosition: surfacePos,
                groundPosition: groundPos
            };
            
            this.showMeasurement(result);
            return result;
        }
        
        return null;
    }
    
    /**
     * 悬停预览
     */
    hoverPreview(position) {
        // 快速 pick 检查
        const picked = this.viewer.scene.pick(position);
        
        if (!picked) {
            this.clearPreview();
            return;
        }
        
        // drillPick 获取上下文
        const context = this.viewer.scene.drillPick(position, 3);
        
        const preview = {
            topObject: {
                name: picked.id?.name,
                type: this.getType(picked)
            },
            layerCount: context.length,
            coordinates: this.getCoordinates(position)
        };
        
        this.showPreview(preview);
    }
    
    /**
     * 地形拾取(穿透)
     */
    pickGround(position) {
        const ray = this.viewer.camera.getPickRay(position);
        return this.viewer.scene.globe.pick(ray, this.viewer.scene);
    }
    
    // 辅助方法
    getProperties(picked) {
        if (picked.id?.properties) {
            return picked.id.properties.getValue(Cesium.JulianDate.now());
        }
        if (picked.getPropertyNames) {
            const props = {};
            picked.getPropertyNames().forEach(name => {
                props[name] = picked.getProperty(name);
            });
            return props;
        }
        return {};
    }
    
    getType(picked) {
        if (picked instanceof Cesium.Cesium3DTileFeature) return '3D Tiles';
        if (picked.primitive instanceof Cesium.Model) return 'Model';
        if (picked.id?.polygon) return 'Polygon';
        if (picked.id?.polyline) return 'Polyline';
        if (picked.id?.billboard) return 'Billboard';
        return 'Primitive';
    }
    
    getLayerColor(index) {
        const colors = [Cesium.Color.RED, Cesium.Color.ORANGE, Cesium.Color.YELLOW, 
                       Cesium.Color.GREEN, Cesium.Color.CYAN, Cesium.Color.BLUE];
        return colors[index % colors.length];
    }
    
    getCoordinates(position) {
        const cartesian = this.viewer.scene.pickPosition(position) || 
                         this.viewer.camera.pickEllipsoid(position);
        if (!cartesian) return null;
        const carto = Cesium.Cartographic.fromCartesian(cartesian);
        return {
            lon: Cesium.Math.toDegrees(carto.longitude).toFixed(6),
            lat: Cesium.Math.toDegrees(carto.latitude).toFixed(6),
            height: carto.height.toFixed(2)
        };
    }
    
    // UI 方法(简化)
    selectAndHighlight(result, picked) { /* ... */ }
    highlightAllLayers() { /* ... */ }
    showResult(result) { console.log('结果:', result); }
    showLayerPicker(layers) { console.table(layers); }
    showMeasurement(m) { console.log(`高度差: ${m.objectHeight.toFixed(2)}m`); }
    showPreview(p) { /* ... */ }
    clearPreview() { /* ... */ }
}

四、3D Tiles 专项拾取

4.1 基础要素拾取

class Cesium3DTilesPicker {
    constructor(viewer, tileset) {
        this.viewer = viewer;
        this.tileset = tileset;
        this.handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
        this.selectedFeatures = new Map();
        
        this.init();
    }
    
    init() {
        // 必须开启深度测试
        this.viewer.scene.globe.depthTestAgainstTerrain = true;
        
        this.handler.setInputAction((e) => {
            this.pickFeature(e.position);
        }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
        
        this.handler.setInputAction((e) => {
            this.hoverFeature(e.endPosition);
        }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
    }
    
    pickFeature(position) {
        // 使用 drillPick 确保拾取到 3D Tiles
        const pickedObjects = this.viewer.scene.drillPick(position, 5);
        
        let feature = null;
        for (const picked of pickedObjects) {
            if (picked instanceof Cesium.Cesium3DTileFeature) {
                feature = picked;
                break;
            }
        }
        
        if (!feature) return;
        
        // 获取 Batch Table 属性
        const properties = {};
        feature.getPropertyNames().forEach(name => {
            properties[name] = feature.getProperty(name);
        });
        
        // 高亮
        this.highlightFeature(feature, Cesium.Color.YELLOW);
        
        // 精确坐标
        const exactPos = this.viewer.scene.pickPosition(position);
        
        console.log('3D Tiles 要素:', {
            featureId: feature.getProperty('id'),
            batchId: feature.batchId,
            properties,
            position: exactPos
        });
    }
    
    hoverFeature(position) {
        const picked = this.viewer.scene.pick(position);
        
        if (picked instanceof Cesium.Cesium3DTileFeature) {
            if (!picked.originalColor) {
                picked.originalColor = picked.color.clone();
            }
            picked.color = Cesium.Color.CYAN.withAlpha(0.6);
        }
    }
    
    highlightFeature(feature, color) {
        if (!this.selectedFeatures.has(feature)) {
            this.selectedFeatures.set(feature, feature.color.clone());
        }
        feature.color = color;
    }
}

4.2 高级:按属性筛选

// 样式筛选高亮
tileset.style = new Cesium.Cesium3DTileStyle({
    color: {
        conditions: [
            ["${height} > 100", "color('red')"],
            ["${height} > 50", "color('orange')"],
            ["true", "color('white')"]
        ]
    },
    show: "${type} === 'building'"
});

// 运行时属性查询
function queryFeaturesByProperty(tileset, propertyName, value) {
    const features = [];
    
    tileset.tileLoad.addEventListener((tile) => {
        const content = tile.content;
        for (let i = 0; i < content.featuresLength; i++) {
            const feature = content.getFeature(i);
            if (feature.getProperty(propertyName) === value) {
                features.push(feature);
                feature.color = Cesium.Color.GREEN.withAlpha(0.8);
            }
        }
    });
    
    return features;
}

五、实战代码库

5.1 相机位置获取

// 笛卡尔坐标(地心)
const position = viewer.camera.position; // Cartesian3

// 地理坐标(经纬度)
const cartographic = viewer.camera.positionCartographic; // Cartographic
const lon = Cesium.Math.toDegrees(cartographic.longitude);
const lat = Cesium.Math.toDegrees(cartographic.latitude);
const height = cartographic.height;

5.2 坐标转换工具

class CoordinateConverter {
    // Cartesian3 → Cartographic
    static toCartographic(cartesian) {
        return Cesium.Cartographic.fromCartesian(cartesian);
    }
    
    // Cartographic → Cartesian3
    static toCartesian(lon, lat, height) {
        return Cesium.Cartesian3.fromDegrees(lon, lat, height);
    }
    
    // 屏幕 → 世界
    static screenToWorld(viewer, screenPos) {
        const ray = viewer.camera.getPickRay(screenPos);
        return viewer.scene.globe.pick(ray, viewer.scene);
    }
    
    // 世界 → 屏幕
    static worldToScreen(viewer, cartesian) {
        return Cesium.SceneTransforms.wgs84ToWindowCoordinates(
            viewer.scene, 
            cartesian
        );
    }
}

5.3 完整测量工具

class CesiumMeasurement {
    constructor(viewer) {
        this.viewer = viewer;
        this.points = [];
    }
    
    // 空间距离
    measureDistance(pos1, pos2) {
        return Cesium.Cartesian3.distance(pos1, pos2);
    }
    
    // 地表距离(沿地形)
    measureGroundDistance(start, end) {
        // 采样地形点计算路径长度
        const carto1 = Cesium.Cartographic.fromCartesian(start);
        const carto2 = Cesium.Cartographic.fromCartesian(end);
        
        const samples = 100;
        let distance = 0;
        
        for (let i = 0; i < samples; i++) {
            const t = i / (samples - 1);
            const lon = Cesium.Math.lerp(carto1.longitude, carto2.longitude, t);
            const lat = Cesium.Math.lerp(carto1.latitude, carto2.latitude, t);
            
            // 获取地形高度
            const carto = new Cesium.Cartographic(lon, lat);
            const height = this.viewer.scene.globe.getHeight(carto);
            carto.height = height || 0;
            
            const pos = Cesium.Cartographic.toCartesian(carto);
            
            if (i > 0) {
                distance += Cesium.Cartesian3.distance(this.prevPos, pos);
            }
            this.prevPos = pos;
        }
        
        return distance;
    }
    
    // 高度差
    measureHeightDifference(surfacePos, groundPos) {
        const carto1 = Cesium.Cartographic.fromCartesian(surfacePos);
        const carto2 = Cesium.Cartographic.fromCartesian(groundPos);
        return carto1.height - carto2.height;
    }
}

六、性能优化指南

6.1 拾取优化策略

问题 解决方案
drillPick 卡顿 设置 limit 参数,限制最大层数
鼠标移动卡顿 使用 pickEllipsoid 替代 pick,或节流处理
3D Tiles 拾取慢 调整 maximumScreenSpaceError,平衡精度与性能
深度缓冲精度低 开启 logarithmicDepthBuffer,避免 Z-fighting

6.2 节流实现

class ThrottledPicker {
    constructor(viewer, interval = 100) {
        this.viewer = viewer;
        this.interval = interval;
        this.lastPickTime = 0;
        this.pending = false;
    }
    
    pick(position) {
        const now = Date.now();
        
        if (now - this.lastPickTime < this.interval) {
            if (!this.pending) {
                this.pending = true;
                setTimeout(() => {
                    this.pending = false;
                    this.doPick(this.lastPosition);
                }, this.interval - (now - this.lastPickTime));
            }
            return;
        }
        
        this.lastPosition = position;
        this.doPick(position);
    }
    
    doPick(position) {
        this.lastPickTime = Date.now();
        // 执行拾取逻辑
        const picked = this.viewer.scene.pick(position);
        // ...
    }
}

七、常见问题排查

7.1 拾取失败排查

现象 原因 解决
pick() 返回 undefined 未开启深度测试 globe.depthTestAgainstTerrain = true
pickPosition() 不准确 深度缓冲精度不足 开启 logarithmicDepthBuffer
globe.pick() 返回 undefined 射线未与地形相交 检查相机位置和射线方向
drillPick() 层数不对 透明对象影响 设置 pickTranslucentDepth = true
3D Tiles 拾取不到 瓦片未加载 等待 tileset.readyPromise

7.2 调试技巧

// 可视化射线
const ray = viewer.camera.getPickRay(position);
const points = [ray.origin, Cesium.Cartesian3.add(
    ray.origin, 
    Cesium.Cartesian3.multiplyByScalar(ray.direction, 10000, new Cesium.Cartesian3()), 
    new Cesium.Cartesian3()
)];

viewer.entities.add({
    polyline: {
        positions: points,
        width: 2,
        material: Cesium.Color.RED
    }
});

// 打印拾取结果详情
console.log('Picked:', picked);
console.log('Primitive:', picked?.primitive?.constructor?.name);
console.log('ID:', picked?.id);
console.log('Properties:', picked?.getPropertyNames?.());

附录:快速参考卡

┌─────────────────────────────────────────────────────────────┐
│                    Cesium 拾取方法速查                        │
├─────────────────────────────────────────────────────────────┤
│ 快速经纬度      →  camera.pickEllipsoid()                    │
│ 精确地形        →  globe.pick(getPickRay())                  │
│ 选择对象        →  scene.pick()                              │
│ 穿透多图层      →  scene.drillPick(limit)                    │
│ 精确高程        →  scene.pickPosition()                      │
│ 3D Tiles        →  drillPick() + instanceOf Cesium3DTileFeature│
├─────────────────────────────────────────────────────────────┤
│ 必须开启:  globe.depthTestAgainstTerrain = true              │
│ 可选开启:  scene.pickTranslucentDepth = true                 │
│           scene.logarithmicDepthBuffer = true                │
└─────────────────────────────────────────────────────────────┘

如需补充 特定行业应用(如 BIM 构件选择、地质剖面分析)或 与外部库集成(如 Turf.js 空间分析)的示例,请告知!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐