Original Unreal Camera System
Original Unreal Camera System
本文主要是介绍虚幻5.5以前的摄像机系统
摄像机工作原理
在游戏中,**摄像机(Camera)就像是玩家的眼睛,决定了我们如何观察这个虚拟世界。
它控制着玩家的视点(POV)位置和视野(FOV)**大小。
类似于现实中的相机,游戏引擎中的摄像机可以挂载在角色身上,也可以独立存在。
虽然一个游戏系统中可以同时存在多个摄像机,但实际游戏画面通常只显示其中一个。
根据设计需求,游戏会在适当的时间切换不同的摄像机,例如释放绝招、播放剧情动画等。
游戏中的摄像机会根据玩家的输入变化来调整所观察到的画面。此外,在游戏的特殊阶段(比如cutscene),摄像机会沿着预设轨道而移动。
游戏的类型多种多样,有第一人称的FPS游戏,有第三人称的动作游戏,还有需要统筹全局来观察的RTS游戏。
简单来说,第一人称就是把POV放在人眼睛的位置,第三人称就是把POV放在人身后一定距离的位置,RTS就是把POV放到离地面很高的位置。
这里说POV要比说摄像机更为准确,因为有的时候摄像机只是一个为了方便大家理解与观察的实体,其实他的位置可以随意,不过为了便于大家理解与使用,我们一般将摄像机位置与视点位置同步。
下图的摄像机位置即为POV的位置,紫色的框即为FOV。

因为POV在游戏中是会随时变化的,所以我们需要在Tick里面去更新他的位置。
所以,从本质上讲,调整摄像机就是不断地更新POV的位置,这样我们也能平滑流畅的观察游戏世界。
如果,我们想要切换不同的视角,那就切换我们的POV。如果想要做一些镜头移动特效,就可以利用一些插值的算法来处理POV的位置。
如果想要做一些视觉特效,可以直接在摄像机上加一些后处理效果。总之,还是那句话,摄像机决定你如何观察游戏世界。
在正式开始介绍虚幻的摄像机系统前,我们先介绍下虚幻摄像机系统中常见的一些术语。
VT(ViewTarget)
ViewTarget,顾名思义,即观察目标,也就是我们想让摄像机跟随的对象。虚幻通过 FTViewTarget 来定义和ViewTarget关联的数据。
/** A ViewTarget is the primary actor the camera is associated with. */
USTRUCT(BlueprintType)
struct FTViewTarget
{
public:
/** Target Actor used to compute POV */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=TViewTarget)
TObjectPtr<class AActor> Target;
/** Computed point of view */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=TViewTarget)
struct FMinimalViewInfo POV;
// 确保摄像机有正确的观察目标,优先是Controller控制的角色,其次切换到被查看的下一个被操控的角色,最后可能是玩家控制器本身
void CheckViewTarget(APlayerController* OwningController);
protected:
/** PlayerState (used to follow same player through pawn transitions, etc., when spectating) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=TViewTarget)
TObjectPtr<class APlayerState> PlayerState;
}
POV (Point of View)
摄像机在游戏世界中观察点(视口)的基本信息。虚幻通过 FMinimalViewInfo 来定义。
FMinimalViewInfo 包含了相机的位置、旋转、FOV 等属性。游戏引擎使用这些信息来渲染相机画面。
FOV (Field of View)
FOV 是光学仪器的视场角,以光学仪器的镜头为顶点,以被测目标的物像可通过镜头看到的最大范围的两条边缘构成的夹角。
FOV 在游戏世界中决定相机的视野,是指相机能够看到的水平或垂直范围,虚幻引擎中以角度来表示。
通常,FOV的推荐范围在 90° 到 110° 之间,这比较符合人眼的观察范围,不易让人晕眩。
Rotation
代表相机的方向,虚幻通过 FRotator 来定义。
- Pitch(俯仰角):
Pitch在虚幻中表示相机绕其右侧轴(Y 轴) 旋转的角度。
正值表示相机向上旋转,负值表示相机向下旋转。 - Yaw(偏航角):
Yaw 在虚幻中表示绕其上方轴(Z 轴) 旋转的角度。
正值表示相机向右旋转,负值表示相机向左旋转。 - Roll(翻滚角):
Roll 在虚幻中表示相机绕其前方轴(X 轴) 旋转的角度。
正值表示相机顺时针旋转,负值表示相机逆时针旋转。


CameraBoom(Camera Arm)
**摄像机臂(CameraBoom)**用于控制游戏中的相机位置、视角和缩放效果。类似于现实中的摄像机吊杆和自拍杆,它的主要功能有:
- 连接相机和角色:
- 摄像机臂将游戏中的相机与角色绑定在一起。它允许你将相机放置在角色周围,以便跟随角色的移动。
- 通过将相机连接到摄像机臂,你可以实现第一人称或第三人称视角,从而让玩家更好地体验游戏世界。
- 控制相机的位置和旋转:
- 调整摄像机臂的长度、角度和旋转,可以精确控制相机的视角。例如,你可以将相机放在角色的后方、上方或侧面,以获得不同的视觉效果。
- 通过旋转摄像机臂,你可以改变相机的方向,使其朝向不同的目标或景物。
- 实现缩放效果:
- 摄像机臂的长度可以调整,从而实现缩放效果。当你拉近或推远摄像机臂时,相机会跟随角色的移动并改变视野。
- 这对于游戏中的缩放效果、远近景切换以及探索大型地图非常有用。
- 避免碰撞和阻挡:
- 摄像机臂会自动调整位置,以确保相机不会被墙壁、地板或其他障碍物所阻挡。
- 这有助于避免相机在游戏中出现奇怪的穿墙现象,同时保持视觉连续性。
虚幻通过 USpringArmComponent 来定义摄像机臂。
/**
* This component tries to maintain its children at a fixed distance from the parent,
* but will retract the children if there is a collision, and spring back when there is no collision.
*
* Example: Use as a 'camera boom' or 'selfie stick' to keep the follow camera for a player from colliding into the world.
*/
UCLASS(ClassGroup=Camera, meta=(BlueprintSpawnableComponent), hideCategories=(Mobility), MinimalAPI)
class USpringArmComponent : public USceneComponent;

