Skip to content

沙盘与防穿帮:RenderLayers

导播台开张当天,老雷又添了两个要求。一:墙上挂一块沙盘——全片场的鸟瞰小图,谁在哪一目了然;二:地面上贴的粉色马克点(演员的走位记号)必须让监视器看得见、正片里绝不能穿帮

第一个要求不需要任何新知识。沙盘 = 第三台相机:order 排最大(叠在所有画面之上),视口缩成右上角一小块,投影锁 ScalingMode::Fixed 把整个片场框进取景框——上一节的三样手艺拼一拼就成。

第二个要求卡住了。马克点是 Sprite,正片机位和沙盘机位都看第 0 层世界……等等,“层”是什么?到目前为止,所有相机看见所有实体是默认事实。要打破它,得请出本章最后一个组件。

谁拍得到谁:RenderLayers

RenderLayers(渲染层)把“谁能拍到谁”变成一道集合题:实体挂一份层号集合,相机也挂一份,两边有交集,实体才会出现在这台相机的画面里。规则只有三条:

  • 不挂 RenderLayers 的实体(和相机)默认属于第 0 层——这就是“所有相机看见所有实体”的真相:大家挤在同一层;
  • 挂了组件就以组件为准:RenderLayers::layer(1) 只属第 1 层,RenderLayers::from_layers(&[0, 1]) 横跨两层,还有 .with(n)/.without(n) 增删层号;
  • 空集合 RenderLayers::none() 与任何集合都不相交——实体直接隐形,连自己人都拍不到。

它不在 prelude 里,得显式引一行 use bevy::camera::visibility::RenderLayers;

方案于是清晰:马克点搬进第 1 层(剧组工作层),正片机位不挂组件(守第 0 层,看不见工作层),沙盘机挂 from_layers(&[0, 1])(两层通吃)。顺手再加一样道具:演员在沙盘里只有几个像素大,给阿燕配一个 72×72 的“图例”大色块,也放第 1 层——沙盘上醒目,正片里无踪。

rust
// 主机位:什么都不挂,默认只看第 0 层
    commands.spawn((
        Camera2d,
        MainLens,
        Projection::Orthographic(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 600.0,
            },
            ..OrthographicProjection::default_2d()
        }),
    ));
    // 沙盘机:order 靠后(叠在主画面上方),看第 0 层和工作层
    commands.spawn((
        Camera2d,
        MinimapLens,
        Camera {
            order: 1,
            clear_color: ClearColorConfig::Custom(Color::srgb(0.09, 0.08, 0.07)),
            ..default()
        },
        Projection::Orthographic(OrthographicProjection {
            // 锁定取景 1200×900 世界单位:整个片场一览无余
            scaling_mode: ScalingMode::Fixed {
                width: 1200.0,
                height: 900.0,
            },
            ..OrthographicProjection::default_2d()
        }),
        RenderLayers::from_layers(&[0, CREW_LAYER]),
    ));

Listing 13-10(节选一):主机位守第 0 层,沙盘机两层通吃(examples/listing-13-10.rs)

沙盘机的 clear_color 用上了第 1 节预告的 ClearColorConfig::Custom:自带一块比夜空更暗的底色,小图四周天然显出一圈“画框”。要是改成 ClearColorConfig::None,沙盘就不擦底、直接把灯笼柱和图例叠画在正片画面上——半透明 HUD 的味道,两种都值得跑一跑。

rust
// 马克点:阿燕的走位记号,只许工作层看见
    for (x, y) in [(0.0, 0.0), (420.0, 0.0), (-420.0, 0.0), (297.0, 210.0), (-297.0, -210.0)] {
        commands.spawn((
            Sprite::from_color(Color::srgb(1.0, 0.3, 0.7), Vec2::splat(16.0)),
            Transform::from_xyz(x, y, -4.0),
            RenderLayers::layer(CREW_LAYER),
        ));
    }

    // 阿燕本体在第 0 层;头顶的沙盘图例在工作层——RenderLayers 不沿层级继承,得自己挂
    commands.spawn((
        Hero,
        Sprite::from_color(Color::srgb(0.85, 0.2, 0.2), Vec2::splat(30.0)),
        Transform::from_xyz(0.0, 0.0, 0.0),
        children![(
            Sprite::from_color(Color::srgb(1.0, 0.4, 0.4), Vec2::splat(72.0)),
            Transform::from_xyz(0.0, 0.0, 5.0),
            RenderLayers::layer(CREW_LAYER),
        )],
    ));

Listing 13-10(节选二):马克点与图例住进工作层(examples/listing-13-10.rs)

图例的挂法藏着一个容易栽跟头的细节:它是阿燕的子实体(第 9 章的 children!,跟着本体走位),但 RenderLayers 不沿层级继承——爹在第 0 层、儿子挂哪层是儿子自己的事,一个实体一份组件,谁也不替谁作主。忘了给子实体单独挂层,它就默默留在第 0 层,正片穿帮没商量。

rust
/// 沙盘停靠右上角:窗口一变就重新量一次
fn dock_minimap(
    windows: Query<&Window>,
    mut resized: MessageReader<WindowResized>,
    mut minimap: Single<&mut Camera, With<MinimapLens>>,
) {
    for message in resized.read() {
        let Ok(window) = windows.get(message.window) else {
            continue;
        };
        // 沙盘宽占窗口四分之一,比例锁 4:3,与取景范围 1200×900 一致
        let width = window.physical_width() / 4;
        let size = UVec2::new(width, width * 3 / 4);
        minimap.viewport = Some(Viewport {
            // 视口坐标原点在窗口左上角,y 朝下
            physical_position: UVec2::new(window.physical_width() - size.x - 16, 16),
            physical_size: size,
            ..default()
        });
    }
}

Listing 13-10(节选三):沙盘停靠右上角,窗口一变重新量(examples/listing-13-10.rs)

停靠的算术全在视口坐标系里做:原点左上、y 朝下,所以“右上角、留 16 物理像素边距”是 x = 窗口宽 − 沙盘宽 − 16y = 16。沙盘宽取窗口的四分之一,高按 4:3 配——和取景框 1200×900 同比例,全场入画不变形。

console
cargo run -p ch13-cameras --example listing-13-10
text
老雷:右上角挂沙盘。马克点只许监视器看见,正片不许穿帮!

跑起来逐项验收:主画面里镜头照常跟拍阿燕——没有粉色马克点,没有大图例,正片干干净净;右上角的沙盘里,十四根灯笼柱排成两行小黄点,五个粉色马克点原地待命,阿燕顶着醒目的大红图例画着“8”字。同一个世界,两台相机,各拍各的真相。

主画面跟拍红色侠客,画面里只有地毯和灯笼柱、没有任何粉色记号;右上角的沙盘小图里却清楚显示着五个粉色马克点和侠客的大号图例

Figure 13-6:同一个世界,两种真相——马克点与图例只存在于沙盘的画面里

层号是有限资源。层号从 0 起步,背后是按位存储的集合——常用范围内(默认 64 层)开销极小。游戏里通常三五层就够用:世界层、HUD 层、小地图专属层、调试层,提前规划比随手乱开有序得多。

分层取景到手,2D 的运镜手艺全齐了。临杀青前还剩一块拼图——老雷下周要拍的特效镜头是三维的,下一节去特效棚见见 Camera3d 和它的透视投影。