UE5.8 摄像机混合与镜头过渡

📑 目录

UE5.8 摄像机混合与镜头过渡

引子:Boss 战前黑屏的两秒钟

林涛的项目《苍鹰》做到第三关时,美术组提交了一段过场动画。内容很标准:玩家角色穿过废墟,镜头缓缓升起,Boss 从烟雾里现身,随后切回战斗镜头。策划的要求只有一句:要让玩家感觉不到游戏和动画的分界。

集成那天出了点问题。动画播放完,画面直接黑了两秒,然后才切回玩家跟随视角。测试组截了一段视频,帧率正常,没有崩溃,就是中间那段黑屏让人以为机器卡住了。

林涛查了一下午,发现问题根本不在 Sequencer,也不在渲染管线。战斗摄像机是一个带弹簧臂的跟随镜头,过场摄像机是 Sequencer 里的 CineCameraActor。动画结束时,系统直接把 ViewTarget 切回了玩家控制器,但两个摄像机的位置和朝向差距太大,PlayerCameraManager 在找不到混合参数的情况下,默认执行了一次硬切,中间的几帧被后处理体积里的暗角效果压成了黑色。

那次事故之后,林涛把 UE5.8 里所有跟摄像机混合相关的机制重新过了一遍。这篇文章就是那次整理的笔记。

战斗镜头剧情镜头Blend 曲线一次完整的镜头过渡

SetViewTargetWithBlend 到底做了什么

在 Unreal Engine 里,玩家看到的画面不是由某个 CameraComponent 直接决定的。PlayerCameraManager 会维护一个 ViewTarget,真正的最终画面由这个 ViewTarget 上的摄像机参数和 CameraManager 自己的后处理叠加而成。

切换镜头最常见的方法就是 PlayerController 上的 SetViewTargetWithBlend

void AMyPlayerController::EnterCinematic(ACineCameraActor* CineCam)
{
    if (CineCam)
    {
        SetViewTargetWithBlend(
            CineCam,
            2.0f,
            VTBlend_Cubic,
            0.0f,
            true
        );
    }
}

这个函数有五个参数:目标 Actor、混合时长、混合函数、混合指数、是否锁定 ViewTarget 方向。前三个参数基本决定了过渡效果,后两个参数在剧情镜头里经常用到。

第一个参数不一定是 CameraActor。任何 Actor 都可以作为 ViewTarget,因为 PlayerCameraManager 会尝试从目标上找到 CameraComponent。如果找不到,它会把目标 Actor 的位置当作视点。所以有些项目会把一个空 Actor 放到场景里作为过渡锚点。

第二个参数是混合时间,单位秒。填 0 就是硬切。很多玩家诟病的镜头抖动,其实是因为策划反复要求把混合时间从 0.3 改成 0.05,结果 every micro-transition 都变成了机械平移。这个值需要和角色移动速度、场景尺度一起调。

第三个参数是混合曲线类型。UE5.8 里提供了 Linear、Cubic、EaseIn、EaseOut、EaseInOut 等选项。不同类型的曲线会让同一时段内的位移、旋转、FOV 变化产生完全不同的观感。

第四个参数是 BlendExp,只对部分曲线生效。它的作用相当于给曲线加一个指数,让过渡开头或结尾更锐利。

第五个参数 bLockOutgoing 比较容易被忽略。如果设为 true,PlayerCameraManager 会锁定旧 ViewTarget 的旋转,直到混合结束。剧情动画结束时切回战斗镜头,把这个参数打开可以避免弹簧臂在过渡期间被玩家输入带偏。

ViewTarget 的生命周期

ViewTarget 不是随便一个指针。PlayerCameraManager 内部会维护当前 ViewTarget 和 PendingViewTarget。一次混合开始时,旧目标不会被立刻销毁,而是作为 OutgoingViewTarget 保留到混合结束。这样做的好处是即使新目标在接下来的几帧里被销毁,摄像机仍然能回到一个有效状态。

