Cesium 同时拾取多个对象与 1024*1024 个坐标

2023-11-03

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.jsgetRayIntersection() 函数),用黄色高亮拾取对象,用绿色标记拾取坐标。

在这里插入图片描述

具体代码

以下代码可直接放入 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();
});

参考资料

Cesium中离屏渲染的应用 - 槑的秘密基地

Cesium Picking 源码

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Cesium 同时拾取多个对象与 1024*1024 个坐标 的相关文章

随机推荐

  • Unity常见问题集(待续)

    1 error CS0101 The namespace global already cont 分析 关于global 错误 我们不能打开两个同一名称的scripts在你的工程里 解决 如果在你的工程里有另一个 Activate Trig
  • 傅里叶变换:周期、非周期 与连续、离散

    也是之前总结的 但是感觉很重要 分享出来 也方便自己不带本子的时候能快速找到 傅里叶变化过程中 在两个域 周期和非周期 连续和离散问题
  • chatGPT本地部署

    chatGPT是一种使用自然语言生成 NLG 技术的聊天机器人 可以使用生成对话模型 GPT 训练出来的模型来回答用户的提问或对话 要在本地部署chatGPT 您需要 安装Python环境 并使用pip安装chatGPT所需的依赖包 下载并
  • spring boot 自动构造模拟数据

    spring boot 自动构造模拟数据 引言 Spring boot代码 实体类对象 pom xml 控制器代码 Swagger测试效果 jar包下载地址 引言 现如今基于微服务架构技术开发的项目 一般会采用前后端分离的模式 该模式的好处
  • #3文献学习总结--边缘计算资源分配与任务调度优化

    文献 边缘计算资源分配与任务调度优化综述 1 系统模型 云 边 端 第 1 层是物联网层 传感器 处理器根据应用需求感知 测量和收集原始数据 在本地处理大量数据或将其上传至计算节点 第 2 层是边缘计算层 位于互联网边缘 靠近数据源 边缘计
  • TensorFlow Seq2Seq Model笔记

    0 tf跑起来一直没有用GPU 尴尬 跑起来发现GPU没用起来 CPU满了 发现装错了 应该装tensorflow gpu 代码测试是否用的是GPU https stackoverflow com questions 38009682 ho
  • 正确选择云服务器的带宽教程

    由于云服务器具有弹性的扩容机制 安全的云端集群分布式存储等 云服务器的优势逐渐凸现出来 我们越来越多的人开始选择云服务器 那么如何选择云服务器配置呢 怎样的云服务器配置才能不浪费呢 计算机数据的最小单位是bit 称之为位 其他还有B KB
  • 1114 计算营业额

    题目描述 编程统计营业员一天的营业额 输入要求 输入若干个数据代表交易金额 由于营业员一天完成的交易次数是不确定的 因此最后附加输入一笔0作为交易金额已全部输入结束的标志 输出要求 输出一天的营业额 保留2位小数 输入样例 100 5 12
  • Cocos Creator3D:发布到 Web 平台

    推荐 将 NSDT场景编辑器 加入你的3D工具链 3D工具集 NSDT简石数字孪生 发布到 Web 平台 打开主菜单的 项目 gt 构建发布 打开构建发布面板 Cocos Creator 3D 提供了两种 Web 平台的页面模板 可以通过
  • 虚拟机中安装Virtualbox,嵌套的虚拟机不能运行64位系统

    https www quora com Can I install Virtualbox in a virtual machine Here is a previous question on Quora about it Is it po
  • 机器学习中,不平衡样本多分类评估指标采用哪些?准确率很高,召回率在少数样本上较低。

    Macro F1 这个指标计算每一类的F1 score然后求算术平均 如果模型在小样本上表现不好 小样本的F1会极大程度上拉低Macro F1 除了F1之外还有Macro recall Macro precision 计算原理是一样的 另
  • 区块链在中国(2):PBFT算法

    上一张我们从分布式系统的角度简单叙述了一下 IBM HyperLedger fabric 的一些基本概念 架构和协议信息 其中最为核心的部分就是共识算法 consensus plugin fabric推荐并实现的就是PBFT这一经典算法 B
  • ListView的性能优化之convertView和viewHolder

    ListView优化大致从以下几个角度 1 复用已经生成的convertView 2 添加viewHolder类 3 缓存数据 图片缓存 4 分页加载 一 复用convertView 首先讲下ListView的原理 ListView中的每一
  • Vue 集成mock.js

    mock js 官网地址 mockjs com mockjs是用来模拟产生一些虚拟的数据 可以让前端在后端接口还没有开发出来时独立开发 我们可以使用真实的url mockjs可以拦截ajax请求 返回设定好的数据 使用方式 这里主要讨论在v
  • com.mysql.jdbc.Driver错误解决方法

    把mysql的驱动包放在web工程下的WEB INF下的lib目录下即可
  • C++ 时间

    精确到秒的 std time 为了获得系统当前时间 目前 C 标准库里面给出的方法是 std time 它返回的结构体是 std time t 这个方法很方便很通用 但它有一些局限 它是精确到秒的 如果您需要更高精度的时间 比如说您需要精确
  • KCP】从零开始深入理解KCP原理(含TCP可靠机制原理), 及源码解析。

    目录 KCP是什么 KCP商业案例 KCP的实现原理 停等式ARQ协议 确认机制 超时重传 连续ARQ 1 回退n帧 go back n ARQ 2 选择重传协议 selective repeat 待补充 滑动窗口协议 TCP应用的ARQ机
  • mysql中 if 函数的使用

    在MySQL中 if 为流程控制函数 效果相当与java语句中的 if else 语法 IF 判断语句 结果一 结果二 注意 当判断语句为true时 为结果一 为false时 为结果二 例子 select if 5 gt 3 大于 小于
  • 回归商业初心,宝尊电商“深耕广拓”缔造品牌电商有质增长

    今年来 受疫情 通胀 能源等因素影响 全球市场都经历了不同程度的 震荡 寒气传递之间 电商行业开始摸索后疫情时代的前进之路 随着财报季来临 市场开始期待从各赛道标杆企业财报中看出行业走势 11月29日 品牌电商第一股 宝尊电商公布了2022
  • Cesium 同时拾取多个对象与 1024*1024 个坐标

    Cesium 拾取案例 在上一文 Cesium 中的离屏渲染 已经了解到 拾取可以通过离屏渲染实现 其中涉及到了颜色缓存和深度缓存的概念 拾取对象 颜色缓存 Cesium 的 Scene pick 只返回选中的第一个物体 根据上文已知原理是