Rerun 自定义可视化:用 Rust 为 Viewer 添加带 GPU 渲染的 HeightField Visualizer
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
本篇技术指南以 Rerun 仓库中的 custom_visualizer 示例 为骨架,系统讲解如何为 Rerun Viewer 扩展一个全新的自定义可视化类型。示例以三维地形HeightField(高度场)为主题:它定义一个自定义 Archetype 存储 2D 高度网格,编写一个 Visualizer System 在每帧查询数据并产出绘制指令,再实现一个基于 WGSL 的 GPU Renderer 完成网格生成、法线计算与色彩映射。读完本文,你将掌握 Rerun 自定义可视化三件套(Archetype → VisualizerSystem → Renderer)的完整链路,并能在自己的应用中复制这套扩展模式。
适用前提:本示例为 Rust 示例,依赖 Rerun 的
native_viewer功能,需要 Rust 工具链与可用的 Vulkan/Metal 等图形后端。仓库当前版本为0.38.0-alpha.1+dev(见 Cargo.toml),示例代码基于 wgpu 抽象层编写。
自定义 Visualizer 的三段式架构
Rerun 的 Viewer 数据流可以概括为:SDK 日志数据 → 数据存储(Chunk Store)→ Visualizer System 查询 → Renderer 绘制。要在这条链路上插入自定义可视化,需要实现三个相互协作的部分(对应示例中三个源文件):
| 部分 | 示例实现 | 作用 |
|---|---|---|
| 自定义 Archetype | height_field_archetype.rs | 定义可视化操作的数据结构,可被 SDK 日志、可被 Viewer 查询 |
| Visualizer System | height_field_visualizer.rs | 每帧查询数据存储,把原始数据转换为可渲染的 Draw Data |
| 自定义 Renderer | height_field_renderer.rs + height_field.wgsl | GPU 侧工作:上传纹理/缓冲、管理 Bind Group、发起 Draw Call |
三者分别实现 Rerun 的三个核心 trait:Archetype(crates/store/re_types_core/src/archetype.rs)、VisualizerSystem(crates/viewer_support/re_viewer_context/src/view/visualizer_system.rs)与Renderer(crates/viewer_support/re_renderer/src/renderer/mod.rs)。最后通过App::extend_view_class将 Visualizer 注册进内置的Spatial3DView(spatial3d_view.rs),使其与点云、网格、Box 等内置可视化器共存于同一个 3D 场景。
第一部分:自定义 Archetype —— 定义数据契约
Archetype 是"一组通常协同工作的 Component 集合"的高层抽象(见 archetype.rs 的 trait 文档)。对 Viewer 来说,Archetype 是数据存储的查询键;对 SDK 用户来说,它是rec.log()的日志单元。
组件描述符与数据结构
HeightField包含三个组件字段,全部以Option<SerializedComponentBatch>存储:
#[derive(Default)] pub struct HeightField { pub buffer: Option<rerun::SerializedComponentBatch>, pub format: Option<rerun::SerializedComponentBatch>, pub colormap: Option<rerun::SerializedComponentBatch>, }其中buffer存放原始高度数据(示例中为 F32 亮度格式图像缓冲),format描述图像尺寸与通道类型,colormap是可选的 GPU 侧颜色映射。每个字段都通过一个静态函数暴露对应的ComponentDescriptor,例如:
pub fn descriptor_buffer() -> rerun::ComponentDescriptor { rerun::ComponentDescriptor { archetype: Some("HeightField".into()), component: "HeightField:buffer".into(), component_type: Some(rerun::components::ImageBuffer::name()), } }ComponentDescriptor把组件与所属 Archetype 绑定起来,是 Visualizer 查询与 UI 匹配的依据。
实现 Archetype trait
HeightField实现了rerun::Archetypetrait,声明其组件分类:
name():返回全限定名"HeightField",作为该 Archetype 的唯一标识;display_name():返回 UI 中显示的可读名"Height Field";required_components():必须提供的组件,这里是buffer与format两个描述符;optional_components():可选组件,这里是colormap。
这与 trait 定义中required_components/recommended_components/optional_components的分类完全对应(archetype.rs),默认实现的all_components()会按序合并三类描述符,供查询与 UI 使用。
构造器与序列化
示例提供了一组链式构造方法:
pub fn new(buffer, format) -> Self // 用 buffer + format 构造 pub fn with_buffer(...) -> Self // 设置高度数据缓冲 pub fn with_format(...) -> Self // 设置图像格式 pub fn with_colormap(...) -> Self // (示例中标记 dead_code)设置颜色映射每个 setter 内部都通过try_serialize_field::<T>(descriptor, [value])将组件序列化为SerializedComponentBatch。最后实现rerun::AsComponents,在as_serialized_batches()中把所有非空组件收集返回——这就是 SDK 在rec.log("terrain", &HeightField::new(...))时实际写入存储的内容。
第二部分:Visualizer System —— 每帧查询并产出 Draw Data
VisualizerSystem是"数据 → 渲染"的枢纽:每帧由 Viewer 调用,负责从数据存储中查询与当前 Archetype 匹配的实体,并生成 Renderer 可消费的DrawData。示例实现位于 height_field_visualizer.rs。
声明可视性与查询信息
首先通过IdentifiedViewSystem::identifier()给出唯一 ID"HeightField",然后实现VisualizerSystem:
fn visualizer_query_info(&self, _app_options) -> VisualizerQueryInfo { VisualizerQueryInfo::buffer_and_format::<ImageBuffer, ImageFormat>( &HeightField::descriptor_buffer(), &HeightField::descriptor_format(), &HeightField::all_components(), ) }buffer_and_format是一个便捷构造器(visualizer_system.rs),它要求查询同时命中 buffer 与 format 两个组件,并声明"相关的 Archetype 是HeightField",同时把全部查询组件加入queried集合——该集合决定了 UI 中哪些组件可见、以及该 Visualizer 何时被"指示"激活。底层通过BufferAndFormatConstraint实现可可视化判定。
execute:核心查询循环
execute()是每帧都要执行的核心方法,返回VisualizerExecutionOutput。执行流程可以拆解为:
- 通过
ctx.render_ctx()拿到 GPU 渲染上下文; - 从
context_systems获取re_view_spatial::TransformTreeContext,用于把实体变换解析到世界空间; - 遍历
query.iter_visualizer_instruction_for(Self::identifier()),得到每个匹配该 Visualizer 的实体与查询指令; - 对每个实体调用
data_result.query_archetype_with_history::<HeightField>(ctx, query, instruction, None),按历史查询该实体的HeightField数据; - 分别读取 buffer、format、colormap 三个组件流,用
re_query::range_zip_1x2逐行/逐帧打包; - 对每个时间戳的高度场调用
draw_data.add_mesh(...)生成一个 GPU 网格。
关键的数据处理细节:
- 校验网格尺寸:
cols < 2 || rows < 2时直接跳过(无法构成三角网格); - 用
bytemuck::cast_slice把&[u8]缓冲解释为&[f32],并校验长度不小于rows * cols; - 在 CPU 上计算 min/max 高度,供着色器做归一化——源码注释明确说明:理想做法是在 GPU 上计算并缓存,但示例保持简单;
- 颜色映射枚举:从 colormap 组件解析,缺省时使用常量
DEFAULT_COLOR_MAP = Colormap::Spectral; - 网格间距:
spacing = 10.0 / (cols.max(rows) - 1),即高度场固定铺满 10×10 米的世界空间; - 为每个实体生成 picking 相关的
PickingLayerObjectId(用实体路径 hash64)与OutlineMaskPreference,保证选中高亮与拾取可用。
最后把HeightFieldDrawData放入output.draw_data返回。每帧所有匹配实体共享同一个 DrawData,其中包含若干个MeshInstance。
第三部分:自定义 GPU Renderer —— 上传、绑组与绘制
Renderer 承担 GPU 侧的全部工作,实现re_renderer::renderer::Renderertrait(定义见 crates/viewer_support/re_renderer/src/renderer/mod.rs)。示例实现位于 height_field_renderer.rs。
数据组织:Config 与 DrawData
HeightFieldConfig<'a>:描述单个高度场网格的完整渲染参数——world_from_obj变换、扁平行主序的heights: &'a [f32]、grid_cols/grid_rows、世界空间间距spacing、min_height/max_height、colormap 枚举值(u32)、picking 与 outline 参数;HeightFieldDrawData:实现re_renderer::renderer::DrawData,内部持有Vec<MeshInstance>。每个MeshInstance包含 bind group、顶点缓冲、索引缓冲、索引数量与是否有 outline。
DrawData::collect_drawables把每个 mesh 以DrawPhase::Opaque | DrawPhase::PickingLayer提交给收集器;若该 mesh 配置了 outline,则额外以DrawPhase::OutlineMask提交——这解释了示例"参与拾取与选中高亮"的能力来源。
上传 GPU 资源:add_mesh
add_mesh完成一块高度场网格的全部资源上传,顺序清晰可复用:
- Uniform 缓冲:
gpu_data::UniformBuffer是一个#[repr(C)]、派生bytemuck::Pod/Zeroable的结构体,其内存布局必须与 WGSL 中同名UniformBuffer结构保持同步(源码注释明确提醒这一点)。其中world_from_obj: wgpu_buffer_types::Mat4、网格尺寸与间距、min/max 高度、colormap、picking 两个 ID、outline mask,以及填充字段_pad0/_pad1/end_padding保证 16 字节对齐与 256 字节 uniform 对齐规则; - 顶点缓冲:把
heights按字节拷贝进mapped_at_creation: true的 VERTEX 缓冲——每个网格顶点只上传一个f32高度值,行/列由顶点索引推导; - 索引缓冲:手动构建三角形网格索引,每个 quad 由两个三角形组成(
tl→bl→tr、tr→bl→br),共(rows-1)*(cols-1)*6个索引,使用索引缓冲让 GPU 顶点缓存复用共享顶点; - Bind group:仅绑定 uniform 缓冲,布局与 Renderer 持有的
bind_group_layout一致。
create_renderer:管线创建
create_renderer完成一次性的 GPU 管线初始化:
- 通过
include_shader_module!("../shader/height_field.wgsl")内联编译 WGSL 着色器; - 创建 bind group layout:binding 0 为 uniform 缓冲,
visibility同时覆盖 VERTEX 与 FRAGMENT 阶段; - 管线布局由
ctx.global_bindings.layout(Rerun 全局绑定,含帧参数、光照等)与自定义 layout 拼接,因此 WGSL 中@group(0)是全局绑定、@group(1) @binding(0)是自定义 uniform; - 顶点缓冲布局只声明一个
Float32属性(每个顶点一个高度值); - 同时创建三条渲染管线:主颜色管线(输出
ViewBuilder::MAIN_TARGET_COLOR_FORMAT)、拾取层管线(输出PickingLayerProcessor::PICKING_LAYER_FORMAT)、outline mask 管线(输出OutlineMaskProcessor::MASK_FORMAT),分别对应三个 fragment 入口。
draw:按相位分发
draw根据当前DrawPhase选择对应管线,然后遍历指令中的 drawable:设置 bind group(pass.set_bind_group(1, ...))、绑定顶点/索引缓冲,最后pass.draw_indexed(0..num_indices, 0, 0..1)发起绘制。
WGSL 着色器:程序化地形网格
着色器 height_field.wgsl 演示了 Rerun 渲染器的 shader 惯例:开头#import <types.wgsl>、<global_bindings.wgsl>、<colormap.wgsl>、<utils/lighting.wgsl>,复用引擎内置的帧参数、colormap 与光照工具。
顶点着色器vs_main的关键逻辑:
- 从
@builtin(vertex_index)推导网格行列:row = v_idx / grid_cols、col = v_idx % grid_cols——所以顶点缓冲只需一个高度值; - 构造对象空间坐标:
position_obj = vec4f(col*spacing, (grid_rows-1-row)*spacing, height, 1.0)。注意 Y 轴翻转,让图像第 0 行位于顶部(大 Y),符合图像坐标习惯; - 经
world_from_obj变换到世界空间,再乘frame.projection_from_world得到裁剪坐标; - 颜色映射:把高度归一化到
[0,1](t = (height - min_height) / (max_height - min_height),区间退化时取 0.5),调用colormap_linear(ubo.colormap, t)生成顶点色。
片段着色器三件套:
fs_main:用屏幕空间导数计算法线——normalize(cross(dpdx(position_world), dpdy(position_world))),这样无需相邻顶点访问或额外法线属性,是紧凑网格的省内存技巧;随后用simple_lighting(normal)做光照着色;fs_main_picking_layer:直接输出picking_layer_object_id与picking_instance_id,支撑点击拾取;fs_main_outline_mask:输出 outline mask,支撑选中高亮描边。
注册与集成:把一切装进 Viewer
所有零件在 main.rs 中组装,这是理解集成方式的样板代码。
Renderer 与 Visualizer 的注册
app.with_render_ctx_mut(|render_ctx| { render_ctx.renderers_mut().register::<HeightFieldRenderer>(); })?; app.extend_view_class( re_sdk_types::blueprint::views::Spatial3DView::identifier(), |registrator| { registrator.register_visualizer::<HeightFieldVisualizer>()?; registrator.register_fallback_provider( height_field_archetype::HeightField::descriptor_colormap().component, |_ctx| height_field_visualizer::DEFAULT_COLOR_MAP, ); Ok(()) }, )?;两个关键点:
- Renderer 注册到渲染上下文,之后 Visualizer 的 DrawData 才能解析到该 Renderer;
- Visualizer 通过
extend_view_class注册到内置Spatial3DView(spatial3d_view.rs)——这就是 README 所说"参与既有 3D 场景、与点/网格/Box 等内置可视化器共存"的实现机制。register_fallback_provider为 colormap 组件注册了缺省值(Colormap::Spectral),保证 Viewer 侧 UI 显示与 Visualizer 内部默认值一致。
数据来源:内置 Recording 与 SDK 服务器
main.rs 同时接入了两条数据通道(app.add_log_receiver(...)):
- 内置 Recording:
builtin_recording()用RecordingStreamBuilder::new("rerun_example_custom_visualizer").memory()在内存中生成 60 帧 512×512 的动画地形,覆盖 10×10 米世界空间,高度由多组正弦/余弦叠加产生;smoothness参数从 0 渐变到 1,使地形从"体素化(按 0.25 米量化)"平滑过渡到"完全光滑",直观展示 GPU 网格动态更新的效果。此外还记录了一个绕地形公转的实心 Box(Boxes3D+Transform3D)作为空间参照。内存 recording 落盘后通过re_log_channel::log_channel转发为LogReceiver注入 Viewer; - gRPC 服务器:
re_grpc_server::spawn_with_recv("0.0.0.0:9876", ...)监听 9876 端口,接受 Python / Rust 日志 SDK 的连接——README 中"示例同时启动一个 SDK 服务器"即指此处。
运行环境与全局配置
- 全局分配器:
re_memory::AccountingAllocator<mimalloc::MiMalloc>,让 Rerun 精确统计内存用量、超限时裁剪数据存储,同时获得 mimalloc 的更快速分配; re_log::setup_logging():日志输出到 stderr,用RUST_LOG=debug等环境变量控制级别;re_crash_handler::install_crash_handlers(...):安装 panic/crash 处理器(启用analyticsfeature 时会上报崩溃);AppEnvironment::Custom("My extended Rerun Viewer"):标记应用环境,用于分析统计。
运行与连接 SDK
启动方式非常简单,在仓库根目录执行:
cargo run -p custom_visualizer启动后你将看到一个带有内置动画地形 Recording 的自定义 Rerun Viewer。README 提示还可以让日志 SDK 连接上来实时注入数据——例如启动 minimal_options 示例并指定--connect:
cargo run -p minimal_options -- --connect两个进程会通过 gRPC(端口 9876)建立连接。该模式适用于将自定义 Visualizer 嵌入自己的 Rerun 应用,并允许任意 SDK 客户端动态推送数据。若需关闭崩溃上报,可去掉 Cargo.toml 中的analyticsfeature 再构建。
扩展指引:如何移植这套模式
从本示例可以提炼出移植到自有数据类型的通用路径:
- 定义自己的 Archetype(实现
Archetype),为每个组件声明ComponentDescriptor; - 实现
VisualizerSystem:用visualizer_query_info声明查询契约,在execute里按实体查询并产出 DrawData; - 实现
Renderer+ WGSL:创建 bind group layout 与多条相位管线(颜色/拾取/outline),在add_mesh风格的上传逻辑中填充 GPU 资源; - 在
main.rs中注册 Renderer、用extend_view_class把 Visualizer 挂到目标视图类(如Spatial3DView),并视需要注册 fallback provider; - 通过
add_log_receiver接入内置 recording 或 gRPC/文件等外部数据源。
更完整的框架性说明可参考VisualizerSystem与Renderer两个 trait 的源码注释(visualizer_system.rs、renderer/mod.rs),以及Archetypetrait(archetype.rs)。若要为自定义数据补充交互能力,还可进一步研究 re_renderer 中PickingLayerProcessor、OutlineMaskProcessor等配套设施。
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考