UE5相机系统初探(二)
上一章节我们实现了一个基本的第三人称相机。这一节我们为相机添加一些功能。
Lag
Spring Arm Component有一组关于Lag的参数,它表示相机的跟随延迟,这个是非常非常使用的功能,在Spring Arm Component中我们可以分别控制相机的位移延迟,以及旋转的延迟。位移的延迟可以设置一个最大的延后距离,这样可以保证相机不会离玩家太远导致玩家跑出了屏幕外。
如上设置后,来看下玩家移动时的延迟效果:
再把Camera Rotation Lag Speed
调到1,观察下旋转的延迟(容易引起晕眩):
location lag背后的逻辑其实很简单,就是一个线性插值,这个插值有两种方案,一种是直接根据delta time和speed插值,还有一种是使用一个小的步长,在delta time区间内不断更新插值目标,来调整相机的位置,这种表现上更丝滑,当然计算开销也会更高。
c++
if (bUseCameraLagSubstepping && DeltaTime > CameraLagMaxTimeStep && CameraLagSpeed > 0.f)
{
const FVector ArmMovementStep = (DesiredLoc - PreviousDesiredLoc) * (1.f / DeltaTime);
FVector LerpTarget = PreviousDesiredLoc;
float RemainingTime = DeltaTime;
while (RemainingTime > UE_KINDA_SMALL_NUMBER)
{
const float LerpAmount = FMath::Min(CameraLagMaxTimeStep, RemainingTime);
LerpTarget += ArmMovementStep * LerpAmount;
RemainingTime -= LerpAmount;
DesiredLoc = FMath::VInterpTo(PreviousDesiredLoc, LerpTarget, LerpAmount, CameraLagSpeed);
PreviousDesiredLoc = DesiredLoc;
}
}
else
{
DesiredLoc = FMath::VInterpTo(PreviousDesiredLoc, DesiredLoc, DeltaTime, CameraLagSpeed);
}
rotation lag也是同理,只不过换成了球形插值。
Shake
目前UE提供的camera shake有两种蓝图,一是DefaultCameraShakeBase
,另一种是LegacyCameraShake
,它们都继承自基类UCameraShakeBase
。
我们希望在角色跑步时,引入camera shake,来加强跑步的观感。同时,camera shake希望是可配置的,也就是说它可以来自不同的蓝图,但都要继承自UCameraShakeBase
。那么需要在类里定义变量:
c++
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
TSubclassOf<UCameraShakeBase> SprintShake;
然后,加入跑步时触发shake和停止shake的逻辑:
c++
void ACF_Character::SprintStart()
{
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
if (SprintShake && GetVelocity().Length() != 0)
{
CurrentSprintShake = GetWorld()->GetFirstPlayerController()->PlayerCameraManager->StartCameraShake(SprintShake, 1.f);
}
}
void ACF_Character::SprintEnd()
{
GetCharacterMovement()->MaxWalkSpeed = DefaultSpeed;
if (SprintShake)
{
GetWorld()->GetFirstPlayerController()->PlayerCameraManager->StopCameraShake(CurrentSprintShake);
CurrentSprintShake = nullptr;
}
}
回到之前创建的camera shake蓝图,我们选择一个shake pattern进行配置:
来看看效果:
StartCameraShake
背后会先从camera shake的缓存池中找可用的instance,如果找不到再去创建shake class对应的instance,然后播放该shake。
c++
UCameraShakeBase* NewInst = ReclaimShakeFromExpiredPool(ShakeClass);
// No old shakes, create a new one
if (NewInst == nullptr)
{
NewInst = NewObject<UCameraShakeBase>(this, ShakeClass);
}
if (NewInst)
{
// Custom initialization if necessary.
if (bIsCustomInitialized)
{
Params.Initializer.Execute(NewInst);
}
// Initialize new shake and add it to the list of active shakes
FCameraShakeBaseStartParams StartParams;
StartParams.CameraManager = CameraOwner;
StartParams.Scale = Scale;
StartParams.PlaySpace = Params.PlaySpace;
StartParams.UserPlaySpaceRot = Params.UserPlaySpaceRot;
StartParams.DurationOverride = Params.DurationOverride;
NewInst->StartShake(StartParams);
}
StopCameraShake
则会停止该shake,并将其加入到缓存池中。
c++
for (int32 i = 0; i < ActiveShakes.Num(); ++i)
{
FActiveCameraShakeInfo& ShakeInfo = ActiveShakes[i];
if (ShakeInfo.ShakeInstance == ShakeInst)
{
ShakeInst->StopShake(bImmediately);
if (bImmediately)
{
ShakeInst->TeardownShake();
SaveShakeInExpiredPoolIfPossible(ShakeInfo);
ActiveShakes.RemoveAt(i, 1);
UE_LOG(LogCameraShake, Verbose, TEXT("UCameraModifier_CameraShake::RemoveCameraShake %s"), *GetNameSafe(ShakeInfo.ShakeInstance));
}
break;
}
}
实际start以及stop的表现逻辑,是交给UCameraShakePattern
这个类来实现的,不同的表现效果都继承自这个类,比如DefaultCameraShakeBase
使用的是UPerlinNoiseCameraShakePattern
。
此外,我们还想实现当玩家起跳落地时,相机也会随之震动的效果。但是我们希望,相机离玩家越近时,震动效果越强,离玩家越远,震动效果越弱。这时可借助PlayWorldCameraShake
接口实现:
c++
void ACF_Character::Landed(const FHitResult& Hit)
{
Super::Landed(Hit);
UGameplayStatics::PlayWorldCameraShake(this, LandedShake, GetActorLocation(), 600.f, 2000.f);
}
代码中PlayWorldCameraShake
的后三个参数分别表示播放camera shake的位置,效果开始衰减的起始距离,完全衰减的距离。来看下相机和玩家不同距离下的效果:
衰减背后的逻辑也很简单,看代码一目了然,就不赘述了。
c++
float APlayerCameraManager::CalcRadialShakeScale(APlayerCameraManager* Camera, FVector Epicenter, float InnerRadius, float OuterRadius, float Falloff)
{
// using camera location so stuff like spectator cameras get shakes applied sensibly as well
// need to ensure server has reasonably accurate camera position
FVector POVLoc = Camera->GetCameraLocation();
if (InnerRadius < OuterRadius)
{
float DistPct = ((Epicenter - POVLoc).Size() - InnerRadius) / (OuterRadius - InnerRadius);
DistPct = 1.f - FMath::Clamp(DistPct, 0.f, 1.f);
return FMath::Pow(DistPct, Falloff);
}
else
{
// ignore OuterRadius and do a cliff falloff at InnerRadius
return ((Epicenter - POVLoc).SizeSquared() < FMath::Square(InnerRadius)) ? 1.f : 0.f;
}
}
camera actor
有时候我们希望使用多个相机,当触发某些条件时,视角可以切换到不同的相机。UE提供了CameraActor
来帮助做这件事情。在场景中创建一个CameraActor,可以发现它其实就是一个预设好的绑定了Camera Component的普通Actor而已:
然后,我们再新写一个Actor类,用于控制这两个相机之间的切换,比如这里我们创建一个矩形区域,玩家如果进入该区域,就需要切换到Camera Actor,离开区域则切换回原有的相机。
c++
void ACF_ViewTargetActor::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
UE_LOG(LogTemp, Warning, TEXT("OnOverlapBegin"));
if (ViewTarget && PC)
{
PC->SetViewTargetWithBlend(ViewTarget, BlendTime);
bIsOverlapped = true;
}
}
void ACF_ViewTargetActor::OnOverlapEnd(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
UE_LOG(LogTemp, Warning, TEXT("OnOverlapEnd"));
if (ViewTarget && PC)
{
PC->SetViewTargetWithBlend(PC->GetPawn(), BlendTime);
bIsOverlapped = false;
}
}
来看看效果:
不过这样的话,玩家就可能离开视角的范围内了,因此,还需要在tick里添加当玩家位于矩形区域时,调整视角的逻辑,让相机一直看向玩家。
c++
if (bIsOverlapped)
{
FRotator Rot = UKismetMathLibrary::FindLookAtRotation(ViewTarget->GetActorLocation(), PC->GetPawn()->GetActorLocation());
ViewTarget->SetActorRotation(Rot);
}
FindLookAtRotation
首先会根据已知的forward轴(x轴)建立起坐标系,构造出旋转矩阵,然后再把这个旋转矩阵转换为Rotator。UE采用的是右手坐标系,按固定轴旋转时使用的是X-Y-Z(roll-pitch-yaw)的左乘旋转顺序,另外旋转方向上,X(roll)和Y(pitch)方向使用的是右手法则,Z(yaw)方向使用的是左手法则。这就导致UE的旋转矩阵和左手系的一般旋转矩阵有差异,表现在X和Y方向是反过来的。
M = [ c o s ( p i t c h ) c o s ( y a w ) s i n ( r o l l ) s i n ( p i t c h ) c o s ( y a w ) − c o s ( r o l l ) s i n ( y a w ) − ( c o s ( r o l l ) s i n ( p i t c h ) c o s ( y a w ) + s i n ( r o l l ) s i n ( y a w ) ) c o s ( p i t c h ) s i n ( y a w ) s i n ( r o l l ) s i n ( p i t c h ) s i n ( y a w ) + c o s ( r o l l ) c o s ( y a w ) c o s ( y a w ) s i n ( r o l l ) − c o s ( r o l l ) s i n ( p i t c h ) s i n ( y a w ) s i n ( p i t c h ) − s i n ( r o l l ) c o s ( p i t c h ) c o s ( r o l l ) c o s ( p i t c h ) ] M = \begin{bmatrix} cos(pitch)cos(yaw) & sin(roll)sin(pitch)cos(yaw) - cos(roll)sin(yaw) & -(cos(roll)sin(pitch)cos(yaw) + sin(roll)sin(yaw)) \\ cos(pitch)sin(yaw) & sin(roll)sin(pitch)sin(yaw) + cos(roll)cos(yaw) & cos(yaw)sin(roll) - cos(roll)sin(pitch)sin(yaw) \\ sin(pitch) & -sin(roll)cos(pitch) & cos(roll)cos(pitch) \end{bmatrix} M= cos(pitch)cos(yaw)cos(pitch)sin(yaw)sin(pitch)sin(roll)sin(pitch)cos(yaw)−cos(roll)sin(yaw)sin(roll)sin(pitch)sin(yaw)+cos(roll)cos(yaw)−sin(roll)cos(pitch)−(cos(roll)sin(pitch)cos(yaw)+sin(roll)sin(yaw))cos(yaw)sin(roll)−cos(roll)sin(pitch)sin(yaw)cos(roll)cos(pitch)
然后就是一些单纯的数学运算了,UE为了只使用arctan计算角度,在计算出pitch和yaw之后,先绕了一圈构造只有pitch和raw的旋转矩阵:
M ′ = [ c o s ( p i t c h ) c o s ( y a w ) − s i n ( y a w ) − s i n ( p i t c h ) c o s ( y a w ) c o s ( p i t c h ) s i n ( y a w ) c o s ( y a w ) − s i n ( p i t c h ) s i n ( y a w ) s i n ( p i t c h ) 0 c o s ( p i t c h ) ] M' = \begin{bmatrix} cos(pitch)cos(yaw) & -sin(yaw) & -sin(pitch)cos(yaw) \\ cos(pitch)sin(yaw) & cos(yaw) & -sin(pitch)sin(yaw) \\ sin(pitch) & 0 & cos(pitch) \end{bmatrix} M′= cos(pitch)cos(yaw)cos(pitch)sin(yaw)sin(pitch)−sin(yaw)cos(yaw)0−sin(pitch)cos(yaw)−sin(pitch)sin(yaw)cos(pitch)
然后分别用M的第二列和第三列点乘M'的第二列,得到:
x = c o s ( y a w ) y = s i n ( y a w ) x = cos(yaw) \\ y = sin(yaw) x=cos(yaw)y=sin(yaw)
这样就可以用arctan计算出yaw了。
c++
UE::Math::TRotator<T> UE::Math::TMatrix<T>::Rotator() const
{
using TRotator = UE::Math::TRotator<T>;
using TVector = UE::Math::TVector<T>;
const TVector XAxis = GetScaledAxis( EAxis::X );
const TVector YAxis = GetScaledAxis( EAxis::Y );
const TVector ZAxis = GetScaledAxis( EAxis::Z );
const T RadToDeg = T(180.0 / UE_DOUBLE_PI);
TRotator Rotator = TRotator(
FMath::Atan2( XAxis.Z, FMath::Sqrt(FMath::Square(XAxis.X)+FMath::Square(XAxis.Y)) ) * RadToDeg,
FMath::Atan2( XAxis.Y, XAxis.X ) * RadToDeg,
0
);
const TVector SYAxis = (TVector)UE::Math::TRotationMatrix<T>( Rotator ).GetScaledAxis( EAxis::Y );
Rotator.Roll = FMath::Atan2( ZAxis | SYAxis, YAxis | SYAxis ) * RadToDeg;
Rotator.DiagnosticCheckNaN();
return Rotator;
}
Reference
[1] Camera Framework Essentials for Games
[3] UE 中的空间坐标系