用 Rerun 的 3D 原语构建实时模拟时钟:Rust 示例逐行拆解
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
本篇技术指南以 examples/rust/clock 示例为主体,讲解如何仅用 Rerun SDK 的Boxes3D、Points3D、Arrows3D三种原语搭建一个带时针、分针、秒针的模拟时钟,并沿 Rerun 时间轴(timeline)实时动画。读完本文,你将掌握log_static与log的用法差异、如何通过set_duration_secs驱动时间轴、如何用RerunArgs控制数据去向,以及为什么这个示例同时是"大量小事件"场景下的绝佳基准测试。
示例概览:一个模拟时钟能教会你什么
该示例位于仓库的 examples/rust/clock,官方 README 用一句话概括了它的本质:"An example visualizing an analog clock with hour, minute and seconds hands using Rerun Arrow3D primitives."(使用 Rerun Arrow3D 原语可视化带时针、分针、秒针的模拟时钟。)
尽管视觉上只是一个时钟,它在教学上覆盖了 Rerun SDK 的三类核心能力:
- 静态场景声明——用
ViewCoordinates声明坐标系、用Boxes3D声明时钟表盘,这些只在时间轴 0 处记录一次,之后不再变化; - 逐帧动态数据——每根指针的端点用
Points3D、指针本身用Arrows3D,每帧(每个时间步)都重新记录; - 时间轴驱动——通过
set_duration_secs把sim_time推进到当前步数对应的秒数,让 Viewer 能以真实时间轴播放动画。
源码注释还点明了它的另一重身份:"This is a great benchmark for many small events."(这是大量小事件场景的绝佳基准。)因为每一帧都会产生 6 条 log 记录(3 根指针 × 2 条:端点 + 箭头),默认跑 10000 步就是约 6 万个小数据事件,非常适合压测 SDK 的日志管线与 Viewer 的更新性能。
快速运行:一条命令启动
示例的依赖与构建方式非常轻量,README 给出的启动命令只有一行:
cargo run --release在仓库根目录执行后,程序会编译clockcrate,启动一个新的 Rerun Viewer 进程并把时钟数据实时送入其中。也可以通过 cargo 的-p参数在任意目录下精确定位到该 crate(源码头部注释同样给出了这条用法):
cargo run -p clock --release -- --help--help会打印出该程序支持的完整命令行参数,具体清单见下文"命令行参数"一节。
依赖构成
examples/rust/clock/Cargo.toml 揭示了示例的依赖面,非常精简:
[dependencies] rerun = { path = "../../../crates/top/rerun", features = ["web_viewer", "clap", "log_setup"] } anyhow.workspace = true clap = { workspace = true, features = ["derive"] }值得注意的几点:
rerun走 path 依赖,直接指向仓库内的 crates/top/rerun,因此必须在本仓库内编译运行;- 启用了三个 feature:
web_viewer(支持--serve启动网页版 Viewer)、clap(复用 SDK 内置的RerunArgs命令行参数解析)、log_setup(提供re_log::setup_logging()初始化日志); edition = "2024"、rust-version = "1.96",对 Rust 工具链版本有要求;- 主程序使用
clapderive 宏来定义参数结构体。
程序入口与参数结构
src/main.rs 的入口部分展示了一个典型的 Rerun Rust 应用骨架:
#[derive(Debug, clap::Parser)] #[clap(author, version, about)] struct Args { #[command(flatten)] rerun: rerun::clap::RerunArgs, #[clap(long, default_value = "10000")] steps: usize, } fn main() -> anyhow::Result<()> { re_log::setup_logging(); use clap::Parser as _; let args = Args::parse(); let (rec, _serve_guard) = args.rerun.init("rerun_example_clock")?; run(&rec, &args) }其中args.rerun.init("rerun_example_clock")是核心一行:它以rerun_example_clock作为 Application ID 创建 RecordingStream,并根据命令行参数决定数据流向。init返回的_serve_guard在 web 模式下需要存活才能持续提供网页服务,因此用_前缀接收并保活到 main 结束。
命令行参数:RerunArgs 全解析
rerun::clap::RerunArgs定义于 crates/top/rerun/src/clap.rs,它是一组开箱即用的 CLI 参数,任何 Rerun 应用都能通过#[command(flatten)]直接复用。结合 clap.rs 的源码注释,参数清单如下:
| 参数 | 默认值 | 作用 |
|---|---|---|
--spawn | true | 启动一个新的 Rerun Viewer 进程并实时喂入数据 |
--save <PATH> | 无 | 将数据保存为.rrd文件而非立即可视化 |
-o, --stdout | false | 把日志数据写到标准输出,便于管道传给 Rerun Viewer |
--connect [URL] | rerun+http://127.0.0.1:9876/proxy | 连接远程 Rerun viewer,scheme 必须是rerun://、rerun+http://或rerun+https://,路径名必须是/proxy |
--serve | false | 启动基于网页的 Rerun Viewer(需要web_viewerfeature) |
--server-memory-limit <LIMIT> | 25% | gRPC 服务器内存上限,如16GB或50%;达到上限后丢弃最旧数据,用于缓冲以便迟到的 viewer 补看 |
--newest-first | false | 新客户端连接时优先回放最新数据 |
--bind <IP> | 0.0.0.0 | gRPC 服务器绑定的 IP 地址 |
示例自身额外定义了--steps参数,默认10000,控制模拟时钟总共走的步数(帧数)。Args结构体还透传了 clap 自带的--help、--version等能力。
静态场景:坐标系与表盘
时钟的"表盘"是静态的,在整个动画过程中只记录一次,因此使用log_static——它把数据关联到时间轴的初始状态(时间 0),与"随时间变化"的动态数据区分开:
rec.log_static("world", &rerun::ViewCoordinates::RIGHT_HAND_Y_UP())?; rec.log_static( "world/frame", &rerun::Boxes3D::from_half_sizes([(LENGTH_S, LENGTH_S, 1.0)]), )?;ViewCoordinates:声明坐标系统
ViewCoordinates::RIGHT_HAND_Y_UP()声明场景采用右手坐标系、Y 轴朝上——这是 3D 可视化的惯例约定。通过把该组件 log 到world实体,Viewer 会据此确定默认相机姿态与坐标系语义,避免出现"表盘躺在地上"之类的视角错位。
Boxes3D:用"半尺寸"定义方形表盘
Boxes3D::from_half_sizes([(LENGTH_S, LENGTH_S, 1.0)])创建了一个三维盒子,LENGTH_S = 20.0,因此表盘是一个40.0 × 40.0 × 2.0的薄板。Rerun 的 3D 盒子原语使用**半尺寸(half-sizes)**定义,即每个维度只给出一半长度,盒子中心位于实体原点(0, 0, 0)。
从源码结构看,log_static的"static"语义与log的关键区别在于:static 数据只与实体路径绑定,不受时间步影响;而后续指针的每一帧数据都属于某个具体时间点。两者可以共存于同一实体层级(这里是world下),由 Rerun 的时间轴机制统一组织。
动态指针:Points3D + Arrows3D 的组合
三根指针由统一的log_hand函数生成,每一帧被调用 3 次(秒针、分针、时针):
fn log_hand( rec: &rerun::RecordingStream, name: &str, step: usize, angle: f32, length: f32, width: f32, blue: u8, ) -> anyhow::Result<()> { let pos = tip(angle * TAU, length); let color = color(angle, blue); rec.set_duration_secs("sim_time", step as f64); rec.log( format!("world/{name}_pt"), &rerun::Points3D::new([pos]).with_colors([color]), )?; rec.log( format!("world/{name}_hand"), &rerun::Arrows3D::from_vectors([pos]) .with_origins([(0.0, 0.0, 0.0)]) .with_colors([color]) .with_radii([width * 0.5]), )?; Ok(()) }指针尖端的坐标计算
指针端点的位置由辅助函数tip计算:
fn tip(angle: f32, length: f32) -> [f32; 3] { [length * angle.sin(), length * angle.cos(), 0.0] }传入的angle是归一化比例(0.0 ~ 1.0),乘以TAU(即2π)后得到弧度角;指针长度作为半径,把端点投影到 XZ 平面(Z 恒为 0,即表盘平面)。各指针在 main.rs 中的角度由步数取模得出:
for step in 0..args.steps { log_hand(rec, "seconds", step, (step % 60) as f32 / 60.0, LENGTH_S, WIDTH_S, 0)?; log_hand(rec, "minutes", step, (step % 3600) as f32 / 3600.0, LENGTH_M, WIDTH_M, 128)?; log_hand(rec, "hours", step, (step % 43200) as f32 / 43200.0, LENGTH_H, WIDTH_H, 255)?; }- 秒针:步数对 60 取模 → 60 步走完一圈,长度
LENGTH_S = 20.0(表盘半径),宽度WIDTH_S = 0.25; - 分针:步数对 3600 取模 → 3600 步一圈(对应 60 秒 × 60 帧/秒),长度
10.0,宽度0.4; - 时针:步数对 43200 取模 → 43200 步一圈(12 小时 × 3600),长度
4.0,宽度0.6。
长度与宽度常量定义在 main.rs,三根指针呈明显的内外层次,保证在表盘上互不遮挡、视觉可区分。
指针的两种呈现:点与箭头
每根指针同时记录了两个实体:
world/{name}_pt:用Points3D::new([pos]).with_colors([color])在指针尖端画一个彩色点,充当"指针端点"标记;world/{name}_hand:用Arrows3D::from_vectors([pos])从原点(0, 0, 0)指向pos画一根箭头,with_radii([width * 0.5])把箭头半径设为指针宽度的一半,视觉上即指针杆。
动态配色的实现
color辅助函数让指针颜色随时间(角度)平滑变化:
fn color(angle: f32, blue: u8) -> rerun::Color { let c = (angle * 255.0) as u8; rerun::Color::from_unmultiplied_rgba(255 - c, c, blue, u8::max(128, blue)) }- R、G 通道随角度在
255→0与0→255之间渐变,形成冷暖色过渡; - B 通道由调用方传入(秒针
0、分针128、时针255),用于区分三根指针; - Alpha 取
max(128, blue),保证半透明可见。
时间轴驱动:set_duration_secs 让动画流动
整个动画的灵魂在于 main.rs 这一行:
rec.set_duration_secs("sim_time", step as f64);它做了两件事:
- 声明/推进一个名为
sim_time的时间轴(timeline); - 把该时间轴的当前值设置为
step秒。
之后的rec.log(...)调用会把数据自动关联到sim_time = step这一时刻。于是 Viewer 中会出现一条可拖拽/可播放的时间轴,步进即可看到秒针绕表盘旋转;Rerun 还会自动为sim_time推断出单调递增的时序,支持连续播放与逐帧跳转。
这正是 Rerun 数据模型的核心思想:每个实体 + 每条时间轴 = 一组按时间索引的组件序列。静态数据用log_static(挂在时间 0),动态数据用log+ 时间轴推进,二者互不干扰。
作为基准测试的价值:大量小事件场景
源码头部注释明确写道:"This is a great benchmark for many small events."从数据结构看,这一点不难理解:
- 默认
steps = 10000,每步产生6 条 log(秒/分/时针 × 端点 + 箭头); - 总计约 6 万个
Points3D/Arrows3D小数据事件在短时间内灌入 SDK 的日志管线; - 这些事件体积极小(单点、单向量、单颜色),考验的是 Rerun 在海量小消息下的编码、缓冲、传输与 Viewer 端更新吞吐,而非大数据块的写入性能。
因此你可以用--steps调节事件总量,配合--spawn / --save / --connect / --serve等数据通道,快速实测不同环节的吞吐差异。例如:
# 1 万步直接可视化 cargo run --release # 100 万步,把数据存成 rrd 文件(验证大批量落盘性能) cargo run -p clock --release -- --steps 1000000 --save clock.rrd # 连接远程 viewer(如局域网内的另一台机器) cargo run -p clock --release -- --connect rerun+http://192.168.1.100:9876/proxy小结
通过 examples/rust/clock 这个不到 100 行的示例,可以完整走通 Rerun Rust SDK 的核心工作流:
RerunArgs::init(application_id)一键完成 RecordingStream 初始化,并根据--spawn/--save/--stdout/--connect/--serve等参数自动决定数据去向;log_static记录不随时间变化的场景(坐标系、表盘);log+set_duration_secs把逐帧动态数据挂到sim_time时间轴上,形成可播放动画;Boxes3D/Points3D/Arrows3D三种原语足以组合出具备真实空间语义的 3D 场景。
它是理解 Rerun"实体-组件-时间轴"数据模型的最小完整样例,也是验证大量小事件吞吐的现成基准。以此为起点,可以继续研读 examples/rust 下其他示例(如 dna、log_file),掌握更多原语与交互模式。
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考