CesiumJS 拾取系统完全指南
·
CesiumJS 拾取系统完全指南
目录
一、核心方法概览
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 空间分析)的示例,请告知!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)