虚幻摄像机关系梳理
下图以PlayerCameraManager为核心点列出了和它相关的类的关系,一些核心的类的介绍会在下面逐渐展开

UCameraComponent & USpringArmComponent
CameraComponent组件一般放在控制角色的根节点上,常规的行为与属性都可以在其中进行设置包括POV位置(即组件的位置),FOV,观察模式(分为正交模式和透视模式),宽高比,后处理效果等。
下图是UCameraComponent在蓝图中和摄像机相关的配置属性**:**

- Camera的选项用于在Editor中显示摄像机,方便调试
SpringArmComponent的作用类似于一个伸缩臂,确保摄像机与角色之间的距离始终保持恒定。
它在遇到碰撞时会缩短距离,而在没有碰撞时会弹回。这对于实现跟随摄像机、避免碰撞以及优化视觉效果非常有用。
下图是USpringArmComponent在蓝图中和摄像机相关的配置属性**:**

CameraActor是虚幻引擎中用于控制视角、创建摄像机效果以及实现各种摄影需求的重要组件**。**
- CameraActor可以作为场景中的默认摄像机,方便用户观察某个场景。
- CameraActor可以用作虚拟世界中的安保摄像头。你可以将其放置在关键区域,监视特定区域的活动。
- CameraActor可以用于制作游戏中的过场动画,例如切换场景、展示剧情或其他特定事件。
虚幻提供电影摄像机(ACineCameraActor)的扩展,专门用于创建电影效果和高度控制的镜头。
/** Specifies which player index will pass input to this actor/component */
UENUM()
namespace EAutoReceiveInput
{
enum Type : int
{
Disabled,
Player0,
Player1,
//...
};
}
// In CameraActor
/** Specifies which player controller, if any, should automatically use this Camera when the controller is active. */
UPROPERTY(Category="AutoPlayerActivation", EditAnywhere)
TEnumAsByte<EAutoReceiveInput::Type> AutoActivateForPlayer;
void ACameraActor::BeginPlay()
{
if (AutoActivateForPlayer != EAutoReceiveInput::Disabled && GetNetMode() != NM_Client)
{
const int32 PlayerIndex = GetAutoActivatePlayerIndex();
// Always put it in the pool of available auto-activate cameras.
GetWorld()->RegisterAutoActivateCamera(this, PlayerIndex);
// If we find a matching PC, bind to it immediately.
APlayerController* PC = UGameplayStatics::GetPlayerController(this, PlayerIndex);
if (PC)
{
PC->SetViewTarget(this);
}
}
Super::BeginPlay();
}
- ACameraActor 在激活 AutoActivateForPlayer 后, 会设置为指定PlayerController的ViewTarget。
- 比如我们如果在关卡启动的时候需要播放一段剧情介绍,就非常实用。
PlayerCameraManger
PlayerCameraManager是虚幻玩家摄像机的管理器,它负责控制摄像机如何追踪和呈现玩家在游戏世界中的视角。
要理解PlayerCameraManager,首先需要了解ViewTarget的概念。
- ViewTarget,即观察目标,它代表了我们希望摄像机跟随的对象。在第三人称游戏中,这个对象通常是玩家控制的角色。
- ViewTarget在PlayerCameraManager中定义,它包含一个Actor对象,这个对象提供了摄像机的理想视角信息以及当前的玩家状态。
简单来说,PlayerCameraManager通过ViewTarget中Actor的位置信息来计算摄像机的POV,并将计算结果存储回ViewTarget中的POV。
PlayerCameraManager负责处理各种视角相关的特效和逻辑。它可以管理不同人称视角的切换,处理摄像机震动和碰撞检测,还可以为摄像机添加镜头特效,如溅血效果等。
此外,PlayerCameraManager还支持Post Process Effects,如色彩校正、景深和运动模糊等。
总而言之,PlayerCameraManager就像玩家在虚拟世界中的“眼睛”,它定义了摄像机如何捕捉和呈现游戏世界的视角,为其他系统(如渲染器)提供了关键的视图属性。
PlayerController与PlayerCameraManger
APlayerCameraManager 是 APlayerContoller的子系统,确保玩家在游戏中获得正确的视角体验。
/** Camera manager associated with this Player Controller. */
UPROPERTY(BlueprintReadOnly, Category=PlayerController)
TObjectPtr<APlayerCameraManager> PlayerCameraManager;
/** PlayerCamera class should be set for each game, otherwise Engine.PlayerCameraManager is used */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=PlayerController)
TSubclassOf<APlayerCameraManager> PlayerCameraManagerClass;
/**
* True to allow this player controller to manage the camera target for you,
* typically by using the possessed pawn as the camera target. Set to false
* if you want to manually control the camera target.
*/
UPROPERTY(EditAnywhere, Category=PlayerController)
bool bAutoManageActiveCameraTarget;
- bAutoManageActiveCameraTarget 激活后会把和PlayerController关联的有效ViewTarget更新到PlayerCameraManager中
APlayerController 负责管理 APlayerCameraManager 的创建,更新和销毁。简而言之,APlayerCameraManager 的生命周期受到 APlayerController 的控制。
- 在切换地图时,会重新创建 APlayerController。创建过程中,会调用 PostInitializeComponents 方法,摄像机系统的初始化就是在这里完成。
此外,在进行关卡无缝切换时,APlayerController 也会根据情况初始化摄像机系统。 - 在执行世界的Tick后,会在其他Actor的更新完毕后,再更新玩家的摄像机信息。
Unity引擎的摄像机更新在LateUpdate中执行,和这个处理是相似的。 - 在 APlayerController 销毁时,APlayerCameraManager 也会随之销毁。
// Create PlayerCameraManager(Code Segments)
void APlayerController::PostInitializeComponents()
{
//...
SpawnPlayerCameraManager();
//...
}
void APlayerController::PostSeamlessTravel()
{
//...
if (PlayerCameraManager == nullptr)
{
SpawnPlayerCameraManager();
}
}
void APlayerController::SpawnPlayerCameraManager()
{
FActorSpawnParameters SpawnInfo;
// ...
if (PlayerCameraManagerClass != NULL)
{
PlayerCameraManager = GetWorld()->SpawnActor<APlayerCameraManager>(PlayerCameraManagerClass, SpawnInfo);
}
else
{
PlayerCameraManager = GetWorld()->SpawnActor<APlayerCameraManager>(SpawnInfo);
}
if (PlayerCameraManager != NULL)
{
PlayerCameraManager->InitializeFor(this);
}
}
// Update PlayerCamera(Code Segments)
void APlayerController::UpdateCameraManager(float DeltaSeconds)
{
if (PlayerCameraManager != NULL)
{
PlayerCameraManager->UpdateCamera(DeltaSeconds);
}
}
void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
{
// Update cameras last. This needs to be done before NetUpdates, and after all actors have been ticked.
for( FConstPlayerControllerIterator Iterator = GetPlayerControllerIterator(); Iterator; ++Iterator )
{
if (APlayerController* PlayerController = Iterator->Get())
{
if (!bIsPaused || PlayerController->ShouldPerformFullTickWhenPaused())
{
PlayerController->UpdateCameraManager(DeltaSeconds);
}
else if (PlayerController->PlayerCameraManager && FCameraPhotographyManager::IsSupported(this))
{
PlayerController->PlayerCameraManager->UpdateCameraPhotographyOnly();
}
}
}
}
// Destory PlayerCameraManager(Code Segments)
void APlayerController::Destroyed()
{
// ...
if (PlayerCameraManager != NULL)
{
PlayerCameraManager->Destroy();
PlayerCameraManager = NULL;
}
}
Warning
虽然摄像机系统的更新是在其他 Actor 更新之后执行的,但是 PlayController 的心跳函数(TickActor)中会根据 PlayerCameraManager 的状态进行一些处理。
尤其是当本地玩家持有当前的 PlayerController 时,PlayerController 会根据持有的 Pawn 的方向和摄像机的 ViewTargetPawn方向进行插值运算(作为备用)。
根据角色的移动情况,还会将 PlayerCameraManager->bShouldSendClientSideCameraUpdate 设置为 true,这样 Camera 的信息会在下一次更新时同步到服务器。
除了管理 APlayerCameraManager 的生命周期外,PlayerController 还提供了一系列与 PlayerCameraManager 相关的公共接口。
我们可以在这些公共接口中处理与摄像机系统有关的功能,例如 CameraStyle、ViewTarget、旋转、FOV、摄像机抖动和后处理等。
接下来,我将介绍当 Controller 接收到鼠标旋转时,如何影响 PlayerCameraManager。

