ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

deck.gl 的 Project/Unproject 演进:从 RFC 设计考量到 Viewport 坐标系统实现

deck.gl 的 Project/Unproject 演进:从 RFC 设计考量到 Viewport 坐标系统实现 deck.gl 的 Project/Unproject 演进从 RFC 设计考量到 Viewport 坐标系统实现【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gldeck.gl 的project/unproject是连接屏幕像素与地理/世界坐标的核心桥梁直接决定了点击拾取、图层局部坐标系、跨视口渲染等功能的正确性。本文以仓库中的《RFC - Project / Unproject Improvements》dev-docs/RFCs/proposals/project-unproject-rfc.md为骨架完整梳理这份 RFC 提出的多视口、多坐标系、自定义投影、反子午线调整与矩阵求逆五类设计考量并逐一对照deck.gl/core当前源码中Viewport类、拾取管线和 shader 投影模块的实现说明这些早期问题如今是如何被解答的。RFC 背景一份 2018 年的概念性草稿这份 RFC 由 Ib Green 于 2018 年 8 月提出状态为Conceptual Draft。其核心主张是随着 deck.gl 多年演进引入了多视口、每图层独立坐标系、可安装投影等高级特性project/unprojectAPI 需要一次系统性增强。RFC 中引用了它所处的演进脉络以下均为仓库内的相关设计文档View Class Extensions RFCv6.1View Class RFCv5.2Multi Viewport RFCv5.0First Person Geospatial Viewport RFCv5.0Infovis Viewport RFCv4.0这份 RFC 本身以问题清单的形式给出五组考量Multiple Views、Multiple Coordinate Systems、Custom Coordinate Systems、Adjustable Ante-Meridian、Model Matrix Inversion。下面按这五组问题逐一展开并给出当前仓库中的实现证据。多视口Multiple Views拾取信息如何归属到具体视口RFC 提出的第一组问题是拾取picking已经会渲染所有视口因此对象可以在任意视口中被拾取但拾取信息并未完整更新。它抛出三个具体问题pickInfo对象是否应包含一个视口描述符引用相对坐标是否应在视口内部解析是否应支持将拾取限制到某个特定视口当前实现pickInfo 携带 viewport 引用并在视口内解析坐标从源码看这三个问题在前两个方向上都已落地。pick-info.ts 中定义的PickingInfo类型L9-L23包含viewport?: Viewport字段——即 RFC 所问的“视口描述符引用”已经存在每个拾取事件都携带了产生该拾取的视口实例。getEmptyPickingInfo函数L34-L81展示了“相对坐标在视口内部解析”的实现方式当存在多个视口时先用getViewportFromCoordinates定位出包含被拾取像素的那个视口然后把全局画布坐标转换为该视口的局部坐标再做反投影// modules/core/src/lib/picking/pick-info.ts (L49-L63) let pickedViewport viewports[0]; if (viewports.length 1) { // Find the viewport that contain the picked pixel pickedViewport getViewportFromCoordinates(pickInfo?.pickedViewports || viewports, {x, y}); } let coordinate: number[] | undefined; if (pickedViewport) { const point [x - pickedViewport.x, y - pickedViewport.y]; // 全局坐标 - 视口局部坐标 if (z ! undefined) { point[2] z; } coordinate pickedViewport.unproject(point); }其中getViewportFromCoordinatesL200-L212从后往前遍历视口列表后绘制者位于上层用viewport.containsPixel(pixel)命中包含目标像素的视口若无命中则回退到第一个视口。这个“取包含像素的视口、坐标先局部化再unproject”的流程正是对 RFC 前两个问题的工程化回答。至于第三个问题限制拾取到特定视口当前公共 API 侧的对应手段是Deck.getViewports(rect)——deck.ts 中该方法接受一个rect参数并委托给viewManager.getViewports(rect)L690-L698拾取路径中同样按{x, y, canvasId}过滤视口L969。可以推断通过约束参与拾取渲染的视口集合即可实现“限制在特定视口内拾取”的效果而非一个单独的白名单参数。另一个与多视口相关的细节是unproject3D选项Deck的pick系列方法支持unproject3D: booleandeck.ts L388、L738-L739开启后info.coordinate会将屏幕x, y反投影到被拾取几何体表面得到三维点_shouldUnproject3DL930会依据图层类型自动决定是否启用默认值见 L945点击事件中会在 L1880 强制以unproject3D: true重新取点。这让拾取结果的坐标系表达在“2D 屏幕面”与“3D 几何面”之间可以精确选择。多坐标系Multiple Coordinate Systems从 Deck 级函数到 Viewport 级 APIRFC 的第二组问题指出每个图层可以有自己的坐标系LNGLAT 或 METER_OFFSETS每个 METER_OFFSETS 图层还可以有自己的坐标原点和 model matrix。由此它追问Deck级别统一的project/unproject还有意义吗是否应该返回所有坐标系下的值是否应该遍历图层列表提取一份坐标系清单还是应该提供“向某个特定图层 id 反投影”的 API当前实现坐标系是图层属性投影则收敛到 Viewport从源码结构看deck.gl 最终没有采用“遍历图层提取坐标系清单”的方案而是把坐标系定义为图层声明的属性、把投影计算收敛到 Viewport 实例。constants.ts 中COORDINATE_SYSTEML24-L53定义了四种坐标系常量这也是公共 API常量值语义LNGLATlnglat位置为[longitude, latitude, elevation]经纬度单位是度高程单位是米METER_OFFSETSmeter-offsets位置为相对坐标原点coordinate origin的米偏移[x, y, z]尺寸单位为米LNGLAT_OFFSETSlnglat-offsets位置为相对坐标原点的经度/纬度偏移度 高程米CARTESIANcartesian位置和尺寸都在视口的 common units 中旧的IDENTITY已废弃并打印警告L57-L62DEFAULT则取LNGLAT或CARTESIAN取决于所在视口是否为地理空间视口L25-L28。而“向特定图层的坐标系做投影”这件事实际上发生在着色器侧GPU 端的投影由projectshader 模块按图层声明的coordinateSystem分发处理project.glsl.ts 中可见COORDINATE_SYSTEM_LNGLAT、COORDINATE_SYSTEM_CARTESIAN、COORDINATE_SYSTEM_METER_OFFSETS等分支L68、L193-L219。也就是说每个图层的坐标系统一在着色器顶点阶段被“解释”为 common space 坐标JS 侧的project/unproject只需处理 Viewport 这一层。这与 docs/api-reference/core/project.md 中的描述一致使用该模块可以确保自定义图层的着色器接受[longitude, latitude, altitude]或[metersX, metersY, metersZ]等各种位置格式。Viewport.project / unproject 的 API 契约JS 侧的统一入口是Viewport类viewport.ts 中的实现是 docs/api-reference/core/viewport.md 所记载 API 的来源// modules/core/src/viewports/viewport.ts (L248-L255) project(xyz: number[], {topLeft true}: {topLeft?: boolean} {}): number[] { const worldPosition this.projectPosition(xyz); const coord worldToPixels(worldPosition, this.pixelProjectionMatrix); const [x, y] coord; const y2 topLeft ? y : this.height - y; return xyz.length 2 ? [x, y2] : [x, y2, coord[2]]; }project(xyz, {topLeft})世界坐标 → 屏幕像素坐标输入[X, Y]返回[x, y]输入[X, Y, Z]返回[x, y, z]z 为像素深度topLeft默认true即返回以左上角为原点的 canvas 坐标。unproject(xyz, {topLeft, targetZ})L267-L282屏幕像素 → 世界坐标。若输入不含z且未指定targetZ返回[X, Y]指定targetZ时把像素反投影到该高程平面并返回[X, Y, targetZ]输入含zNDC 深度时返回完整三维点。从实现看project与unproject的核心是两枚预计算的 4x4 矩阵。_initMatricesL458-L534中视口矩阵NDC → 像素与viewProjectionMatrix相乘得到pixelProjectionMatrix而pixelUnprojectionMatrix直接对前者求逆得到L529mat4.scale(viewportMatrix, viewportMatrix, [this.width / 2, -this.height / 2, 1]); mat4.translate(viewportMatrix, viewportMatrix, [1, -1, 0]); mat4.multiply(pixelProjectionMatrix, viewportMatrix, this.viewProjectionMatrix); this.pixelProjectionMatrix pixelProjectionMatrix; this.pixelUnprojectionMatrix mat4.invert(createMat4(), this.pixelProjectionMatrix);值得注意的一点是Viewport被刻意设计为不可变对象——类头注释L121-L126写明它只有访问器、参数变化时应新建实例。这使得所有矩阵包括求逆结果只在构造时计算一次运行期的project/unproject就是一次矩阵乘法级别的轻量操作。自定义坐标系与可安装投影projectFlat / projectPosition 钩子RFC 的第三组问题更具前瞻性“甚至存在支持完全不同的、可安装投影的想法。这些投影 presumably 会向 JS 暴露project/unproject函数。它们能被整合进一个通用的 project/unproject 系统吗”当前架构对它的回答是分层钩子Viewport把投影拆成了两层——非线性的“平面投影”部分与线性的“矩阵投影”部分。viewport.ts 中projectFlat(xyz)L308-L318地理视口下调用math.gl/web-mercator的lngLatToWorld把经纬度映射到 Mercator world space并把纬度方向钳制到[-318, 830]对应 shader 中 ±89.9° 的钳制行为源码注释 L310-L315 有说明非地理视口直接返回原值。unprojectFlat(xyz)L328-L333反向调用worldToLngLat。projectPosition(xyz)L287-L291先projectFlat处理 XY再把 Z 乘以distanceScales.unitsPerMeter[2]换算到 common space。unprojectPosition(xyz)L293-L297反向换算Z 乘以distanceScales.metersPerUnit[2]。distanceScalesunitsPerMeter/metersPerUnit是地理视口与普通视口之间的“单位桥”在_initPropsL423-L455中对地理视口用getDistanceScales({latitude, longitude})初始化getDistanceScales(coordinateOrigin)公开方法L355-L364还支持以任意坐标原点重新计算——这正是每图层坐标原点coordinate origin场景的基础。“可安装投影”的落点则是 Viewport 的子类体系。docs/api-reference/core/viewport.md 说明Viewport通常不由应用直接创建而是由View描述符通过View.makeViewport生成modules/core/src/viewports/目录下的WebMercatorViewport、GlobeViewport、OrbitViewport、OrthographicViewport各自重写投影相关的钩子例如GlobeViewport对球面坐标做特殊处理OrbitViewport支持 3D 轨道相机。从源码结构看任何“新投影”都可以通过提供自定义View/Viewport类接入通用体系而projectFlat/unprojectFlat这类钩子就是 RFC 设想的“向 JS 暴露 project/unproject 函数”的扩展点。对上层应用而言最常用的入口是Deck.getViewports()deck.ts L690-L698拿到当前视口数组后再调用实例方法拾取结果中的info.viewport与info.coordinate则是同一体系在事件侧的体现。可调反子午线Adjustable Ante-MeridianprojectionMode 的自动切换RFC 的第四组问题关注着色器与 JS 侧计算的一致性“着色器现在可以动态调整反子午线ante-meridian的‘放置位置’JS 函数是否也应该这样做以确保 shader 侧与 JS 侧的渲染计算始终对齐图层能否使用不同的反子午线”当前实现中这个动态调整由Viewport.projectionMode属性承担viewport.ts L205-L212get projectionMode(): number { if (this.isGeospatial) { return this.zoom 12 ? PROJECTION_MODE.WEB_MERCATOR : PROJECTION_MODE.WEB_MERCATOR_AUTO_OFFSET; } return PROJECTION_MODE.IDENTITY; }从源码结构看当地理视口缩放级别低于 12 时采用标准WEB_MERCATOR模式世界坐标原点即本初子午线而放大到 12 级以上后切换为WEB_MERCATOR_AUTO_OFFSET模式——即把“偏移原点”跟随视口中心移动以避免高精度浮点下远离本初子午线处的精度丢失。这个projectionMode会同时传给 JS 侧计算viewMatrix、viewProjectionMatrix与 GPU 侧作为projectshader 模块的 uniform从而保证 shader 与 JS 使用同一套坐标“放置”回答了 RFC 关于一致性的追问。至于“每个图层能否使用不同反子午线”当前 API 中projectionMode是 Viewport 级属性而非 Layer 级属性结合每图层已有独立 coordinate origin 与 model matrix 的事实可以推断这一诉求已被“每图层坐标系 每视口 projectionMode”的组合间接覆盖而非以独立反子午线参数的形式存在。模型矩阵求逆Model Matrix Inversion预计算而非按需计算RFC 的最后一组问题非常务实“很多 model matrix 求逆的代价不算便宜是否应该按需on demand计算”答案体现在Viewport不可变设计带来的一次性预计算策略中。viewport.ts 构造函数只执行一次_initMatrices其中viewMatrixInverse mat4.invert([], this.viewMatrix)L506——视图矩阵的逆只算一次随后用于分解出cameraPositionL509pixelUnprojectionMatrix mat4.invert(createMat4(), this.pixelProjectionMatrix)L529-L533——反投影矩阵直接由正投影矩阵求逆一次得到若矩阵不可逆则仅记录log.warn(Pixel project matrix not invertible)而不抛错。运行期的project/unproject/getBounds后者在 L339-L353 用四个角点unproject求得[minX, minY, maxX, maxY]都不再触碰求逆操作。换句话说RFC 担心的“反复求逆”被转移到了视口实例重建的瞬间而视口重建本身只在视图状态变化时发生。对modelMatrix参数_initPropsL436-L442中也只在构造时用new Matrix4(modelMatrix).transformAsVector(position, [])应用一次用于把position变换到视口中心坐标系运行期不再重复。小结一份概念草稿如何被架构消化回顾这份 2018 年的 project-unproject RFC五组问题与当前代码的对应关系可以归纳为RFC 考量当前实现位置状态pickInfo 携带视口引用、视口内解析相对坐标pick-info.ts 的viewport字段与getEmptyPickingInfo已落地限制拾取到特定视口Deck.getViewports(rect)的视口过滤 拾取路径的canvasId约束deck.ts L690-L698、L969以视口集合约束方式实现每图层坐标系的 project/unproject图层声明COORDINATE_SYSTEMconstants.ts L24-L53GPU 侧由 project.glsl.ts 按系统分发已落地JS 侧收敛到 Viewport 级 API可安装投影View/Viewport子类 projectFlat/projectPosition钩子viewport.ts L287-L333以继承扩展方式落地反子午线 JS/shader 一致性projectionMode的WEB_MERCATOR与WEB_MERCATOR_AUTO_OFFSET自动切换viewport.ts L205-L212已落地为视口级属性矩阵求逆成本视口不可变、构造时一次性预计算viewMatrixInverse与pixelUnprojectionMatrixviewport.ts L506、L529以预计算替代按需计算对开发者而言实用的结论是日常使用只需记住三个入口——deck.getViewports()取视口、viewport.project()/viewport.unproject()做坐标转换注意topLeft与targetZ两个选项的语义、以及拾取回调里现成的info.viewport与info.coordinate而当你需要接入非标准投影时继承Viewport并覆盖projectFlat/unprojectFlat之类的钩子就是这份 RFC 所预想的“通用 project/unproject 系统”的官方扩展路径。更多 API 细节可继续参阅 docs/api-reference/core/viewport.md、docs/api-reference/core/project.md 与 docs/api-reference/core/deck.md。【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表