architecture-beta
    group api(game_engine)[UE5.8 Camera System]
        service pc(PlayerController) in api
        service pcm(PlayerCameraManager) in api
        service vt(ViewTarget Actor) in api
        service cc(CameraComponent) in api
        service sq(Sequencer) in api
        service cr(CameraRigRail) in api

        pc:pcm -- calls and owns
        pcm:vt -- tracks current/pending
        vt:cc -- provides transform and FOV
        sq:vt -- drives camera cuts
        cr:cc -- moves camera along path

混合期间每一帧都会发生几件事。PlayerCameraManager 先调用 UpdateViewTarget,根据当前 ViewTarget 和 PendingViewTarget 计算插值权重。然后它会分别读取两个目标的摄像机参数:位置、旋转、FOV、景深、曝光、后处理等。最后按照混合曲线把两个状态合并成最终画面。

混合权重通常记作 α\alpha,取值在 [0,1][0,1] 之间。当前时间为 τ\tau,总混合时间为 TblendT_{blend},那么归一化时间就是:

t=τTblendt = \frac{\tau}{T_{blend}}

不同类型的曲线会把 tt 映射成不同的 α\alpha。Linear 直接就是 α=t\alpha=t。Cubic 在两端减速:

α(t)=3t22t3\alpha(t) = 3t^2 - 2t^3

EaseIn 让开头慢、结尾快,EaseOut 反过来,EaseInOut 则是两端都慢。 cinematic 镜头通常用 EaseInOut,因为它最接近人眼扫视时的加减速习惯。

混合结束时,PlayerCameraManager 会调用 OnBlendComplete。如果有监听事件绑定在这里,可以安全地恢复玩家输入、重新显示 HUD 或者触发下一段剧情。不要把它和 Sequencer 的 Finished 事件混为一谈,后者只表示 Sequence 播放完毕,不一定表示摄像机混合结束。

Blend 曲线与 Ease In/Out

同样的两个摄像机,用不同的曲线混合,观感差别很大。Linear 适合短平快的功能性切换,比如玩家换弹时镜头轻微后撤。Cinematic 切换则几乎总是用 EaseInOut,让玩家感觉不到速度突变。

实际项目中经常需要同时混合多个参数:位置、旋转、FOV、对焦距离。这些参数不一定共用同一条曲线。比如位置用 EaseInOut,FOV 用 Linear,后处理权重再用一条自定义曲线。UE5.8 的 PlayerCameraManager 默认对所有参数使用同一条曲线,但你可以通过继承 APlayerCameraManager 并重写 BlendViewTargets 来实现参数级控制。

下面是几种常用曲线的归一化表达式:

αEaseIn(t)=t2\alpha_{\text{EaseIn}}(t) = t^2

αEaseOut(t)=2tt2\alpha_{\text{EaseOut}}(t) = 2t - t^2

αEaseInOut(t)=t2(32t)\alpha_{\text{EaseInOut}}(t) = t^2(3 - 2t)

αSmoothStep(t)=t2kt2k+(1t)2k\alpha_{\text{SmoothStep}}(t) = \frac{t^{2k}}{t^{2k} + (1 - t)^{2k}}

其中 kk 是 Sharpness 系数,kk 越大,曲线中间越陡,两端平台期越长。这个公式虽然不在引擎默认枚举里,但 Custom Blend Curve 本质上就是这个思路。

如果你用了 Custom Blend Curve,一定要记得在 Curve Asset 里把首尾两个关键帧的时间分别钉在 0 和 1,值钉在 0 和 1。否则混合会在中间卡住,或者提前到达终点。林涛就踩过这个坑:一条自定义曲线开头不是 0,结果每次切镜头都会先闪一下旧画面。

graph LR
    A[战斗镜头参数] -->|Linear| B[均匀过渡]
    A -->|EaseIn| C[起始缓慢]
    A -->|EaseOut| D[结尾缓慢]
    A -->|EaseInOut| E[两端缓慢]
    A -->|Custom Curve| F[按需求塑形]
    B --> G[最终画面]
    C --> G
    D --> G
    E --> G
    F --> G

Camera Rig 与轨道相机

