UE5相机系统初探(二)

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](https://dev.epicgames.com/community/learning/courses/RRr/unreal-engine-camera-framework-essentials-for-games/wv7n/unreal-engine-camera-framework-essentials-for-games-overview) \[2\] [UE4-SceneComponent的理解](https://blog.csdn.net/boonti/article/details/82909884) \[3\] [UE 中的空间坐标系](https://dreamanddead.github.io/card/UE-%E4%B8%AD%E7%9A%84%E7%A9%BA%E9%97%B4%E5%9D%90%E6%A0%87%E7%B3%BB) \[4\] [欧拉角、四元数、旋转矩阵推导及相互关系](https://zhaoxuhui.top/blog/2018/03/13/RelationBetweenQ4&R&Euler.html)

相关推荐
Heaphaestus,RC9 小时前
Slate到UMG的封装原理揭秘
开发语言·ue5
游乐码18 小时前
Unity(十六)切换场景及鼠标相关
unity·游戏引擎
FakeEnd19 小时前
Unity开发笔记6
笔记·unity·游戏引擎
游乐码19 小时前
Unity(十七)Unity随机数及Unity委托
unity·游戏引擎
RPGMZ21 小时前
RPGMZ游戏引擎 一个窗口 文本居中显示
开发语言·javascript·游戏引擎·rpgmz
归真仙人1 天前
【UE】VR一体机转场
ue5·ue4·vr·虚幻引擎·unreal engine
tohand1 天前
Unity 完美假阴影实现文档
unity·游戏引擎
洋洋06171 天前
UE4/UE5 引擎常见面试题总结(1)
ue5·ue4
nnsix1 天前
Unity 动画 Avatar 笔记
笔记·unity·游戏引擎
拾忆丶夜1 天前
unity 热力图学习
学习·unity·游戏引擎