deck.gl GLSL Accessor 提案精读:用字符串 Accessor 让层在 GPU 端直读二进制数据
deck.gl GLSL Accessor 提案精读用字符串 Accessor 让层在 GPU 端直读二进制数据【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl本文以 deck.gl 开发者 RFC《GLSL Accessors》dev-docs/RFCs/proposals/glsl-accessor-rfc.md为主体完整解析其核心设计——允许把层的 accessor 属性设置为一段 GLSL 代码字符串从而跳过 CPU 端属性生成、直接消费列式二进制数据并在 GPU 上对数据做逐点加工同时结合当前仓库中的实际着色器源码、AttributeManager 实现与后续落地的 Shader Hooks 机制说明这一提案的影响脉络与最终走向。提案背景Binary-first 架构下的属性生成瓶颈这份 RFC 由 Ib Green 于 2018 年 6 月 13 日提出状态为Draft它明确标注自己是 性能路线图 中 Binary Attribute Support 专项的一部分。该路线图设定的长期目标是deck.gl 各层能够直接消费二进制数据且代码路径优先保证二进制场景下的零开销/最小拷贝。同一专项下还关联了几份姊妹 RFC二进制数据支持 RFC、纹理属性 RFC、分块数据支持 RFC。在传统的使用方式中deck.gl 层的 accessor如getPosition、getColor是 JavaScript 函数AttributeManager会在 CPU 上遍历整个数据集把每个对象计算成固定的顶点属性数组Float32Array等再上传为 WebGL buffer。这个流程在数据量很大、且数据本身已经是二进制列式存储例如 Apache Arrow时会带来双重成本时间成本CPU 端要为每个对象做一遍完整的属性生成迭代内存成本中间会额外产出一整套属性数组即使这些数组的内容只是对原始列的简单重排或组合。RFC 的 Summary 部分给出了该提案的三点收益完全或部分跳过 CPU 端属性生成skip CPU side attribute generation直接工作在提供的二进制数据上如二进制表允许对数据做额外 GPU 侧处理从而支撑极为高性能的自定义效果。一句话概括其核心机制把某个 accessor 设置成一个字符串这个字符串就不再被解释为函数名而是被当作 GLSL 代码片段注入到层的着色器中覆盖该属性的默认取值逻辑。API 示例一直接消费列式二进制数据RFC 给出的第一个示例针对列式存储的典型痛点像 Apache Arrow 这样的列式系统会把经纬度存成两个独立的float列而不是一个vec2/vec3。即便把这些列直接暴露给 GPU 作为 attribute着色器期望的vec3位置属性也无法直接对号入座——除非允许用户用一小段 GLSL 覆盖从属性提取位置的逻辑即getPositionnew CircleLayer({ // 注入新的 attributes glsl: { attributes: { longitude: float, latitude: float }, }, // 定义自定义 glsl accessor字符串值的 accessor 会被当作 GLSL 处理 getPosition: return vec3(longitude, latitude, 0.);, // 为注入的 attributes 提供实际数据 attributes: { longitude: new Float32Array(...), latitude: new Float32Array(...) }, })配套地CircleLayer的顶点着色器需要轻量重构——不再直接读取instancePositions属性而是改调一个可被覆盖的 GLSL 函数// 着色器模块系统会例如按行替换 getPosition vec3 getPosition() { return instancePosition; } main() { const position getPosition(); // 注意层不再假定 position instancePositions而是调用可覆盖的 GLSL 函数 }这样两个平行的float列无需在 JavaScript 中预先拼成vec3数组即可在 GPU 端组装成位置CPU 侧的属性生成被完全绕过。API 示例二在 GPU 端把数据适配成层需要的格式第二个示例展示了数据形状与层需求不匹配时的另一类用法假设点云二进制数据每个点只带一个reflectance反射率值而PointCloudLayer只支持逐点指定 RGBA 颜色。当然可以给getColor传一个 JS 函数、在 CPU 上构造完整的 RGBA 数组但 RFC 认为这部分时间和内存都可以省掉改在着色器里完成PointCloudLayer({ // 注入新的 attributes 和 uniforms glsl: { attributes: { reflectance }, uniforms: { alpha } }, // 定义自定义 glsl accessor字符串值的 accessor 会被当作 GLSL 处理 getColor: return vec4(reflectance, reflectance, reflectance, alpha);, // 为注入的 attributes 和 uniforms 提供实际值 attributes: { reflectance: new Float32Array(...) }, uniforms: { alpha: 0.5 } })这里可以看到提案 API 的三个组成部分组成部分作用数据流glsl.attributes声明注入到着色器的自定义顶点属性含类型数据经attributesprop 提供声明注入 shader 源码glsl.uniforms声明注入到着色器的自定义 uniform值经uniformsprop 提供声明注入 shader 源码字符串值 accessorgetPosition/getColor…用 GLSL 片段覆盖该属性的默认取值逻辑替换 shader 中的默认 accessor 函数体对 AttributeManager 的改造方案RFC 的 Proposals 部分原文标注仍在开发中给出了对属性管理系统的改动要点字符串值 accessor 的存在会让AttributeManager跳过该 accessor 对应属性的生成——这正是省掉 CPU 端迭代的关键attributesprop 的存在会让AttributeManager把这些属性纳入生成的 attribute map该 map 会被传给 luma.gl 的Model把这些属性的 GLSL 声明注入到着色器源码中uniformsprop 的存在会把 uniform 加入生成的 uniform map同样传给 luma.glModel把该 uniform 的声明注入着色器源码。RFC 还附带了一条实现细节注记像reflectance这种大小为 1 的Float32Array属性可以直接以值形式传入而一般的 typed array 则通常需要包装成描述 size、type 等元信息的常规 attribute descriptor 对象Accessor。从当前仓库的 AttributeManager 源码 结构看它今天仍然以accessor 名 → 生成的 buffer为核心组织更新逻辑其中对accessorName是否字符串的判断用于区分 accessor 名与直接传入的 buffer这与 RFC 设想的检测 accessor 类型再决定走生成还是跳过的路径是一致的但该检测目前并不包含字符串即 GLSL的语义。层着色器的重构形态以 PointCloudLayer 为例要让 GLSL accessor 生效希望支持它的层必须把直接读属性改造成通过函数读属性。RFC 给出了重构后PointCloudLayer顶点着色器的完整示例#define SHADER_NAME point-cloud-layer-vertex-shader varying vec4 vColor; varying vec2 unitPosition; // 默认 attributes 和 uniforms attribute vec3 positions; attribute vec3 instanceNormals; attribute vec4 instanceColors; attribute vec3 instancePositions; attribute vec2 instancePositions64xyLow; attribute vec3 instancePickingColors; uniform float opacity; uniform float radiusPixels; // NEW // BEGIN 默认 accessor 实现 float getRadius() { return instanceRadius; } // vec4 getColor() { return instanceColors; } // END default accessors // NEW // BEGIN 自定义注入的attributes、uniforms 与 accessors in float reflectance; uniform float alpha; vec4 getColor() { return vec4(reflectance, reflectance, reflectance, alpha); } // END Custom (injected) accessors void main(void) { // NEW // 调用 GLSL accessors vec3 position getPosition(); vec2 position64xyLow getPosition64(); float instanceNormal getNormal(); float instanceRadius getRadius(); vec4 color getColor(); vec3 pickingColor getPickingColor(); // position on the containing square in [-1, 1] space unitPosition position.xy; // 找到点的中心并加上当前顶点 vec4 position_worldspace; gl_Position project_position_to_clipspace(positions, positions64xyLow, vec3(0.), position_worldspace); gl_Position project_pixel_to_clipspace(positions.xy * radiusPixels); // 应用光照 float lightWeight lighting_getLightWeight(position_worldspace.xyz, // w 分量恒为 1.0 project_normal(normals)); // 把不透明度应用到实例颜色或返回实例 picking 颜色 vColor vec4(lightWeight * color.rgb, color.a * opacity) / 255.; // 设置渲染到 picking fbo 的颜色也用于选中高亮判断 picking_setPickingColor(pickingColor); }注意示例中的分层注释块BEGIN/END default accessors与BEGIN/END Custom (injected)。这暗示了着色器拼装机制——默认实现作为占位函数存在用户提供的 GLSL 片段按行/按块替换或追加最终main()只调用统一命名的 accessor 函数而具体实现可被替换。作为对照当前仓库中 PointCloudLayer 的真实顶点着色器 仍采用直接读属性的写法geometry.worldPosition instancePositions;、vColor vec4(lightColor, instanceColors.a * layer.opacity);没有任何getPosition()/getColor()形式的 accessor 函数——这印证了字符串 accessor 机制在主线代码中尚未按 RFC 设想落地该 RFC 至今保持 Draft 状态。设计讨论为什么选择重载 accessor作为注入点RFC 的 Design Discussions 部分回答了一个关键设计问题让用户重定义层 GLSL 中某些良定义的函数究竟应该重载现有 accessor还是另起一套可覆盖函数或者两者都做重载 accessor 的最大优势它把这个新特性绑定到一套已经完善、有文档的既有配置系统上——用户本来就会为层配置getPosition、getColor现在只需改变赋值的值类型从 JS 函数变为 GLSL 字符串即可学习成本与文档成本都很低表面上的劣势它强迫用户在 JS 生成函数与 GLSL 覆盖字符串之间二选一。RFC 认为这可以通过支持对象描述符或约定某种glsl 模板字符串语法来消解对另一套独立可覆盖函数持保留态度层的着色器固然可以在合适的时机额外定义一些可覆盖函数但这些函数需要单独文档化。RFC 要求先看到具体示例、理解为什么它们无法合理地表达为新的 accessor并确认这类函数能以结构化、可维护、非临时的方式添加才考虑提供支持。复杂度与未决问题RFC 诚实列出了一批尚未解决的工程问题这也是它停留在 Draft 的重要原因组合层Composite Layers支持由于提案 API 是基于 prop 的组合层向子层转发 prop 的通用机制理论上可用但实际使用中的可实践性不明确。命名冲突需要为层着色器中的 GLSL 函数与变量定义/强化命名规范或前缀以最大化可预期性、最小化与用户 GLSL accessor 代码的冲突。这一点在后来实际落地的 Shader Hooks 机制中得到了呼应——钩子统一采用DECKGL_前缀见下文。过渡动画Transitions对注入的 attributes 似乎可以实现 transitions但可能需要某种描述符元数据让用户指定是否以及如何进行过渡。其他潜在问题位置类tessellatingaccessor是否要让铺格子式 accessor如生成路径顶点的getPath一类也支持 GLSL 注入而不仅是描述性 accessorTesselated Layers对需要 CPU 端细分/三角化的层如何适用RFC 中该条仅开了头非一一对应 accessor当一个 accessor 的输入输出不是 1:1 关系时能否以某种通用性支持 GLSL accessor是否必须为 GLSL 函数提供另一套接口未来扩展抽象语法、动态着色器再生成与片元着色器支持更抽象的 GLSL accessor 语法RFC 承认直接写 GLSL 强大且酷但对许多用户可能有些吓人而且 GLSL 本身存在多个版本未来移植到 WebGPU 时还可能再添一种着色器语法。因此值得定义一种简单语法/格式由它生成GLSL accessor把底层方言差异隔离掉。动态着色器再生成RFC 特意标注这部分很可能应拆成独立 RFC。一旦允许 prop 影响着色器源码就必须二选一检测这些 prop 的变化重新编译着色器并重新链接程序或在文档中明确声明着色器只在创建时生成。核心困难在于GLSL 着色器的重编译与重链接往往很慢有时慢到数秒甚至半分钟以上且同步发生在主 WebGL 线程会冻结所有渲染甚至整个应用。如果要支持基于 prop 变化的动态重编译就必须格外小心避免并对误触发检测/告警无谓的重编译——用户可能意识不到自己传入的 prop 正在不断触发重编译导致应用性能崩塌。RFC 在此列出了若干规避缓慢着色器编译的思路延迟到首次绘制时再链接、利用KHR_parallel_shader_compile扩展等业界方案供后续设计参考。片元着色器支持按 RFC 原文的写法该提案没有给层用户提供修改片元着色器的途径而且这个体系向片元着色器扩展得很不自然属性attributes在片元着色器中不可用这些数据必须显式地作为varying传过去而 varying 占用的是一个非常有限的资源池往往只有 8 个varying/vec4槽位可能需要实现一个varying 压缩器并引入更多语义更复杂的 prop 来描述数据流。当然如果用户自定义的片元 GLSL 函数只允许使用已有数据系统会简单得多。对照当前仓库提案的思想如何演化落地虽然 GLSL accessor 本身停留在 Draft但当前仓库中可以清晰看到它的两条遗产线。第一条线Shader Hooks ——可覆盖函数思想的实际形态。2019 年的 Layer Shader Hooks RFC小纪Xiaoji Chen针对的正是在子类层中向官方层着色器注入自定义代码的痛点它借鉴了本 RFC 讨论过的定义可覆盖函数方向最终落地为一套标准化、有前缀的注入点。当前文档 Writing Shaders 列出的标准钩子包括vs:DECKGL_FILTER_SIZE/vs:DECKGL_FILTER_GL_POSITION/vs:DECKGL_FILTER_COLOR顶点着色器分别作用于投影前的尺寸、投影后的 clip 空间位置、颜色fs:DECKGL_FILTER_COLOR片元着色器颜色vs:#decl、fs:#decl、#main-start、#main-end等通用声明/主体注入点。从当前源码看这套机制已经全面铺开ScatterplotLayer 顶点着色器 中DECKGL_FILTER_SIZE(offset, geometry);与DECKGL_FILTER_COLOR(vFillColor, geometry);分别出现在投影与颜色赋值处PointCloudLayer 顶点着色器 同样在offset计算、gl_Position投影、vColor赋值三处调用对应钩子并配合VertexGeometry结构体geometry.worldPosition、geometry.position、geometry.pickingColor等传递几何上下文。可以推断这是本 RFC 中在着色器关键时机暴露可覆盖函数这一设想的现实版本注入点从覆盖 accessor 函数体变成了在固定时机调用用户注入的 filter 函数虽然牺牲了一点表达自由度换来了跨层一致、可文档化、跨小版本稳定的公共 API。第二条线Binary-first 数据路径。RFC 的原始动机——绕过 CPU 属性生成、直读二进制列——由同路线图下的其他 RFC 承接推进Binary Data RFC 与开发者指南 Binary Data 提供了把 typed array 直接作为 accessor 输入、由层零拷贝消费的路径64 位属性 RFC 甚至明确提到允许层编写描述映射关系的 GLSL 片段可作为 GLSL accessor RFC 的一个可能先行用例。当前 AttributeManager 中已能区分accessor 名与直接传入的 buffer正是这条二进制直通路线的实现基础。小结这份 RFC 今天仍值得读《GLSL Accessors》虽然以 Draft 状态留在 dev-docs/RFCs/proposals/ 目录中未以原始形态实现但它的价值在于把层行为的定制点应该放在哪一层这个问题完整摊开CPU 侧 JS accessor最通用、最易调试但承担全部属性生成的时间与内存成本字符串 accessor 即 GLSL本 RFC 提案表达力最强、GPU 直读二进制但引入命名冲突、着色器重编译、片元端扩展等一系列未决问题标准 Shader Hooks后续实际落地在可维护性与灵活性之间取平衡成为当前自定义层修改官方层渲染行为的主通道。对读者的直接实用建议如果你的目标是给官方层加过滤/变色/位移效果请使用当前仓库中已成体系的 Standard Shader Hooks如果你的场景是大规模列式二进制数据如 Arrow 表需要逐点 GPU 加工本 RFC 中的glsl.attributes 字符串 accessor 模式仍是理解 deck.gl 架构取舍、以及自研此类扩展时应遵循的设计蓝图——尤其是层着色器必须重构为调用可覆盖函数注入块以BEGIN/END注释界定命名前缀防止冲突这三条来自原文的架构约束。【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考