剧情镜头里很少把摄像机直接绑在角色上。更多时候会用到 Camera Rig Rail 或 Camera Rig Crane。轨道相机让摄像机沿一条样条移动,Crane 则模拟摇臂效果。两者都支持在 Sequencer 里打关键帧,也支持运行时由蓝图驱动。

Camera Rig Rail 的核心组件是 USplineComponent。摄像机会沿着样条的某个坐标移动,这个坐标可以通过 CurrentPositionOnRail 控制。剧情里常见的推进镜头就是让这个值从 0 走到 1,同时让摄像机的 LookAt 目标保持指向角色。

林涛的项目里有一个解谜关卡,玩家站在祭坛中央,四周的符文依次点亮。每个符文点亮时,镜头要沿着环形轨道滑到对应角度。实现方案是在关卡里放一条环形 Camera Rig Rail,上面挂一个 CineCameraActor。每次事件触发时,不移动摄像机本身,而是改变 CurrentPositionOnRail。配合 SetViewTargetWithBlend,过渡非常平滑。

flowchart LR
    subgraph 轨道相机系统
        Rail[Camera Rig Rail]
        Spline[USplineComponent]
        Cam[CineCameraActor]
    end
    Event[符文点亮事件] -->|设置 CurrentPositionOnRail| Rail
    Rail --> Spline
    Spline -->|更新 transform| Cam
    Cam -->|SetViewTargetWithBlend| PCM[PlayerCameraManager]

Crane 的用法和 Rail 类似,但它有两个主要参数:Crane Pitch 和 Crane Yaw。Pitch 控制俯仰,Yaw 控制水平旋转。运行时驱动 Crane 需要小心万向节死锁问题,尤其是在 Pitch 接近 90 度时。如果剧情需要垂直俯拍,最好直接把摄像机放到 Crane 顶端的 Socket 上,而不是依赖 Crane 自身的旋转。

Sequencer 中的相机切换

Sequencer 处理相机切换主要依赖 Camera Cuts 轨道。这个轨道上的每一段都绑定一个 CameraActor 或 Possessable。播放时,Sequencer 会通过 SetViewTargetWithBlend 把当前相机设为 ViewTarget。轨道段之间的过渡可以在轨道上设置 Blend Time。

UE5.8 里一个容易忽略的细节是:Sequencer 的 Blend Time 和 PlayerCameraManager 的 Blend Time 是两套东西。Sequencer 在轨道段之间切镜时,会先读 Sequencer 自己的 Blend Time 设置;如果它找不到,才会退回到 PlayerCameraManager 的默认设置。结果经常是动画师在 Sequencer 里调好了混合时间,到了游戏里发现又被 PlayerCameraManager 覆盖了。

解决方案是在项目设置里统一 Sequencer 的默认 Camera Blend Time,或者在 Level Sequence Actor 的 Details 面板里勾选 Override Instance Data,为每个实例单独指定混合参数。

Sequencer 还有一种更暴力的做法:不依赖 Camera Cuts 轨道,直接在轨道上写 CameraComponent 的 Transform。这样做的好处是镜头之间没有 ViewTarget 切换,完全由 Sequencer 自己插值。坏处是混合曲线只能由关键帧切线控制,无法复用 PlayerCameraManager 的 BlendFunc。

《苍鹰》里的 Boss 登场动画用的就是 Camera Cuts 轨道。前 5 秒是一个环绕轨道镜头,第 5 秒切到一个特写,第 8 秒再切回宽镜。林涛把每个切点都设置了 0.6 秒的 EaseInOut 混合,完全覆盖了 PlayerCameraManager 的默认值,才消除了那段黑屏。

sequenceDiagram
    participant S as Sequencer
    participant PCM as PlayerCameraManager
    participant VT as ViewTarget
    participant P as PlayerController

    P->>PCM: 当前跟随镜头
    S->>PCM: Camera Cut A (轨道镜头)
    PCM->>VT: SetViewTargetWithBlend, 0.6s
    Note over PCM,VT: 混合期间同时读取旧镜头与新镜头参数
    PCM-->>P: 输出最终画面
    S->>PCM: Camera Cut B (特写镜头)
    PCM->>VT: 新一轮混合
    S->>PCM: Sequence 结束
    PCM->>VT: 切回玩家跟随镜头

