自学内容网 自学内容网

UE5相机系统初探(二)

UE5相机系统初探(二)

上一章节我们实现了一个基本的第三人称相机。这一节我们为相机添加一些功能。

Lag

Spring Arm Component有一组关于Lag的参数,它表示相机的跟随延迟,这个是非常非常使用的功能,在Spring Arm Component中我们可以分别控制相机的位移延迟,以及旋转的延迟。位移的延迟可以设置一个最大的延后距离,这样可以保证相机不会离玩家太远导致玩家跑出了屏幕外。

UE5相机系统初探2-1

如上设置后,来看下玩家移动时的延迟效果:

在这里插入图片描述

再把Camera Rotation Lag Speed调到1,观察下旋转的延迟(容易引起晕眩):

在这里插入图片描述

location lag背后的逻辑其实很简单,就是一个线性插值,这个插值有两种方案,一种是直接根据delta time和speed插值,还有一种是使用一个小的步长,在delta time区间内不断更新插值目标,来调整相机的位置,这种表现上更丝滑,当然计算开销也会更高。

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

UE5相机系统初探2-4

我们希望在角色跑步时,引入camera shake,来加强跑步的观感。同时,camera shake希望是可配置的,也就是说它可以来自不同的蓝图,但都要继承自UCameraShakeBase。那么需要在类里定义变量:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
TSubclassOf<UCameraShakeBase> SprintShake;

UE5相机系统初探2-5

然后,加入跑步时触发shake和停止shake的逻辑:

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进行配置:

UE5相机系统初探2-6

来看看效果:

在这里插入图片描述

StartCameraShake背后会先从camera shake的缓存池中找可用的instance,如果找不到再去创建shake class对应的instance,然后播放该shake。

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,并将其加入到缓存池中。

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接口实现:

void ACF_Character::Landed(const FHitResult& Hit)
{
Super::Landed(Hit);
UGameplayStatics::PlayWorldCameraShake(this, LandedShake, GetActorLocation(), 600.f, 2000.f);
}

代码中PlayWorldCameraShake的后三个参数分别表示播放camera shake的位置,效果开始衰减的起始距离,完全衰减的距离。来看下相机和玩家不同距离下的效果:

在这里插入图片描述

在这里插入图片描述

衰减背后的逻辑也很简单,看代码一目了然,就不赘述了。

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而已:

UE5相机系统初探2-10

然后,我们再新写一个Actor类,用于控制这两个相机之间的切换,比如这里我们创建一个矩形区域,玩家如果进入该区域,就需要切换到Camera Actor,离开区域则切换回原有的相机。

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里添加当玩家位于矩形区域时,调整视角的逻辑,让相机一直看向玩家。

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)0sin(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了。

UE::Math::TRotator<T> UE::Math::TMatrix<T>::Rotator() const
{
using TRotator = UE::Math::TRotator<T>;
using TVector = UE::Math::TVector<T>;
const TVectorXAxis= GetScaledAxis( EAxis::X );
const TVectorYAxis= GetScaledAxis( EAxis::Y );
const TVectorZAxis= 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 TVectorSYAxis= (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

[2] UE4-SceneComponent的理解

[3] UE 中的空间坐标系

[4] 欧拉角、四元数、旋转矩阵推导及相互关系


原文地址:https://blog.csdn.net/weixin_45776473/article/details/144174466

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!