从上图可以看到:当PlayerCameraManager通过ProcessViewRotation计算出移动后的Roation, 这个Rotation会赋值给ControlRotation以及Controller所控制的Pawn(也就包括他身上的CameraComponent组件)。
最后,经过上面流程,我们的CameraComponent朝向已经被修改,PlayerCameraManager在更新POV的Rotation时获取CameraComponent的朝向即可。
当然如果Controller这个时候并没有控制Pawn, 或者Pawn上面没有挂载PlayerCameraManager, 在PlayerCameraManager更新POV朝向的时候,会获取Controller的ControlRotation。
设置视口对象 - SetViewTarget()
在PlayerCameraManager初始化的时候需要设置ViewTarget, 用来在渲染时提供需要的视角信息。
- PlayerController提供了相同的方法,适用于Gameplay的特殊更新
这个函数参数里可以设置混合时间,设置之后就不是瞬间切换画面,而是在指定的时间中移动到新位置。移动的方式是插值平移,这种平移效果普通,所以就只记录代码的位置,不看具体逻辑了。
void APlayerCameraManager::SetViewTarget(class AActor* NewTarget, struct FViewTargetTransitionParams TransitionParams)
{
//判断是否有混合时间,默认参数为0,会走else分支
if (TransitionParams.BlendTime > 0)
{
//混合过渡逻辑,略
}
else
{
//立刻设置视角信息
AssignViewTarget(NewTarget, ViewTarget);
//当前ViewTarget的检查更新
ViewTarget.CheckViewTarget(PCOwner);
}
}
//实际的设置ViewTarget的方法
void APlayerCameraManager::AssignViewTarget(AActor* NewTarget, FTViewTarget& VT, struct FViewTargetTransitionParams TransitionParams)
{
VT.Target = NewTarget;
// AspectRatio 屏幕宽高比,值为宽度/高度,类型是float
VT.POV.AspectRatio = DefaultAspectRatio;
VT.POV.bConstrainAspectRatio = bDefaultConstrainAspectRatio;
VT.POV.FOV = DefaultFOV;
}
FTViewTarget会记录最终用于计算POV的Actor, 这个Actor可以是场景中任意的对象。如果是和玩家有关, 则会根据OwningController的状态来决定,可能对象是控制器或者控制的Pawn。
更新 - DoUpdateCamera()
我们上面有提到执行世界的Tick()时,会在其它Actor的更新完成后,再更新玩家的摄像机信息。
Info
void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
-> void APlayerController::UpdateCameraManager(float DeltaSeconds)
-> void APlayerCameraManager::UpdateCamera(float DeltaTime)
-> void APlayerCameraManager::DoUpdateCamera(float DeltaTime)
虚幻对DoUpdateCamera的介绍是PlayerCameraManager中实际的更新方法,看一下具体内容:
void APlayerCameraManager::DoUpdateCamera(float DeltaTime)
{
//没有处于过渡状态时
if ((PendingViewTarget.Target == NULL) || !BlendParams.bLockOutgoing)
{
//更新当前的ViewTarget
UpdateViewTarget(ViewTarget, DeltaTime);
}
//处于过渡状态时
if (PendingViewTarget.Target != NULL)
{
//混合过渡逻辑,略
}
//缓存POV结果,用来给其它代码访问
FillCameraCache(NewPOV);
}
更新视口对象 - UpdateViewTarget()
ViewTarget如果是CameraActor,就调用摄像机组件的GetCameraView()。
一般情况是Pawn,调用的就是UpdateViewTargetInternal(),这个方法内部又调用ViewTarget的CalcCamera()。
void APlayerCameraManager::UpdateViewTarget(FTViewTarget& OutVT, float DeltaTime)
{
if (ACameraActor* CamActor = Cast<ACameraActor>(OutVT.Target))
{
// 如果ViewTarget是CameraActor,调用摄像机组件的GetCameraView()
CamActor->GetCameraComponent()->GetCameraView(DeltaTime, OutVT.POV);
}
else
{
// 不同CameraStyle的处理,一般为Default,走else分支
if (CameraStyle == NAME_Fixed)
//略
else
{
//实际的摄像机更新逻辑
UpdateViewTargetInternal(OutVT, DeltaTime);
}
}
if (!bDoNotApplyModifiers || bAlwaysApplyModifiers)
{
// 应用摄像机修改器,内置晃动就是在这里实现,略
ApplyCameraModifiers(DeltaTime, OutVT.POV);
}
// 摄像机管理器移动到ViewTarget位置
SetActorLocationAndRotation(OutVT.POV.Location, OutVT.POV.Rotation, false);
// 更新摄像机的镜头效果,略
UpdateCameraLensEffects(OutVT);
}
void APlayerCameraManager::UpdateViewTargetInternal(FTViewTarget& OutVT, float DeltaTime)
{
// 判断蓝图是否有接管,没有就走else
if (BlueprintUpdateCamera(OutVT.Target, OutLocation, OutRotation, OutFOV))
{
OutVT.POV.Location = OutLocation;
OutVT.POV.Rotation = OutRotation;
OutVT.POV.FOV = OutFOV;
}
else
{
// 计算ViewTarget的摄像机属性,子类可以实现自己的版本
OutVT.Target->CalcCamera(DeltaTime, OutVT.POV);
}
}
Info
摄像机支持的****摄像机模式(CameraStyle)有那些?