战斗镜头与剧情镜头的过渡设计

战斗镜头和剧情镜头的设计目标不同。战斗镜头要保留空间感,让玩家能判断敌我距离;剧情镜头要控制情绪,强调构图和节奏。两者切换时如果处理不好,玩家会产生强烈的割裂感。

最常见的做法是三段式过渡。

第一阶段是预过渡。在玩家触发剧情事件前,把战斗摄像机慢慢推到一个相对稳定的构图。比如玩家走到门口时,弹簧臂缩短,镜头略微上扬。这样切到剧情镜头时,两个画面的差异不会太大。

第二阶段是混合。这个阶段禁用玩家输入对摄像机的影响,但保留角色动画和场景时间轴。BlendFunc 用 EaseInOut,BlendTime 根据两个镜头之间的距离决定。如果两个镜头位置相距很近,0.3 秒就够了;如果是从天空俯瞰切到地面特写,可能需要 1.5 秒以上。

第三阶段是后过渡。剧情动画结束后,不要立刻把控制权交还给玩家。可以先用 0.2 秒把 HUD 淡入,再恢复输入。这样玩家不会在一瞬间被操作提示和视角变化同时轰炸。

HUD 的显隐也要和摄像机混合同步。PlayerCameraManager 本身不管理 HUD,但可以通过监听 OnBlendComplete 来触发 HUD 动画。另一种做法是在动画 Sequence 里加 Event Track,在混合开始和结束的关键帧上触发 Gameplay Tag 或 Interface 调用。

还有一种更隐晦的问题:剧情镜头经常使用 CineCameraActor 上的 Filmback 和 Focus 设置,而战斗镜头用 CameraComponent。两者在后处理栈上的差异可能导致画面色调突变。UE5.8 的 FMinimalViewInfo 会在混合时插值 FOV 和 AspectRatio,但不会自动混合 PostProcess 设置。如果项目对色调连续性要求高,需要在 Custom CameraManager 里手动插值 PostProcess Settings 的权重。

混合中断与恢复

剧情动画播放过程中,玩家可能会跳过动画,或者触发了新的剧情事件。这时当前混合会被打断。UE5.8 的 PlayerCameraManager 支持在混合进行中再次调用 SetViewTargetWithBlend,旧的新目标会立刻变成 OutgoingViewTarget,新的目标变成 PendingViewTarget。

这种机制很方便,但也容易出问题。假设玩家连续触发两个剧情事件:第一个事件让镜头切到 A,第二个事件让镜头切到 B。如果第二个调用发生在第一个混合还没结束时,画面会直接从当前中间态切向 B,而不是从 A 切向 B。观感上就像镜头被某种外力猛拽了一下。还有一种情况更容易被忽略:玩家在游戏设置里打开了暂停菜单。暂停菜单本身不会停止摄像机混合,因为 CameraManager 的 Tick 通常还在运行;如果此时菜单又触发了一次镜头重置,混合起点就会落在暂停画面上。

处理方法是维护一个期望 ViewTarget 栈。每次请求切换时,先判断当前是否正在混合。如果是,先把当前目标压栈,等新混合结束后再决定是否恢复。更简单的方法是记录上一次稳定的 ViewTarget,而不是使用混合中的中间状态作为起点。

void AMyPlayerCameraManager::SafeBlendTo(AActor* NewTarget, float BlendTime)
{
    AActor* StableStart = GetViewTarget();
    if (PendingViewTarget.IsValid())
    {
        StableStart = PendingViewTarget.Get();
    }
    CachedOutgoing = StableStart;
    SetViewTargetWithBlend(NewTarget, BlendTime, VTBlend_Cubic);
}

void AMyPlayerCameraManager::RestoreGameplayCamera()
{
    if (CachedOutgoing.IsValid())
    {
        SetViewTargetWithBlend(CachedOutgoing.Get(), 0.8f, VTBlend_EaseInOut);
        CachedOutgoing.Reset();
    }
}

