Cesium 基于 Framebuffer 的几种 pick 方式

原文链接

上篇聊了关于GPU拾取的几种方式。GPU 拾取的多重奇幻之旅:颜色编码、G-Buffer 与光线追踪的全景探索

本篇来聊聊Cesium应用的两种拾取方式: 颜色编码, 深度缓存。

Cesium相关 pick API这里就不介绍咯, 可以直接去官方文档查阅!

总的来说, 一般在渲染引擎会提供在交互拾取时所需的 object和position。

Cesium是使用 颜色编码 拾取物体或物体上的metadata信息, 使用 深度缓存 提取当前像素上的 worldPosition。

先来看看基于颜色编码拾取物体流程: (以Primitive为例)

  1. 在批量处理数据时会为object或实例创建一个唯一的整数ID并转换为颜色存储到batchTable或batchTexture里(颜色编码最多 4294967295 个, 就是当前可以维护这些数量的可拾取物, 超出会报错), 且颜色编码的映射维护在上下文里的 pickObjects里(着色器里会通过 czm_batchTable_pickColor 函数获取到当前的编码ID)。
  2. 在 createCommands 之后, 会依据配置 ‘allowPicking’ 给当前 command添加 pickID: ‘v_pickColor’。
  3. 在 Scene 进行 Frame Render 的时候, 会更新 DerivedCommand, 这时候会依据当前 Command 是否具有 pickID 而决定是否 创建 输出颜色编码 的ShaderProgram 及 Command。    
  4. 在调用 scene.pick时, 会将当前 pass 通道的其它值改为false, pick改为true,更新pickFramebuffer, 及停止其他渲染工作。
  5. 绑定 pickFramebuffer, 执行 pickCommand, 执行 Draw Call, 将当前颜色编码通过 out_FragColor 输出到当前FBO上。
  6. 以当前鼠标点为中心, 调用 readPixels 读取长宽默认为 3 的正方形上的像素值, 然后通过螺旋扫描算法 寻找当前颜色编码对应的primitive。

虽然说 颜色编码 在GPU拾取中并不是最好的方案, 但Cesium本身从Real-Time Rendering角度架构设计, 对 颜色编码 做了很多优化方案: 诸如: Scissor Test, Culling Volumn等 弥补 颜色编码在超大场景中可能频繁Draw Call带来的通信成本。

下面来聊聊基于 深度缓存 的pickPosition:

  1. 从render开始, Cesium就会准备 pickDepthbuffer 来接收 片元着色器 输出的 gl_FragDepth (一般会处理成 logDepth 对数深度, 默认不包含透明物体的深度值)。
  2. 当Scene的 pickTranslucentDepth 设置为true时, Cesium 在处理 Current Frame 中的 commands的时候, 会重新写入深度缓存, 但大致的设计流程是和 pickObject一样的(透明物体的处理涉及到 MRT, OIT, Blend 等其它渲染内容, 打算后面仔细说说)。
  3. 从 depthBuffer 中获取深度值, 然后进行世界坐标的转换, 可以一起仔细看看这一块代码。

/**
 * @private
 */
SceneTransforms.drawingBufferToWorldCoordinates = function (
  scene,
  drawingBufferPosition,
  depth,
  result,
) {
  const context = scene.context;
  const uniformState = context.uniformState;
  const currentFrustum = uniformState.currentFrustum;
  const near = currentFrustum.x;
  const far = currentFrustum.y;
  
  // 默认是开启 logDepth 来消除 z-fighting
  if (scene.frameState.useLogDepth) {
    // transforming logarithmic depth of form
    // log2(z + 1) / log2( far + 1);
    // to perspective form
    // (far - far * near / z) / (far - near)
    const log2Depth = depth * uniformState.log2FarDepthFromNearPlusOne;
    const depthFromNear = Math.pow(2.0, log2Depth) - 1.0;
    depth = (far * (1.0 - near / (depthFromNear + near))) / (far - near);
  }

  // 这里得到的深度值是由 gl_FragDepth 输出的标准设备坐标, 
  // 所以要有完整的 x, y, z 的NDC坐标去反推世界坐标
  const viewport = scene.view.passState.viewport;
  const ndc = Cartesian4.clone(Cartesian4.UNIT_W, scratchNDC);
  ndc.x = ((drawingBufferPosition.x - viewport.x) / viewport.width) * 2.0 - 1.0;
  ndc.y =
    ((drawingBufferPosition.y - viewport.y) / viewport.height) * 2.0 - 1.0;
  ndc.z = depth * 2.0 - 1.0;
  ndc.w = 1.0;
  let worldCoords;
  let frustum = scene.camera.frustum;
  // 正交投影的变换
  if (!defined(frustum.fovy)) {
    const offCenterFrustum = frustum.offCenterFrustum;
    if (defined(offCenterFrustum)) {
      frustum = offCenterFrustum;
    }
    worldCoords = scratchWorldCoords;

    // 由于正交投影是线性变换, 
    // 只需将NDC坐标从标准设备空间转换到正交视锥体即可,得到投影坐标
    worldCoords.x =
      (ndc.x * (frustum.right - frustum.left) + frustum.left + frustum.right) *
      0.5;
    worldCoords.y =
      (ndc.y * (frustum.top - frustum.bottom) + frustum.bottom + frustum.top) *
      0.5;
    worldCoords.z = (ndc.z * (near - far) - near - far) * 0.5;
    worldCoords.w = 1.0;
    // 投影坐标 乘以 视图逆矩阵 得到 世界坐标
    worldCoords = Matrix4.multiplyByVector(
      uniformState.inverseView,
      worldCoords,
      worldCoords,
    );
  } else {
    // 如果是透视投影, 直接与视图投影逆矩阵相乘, 得到世界坐标
    worldCoords = Matrix4.multiplyByVector(
      uniformState.inverseViewProjection,
      ndc,
      scratchWorldCoords,
    );
    // 将世界坐标转换为标准的笛卡尔坐标, 
    // 由于正交投影没有透视作用, w会一直为 1
    // Reverse perspective divide
    const w = 1.0 / worldCoords.w;
    Cartesian3.multiplyByScalar(worldCoords, w, worldCoords);
  }
  return Cartesian3.fromCartesian4(worldCoords, result);
};

