Cesium 拾取案例
在上一文 Cesium 中的离屏渲染 已经了解到,拾取可以通过离屏渲染实现,其中涉及到了颜色缓存和深度缓存的概念。
拾取对象(颜色缓存)
Cesium 的 Scene.pick
只返回选中的第一个物体,根据上文已知原理是进行从颜色缓存中读取颜色值,再将其转为对象 ID,从而找出对应对象。
可以通过修改 pick 的视锥体和拾取方式,实现同时拾取一个范围内多个物体的效果。
拾取坐标(深度缓存)
Cesium 的 pickPosition
或者 sampleHeight
这类的拾取坐标的方法,原理都是进行深度测试,再将深度值转化为世界坐标,通常只能拾取到一个位置(或者说一个方向上的位置)。
如果直接使用 Cesium 定义的方式实现同时拾取 m 个点,就需要额外增加 m 个渲染批次,如果 m 为上百万,那计算损耗是非常大的。但是,如果我们可以定义 pick 的视锥,并规定深度图的大小为 n*n,通过一次离屏的深度测试后,再将深度图上的点全部转化为世界坐标,就可以实现仅通过增加一趟额外的渲染,就能够拾取到一定范围内的 n*n
个坐标点。
⚠️ 需要注意的是,手动创建深度缓存并不能渲染未加载过的数据资源。也就是说,没有被首次渲染的物体,拾取不到其表面位置;而被渲染过的物体,尽管由于当前相机位置不渲染该级别物体,仍能正确拾取到其表面位置。
同时拾取多个对象与 1024*1024 个坐标
下面的示例中,为了方便计算,所以 pick 的视锥使用的是正交投影视锥,宽高都是 1000,深度图的尺寸是 1024*1024(核心代码参考 Picking.js
的 getRayIntersection()
函数),用黄色高亮拾取对象,用绿色标记拾取坐标。
具体代码
以下代码可直接放入 Cesium 沙盒 运行
// 1. 创建基础场景
const viewer = new Cesium.Viewer('cesiumContainer', {
animation: false,
timeline: false,
fullscreenButton: false,
geocoder: false,
homeButton: false,
navigationHelpButton: false,
sceneModePicker: false,
baseLayerPicker: false,
creditContainer: document.createElement('div')
});
viewer.scene.globe.depthTestAgainstTerrain = true;
const initialPosition = Cesium.Cartesian3.fromDegrees(-74.01881302800248, 40.69114333714821, 753);
const initialOrientation = new Cesium.HeadingPitchRoll.fromDegrees(
21.27879878293835,
-21.34390550872461,
0.0716951918898415
);
viewer.scene.camera.setView({
destination: initialPosition,
orientation: initialOrientation,
endTransform: Cesium.Matrix4.IDENTITY
});
const tileset = new Cesium.Cesium3DTileset({
url: Cesium.IonResource.fromAssetId(75343)
});
viewer.scene.primitives.add(tileset);
// 创建坐标点集合容器
const pointPrimitives = viewer.scene.primitives.add(new Cesium.PointPrimitiveCollection());
// 创建视锥体集合容器
const frustumPrimitives = viewer.scene.primitives.add(new Cesium.PrimitiveCollection());
//#region ----------------------- CORE --------------------------------------------------
// 创建常量,避免循环中多次实例化
const pickTilesetPassState = new Cesium.Cesium3DTilePassState({
pass: Cesium.Cesium3DTilePass.PICK
});
const scratchRectangle = new Cesium.BoundingRectangle(0, 0, 3, 3);
const scratchColorZero = new Cesium.Color(0, 0, 0, 0);
const packedDepthScale = new Cesium.Cartesian4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 16581375.0); // 1, 1 / 255, 1 / (255^2), 1 / (255^3)
// 根据射线更新相机参数
function updateOffscreenCamera(position, direction, up, width, height, camera) {
const right = Cesium.Cartesian3.cross(direction, up, new Cesium.Cartesian3());
camera.position = position;
camera.direction = direction;
camera.up = up;
camera.right = right;
camera.frustum.width = width;
camera.frustum.aspectRatio = width / height;
return camera.frustum.computeCullingVolume(camera.positionWC, camera.directionWC, camera.upWC);
}
// 可视化相机视锥体
function getFrustumOutline(camera) {
const matrix = new Cesium.Matrix3();
const cameraLeftWC = Cesium.Cartesian3.negate(camera.rightWC, new Cesium.Cartesian3());
Cesium.Matrix3.setColumn(matrix, 0, cameraLeftWC, matrix);
Cesium.Matrix3.setColumn(matrix, 1, camera.upWC, matrix);
Cesium.Matrix3.setColumn(matrix, 2, camera.directionWC, matrix);
const geometryInstances = new Cesium.GeometryInstance({
geometry: new Cesium.FrustumOutlineGeometry({
frustum: camera.frustum,
origin: camera.positionWC,
orientation: Cesium.Quaternion.fromRotationMatrix(matrix, new Cesium.Quaternion())
}),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.WHITE)
}
});
return new Cesium.Primitive({
geometryInstances,
appearance: new Cesium.PerInstanceColorAppearance({
flat: true,
translucent: false
}),
asynchronous: false
});
}
// 获取深度缓存中的深度值
//! REFERENCE: Cesium.PickDepth.getDepth
function getDepth(context, x, y, width, height, framebuffer) {
// 获取颜深度缓存中的所有像素值
const pixels = context.readPixels({
x,
y,
width,
height,
framebuffer
});
const packedDepthArray = Cesium.Cartesian4.unpackArray(pixels);
// 像素值转深度值
return packedDepthArray.map((t) => {
Cesium.Cartesian4.divideByScalar(t, 255.0, t);
return Cesium.Cartesian4.dot(t, packedDepthScale);
});
}
// 纹理像素值转 RGBA 颜色值
//! REFERENCE: Cesium.PickFramebuffer.end
function pixelsToColor(pixels) {
const steps = pixels.length / 4;
const res = [];
for (let i = 0; i < steps; i++) {
const color = new Cesium.Color();
const index = i * 4;
color.red = Cesium.Color.byteToFloat(pixels[index]);
color.green = Cesium.Color.byteToFloat(pixels[index + 1]);
color.blue = Cesium.Color.byteToFloat(pixels[index + 2]);
color.alpha = Cesium.Color.byteToFloat(pixels[index + 3]);
const target = res.find((t) => Cesium.Color.equals(t, color));
if (!target) {
res.push(color);
}
}
return res;
}
// 通过颜色缓存拾取对象
//! REFERENCE: Cesium.PickFramebuffer.end
function pickObjects(framebuffer, context, viewport) {
const width = Cesium.defaultValue(viewport.width, 1);
const height = Cesium.defaultValue(viewport.height, 1);
// 获取颜色缓存中的所有像素值
const pixels = context.readPixels({
x: 0,
y: 0,
width,
height,
framebuffer
});
// 像素值转 RGBA
const colors = pixelsToColor(pixels);
// 通过颜色 ID 查找拾取对象
return colors.map((t) => context.getObjectByPickColor(t));
}
// 通过深度缓存拾取坐标
//! REFERENCE: Cesium.Picking getRayIntersection
function pickPositions(ray, picking, scene, width, height, depthMapSize) {
const res = [];
const { context } = scene;
if (!context.depthTexture) { return res; }
const view = picking._pickOffscreenView;
const { camera } = view;
const numFrustums = view.frustumCommandsList.length;
const offset = new Cesium.Cartesian3();
for (let i = 0; i < numFrustums; ++i) {
// 获取每个视锥体的深度缓存
const pickDepth = picking.getPickDepth(scene, i);
const depths = getDepth(context, 0, 0, depthMapSize, depthMapSize, pickDepth.framebuffer);
for (let j = 0, len = depths.length; j < len; j++) {
const depth = depths[j];
if (Cesium.defined(depth) && depth > 0.0 && depth < 1.0) {
// 根据视锥体远近截面计算出相机到物体表面的距离
const renderedFrustum = view.frustumCommandsList[i];
const near = renderedFrustum.near * (j !== 0 ? scene.opaqueFrustumNearOffset : 1.0);
const distance = near + depth * (renderedFrustum.far - near);
// 将深度图像素点位置映射到世界坐标(以中心点相机位置为起始点进行偏移映射)
const column = Math.floor(j / depthMapSize);
const row = j % depthMapSize;
const columnScalar = ((column - depthMapSize / 2) * height) / depthMapSize;
const rowScalar = ((row - depthMapSize / 2) * width) / depthMapSize;
const point = new Cesium.Cartesian3();
Cesium.Cartesian3.multiplyByScalar(camera.up, columnScalar, offset);
Cesium.Cartesian3.add(offset, camera.position, point);
Cesium.Cartesian3.multiplyByScalar(camera.right, rowScalar, offset);
Cesium.Cartesian3.add(offset, point, point);
// 利用射线获取坐标高程
const clonedRay = Cesium.Ray.clone(ray);
clonedRay.origin = point;
const position = Cesium.Ray.getPoint(clonedRay, distance);
if (!res[j]) {
res[j] = position;
}
}
}
}
return res;
}
//! REFERENCE: Cesium.Picking getRayIntersection
function getRayIntersecting(camera, width, height, depthMapSize) {
// 2. 创建拾取相机
const { scene } = viewer;
const ray = new Cesium.Ray(camera.position, camera.direction);
const picking = new Cesium.Picking(viewer.scene);
const { context, frameState } = scene;
const view = picking._pickOffscreenView;
const boundingRectangle = new Cesium.BoundingRectangle(0, 0, depthMapSize, depthMapSize);
view.viewport = boundingRectangle;
view.passState.viewport = boundingRectangle;
scene.view = view;
updateOffscreenCamera(ray.origin, ray.direction, camera.up, width, height, view.camera);
// 3. 更新拾取相机对应的帧缓存
Cesium.BoundingRectangle.clone(view.viewport, scratchRectangle);
const passState = view.pickFramebuffer.begin(scratchRectangle, view.viewport);
scene.jobScheduler.disableThisFrame();
scene.updateFrameState();
frameState.invertClassification = false;
frameState.passes.pick = true;
frameState.passes.offscreen = true;
frameState.tilesetPassState = pickTilesetPassState;
context.uniformState.update(frameState);
scene.updateEnvironment();
scene.updateAndExecuteCommands(passState, Cesium.Color.TRANSPARENT);
scene.resolveFramebuffers(passState);
// 4. 通过颜色缓存拾取对象
const objs = pickObjects(view.pickFramebuffer._fb._framebuffer, context, scratchRectangle);
// 5. 通过深度缓存拾取坐标
const positions = pickPositions(ray, picking, scene, width, height, depthMapSize);
// 6. 绘制相机视锥体(可选)
frustumPrimitives.add(getFrustumOutline(view.camera));
scene.view = scene.defaultView;
context.endFrame();
return { objs, positions };
}
//#regionend ----------------------- CORE --------------------------------------------------
// 拾取方法入口函数
function pick() {
const { objs, positions } = getRayIntersecting(viewer.camera, 1000, 1000, 1024);
objs.forEach((obj) => {
if (obj && obj instanceof Cesium.Cesium3DTileFeature) {
obj.color = Cesium.Color.YELLOW;
}
});
positions.forEach((position) =>
pointPrimitives.add({
pixelSize: 5,
color: Cesium.Color.GREEN,
position
})
);
}
// Cesium 沙盒快速添加测试按钮
Sandcastle.addToolbarButton("pick", pick);
Sandcastle.addToolbarButton("clear", function () {
pointPrimitives.removeAll();
frustumPrimitives.removeAll();
tileset.style = new Cesium.Cesium3DTileStyle();
});