这段代码的关键是 PendingViewTarget。不要直接从 GetViewTarget() 拿起点,因为返回值在混合期间是实时变化的。PendingViewTarget 才是混合最终要到达的目标。

另一个常见场景是跳过动画。玩家按 Start 跳过时,剧情镜头应该快速切回玩家摄像机,但不应该用硬切,而是用 0.2 秒左右的短混合。这样看起来是镜头被加速拉回来,而不是画面闪烁。

多人游戏中的相机同步

多人游戏的摄像机同步比单机复杂得多。Unreal 的网络模型中,摄像机本身通常只在所属客户端上存在,服务器不会渲染画面。PlayerCameraManager 默认只在本地客户端上运行,服务器上的 CameraManager 只负责计算视锥和复制相关数据。

最简单的同步需求是第三人称视角的跟随镜头。这个镜头由客户端自己更新,不需要服务器参与。服务器只需要知道玩家的 Pawn 位置和旋转,就能判断其他玩家是否在这个客户端的视野内。

剧情镜头则不同。如果一段过场动画要在所有客户端同时播放,Sequence 的播放通常由服务器触发,通过网络复制到各个客户端。每个客户端本地再执行 SetViewTargetWithBlend。由于网络延迟,不同客户端开始混合的时间会有细微差别,但玩家彼此之间不会看到对方的镜头,所以这个差别可以接受。

真正麻烦的是需要服务器权威判断的镜头。例如一个技能把玩家拉到空中,服务器要确定所有玩家看到这个拉起的镜头。一种做法是在服务器上选择一个 ViewTarget,然后通过 PlayerController 的 RPC 通知客户端执行混合。这个 RPC 要带上服务器时间戳,客户端收到后根据延迟做时间补偿。

flowchart TD
    A[服务器触发剧情事件] --> B{是否需要统一镜头}
    B -->|是| C[服务器选择 ViewTarget]
    C --> D[RPC 通知所有客户端]
    D --> E[客户端本地 SetViewTargetWithBlend]
    E --> F[根据 RTT 补偿 Blend 起始时间]
    B -->|否| G[客户端各自播放本地 Sequence]
    G --> H[由客户端 CameraManager 驱动]

观战模式是另一个同步热点。 spectators 的 ViewTarget 可以在不同玩家之间切换。UE5.8 的 PlayerController 提供了 SetViewTarget 的复制版本,但混合参数通常不会跨网络同步。如果 spectators 希望看到和原玩家一样的镜头过渡,需要在服务端记录当前混合状态,并在 spectator 的客户端上重建一次混合。

还有一个容易踩的坑:Replication Graph 会根据每个客户端的 ViewTarget 计算相关性。如果本地客户端在混合期间 ViewTarget 变成了远处的 CineCameraActor,Replication Graph 可能会认为远处的物体也处于相关范围,从而增加网络负载。对于短镜头来说问题不大,但对于长镜头或大范围轨道镜头,需要检查 NetCullDistanceSquared 和相关性设置。

一条检查清单

把林涛项目里最后定的规范放在这里,方便复制:

  • 所有剧情镜头切换都通过 SetViewTargetWithBlend 完成,禁止直接改 ViewTarget。
  • 默认剧情混合用 EaseInOut,时长 0.6 到 1.2 秒;战斗内功能性切换用 Cubic,时长 0.15 到 0.3 秒。
  • Sequencer 的 Camera Cuts 轨道必须设置 Blend Time,并勾选 Override Instance Data。
  • 混合开始和结束分别触发 HUD 淡入淡出,不要和输入恢复同一帧。
  • 多人剧情事件优先用服务器 RPC 触发,客户端本地执行混合,带时间戳补偿。
  • 自定义 Blend Curve 首尾必须钉在 (0,0) 和 (1,1)。

摄像机混合看起来只是两个画面之间的渐变,但它连接了输入、叙事、性能和多人同步。把这条链路理清楚之后,《苍鹰》后面几关的镜头切换再也没出过黑屏事故。林涛说,下次再遇到类似问题,他第一步会先打开 PlayerCameraManager 的 Debug 视图,而不是去调 Sequencer。