ARTICLE DETAIL

资讯详情

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

deck.gl 图层子类化(Subclassed Layers)完全指南:扩展属性计算、Uniform 与 Shader

deck.gl 图层子类化(Subclassed Layers)完全指南:扩展属性计算、Uniform 与 Shader deck.gl 图层子类化Subclassed Layers完全指南扩展属性计算、Uniform 与 Shader【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gldeck.gl 的图层在设计之初就刻意保证了可扩展性——几乎每个核心图层都支持通过 JavaScript 子类化的方式在不改动框架源码的前提下增加新功能包括覆写生命周期方法、追加 GPU 顶点属性attribute、传入自定义 uniform甚至整体替换顶点/片元着色器。本文以官方开发指南 subclassed-layers.md 为主线结合仓库内modules/layers与modules/core的真实源码实现逐层拆解子类化图层这一核心技术读完你即可动手为PathLayer、ScatterplotLayer、PointCloudLayer等官方图层编写带自定义视觉能力的子类。为什么需要子类化图层deck.gl 的定位是开箱即用的 WebGL2/WebGPU 可视化框架官方提供了大量现成图层散点、路径、多边形、文本等。但真实业务场景中差一点点的需求比比皆是路径需要按段着色、散点需要圆角矩形、点云需要逐点半径。官方指南给出的方法论很明确如果某个小功能在图层中缺失子类化subclassing通常是最好的补充手段。子类化之所以可行是因为 deck.gl 的图层体系建立在以下两个公开契约之上生命周期方法可覆写initializeState、updateState、draw、getShaders等是每个图层的公开接口子类覆写时通过super.xxx()保留父类行为着色器与属性可替换每个图层通过getShaders()返回自己的 GLSL 源码与着色器模块通过AttributeManager注册实例化属性这两者都是子类可以直接干预的扩展点。从源码看Layer.getShaders 的默认实现会把图层自带 shader 与this.props.extensions的 shader 合并后再交给 luma.gl 的 shader assembler这意味着子类化与扩展机制共用同一套合并管线稳定性有保障。此外官方还建议如果你自己编写了一个可复用的自定义图层同样应该暴露getShaders()并允许别人选择性覆写 shader这样你的图层才能被二次子类化。覆写属性计算为 PathLayer 添加按段颜色官方指南的第一个完整示例是为PathLayer增加每段路径不同颜色的能力。标准PathLayer的getColor只支持返回一个颜色而子类化后我们可以让 accessor 返回颜色数组每个路径段对应一种颜色。// 为 PathLayer 添加按段着色的示例 import {PathLayer} from deck.gl/layers; // 允许 accessor: getColorFunction可选 // 返回一个颜色RGBA 数字数组或颜色数组数组的数组。 export default class MultiColorPathLayer extends PathLayer { initializeState() { super.initializeState(); this.getAttributeManager().addInstanced({ instanceColors: { size: 4, type: unorm8, normalized: true, update: this.calculateColors } }) } calculateColors(attribute) { const {data, getPath, getColor} this.props; const {value} attribute; let i 0; for (const object of data) { const path getPath(object); const color getColor(object); if (Array.isArray(color[0])) { if (color.length ! path.length) { throw new Error(PathLayer getColor() returned a color array, but the number of colors returned doesnt match the number of segments in the path); } color.forEach((segmentColor) { value[i] segmentColor[0]; value[i] segmentColor[1]; value[i] segmentColor[2]; value[i] isNaN(segmentColor[3]) ? 255 : segmentColor[3]; }); } else { for (let ptIndex 1; ptIndex path.length; ptIndex) { value[i] color[0]; value[i] color[1]; value[i] color[2]; value[i] isNaN(color[3]) ? 255 : color[3]; } } } } }该示例源自 dcposch 的贡献。这个示例揭示了子类化的两个关键动作在initializeState()中追加属性注册通过this.getAttributeManager().addInstanced(...)注册名为instanceColors的实例化属性。size: 4表示每个实例占用 4 个分量RGBAtype: unorm8表示使用归一化的 8 位无符号整数存储normalized: true让 GPU 端自动把0-255映射到0-1。提供update回调AttributeManager会在数据变化时调用calculateColors逐对象遍历并把计算出的颜色写入属性底层的类型化数组attribute.value。仓库源码可以印证这条链路的真实性。在 attribute-manager.ts 中add()与addInstanced()分别对应逐顶点与逐实例两种 stepMode后者正是所有 deck.gl 图层渲染大数据集时的默认姿势而官方PathLayer的类型定义 path-layer.ts 中getColor的签名本来就是AccessorDataT, Color | Color[]也就是说每段颜色正是官方预留的能力窗口子类化只是把这份能力真正填充进 GPU 缓冲区。需要注意这种手写update的方式意味着属性重算逻辑完全由你负责且 deck.gl 的属性变更检测采用浅比较shallow compare如果你在外部直接修改了数据对象内部的字段需要手动触发重渲染可参考 attribute-management.md 中关于updateTriggers与浅比较的说明。覆写 Shaders替换图层着色器每个核心图层在初始化时都会调用getShaders()它返回一个描述该图层用什么着色器和着色器模块的对象。官方文档明确列出了这个对象的四个字段字段类型说明vsstring顶点着色器vertex shader的 GLSL 源码fsstring片元着色器fragment shader的 GLSL 源码modulesArray需要使用的着色器模块列表如project32、picking、color等injectObject从注入点injection point到自定义 GLSL 代码的映射用于向官方 shader 预置位置注入代码关于如何编写可与 deck.gl 无缝协作的 shader投影模块、光照模块、fp64、picking 以及标准注入点请参阅 writing-shaders.md。官方指南给出了一条非常重要的工程规范当你覆写getShaders()时应当把未修改的 shader 和modules原样透传下去只替换需要改动的部分。这样做的目的是保证可组合性——其他人基于你的图层继续子类化时不会被你悄悄丢弃的模块或注入点坑到。推荐的写法是Object.assign({}, super.getShaders(), {...})只覆盖你想改的字段。从源码看官方图层正是这么实现的。以 scatterplot-layer.ts 为例它的getShaders()返回vs、fs、modules: [project32, color, picking, scatterplotUniforms, ...]以及按需添加的definespoint-cloud-layer.ts 同样在modules中罗列了project32, color, gouraudMaterial, picking, pointCloudUniforms。覆写时务必保留这些模块——project32承担了投影计算picking提供了拾取能力丢掉它们会让图层失去 deck.gl 的坐标系统与事件拾取支持。定义额外的 Uniform圆角矩形散点图Uniform 是 GPU 绘制的全局参数。如果你希望给自定义 shader 传递额外参数如圆角半径官方推荐的方式是覆写draw()方法——而不是在getShaders()里硬编码。因为在 deck.gl 中draw()是每个渲染帧都会执行的地方uniforms参数携带当前帧的投影矩阵、视图参数等在这里追加自定义 uniform 最自然。// rounded-rectangle-layer.js // 示例在 ScatterplotLayer 中绘制圆角矩形来代替圆形 import {ScatterplotLayer} from deck.gl/layers; import customFragmentShader from ./rounded-rectangle-layer-fragment; export default class RoundedRectangleLayer extends ScatterplotLayer { draw({uniforms}) { super.draw({ uniforms: { ...uniforms, cornerRadius: this.props.cornerRadius } }) } getShaders() { // 使用 object.assign 确保不会覆盖现有字段如 vs、modules 等 return Object.assign({}, super.getShaders(), { fs: customFragmentShader }); } } RoundedRectangleLayer.defaultProps { // cornerRadius: 矩形四角的圆角程度 // 0 - 矩形。1 - 圆形。 cornerRadius: 0.1 }配套的片元着色器使用这个 uniform关于编写 shader 的更多内容见 writing-shaders.md// rounded-rectangle-layer-fragment.js // 复制自 scatterplot-layer-fragment.glsl.js 并做了适配 // 修改部分均已注释 export default \ #define SHADER_NAME rounded-rectangle-layer-fragment-shader precision highp float; uniform float cornerRadius; varying vec4 vFillColor; varying vec2 unitPosition; void main(void) { float distToCenter length(unitPosition); /* 计算圆角矩形对应的裁剪半径 */ float threshold sqrt(2.0) * (1.0 - cornerRadius) 1.0 * cornerRadius; if (distToCenter threshold) { gl_FragColor vFillColor; } else { discard; } gl_FragColor picking_filterHighlightColor(gl_FragColor); gl_FragColor picking_filterPickingColor(gl_FragColor); } ;这里有两个值得注意的实现细节cornerRadius的语义0是直角矩形1是完美圆形默认值0.1。其原理是让片元根据到中心点的距离与一个插值后的阈值比较超出阈值直接discard。discard优于透明色正如 writing-shaders.md 中的提示在片元着色器里用discard丢弃片元比输出alpha 0更快且不会污染深度缓冲。draw()覆写的模式与官方源码完全同构。看 point-cloud-layer.ts 的draw({uniforms})它从this.props取出pointSize、sizeUnits通过model.shaderInputs.setProps({pointCloud: ...})传给着色器再调用model.draw(this.context.renderPass)。你的子类在调用super.draw()前展开spread并追加cornerRadius正是同一套每帧同步 props → uniform的机制。当前版本 deck.gl 使用 luma.gl 的shaderInputs统一管理 uniform block如果你在getShaders()中通过modules引入自定义 shader module则应像 layer-extensions.md 中的示例那样用model.shaderInputs.setProps(...)来更新 uniform。定义额外的属性为 PointCloudLayer 添加逐点半径如果说覆写 shader 是改渲染算法那么新增属性就是给 shader 喂新数据。官方指南的第三个示例是为PointCloudLayer增加逐点半径instanceRadiusPixels。做法同样是两件事在initializeState()里通过 attribute manager 注册新属性在getShaders()里替换为使用新属性的顶点着色器。// my-point-cloud-layer.js // 示例为点云图层添加逐点大小 import {PointCloudLayer} from deck.gl; import vertexShader from my-point-cloud-layer-vertex; export default class MyPointCloudLayer extends PointCloudLayer { initializeState() { super.initializeState(); this.state.attributeManager.addInstanced({ instanceRadiusPixels: {size: 1, accessor: getRadius} }); } getShaders() { return Object.assign({}, super.getShaders(), { vs: vertexShader, }); } } MyPointCloudLayer.defaultProps { // 返回点的像素半径 getRadius: {type: accessor, value: 1} };注意这里属性定义只用了size和accessor两个字段比第一个示例简洁得多——因为颜色需要自定义的update回调做数组展开而半径是简单的标量取值交给默认的 accessor 机制即可。访问器getRadius需要在defaultProps中声明为{type: accessor, value: 1}这与仓库中官方PointCloudLayer的defaultPropspoint-cloud-layer.ts声明getPosition、getNormal、getColor的方式一致。配套的顶点着色器使用这个新属性更多内容见 writing-shaders.md// my-point-cloud-layer-vertex.js // 复制自 point-cloud-layer-vertex.glsl.js 并做了适配 // 修改部分均已注释 export default \ #define SHADER_NAME point-cloud-layer-vertex-shader attribute vec3 positions; attribute vec3 instanceNormals; attribute vec4 instanceColors; attribute vec3 instancePositions; attribute vec3 instancePositions64Low; /* 新增属性 */ attribute float instanceRadiusPixels; uniform float opacity; varying vec4 vColor; varying vec2 unitPosition; void main(void) { unitPosition positions.xy; vec4 position_commonspace; gl_Position project_position_to_clipspace(instancePositions, instancePositions64Low, vec3(0.), position_commonspace); /* 将统一半径 radiusPixels 替换为逐点 instanceRadiusPixels */ gl_Position.xy project_pixel_size_to_clipspace(positions.xy * instanceRadiusPixels); vec3 lightColor lighting_getLightColor(instanceColors.rgb, project.cameraPosition, position_commonspace.xyz, project_normal(instanceNormals)); vColor vec4(lightColor, instanceColors.a * opacity) / 255.0; picking_setPickingColorFromInstanceID(); } ;这段 GLSL 本身就是一张deck.gl 图层着色器配方表值得逐行消化project_position_to_clipspace(...)来自project32模块负责把经纬度/世界坐标投影到裁剪空间instancePositions64Low是 64 位精度投影的高精度补偿分量project_pixel_size_to_clipspace(...)把像素尺寸换算成裁剪空间偏移这正是像素半径能随缩放保持一致的原因lighting_getLightColor(...)来自光照模块picking_setPickingColorFromInstanceID()来自 picking 模块——再次印证保留父类的modules才能继续享受这些能力与属性注册的对应关系GLSL 里的attribute声明必须与addInstanced注册的名字一致instanceRadiusPixels而positions这类逐顶点属性来自图层内部的Geometry。对照仓库中官方 point-cloud-layer.ts 的initializeState()可以看到它注册了instancePositions、instanceNormals、instanceColors三个实例化属性且都带transition: true支持属性过渡动画。你的子类通过super.initializeState()先拿到这套基线再用addInstanced追加自己的属性即可两套属性互不干扰。扩展点总览生命周期方法一览综合官方指南与仓库源码子类化图层可干预的扩展点如下表方法调用时机典型用途initializeState()图层初始化时调用一次注册额外属性、创建 GPU 资源getShaders()图层初始化、扩展或属性变化导致重建模型时替换/注入 shader 与模块updateState(params)每次 props 或数据变化后同步自定义 props、更新 uniform、失效属性draw({uniforms})每个渲染帧追加自定义 uniform执行绘制其中getShaders()的合并机制在源码中有明确体现Layer.getShaders 会先合并this.context.defaultShaderModules再遍历this.props.extensions逐个合并扩展返回的 shader——这正是子类覆写与扩展注入两条路径的统一入口。也就是说当你覆写getShaders()时只要保留super.getShaders()的返回值并选择性覆盖扩展与子类两套机制就能叠加工作。从子类化到 Layer Extension子类化虽然强大但有个现实痛点当你需要在多个图层上加同样的功能时就得为每个图层各写一个子类。官方指南明确指出了这条升级路径有时我们需要子类化多个图层来添加相似的功能。Layer extension 是一种将子类化代码泛化、复用和共享的方式。例如要给GeoJsonLayer它内部由ScatterplotLayer、PathLayer、SolidPolygonLayer组合而成统一加红色高亮过滤子类化方案需要分别改写三个子类再用_subLayerProps注入而 Layer Extension 方案只需要实现一次RedFilter extends LayerExtension然后通过extensions: [new RedFilter()]挂到复合图层上框架会自动把扩展传递给所有子图层。完整的LayerExtension接口getShaders、initializeState、updateState、draw、finalizeState、getSubLayerProps与可运行示例见 layer-extensions.md。编写子类图层的最佳实践结合官方指南与仓库实现总结几条可直接落地的实践准则优先子类化而非复制官方图层都实现了getShaders()并返回结构化 shader 对象直接extends官方图层如PathLayer、ScatterplotLayer、PointCloudLayer是最低成本的扩展方式参见 primitive-layers.md 对图层类型的介绍。覆写getShaders()时用Object.assign({}, super.getShaders(), {...})只覆盖目标字段保留vs、modules、inject等未修改部分确保投影、光照、拾取等基础设施不丢失。额外数据走属性额外参数走 uniform逐对象变化的数据颜色、半径、偏移用AttributeManager.addInstanced()注册为实例化属性全局性的渲染参数圆角半径、过滤开关在draw()里通过uniforms传入。当前版本使用model.shaderInputs.setProps()更新 shader module 的 uniform 值。新属性必须在 GLSL 中同步声明addInstanced注册的属性名要与顶点着色器中的attribute变量名一一对应漏声明或改名都会导致渲染异常。小改动优先用 inject 而不是重写整个 shader如果只是想在官方 shader 的固定位置插入少量逻辑优先使用inject配合标准注入点如fs:DECKGL_FILTER_COLOR它们被视为跨小版本稳定的公开 API具体注入点清单见 writing-shaders.md。多图层复用请升级为 Layer Extension一旦同一段逻辑需要作用于多个图层及时把它封装成扩展避免复制粘贴参见 layer-extensions.md。子类化是 deck.gl 从使用框架走向掌控框架的分水岭。掌握了属性注册、shader 覆写、uniform 传递这三板斧再加上 Layer Extension 的复用思想你就能在保持与官方图层同等渲染性能的前提下随心所欲地为任何图层注入自定义视觉能力。【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表