Skip to content

机位即实体:移动与跟拍

片场搭好了,老雷喊出第一个调度:阿燕满场走一个大“8”字,1 号机全程跟住他。

走位是第 12 章的手艺——每帧按时间公式盖写 translation。跟拍是新课题,但想一想相机的身份,答案自己浮出来:相机是实体,实体有 Transform,那么移动镜头就是改相机的 translation,跟移动阿燕没有任何区别。

第一版跟拍最直白:阿燕在哪,镜头中心就在哪。

rust
/// 阿燕的走位:满场跑一个大“8”字
fn walk_hero(time: Res<Time>, mut hero: Single<&mut Transform, With<Hero>>) {
    let t = time.elapsed_secs() * 0.5;
    hero.translation.x = 420.0 * t.sin();
    hero.translation.y = 210.0 * (2.0 * t).sin();
}
/// 1 号机:阿燕在哪,镜头中心就在哪
fn follow_hero(
    mut lens: Single<&mut Transform, (With<Camera2d>, Without<Hero>)>,
    hero: Single<&Transform, With<Hero>>,
) {
    lens.translation.x = hero.translation.x;
    lens.translation.y = hero.translation.y;
    // z 留在原地:2D 相机的 z 不参与构图,但别让它乱跑
}

Listing 13-3:跟拍第一版——逐帧把相机坐标钉在阿燕身上(examples/listing-13-03.rs)

两个系统在 Update 里串成 (walk_hero, follow_hero).chain()——第 6 章讲过的执行顺序在这里是刚需:镜头必须读到本帧走完位的新坐标,否则永远慢一帧。follow_hero 的查询过滤器也值得一看:With<Camera2d>Without<Hero>,跟官方示例一个写法——两个参数一个可变借相机的 Transform、一个只读阿燕的 TransformWithout 向调度器证明两边不会摸到同一个实体(第 4 章的借用功课)。

console
cargo run -p ch13-cameras --example listing-13-03
text
老雷:阿燕走起来,1 号机跟住他!

跑起来你会看见一个有趣的景象:阿燕一动不动地钉在窗口正中央,反倒是灯笼柱、地毯、整个片场在“倒着”滑动。这不是错觉,是跟拍的本质——镜头与主角零相对运动,于是世界反向流动。盯住任何一根灯笼柱看几秒,它划过画面的轨迹正是阿燕走位的镜像。

还有个小细节:follow_hero 只抄 x 和 y,z 留在原地。2D 相机的 z 不参与构图(下一节讲投影时你会看到为什么),但乱改它可能把实体甩出可见范围——第 12 章说过默认相机只见 z 轴 ±1000,那个范围是以相机自己的 z 为基准量的。让相机的 z 待在出厂位置,是省心的习惯。

镜头要像人扛的

老雷看了两秒回放就摇头:太硬了。真人摄影师跟拍是有惯性的——主角突然变向,镜头总要慢半拍才追上来,画面才有“呼吸”。逐帧硬抄坐标,等于把摄影机焊在演员头顶。

数学上,“慢半拍地追”就是插值:镜头每帧不是跳到目标点,而是向目标点靠近一段。第 12 章用过 lerp 取两点中间;但跟拍是每帧连续进行的,直接 lerp(target, 0.1) 的追赶速度会随帧率波动——帧率翻倍,每秒“靠近 10%”就执行了双倍次数。Bevy 为这个场景备了专门的工具:smooth_nudge,帧率无关的平滑逼近,来自 StableInterpolate 这个 trait(与 Vec3 一样从 bevy_math 进了 prelude)。

rust
/// 1 号机:每帧向阿燕“追近一段”,永远追不死——镜头便有了延迟感
fn follow_hero(
    time: Res<Time>,
    mut lens: Single<&mut Transform, (With<Camera2d>, Without<Hero>)>,
    hero: Single<&Transform, With<Hero>>,
) {
    let target = hero.translation.with_z(lens.translation.z);
    lens.translation.smooth_nudge(&target, 2.0, time.delta_secs());
}

Listing 13-4:跟拍第二版——smooth_nudge 给镜头装上“人手”(examples/listing-13-04.rs)

smooth_nudge(&target, decay_rate, delta) 三个参数:目标点、衰减速率、本帧时长。衰减速率越大追得越紧——2.0 是松弛的跟拍手感,8.0 就接近硬跟随了。把 delta_secs() 喂进去之后,无论 30 帧还是 144 帧,镜头的追赶节奏完全一致。targethero.translation.with_z(lens.translation.z) 拼出来:x、y 追阿燕,z 永远是相机自己的——上一段的习惯,用一个方法就守住了。

console
cargo run -p ch13-cameras --example listing-13-04
text
老雷:镜头柔一点——像人扛的,不是钉死的。

这一版里阿燕不再钉死在中心:他疾走时会“甩开”镜头小半个身位,停步转向时镜头又柔柔地荡回来对准他。两版示例只差 follow_hero 的三行实现,运镜的气质天差地别——值得来回切换跑几次,体会 decay_rate 改成 0.510.0 的手感。

并排动图:左侧硬跟随的画面里红色侠客始终钉在正中央,右侧 smooth_nudge 的画面里侠客在中心附近游移,镜头带着延迟追赶

Figure 13-2:两版跟拍同台对比(动图)——左路阿燕焊死在画面中央,右路镜头慢半拍地追,世界的流动也因此柔和

机位会跟人了,但小满那笔旧账还没销:镜头此刻到底拍到了哪一块世界?答案在相机的另一位随从身上——下一节拆 Projection