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

[2] UE4-SceneComponent的理解

[3] UE 中的空间坐标系

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

相关推荐
suzh1qian15 小时前
Unity类银河战士恶魔城学习总结(P156 Audio Settings音频设置)
学习·unity·c#·游戏引擎
暮志未晚Webgl16 小时前
115. UE5 GAS RPG 实现角色死亡后从存档点复活
数码相机·ue5
书鸢123616 小时前
UE5 Does Implement Interface 节点
ue5
大梦百万秋17 小时前
C++ 游戏开发:跨平台游戏引擎的构建与优化
开发语言·c++·游戏引擎
tealcwu19 小时前
【Unity基础】Unity中Transform.forward的详解与应用
unity·游戏引擎
W Y1 天前
【Unity-摩擦力】
unity·游戏引擎
高远-临客1 天前
unity读取mysql5.7版本示例
unity·游戏引擎
suzh1qian1 天前
Unity类银河战士恶魔城学习总结(P155 More example on audio effects更多的音效细节)
学习·游戏·unity·c#·游戏引擎
Lowjin_1 天前
Unity中,Canva的三种渲染模式
unity·游戏引擎
小白球滚芝麻1 天前
Unity下载文件断点续下
android·unity·c#·游戏引擎·vr