上面是从得到深度值到输出笛卡尔世界坐标的主要流程, 我们平时使用渲染引擎不会考虑到这些, 渲染引擎默认处理流程 世界坐标 -> 本地坐标 -> 投影坐标 -> NDC标准设备坐标, pickPosition 的坐标流程逆推。

这里提到了坐标转换, 本人向提一嘴之前写的一个library, 类似于 babylon 的 gizmo, 有兴趣的话可以研究一下, 点个star

在cesium场景中通过拖拽轴对几何体做几何变换

下一篇打算聊聊 MRT 或 OIT

### 使用 Cesium 配置 Node.js 环境 要在 Node.js 环境中使用 Cesium,需要完成以下几个方面的配置和集成工作: #### 安装必要的依赖项 Cesium 提供了官方支持的 npm 包 `cesium`,可以通过以下命令安装: ```bash npm install cesium --save ``` 这一步会下载并安装 Cesium 及其所需的依赖项到项目目录下[^3]。 #### 初始化 Cesium Ion Token 为了加载某些功能(如 3D 地形),需要设置有效的 Cesium Ion Token。可以在项目的入口文件中初始化该 token: ```javascript const Cesium = require('cesium'); // 设置 Cesium Ion Token if (!Cesium.Ion.defaultAccessToken) { Cesium.Ion.defaultAccessToken = 'YOUR_CESIUM_ION_TOKEN'; } ``` #### 创建基本的应用程序结构 下面是一个简单的示例,展示如何在 Node.js 中启动一个基于 Express 的服务器来托管 Cesium 应用程序: ```javascript const express = require('express'); const path = require('path'); const app = express(); // 指定静态资源路径 app.use(express.static(path.join(__dirname, 'public'))); // 路由处理 app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); }); // 启动服务 const port = process.env.PORT || 8080; app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); ``` 在此基础上,可以创建一个 HTML 文件 (`index.html`) 来嵌入 Cesium Viewer: ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cesium Example</title> <script src="/cesium/Cesium.js"></script> <link href="/cesium/Widgets/widgets.css" rel="stylesheet"> <style> html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; } </style> </head> <body> <div id="cesiumContainer"></div> <script> const viewer = new Cesium.Viewer('cesiumContainer', { terrainProvider: Cesium.createWorldTerrain() }); </script> </body> </html> ``` #### 复制 Cesium Assets 到公共目录 由于 Cesium 的 JavaScript 和 CSS 文件较大,默认不会自动复制到构建输出目录中。因此,在开发阶段需手动将其复制至静态资源目录: ```bash mkdir public/cesium cp -r ./node_modules/cesium/Build/Cesium public/cesium/ ``` #### 生产环境优化 对于生产部署,建议通过 Webpack 或其他打包工具进一步压缩和优化资产文件。以下是 Webpack 插件的一个简单实现方式: ```javascript const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { plugins: [ new CopyWebpackPlugin({ patterns: [ { from: './node_modules/cesium/Build/Cesium', to: 'cesium' }, ], }), ], }; ``` 以上步骤涵盖了从基础配置到高级优化的过程,能够帮助开发者快速搭建起一个运行于 Node.js 平台上的 Cesium 应用程序[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值