默认构造的模式是:

我们常用的有自由摄像机、第一人称、第三人称等。旁观者模式是类似一种自由摄像机的模式。
PlayerCameraManager里面在更新POV时会判断当前的CameraStyle,从而进行不同的计算,比如我们从第三人称视角切换到第一人称视角,POV相对玩家的位置就会改变(从背后变为眼睛附近)。
有一点也要注意一下,不同摄像机的碰撞检测通道可能是不同的。
Info
获取摄像机视角有那些方式?
UCameraComponent可以获得当前相机的POV信息:
// GetCameraView
void UCameraComponent::GetCameraView(float DeltaTime, FMinimalViewInfo& DesiredView)
{
//...
//是否使用叠加偏移,就是在摄像机位置上在增加一次transform
if (bUseAdditiveOffset)
{
FTransform OffsetCamToBaseCam = AdditiveOffset;
FTransform BaseCamToWorld = GetComponentToWorld();
FTransform OffsetCamToWorld = OffsetCamToBaseCam * BaseCamToWorld;
DesiredView.Location = OffsetCamToWorld.GetLocation();
DesiredView.Rotation = OffsetCamToWorld.Rotator();
}
else
{
DesiredView.Location = GetComponentLocation();
DesiredView.Rotation = GetComponentRotation();
}
//使用摄像机组件的属性做为视口信息
DesiredView.FOV = bUseAdditiveOffset ? (FieldOfView + AdditiveFOVOffset) : FieldOfView;
DesiredView.AspectRatio = AspectRatio;
DesiredView.bConstrainAspectRatio = bConstrainAspectRatio;
DesiredView.bUseFieldOfViewForLOD = bUseFieldOfViewForLOD;
DesiredView.ProjectionMode = ProjectionMode;
DesiredView.OrthoWidth = OrthoWidth;
DesiredView.OrthoNearClipPlane = OrthoNearClipPlane;
DesiredView.OrthoFarClipPlane = OrthoFarClipPlane;
//...
//后处理属性设置
DesiredView.PostProcessBlendWeight = PostProcessBlendWeight;
if (PostProcessBlendWeight > 0.0f)
{
DesiredView.PostProcessSettings = PostProcessSettings;
}
//...
}
Actor调用摄像机组件的 GetCameraView()计算摄像机视角,Controller重新实现了这个方法。
// CalcCamera
void AActor::CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult)
{
//如果有摄像机组件,就用第一个激活的摄像机组件
if (bFindCameraComponentWhenViewTarget)
{
TInlineComponentArray<UCameraComponent*> Cameras;
GetComponents(/*out*/ Cameras);
for (UCameraComponent* CameraComponent : Cameras)
{
if (CameraComponent->IsActive())
{
CameraComponent->GetCameraView(DeltaTime, OutResult);
return;
}
}
}
//没有就用actor眼睛位置作为视角信息
GetActorEyesViewPoint(OutResult.Location, OutResult.Rotation);
}
void APlayerController::CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult)
{
// 返回控制的pawn、spectator或者自身初始的位置
OutResult.Location = GetFocalLocation();
OutResult.Rotation = GetControlRotation();
}
如果没有摄像机组件,就用Actor眼睛的位置。
// GetActorEyesViewPoint
void AActor::GetActorEyesViewPoint( FVector& OutLocation, FRotator& OutRotation ) const
{
//眼睛位置默认就是这个actor的位置和角度,子类可以重新实现
OutLocation = GetActorLocation();
OutRotation = GetActorRotation();
}
void APawn::GetActorEyesViewPoint( FVector& out_Location, FRotator& out_Rotation ) const
{
out_Location = GetPawnViewLocation();
// 如果被控制使用Controller的角度,否则是自身的
out_Rotation = GetViewRotation();
}
void APlayerController::GetActorEyesViewPoint( FVector& out_Location, FRotator& out_Rotation ) const
{
// 控制Pawn作为眼睛位置.
if (GetPawnOrSpectator() != NULL)
{
GetPawnOrSpectator()->GetActorEyesViewPoint(out_Location, out_Rotation);
}
else
{
out_Location = PlayerCameraManager ? PlayerCameraManager->GetCameraLocation() : GetSpawnLocation();
out_Rotation = GetControlRotation();
}
}
渲染时获取视角 - CalcSceneView()
在Tick()更新完游戏逻辑后,会调用Draw方法渲染画面。渲染时,第一步就是调用CalcSceneView()获取玩家视角,摄像机管理器就是在这里用来提供视角的。
// UGameViewportClient::Draw
void UGameViewportClient::Draw(FViewport* InViewport, FCanvas* SceneCanvas)
{
//ViewFamily用来记录一组视角信息,因为可能有多个本地玩家
FSceneViewFamilyContext ViewFamily(FSceneViewFamily::ConstructionValues(
InViewport,
MyWorld->Scene,
EngineShowFlags)
.SetRealtimeUpdate(true));
for (FLocalPlayerIterator Iterator(GEngine, MyWorld); Iterator; ++Iterator)
{
ULocalPlayer* LocalPlayer = *Iterator;
//计算玩家的视角,获取的View会加入ViewFamily中
FSceneView* View = LocalPlayer->CalcSceneView(&ViewFamily, ViewLocation, ViewRotation, InViewport, nullptr, PassType);
}
//ViewFamily作为参数,传入渲染模块,完成摄像机系统的任务。
GetRendererModule().BeginRenderingViewFamily(SceneCanvas,&ViewFamily);
}
看下LocalPlayer的CalcSceneView()是怎么计算视口的:
// ULocalPlayer::CalcSceneView
FSceneView* ULocalPlayer::CalcSceneView( class FSceneViewFamily* ViewFamily,
FVector& OutViewLocation,
FRotator& OutViewRotation,
FViewport* Viewport,
class FViewElementDrawer* ViewDrawer,
EStereoscopicPass StereoPass)
{
FSceneViewInitOptions ViewInitOptions;
FMinimalViewInfo ViewInfo;
//获取更新时计算好的视口信息到ViewInfo变量
GetViewPoint(ViewInfo, StereoPass);
//用ViewInfo变量填充ViewInitOptions变量
ViewInitOptions.bUseFieldOfViewForLOD = ViewInfo.bUseFieldOfViewForLOD;
ViewInitOptions.FOV = ViewInfo.FOV;
ViewInitOptions.DesiredFOV = ViewInfo.DesiredFOV;
// Fill out the rest of the view init options
ViewInitOptions.ViewFamily = ViewFamily;
//这里处理隐藏的组件,略
PlayerController->BuildHiddenComponentList(OutViewLocation, /*out*/ ViewInitOptions.HiddenPrimitives);
//用ViewInitOptions变量做参数,创建FSceneView
FSceneView* const View = new FSceneView(ViewInitOptions);
//把FSceneView变量加入到ViewFamily中,完成视角信息的获取
ViewFamily->Views.Add(View);
}
PlayerCameraManager提供了获取POV的接口:
// GetViewPoint
void ULocalPlayer::GetViewPoint(FMinimalViewInfo& OutViewInfo, EStereoscopicPass StereoPass) const
{
//从PlayerCameraManager中取数据
OutViewInfo = PlayerController->PlayerCameraManager->GetCameraCachePOV();
OutViewInfo.FOV = PlayerController->PlayerCameraManager->GetFOVAngle();
//通过PlayerController调用CameraManager的功能
PlayerController->GetPlayerViewPoint(/*out*/ OutViewInfo.Location, /*out*/ OutViewInfo.Rotation);
}
void APlayerController::GetPlayerViewPoint( FVector& out_Location, FRotator& out_Rotation ) const
{
//从PlayerCameraManager中取数据
PlayerCameraManager->GetCameraViewPoint(out_Location, out_Rotation);
}
void APlayerCameraManager::GetCameraViewPoint(FVector& OutCamLoc, FRotator& OutCamRot) const
{
//从更新时算好的缓存中,取出数据
const FMinimalViewInfo CurrentPOV = GetCameraCachePOV();
OutCamLoc = CurrentPOV.Location;
OutCamRot = CurrentPOV.Rotation;
}
切换Pawn时摄像机的变化
在PlayerController的控制方法OnPossess()里,会设置CameraActor或者Pawn为ViewTarget。
//OnPosses
void APlayerController::OnPossess(APawn* PawnToPossess)
{
if (bAutoManageActiveCameraTarget)
{
//自动管理摄像机对象
AutoManageActiveCameraTarget(GetPawn());
}
}
void APlayerController::AutoManageActiveCameraTarget(AActor* SuggestedTarget)
{
//启用自动管理时
if (bAutoManageActiveCameraTarget)
{
//查找场景中的CameraActor,如果找到就设置为ViewTarget
ACameraActor* AutoCameraTarget = GetAutoActivateCameraForPlayer();
if (AutoCameraTarget)
{
SetViewTarget(AutoCameraTarget);
return;
}
//没有找到CameraActor就用参数里的Actor(就是被控制的Pawn)作为ViewTarget
SetViewTarget(SuggestedTarget);
}
}
在取消控制方法OnUnPossess里,如果ViewTarget是Pawn时,就会把ViewTarget改回PlayerController。
// OnUnPosses
void APlayerController::OnUnPossess()
{
if (GetViewTarget() == GetPawn())
{
//设置ViewTarget为PlayerController
SetViewTarget(this);
}
}
Info
PlayerController的SetViewTarget方法,只是调用了PlayerCameraManager的方法。
小结
PlayerCameraManager的初始化,是在创建PlayerController时执行的。
引擎Tick时,先计算好视角信息缓存起来。在渲染时,PlayerCameraManager的作用就是提供玩家的视角信息。
在控制Pawn时,会把Pawn设置为ViewTarget,这样玩家就能看到Pawn的视角了。取消控制时视角会改回PlayerController。
摄像机特殊处理
虚幻引擎提供了完善的摄像机震动支持,模拟了相机在特定情况下的震动或晃动, 以增加真实感。
虚幻中和震动相关的类之间的关系如下:

要添加摄像机震动,首先需要定制你的摄像机震动效果。大致如下:
- 创建新的相机震动蓝图,继承自 CameraShakeBase。
- 在相机震动蓝图中配置相机震动的样式和参数。
- 在关联蓝图中应用这个相机震动。
- PlayWorldCameraShake: 影响所有附近本地玩家的世界内摄像机抖动,并根据距离进行衰减。

- PlayeController: 直接应用到玩家控制器上

- PlayWorldCameraShake: 影响所有附近本地玩家的世界内摄像机抖动,并根据距离进行衰减。
摄像机震动的应用简单流程

震源
在虚幻引擎中,震源(ACameraShakeSourceActor)的作用是触发场景区域的震动影响。
通过配置CameraShakeSourceComponent上的相机震动类,我们可以直接应用这些震动效果到场景中的所有玩家相机上,从而产生影响。
具体实现中是通过APlayerCameraManager::StartCameraShakeFromSource来触发的。
相机震动预览器
虚幻提供了Camera Shake Previewer,方便我们预览震动的效果,打开方式如下:

Info
思考:虚幻的相机震动支持可应用于那些场合?
- 在大型NPC(如巨人)靠近的时候,通过增加震源影响玩家
- 在射击,近战武器碰撞时应用短促的相机抖动效果
- 在地震等特定场景效果中,添加震源来影响范围的玩家
思考:对相机震动我们可以做一些什么扩展?
// Add Settings
if (UMyGameUserSettings* GameSettings = UMyGameUserSettings::GetMyGameUserSettings())
{
if (GameSettings->GameplaySettings.bAllowCameraShake)
{
return Super::StartCameraShake(...);
}
}
虚幻引擎的GAS对相机震动进行了扩展,具体细节请查看FGameplayCueNotify_CameraShakeInfo
摄像机镜头粒子特效
虚幻中的最新镜头粒子效果支持类是通过ANiagaraLensEffectBase进行扩展的,而旧版本则使用了AEmitterCameraLensEffectBase。
这两者都实现了ICameraLensEffectInterface接口,并在实例化时注册到APlayerCameraManager中。
APlayerCameraManager维护了一个镜头效果的数组CameraLensEffects,它负责管理所有激活的镜头粒子,并在UpdateViewTarget的最后进行更新。
当我们需要添加某个特定的镜头效果时,可以通过在蓝图中继承ANiagaraLensEffectBase并配置相应的效果,然后通过AddGenericCameraLensEffect将其添加到玩家的摄像机管理类中。
同样,在不需要这个效果时,也可以使用RemoveGenericCameraLensEffect来移除镜头效果。
下面是一个镜头污垢效果示例:

