Skip to content

Gizmos:保留模式与 ConfigGroup

即时模式 Gizmos 每帧重新提交——对于跟踪物体位置、实时可视化状态这类"每帧都变"的场景,这是最自然的方式。但如果你要画的东西基本不动(比如一个固定的参考网格、一个静态的包围圈),每帧重新提交就是浪费。

Bevy 的解决方案是保留模式(retained mode):把 Gizmo 数据存进 GizmoAsset,挂到实体上,引擎只在数据变化时重新上传。

2D 与 3D 图元一览

先把上一节的"分野"落地——Listing 27-2 在同一个窗口里同时展示 2D 和 3D Gizmos:

rust
gizmos.line_2d(Vec2::new(-200., 0.), Vec2::new(200., 0.), Color::WHITE);
    gizmos.rect_2d(Isometry2d::IDENTITY, Vec2::splat(160.), Color::srgb(0.9, 0.3, 0.3));
    gizmos.circle_2d(Isometry2d::IDENTITY, 100., Color::srgb(0.2, 0.7, 0.4));
    gizmos.arrow_2d(
        Vec2::ZERO,
        Vec2::from_angle(t) * 80.,
        Color::srgb(0.3, 0.5, 0.9),
    );

Listing 27-2(节选):2D 图元——线、矩形、圆、箭头

2D 方法接受 Vec2Isometry2d(位置 + 旋转),绘制在 2D 相机的平面上。

rust
gizmos.rect(
        Isometry3d::new(Vec3::new(-2., 1., 0.), Quat::from_rotation_y(PI / 4.)),
        Vec2::splat(1.5),
        Color::srgb(0.9, 0.6, 0.2),
    );
    gizmos.cube(
        Transform::from_translation(Vec3::new(0., 0.75, 0.)).with_scale(Vec3::splat(1.5)),
        Color::srgb(0.2, 0.5, 0.9),
    );
    gizmos
        .circle(Quat::from_rotation_x(PI / 2.), 2., Color::srgb(0.8, 0.3, 0.8))
        .resolution(64);
    gizmos
        .arc_3d(PI, 1.2, Isometry3d::new(Vec3::new(2., 1., 0.), Quat::IDENTITY), Color::srgb(0.3, 0.8, 0.8))
        .resolution(32);

Listing 27-2(节选):3D 图元——矩形、立方体、圆、弧

rect 需要一个 Isometry3d(位置 + 朝向)和尺寸;cube 接受 Transform(可以用缩放变成长方体);circle 的第一个参数是旋转——Quat::from_rotation_x(PI / 2.) 让圆平躺在 XZ 平面上;arc_3d 画圆弧,参数依次是弧度、半径、位姿和颜色。

运行:

console
cargo run -p ch27-devtools --example listing-27-02

2D 相机(order 0)和 3D 相机(order 1)同时生效。2D 图元画在底层,3D 场景叠在上面。

GizmoConfigGroup:分组控制

当你在同一个场景里有多组 Gizmos(比如"碰撞体可视化"和"路径可视化"),你可能想独立控制它们的线宽、颜色、开关。GizmoConfigGroup 就是为此设计的。

定义一个自定义组只需要三行:

rust
#[derive(Default, Reflect, GizmoConfigGroup)]
struct MyDebugGizmos;

然后在 App 里注册:

rust
app.init_gizmo_group::<MyDebugGizmos>();

使用时,把 Gizmos 的泛型参数换成你的组:

rust
fn draw_gizmos(
    mut default_gizmos: Gizmos,
    mut my_gizmos: Gizmos<MyDebugGizmos>,
    // ...
) {
    default_gizmos.cross(pos, 0.4, RED);   // 默认组
    my_gizmos.sphere(pos, 0.6, RED);       // 自定义组
}

运行时可以通过 GizmoConfigStore 资源动态修改每组的配置:

rust
fn toggle_config(mut config_store: ResMut<GizmoConfigStore>) {
    let (config, _) = config_store.config_mut::<MyDebugGizmos>();
    config.enabled = !config.enabled;
    config.line.width = 5.0;
}

GizmoConfig 的字段包括:

字段类型默认值说明
enabledbooltrue总开关
line.widthf322.0线宽(像素)
line.perspectiveboolfalse线宽是否随透视缩放
line.styleGizmoLineStyleSolid实线 / 虚线 / 点线
line.jointsGizmoLineJointNone线段连接样式
depth_biasf320.0深度偏移,负值让 Gizmo 画在所有物体前面

保留模式:GizmoAsset 与 Gizmo 组件

即时模式的 Gizmos 系统参数适合动态数据;保留模式的 GizmoAsset + Gizmo 组件适合静态或低频更新的数据:

rust
let mut gizmo = GizmoAsset::new();
    for i in 0..36 {
        let angle = i as f32 * 10_f32.to_radians();
        let (sin, cos) = ops::sin_cos(angle);
        gizmo.line(
            Vec3::new(cos, 0.5, sin),
            Vec3::new(cos * 1.3, 0.5, sin * 1.3),
            YELLOW,
        );
    }

    commands.spawn((
        Gizmo {
            handle: gizmo_assets.add(gizmo),
            line_config: GizmoLineConfig {
                width: 3.,
                ..default()
            },
            ..default()
        },
        Transform::from_xyz(-2., 0., 0.),
    ));

GizmoAsset 是一个资产,Gizmo 组件把它挂到实体上。Gizmo 组件的 handle 指向资产,line_config 控制这组 Gizmos 的线宽等属性。实体的 Transform 控制 Gizmo 在世界中的位置。

性能差异的核心:即时模式每帧重建命令缓冲区,保留模式只在资产内容变化时重新上传 GPU 数据。画 30000 条静态线时差距明显;画几条每帧都动的线,即时模式反而更简单高效。

完整示例

rust
use bevy::color::palettes::css::*;
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_gizmo_group::<MyDebugGizmos>()
        .add_systems(Startup, setup)
        .add_systems(Update, draw_gizmos)
        .run();
}

// ANCHOR: group
#[derive(Default, Reflect, GizmoConfigGroup)]
struct MyDebugGizmos;
// ANCHOR_END: group

fn setup(
    mut commands: Commands,
    mut gizmo_assets: ResMut<Assets<GizmoAsset>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // ANCHOR: retained
    let mut gizmo = GizmoAsset::new();
    for i in 0..36 {
        let angle = i as f32 * 10_f32.to_radians();
        let (sin, cos) = ops::sin_cos(angle);
        gizmo.line(
            Vec3::new(cos, 0.5, sin),
            Vec3::new(cos * 1.3, 0.5, sin * 1.3),
            YELLOW,
        );
    }

    commands.spawn((
        Gizmo {
            handle: gizmo_assets.add(gizmo),
            line_config: GizmoLineConfig {
                width: 3.,
                ..default()
            },
            ..default()
        },
        Transform::from_xyz(-2., 0., 0.),
    ));
    // ANCHOR_END: retained

    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0., 3., 6.).looking_at(Vec3::ZERO, Vec3::Y),
    ));
    commands.spawn((
        PointLight::default(),
        Transform::from_xyz(4.0, 8.0, 4.0),
    ));
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::new(1., 1., 1.))),
        MeshMaterial3d(materials.add(Color::srgb(0.6, 0.6, 0.6))),
        Transform::from_xyz(0., 0.5, 0.),
    ));
}

// ANCHOR: immediate_vs_retained
fn draw_gizmos(
    mut default_gizmos: Gizmos,
    mut my_gizmos: Gizmos<MyDebugGizmos>,
    time: Res<Time>,
) {
    let t = time.elapsed_secs();
    let pos = Vec3::new(ops::cos(t) * 2., 0.5, ops::sin(t) * 2.);

    // 默认组:跟踪物体位置的十字标
    default_gizmos.cross(pos, 0.4, RED);

    // 自定义组:红色包围球
    my_gizmos.sphere(pos, 0.6, RED);
}
// ANCHOR_END: immediate_vs_retained

Listing 27-3:ConfigGroup 分组与保留模式 GizmoAsset

运行:

console
cargo run -p ch27-devtools --example listing-27-03

你会看到一个灰白色立方体、一个由 36 条放射线组成的静态"太阳"(保留模式),以及一个围绕立方体旋转的十字标和红色包围球(即时模式,分属两个 ConfigGroup)。