在虚幻引擎中,可以通过两种方式实现摄像机的后处理效果。
- PostProcessVolume
- 在场景里面添加PostProcessVolume,然后摄像机处于该体积内才能产生效果。
- 进入标题的官网链接可查看细节
- Set PostProcessSettings in CameraComponent
在摄像机组件直接添加与设置后处理效果(例如曝光、色调映射、景深等),这样不需要把摄像机放在某个特别的位置。

可以通过代码控制后处理的激活:
// 启用后处理效果 MyPawn->CameraComponent->PostProcessSettings.bOverride_VignetteIntensity = true; // 禁用后处理效果 MyPawn->CameraComponent->PostProcessSettings.bOverride_VignetteIntensity = false;
摄像机平滑移动和延迟追踪
在大部分游戏中,为了得到更好的游戏体验,摄像机的移动都是平滑的。因为摄像机是在每帧都进行更新,所以我们只要保持与我们的ViewTarget固定的距离,就可以得到平滑的效果。
但是如果我们想从当前摄像机立刻切换到另一个摄像机机位,或者我们的ViewTarget发生瞬移,这时候如果不做任何处理,摄像机就会突然的切换(有的时候这样也没什么不好)。
假如我们觉得这样切换有点突然,我们只要简单的处理一下就可以了。设置一个插值,让当前的位置逐渐插值到目标位置。(FMath::VInterpTo(当前位置,目标位置, DeltaTime,速度);)
同理,如果想做一个摄像机延迟效果(就是玩家可能突然用技能走出一大段位移,为了体现效果,想让摄像机慢慢的追上玩家),也可以使用类似的方法来实现。
如果有兴趣,可以参考一下虚幻的第三人称官方例程,然后找到弹簧组件,搜索EnableCameraLag属性,试试效果。当然也可以到代码里面看看他实现的细节。
摄像机淡入淡出
在许多游戏场景中,摄像机的淡入(fade in)和淡出(fade out)效果得到了广泛应用。
例如,在剧情演示中,淡入淡出的使用可以生动地描绘时间的流逝。当角色复活时,淡入效果则象征着生命的重生。
在虚幻引擎中,你可以通过蓝图来实现摄像机的淡入和淡出效果。以下是一些方法:
通过APlayerCameraManager触发
- 玩家摄像机管理器提供了一组处理淡入淡出的方法
- 调用StartCameraFade(...) 来实现快速的淡入淡出效果。StopCameraFade()停止当前的淡入淡出处理。
// Update Fading in DoUpdateCamera
void APlayerCameraManager::DoUpdateCamera(float DeltaTime)
{
if (bEnableFading)
{
if (bAutoAnimateFade)
{
FadeTimeRemaining = FMath::Max(FadeTimeRemaining - DeltaTime, 0.0f);
if (FadeTime > 0.0f)
{
FadeAmount = FadeAlpha.X + ((1.f - FadeTimeRemaining / FadeTime) * (FadeAlpha.Y - FadeAlpha.X));
}
if ((bHoldFadeWhenFinished == false) && (FadeTimeRemaining <= 0.f))
{
// done
StopCameraFade();
}
}
if (bFadeAudio)
{
ApplyAudioFade();
}
}
}
通过Widget蓝图的动画触发
- 打开你的小部件蓝图(UserWidget Blueprint),创建一个新的Widget动画。
你可以在动画编辑器中刷新 Render Opacity,以实现淡入淡出效果。 - 创建的UWidgetAnimation保存当前的UserWidget中。
可以在Widget编辑窗口的下面找到Animations.
- 在Widget的逻辑中触发这个动画

摄像机拍照
在游戏开发中,我们经常会提供摄像机拍照功能,以便玩家捕获游戏场景,生成漂亮的图片并保存下来。
虚幻引擎对摄像机拍照提供了多种支持,包括自定义摄影模式,下面会一一介绍。
自定义摄影模式
首先,我们来看一下虚幻引擎中的自定义摄影模式。在这个模式中,FCameraPhotographyManager和APlayerCameraManager两个类起着关键的作用。
FCameraPhotographyManager是摄影模式的核心管理器。它提供了一套接口来控制和管理虚幻引擎中的摄影会话。
这包括启动和停止会话,更新摄像机和后处理设置,以及显示或隐藏摄影UI的控件。
虽然它没有直接实现ICameraPhotography接口,但它有一套与之相似的接口,并且可以动态激活实现了ICameraPhotographyModule的插件。
// Core Interfaces
/**
* The public interface of the CameraPhotographyModule
*/
class ICameraPhotographyModule : public IModuleInterface, public IModularFeature
{
public:
static inline bool IsAvailable()
{
return FModuleManager::Get().IsModuleLoaded(GetModularFeatureName());
}
/**
* Attempts to create a new photography interface
*
* @return Interface to the photography implementation, if we were able to successfully create one
*/
virtual TSharedPtr< class ICameraPhotography > CreateCameraPhotography() = 0;
};
class ICameraPhotography
{
public:
ICameraPhotography() {};
virtual ~ICameraPhotography() {};
virtual bool UpdateCamera(FMinimalViewInfo& InOutPOV, APlayerCameraManager* PCMgr) = 0;
virtual void UpdatePostProcessing(FPostProcessSettings& InOutPostProcessingSettings) = 0;
virtual void StartSession() = 0;
virtual void StopSession() = 0;
virtual bool IsSupported() = 0;
virtual void SetUIControlVisibility(uint8 UIControlTarget, bool bIsVisible) = 0;
virtual void DefaultConstrainCamera(const FVector NewCameraLocation, const FVector PreviousCameraLocation, const FVector OriginalCameraLocation, FVector& OutCameraLocation, APlayerCameraManager* PCMgr) = 0;
virtual const TCHAR* const GetProviderName() = 0;
};
- 开发者只需要创建一个插件,这个插件继承自ICameraPhotographyModule, 它内部的核心类实现ICameraPhotography接口。
- 一旦这个插件被创建,FCameraPhotographyManager就会自动加载并使用这个插件。这样,开发者就可以使用自定义虚幻引擎的摄影模式。
APlayerCameraManager是玩家摄像机的管理器。在摄影模式中,它主要负责更新摄影摄像机的视角(通过UpdatePhotographyCamera方法)。
如果允许摄影模式(通过AllowPhotographyMode方法检查),它会更新摄影摄像机,并根据需要进行摄像机切换。
此外,它还有一个bGameCameraCutThisFrame属性,用于标记是否在这一帧进行了摄像机切换,这对渲染器(如是否使用上一帧的遮挡查询和运动模糊)有影响。
// relative codes
bool APlayerCameraManager::UpdatePhotographyCamera(FMinimalViewInfo& NewPOV)
{
// update photography camera, if any
return FCameraPhotographyManager::Get().UpdateCamera(NewPOV, this);
}
void APlayerCameraManager::UpdatePhotographyPostProcessing(FPostProcessSettings& InOutPostProcessingSettings)
{
FCameraPhotographyManager::Get().UpdatePostProcessing(InOutPostProcessingSettings);
}
void APlayerCameraManager::DoUpdateCamera(float DeltaTime)
{
FMinimalViewInfo NewPOV = ViewTarget.POV;
//...
if (AllowPhotographyMode())
{
const bool bPhotographyCausedCameraCut = UpdatePhotographyCamera(NewPOV);
bGameCameraCutThisFrame = bGameCameraCutThisFrame || bPhotographyCausedCameraCut;
}
// Cache results
FillCameraCache(NewPOV);
}
// Call in LevelTick(UWorld::Tick)
virtual void UpdateCameraPhotographyOnly();
// Call in ULocalPlayer::CalcSceneView
virtual void UpdatePhotographyPostProcessing(FPostProcessSettings& InOutPostProcessing);
总的来说,FCameraPhotographyManager和APlayerCameraManager在摄影模式中共同工作,以提供一种灵活和强大的方式来控制和管理虚幻引擎中的摄影会话。这包括摄像机的更新,后处理设置的覆盖,以及摄影UI控件的显示和隐藏等功能。
USceneCaptureComponent2D
在虚幻引擎中,你也可以使用USceneCaptureComponent2D捕获的场景渲染到一个UTextureRenderTarget2D对象上,然后将这个纹理保存为PNG文件。以下是一个简单的步骤说明:
- 创建并设置USceneCaptureComponent2D:
首先,你需要在你的角色或其他对象上创建一个USceneCaptureComponent2D组件,并设置其位置和方向以确定捕获的场景视图。 - 创建UTextureRenderTarget2D:
然后,你需要创建一个UTextureRenderTarget2D对象,并将其分配给USceneCaptureComponent2D的TextureTarget属性。这个纹理对象将用于存储捕获的场景渲染结果。 - 调用CaptureScene:
在适当的时机(例如,游戏开始时或按下某个键时),调用USceneCaptureComponent2D的CaptureScene方法。
这将立即捕获当前场景的视图,并将渲染结果存储在TextureTarget指定的纹理中。 - 保存纹理到PNG:
最后,你可以使用FTextureRenderTarget2DResourceCreateBitmap方法将像素数据保存为PNG文件。
//sample
// 假设你已经有了一个USceneCaptureComponent2D对象和一个UTextureRenderTarget2D对象
USceneCaptureComponent2D* SceneCapture;
UTextureRenderTarget2D* RenderTarget;
// 捕获场景到纹理
SceneCapture->TextureTarget = RenderTarget;
SceneCapture->CaptureScene();
// 从纹理中读取像素数据
FTextureRenderTargetResource* RenderTargetResource = RenderTarget->GameThread_GetRenderTargetResource();
FReadSurfaceDataFlags ReadPixelFlags(RCM_UNorm);
ReadPixelFlags.SetLinearToGamma(true); // if the render target is sRGB
TArray<FColor> OutBMP;
RenderTargetResource->ReadPixels(OutBMP, ReadPixelFlags);
// 将像素数据保存为PNG文件
FString FilePath = FPaths::ProjectDir() + TEXT("SavedScene.png");
FFileHelper::CreateBitmap(*FilePath, RenderTarget->SizeX, RenderTarget->SizeY, OutBMP.GetData());
Warning
请注意,这只是一个基本的示例,实际的代码可能需要根据你的具体需求进行修改。
例如,你可能需要处理线程同步问题,或者在保存文件之前检查文件路径是否有效等。
屏幕截图
虚幻引擎提供了屏幕截图功能,我们可以把捕获的游戏画面作为"相片"输出。这主要通过FScreenshotRequestProcessScreenShots两个函数实现。
FScreenshotRequest::RequestScreenshot是一个静态函数,用于请求一个屏幕截图。你可以通过传递一个文件路径作为参数来调用它,这个文件路径就是你希望保存截图的位置。
例如,下面的代码会在下一帧捕获当前屏幕的截图,并将其保存到"D:/MyScreenshot.png"。第二个和第三个参数分别控制是否应该显示UI和是否应该使用高分辨率。
Info
FScreenshotRequest::RequestScreenshot(TEXT("D:/MyScreenshot.png"), false, false);
/**
* Requests a new screenshot with a specific filename
*
* @param InFilename The filename to use
* @param bInShowUI Whether or not to show Slate UI
* @param bAddFilenameSuffix Whether an auto-generated unique suffix should be added to the supplied filename
*/
static ENGINE_API void RequestScreenshot(const FString& InFilename, bool bInShowUI, bool bAddFilenameSuffix, bool bHdrScreenshot=false);
UGameViewportClient::ProcessScreenShots函数则是在每一帧中被调用,用于处理所有的屏幕截图请求。
这个函数会检查FScreenshotRequest中的截图请求队列,如果队列中有请求,它就会捕获当前的帧缓冲区并保存为图像文件。
Warning
请注意截图的时机和保存的位置:
你需要在适当的时机(例如,游戏事件触发,或者玩家按下某个键)调用FScreenshotRequest::RequestScreenshot来请求截图。
同时,你需要确保提供的文件路径是有效的,且你有权限写入文件。
请注意虚幻引擎的屏幕截图功能可能会受到渲染设置的影响,例如分辨率、后处理效果等。
如果你需要获取与玩家实际看到的游戏画面完全一致的截图,你可能需要确保这些设置与游戏运行时的设置一致。