UE5:相机震动CameraShake源码分析

本文将会分析UE5中相机震动的调用流程,会简要地分析UCameraModifier_CameraShake、UCameraShakeBase等相关类的调用过程。

阅读本文,你至少需要使用或者了解过CameraModifier。

主要结构

FAddCameraShakeParams

初始化CameraShakeInstance用的参数结构体。

UCameraShakeSourceComponent指针

这个对象可以认为是震动的源头,UE封装了根据距离衰减的功能。如果不指定,那么震动会应用于全世界。

ECameraShakePlaySpace PlaySpace

枚举类型,记录这个震动效果是在相机控件还是World控件

另外还有一个回调函数FOnInitializeCameraShake类型的Initializer,该回调会在创建ShakeInstance时执行。

FActiveCameraShakeInfo

他是一个USTRUCT

保存了已激活的CameraShake的相关信息,包括CameraShakeBase(也就是震动实例)指针,以及对CameraShakeSourceComponent的弱引用等。

UCameraShakeBase

我将这个类及其派生类称为震动实例,下文会反复用到这个称呼。

震动实例类的基类,由UCameraModifier_CameraShake创建并管理,该类及其派生类充当了一个屏幕震动”执行者“的角色。屏幕震动的具体参数,如震动偏移、镜头位置,FOV的变化等与屏幕震动相关的参数,都会在这个类(派生类)中定义,并随着传入的delta时间而变化,最终通过UpdateAndApplyCameraShake函数传出。

其中有一些比较重要的函数

StartShake(APlayerCameraManager* Camera, float Scale, ECameraShakePlaySpace InPlaySpace, FRotator UserPlaySpaceRot = FRotator::ZeroRotator)

开始震动,初始化一系列参数。

UpdateAndApplyCameraShake(float DeltaTime, float Alpha, FMinimalViewInfo& InOutPOV)

更新震动参数,并将一些涉及到镜头变化的参数通过InOutPOV传出来。

TObjectPtr<UCameraShakePattern> RootShakePattern

CameraShakePattern指针,其作用就是定义了相机震动的调用模式。

目前看来作用十分有限,大家看看源码就知道其具体是做什么的了。

FCameraShakeState State;

保存震动实例的状态,主要就这点参数。

很想吐槽UE把各种参数细细地封装进了很多看起来类似的结构体中,出于对UE开发工程师的敬畏,姑且还是认为这样是”优秀“的做法。

FCameraShakeInfo ShakeInfo; // Duration、BlendIn和BlendOut时间

float ElapsedTime;
bool bIsActive : 1;
bool bHasBlendIn : 1;
bool bHasBlendOut : 1;

在后面的流程分析中会更加详细地说明这个类。

UCameraModifier_CameraShake

震动的发起者

其最主要的成员是TArray<FActiveCameraShakeInfo> ActiveShakes,这个数组保存了当前激活的所有Shake。

比较重要的成员函数:

UCameraShakeBase* AddCameraShake(TSubclassOf<UCameraShakeBase> NewShake, const FAddCameraShakeParams& Params)

根据传入的UClass和Param,创建并启动一个震动效果。

有几个比较重要的成员和函数

TArray<FActiveCameraShakeInfo> ActiveShakes

保存了当前活跃的震动实例

TMap<TSubclassOf<UCameraShakeBase>, FPooledCameraShakes> ExpiredPooledShakesMap

已经失效(过期)的震动实例,保存在这个Map中用于复用,类似于对象池,主要是出于减少性能开销的考虑。

bool ModifyCamera(float DeltaTime, struct FMinimalViewInfo& InOutPOV)

该函数的作用就是根据传入的InOutPOV数据,对其中的一些参数根据当前Shake效果进行调整,以引用的方式返回。

调用该函数可以获得应用过CameraShake的InOutPOV,以此来调整画面的效果。

流程说明

震动的开始

一个屏幕震动开始的调用流程如下,接下来会逐一讲解:

UCameraModifier_CameraShake::AddCameraShake->StartShake->StartShakePattern(虚函数)-> StartShakePatternImpl ->DoStartShake(是MatineeCameraShake的implement)

UCameraModifier_CameraShake::AddCameraShake

从相机震动的入口开始说明。一般要开启一个相机震动,就需要调用CameraModifier_CameraShake的AddCameraShake函数。该函数创建了一个相机震动的实例CameraShakeBase,启动震动实例并将其纳入Modifier的管理(ActiveShakes数组)之中。以下我省略了部分我认为不重要的代码,我们只关注其核心部分。

UCameraShakeBase* UCameraModifier_CameraShake::AddCameraShake(TSubclassOf<UCameraShakeBase> ShakeClass, const FAddCameraShakeParams& Params)
{
    ...
	if (ShakeClass != nullptr)
	{
		float Scale = Params.Scale;
		const UCameraShakeSourceComponent* SourceComponent = Params.SourceComponent;
		const bool bIsCustomInitialized = Params.Initializer.IsBound();

		// 分屏相关调整
		...
		
        // 判断震动实例是否是单例,如果是单例,根据是否有初始化回调来选择是重启震动还是直接删除
		UCameraShakeBase const* const ShakeCDO = GetDefault<UCameraShakeBase>(ShakeClass);
		const bool bIsSingleInstance = ShakeCDO && ShakeCDO->bSingleInstance;
		if (bIsSingleInstance)
		{
			// Look for existing instance of same class
			for (FActiveCameraShakeInfo& ShakeInfo : ActiveShakes)
			{
				UCameraShakeBase* ShakeInst = ShakeInfo.ShakeInstance;
				if (ShakeInst && (ShakeClass == ShakeInst->GetClass()))
				{
					if (!ShakeInfo.bIsCustomInitialized && !bIsCustomInitialized)
					{
						// Just restart the existing shake, possibly at the new location.
						// Warning: if the shake source changes, this would "teleport" the shake, which might create a visual
						// artifact, if the user didn't intend to do this.
                        // 根据新的SourceComponet重启震动
						ShakeInfo.ShakeSource = SourceComponent;
						ShakeInst->StartShake(CameraOwner, Scale, Params.PlaySpace, Params.UserPlaySpaceRot);
						return ShakeInst;
					}
					else
					{
						// If either the old or new shake are custom initialized, we can't
						// reliably restart the existing shake and expect it to be the same as what the caller wants. 
						// So we forcibly stop the existing shake immediately and will create a brand new one.
                        // 如果ShakeInst有自定义的初始化,那么直接将其停止并”拆卸“
						ShakeInst->StopShake(true);
						ShakeInst->TeardownShake();
						// Discard it right away so the spot is free in the active shakes array.
						ShakeInfo.ShakeInstance = nullptr;
					}
				}
			}
		}

		// Try to find a shake in the expired pool
        // 尝试复用之前过期的震动实例
		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
			NewInst->StartShake(CameraOwner, Scale, Params.PlaySpace, Params.UserPlaySpaceRot);

			// Look for nulls in the array to replace first -- keeps the array compact
            // 从ActiveShakes数组中找一个空位,以保证数组紧凑
			bool bReplacedNull = false;
			for (int32 Idx = 0; Idx < ActiveShakes.Num(); ++Idx)
			{
				FActiveCameraShakeInfo& ShakeInfo = ActiveShakes[Idx];
				if (ShakeInfo.ShakeInstance == nullptr)
				{
					ShakeInfo.ShakeInstance = NewInst;
					ShakeInfo.ShakeSource = SourceComponent;
					ShakeInfo.bIsCustomInitialized = bIsCustomInitialized;
					bReplacedNull = true;
				}
			}

			// no holes, extend the array
			if (bReplacedNull == false)
			{
				FActiveCameraShakeInfo ShakeInfo;
				ShakeInfo.ShakeInstance = NewInst;
				ShakeInfo.ShakeSource = SourceComponent;
				ShakeInfo.bIsCustomInitialized = bIsCustomInitialized;
				ActiveShakes.Emplace(ShakeInfo);
			}
		}
		return NewInst;
	}

	return nullptr;
}

可以看到该函数做了以下主要工作

  1. 尝试复用原有的震动实例,并重新启动震动实例
  2. 如果不能复用,则直接新建一个,调用StartShake函数启动震动
  3. 将新建的震动实例纳入ActiveShakes数组中进行管理

其中还有一些优化内存空间的小技巧,可以学习一下。

void UCameraShakeBase::StartShake(APlayerCameraManager* Camera, float Scale, ECameraShakePlaySpace InPlaySpace, FRotator UserPlaySpaceRot)

虽然这个函数起名叫StartShake,但是它并不是虚函数,也不直接启动震动实例。实际上UE在下面又做了一层抽象,由UCameraShakePattern来负责调用具体震动实例的启动。

这个函数功能并不复杂,初始化了一点参数,并调用UCameraShakePattern::StartShakePattern函数。

void UCameraShakeBase::StartShake(APlayerCameraManager* Camera, float Scale, ECameraShakePlaySpace InPlaySpace, FRotator UserPlaySpaceRot)
{
	// Remember the various settings for this run.
	// Note that the camera manager can be null, for example in unit tests.
	CameraManager = Camera;
	ShakeScale = Scale;
	PlaySpace = InPlaySpace;
	UserPlaySpaceMatrix = (InPlaySpace == ECameraShakePlaySpace::UserDefined) ? 
		FRotationMatrix(UserPlaySpaceRot) : FRotationMatrix::Identity;

	// Acquire info about the shake we're running, and initialize our transient state.
	const bool bIsRestarting = State.IsActive();
	FCameraShakeInfo ActiveInfo;
	GetShakeInfo(ActiveInfo);
	State.Initialize(ActiveInfo);

	// Let the root pattern initialize itself.
	if (RootShakePattern)
	{
		FCameraShakeStartParams StartParams;
		StartParams.bIsRestarting = bIsRestarting;
		RootShakePattern->StartShakePattern(StartParams);
	}
}

一直到State.Initialize(ActiveInfo)这一行,都是在初始化震动实例的各种参数,包括相机管理器、震动强度(Scale)、FCameraShakeState里面的参数(持续时间、blendin、blendout)等。

接着执行了RootShakePattern->StartShakePattern(StartParams),这里才是震动开始的入口。

这里出现的FCameraShakeStartParams只有一个成员:bIsRestarting。用于判断是否是重新启动的震动。

UCameraShakePattern::StartShakePattern(const FCameraShakeStartParams& Params)

这个函数非常简单,调用了一个虚函数StartShakePatternImpl。直到这里,才体现出这个震动实例的抽象性。

通过继承UCameraShakePattern和UCameraShakeBase,我们就可以自定义各种各样的震动实例,然后以不同的方式去执行它。

void UCameraShakePattern::StartShakePattern(const FCameraShakeStartParams& Params)
{
	StartShakePatternImpl(Params);
}

震动开始-以LegacyCameraShake为例

在UE4.26之前,这个类被称作UCameraShake,之后改名成为了UMatineeCameraShake。 到了UE5.1,这个类被改名称为了LegacyCameraShake,翻译过来就是过时的相机震动。笔者不知道UE还推出了什么先进的相机震动,但至少在UE5之前这个类还是被广泛使用的。借由分析这个类,我们继续梳理相机震动的流程。

值得一提的是,在UE5之前,UCameraShakeBase有两个基本子类:

UMatineeCameraShake: Legacy camera shake which can do either oscillation or run camera anims.(传统的屏幕震动,可以支持抖动或播放相机动画)
USequenceCameraShake: A camera shake that plays a sequencer animation.(播放定序器动画的屏幕震动)

UE5.1之后就合并为了LegacyCameraShake。

上文提到,我们需要继承UCameraShakePattern和UCameraShakeBase,并重写其关键虚函数StartShakePatternImpl,才能使屏幕震动的流程继续。这里我们以UCameraShakeBase的子类ULegacyCameraShake为例,看看震动开始时具体做了什么事情

ULegacyCameraShakePattern::StartShakePatternImpl(const FCameraShakeStartParams& Params)

先看看LegacyCameraShake配套的LegacyCameraShakePattern。上文我一直没有具体说明这个CameraShakePattern到底是啥东西,其实看看下面的代码就知道,其实这个类的作用就是调用CameraShakeBase里的接口,起到了一层抽象的作用。说起来比较混乱,还是看代码吧:

void ULegacyCameraShakePattern::StartShakePatternImpl(const FCameraShakeStartParams& Params)
{
	ULegacyCameraShake* Shake = GetShakeInstance<ULegacyCameraShake>();
	Shake->DoStartShake(Params);
}

很简单,就是获取Pattern所属的震动实例,然后再调用这个实例的函数。

也就是说,这个调用过程是这样的:CameraShakeBase::StartShake->CameraShakePattern::StartShakePattern->CameraShake::DoStartShake(这个函数可以是子类任意一个函数)

总之这个调用链小小地兜了个圈子又回来了。笔者能力有限,不能很好地说明这种做法的好处在哪。但我们现在并不关心为什么要这样写,只需要知道ShakePattern里的函数基本上都是这种写法,只是绕了一圈调用震动实例的函数罢了。

ULegacyCameraShake::DoStartShake(const FCameraShakeStartParams& Params)

在下面的代码中,几乎所有的变量都是ULegacyCameraShake的成员变量,其含义基本与变量名一致,都是震动相关的参数,例如持续时间、位置便宜、混合时间等。这些参数通常都会暴露在蓝图中供用户自定义,最后统一在这个类里面进行初始化并进行使用。

void ULegacyCameraShake::DoStartShake(const FCameraShakeStartParams& Params)
{
	const float EffectiveOscillationDuration = (OscillationDuration > 0.f) ? OscillationDuration : TNumericLimits<float>::Max();

	// init oscillations
	if (OscillationDuration != 0.f)
	{
		if (OscillatorTimeRemaining > 0.f)
		{
			// this shake was already playing
			OscillatorTimeRemaining = EffectiveOscillationDuration;

			if (bBlendingOut)
			{
				bBlendingOut = false;
				CurrentBlendOutTime = 0.f;

				// stop any blendout and reverse it to a blendin
				if (OscillationBlendInTime > 0.f)
				{
					bBlendingIn = true;
					CurrentBlendInTime = OscillationBlendInTime * (1.f - CurrentBlendOutTime / OscillationBlendOutTime);
				}
				else
				{
					bBlendingIn = false;
					CurrentBlendInTime = 0.f;
				}
			}
		}
        // 各种参数的初始化
		else
		{
			RotSinOffset.X = FFOscillator::GetInitialOffset(RotOscillation.Pitch);
			RotSinOffset.Y = FFOscillator::GetInitialOffset(RotOscillation.Yaw);
			RotSinOffset.Z = FFOscillator::GetInitialOffset(RotOscillation.Roll);

			LocSinOffset.X = FFOscillator::GetInitialOffset(LocOscillation.X);
			LocSinOffset.Y = FFOscillator::GetInitialOffset(LocOscillation.Y);
			LocSinOffset.Z = FFOscillator::GetInitialOffset(LocOscillation.Z);

			FOVSinOffset = FFOscillator::GetInitialOffset(FOVOscillation);

			InitialLocSinOffset = LocSinOffset;
			InitialRotSinOffset = RotSinOffset;
			InitialFOVSinOffset = FOVSinOffset;

			OscillatorTimeRemaining = EffectiveOscillationDuration;

			if (OscillationBlendInTime > 0.f)
			{
				bBlendingIn = true;
				CurrentBlendInTime = 0.f;
			}
		}
	}

	// Anim Seq相关
	...

}

总之,震动的开始到这里基本上就告一段落了。这时候会产生一个问题:震动实例已经初始化完成,后续我们又该怎么获取这些震动的信息,并把它应用到我们的相机上面来呢?

震动过程

调用链如下。

UWorld::Tick->APlayerController::UpdateCameraManager->....-> APlayerCameraManager::ApplyCameraModifiers->UCameraModifier_CameraShake::ModifyCamera->UCameraShakeBase::UpdateAndApplyCameraShake->UCameraShakePattern::UpdateShakePattern->ULegacyCameraShakePattern::UpdateShakePatternImpl(以LegacyCameraShake为例)->ULegacyCameraShake::DoUpdateShake

UCameraModifier_CameraShake::ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV)

注意一下InOutPOV这个参数。 FMinimalViewInfo保存了可以应用到相机上的各种参数,如坐标、旋转、FOV等。

在World的Tick中,每一帧都会尝试调用Modifier的ModifyCamera,并获取其执行的结果。

值得一提的是,ModifyCamera并不直接作用于相机,它只是将经过Modifier处理的参数通过InOutPOV引用传递了出来,在后续的其他模块中应用到相机上。

所以Modifier的工作原理是这样的

  1. 在外部(例如CameraManager)获取当前的POV信息
  2. 调用ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV),将POV信息作为参数传进去
  3. 根据不同的ModifyCamera实现,对InOutPOV进行调整,最后将调整的结果引用传参出来。
  4. 外部获取调整后的InOutPOV,在其他地方对相机应用调整过的InOutPOV

接下来具体看看CameraShake的Modifier做了什么:

bool UCameraModifier_CameraShake::ModifyCamera(float DeltaTime, FMinimalViewInfo& InOutPOV)
{
	// Call super where modifier may be disabled
	Super::ModifyCamera(DeltaTime, InOutPOV);

	// If no alpha, exit early
    // alpha可以理解为震动的整体强度
	if( Alpha <= 0.f )
	{
		return false;
	}

	// Update and apply active shakes
	if( ActiveShakes.Num() > 0 )
	{
		for (FActiveCameraShakeInfo& ShakeInfo : ActiveShakes)
		{
			if (ShakeInfo.ShakeInstance != nullptr)
			{
				// Compute the scale of this shake for this frame according to the location
				// of its source.
				float CurShakeAlpha = Alpha;
				if (ShakeInfo.ShakeSource.IsValid())
				{
					const UCameraShakeSourceComponent* SourceComponent = ShakeInfo.ShakeSource.Get();
                    // 根据振动源与相机的距离计算衰减系数
					const float AttenuationFactor = SourceComponent->GetAttenuationFactor(InOutPOV.Location);
					CurShakeAlpha *= AttenuationFactor;
				}
				// 实际修改InOutPOV的地方
				ShakeInfo.ShakeInstance->UpdateAndApplyCameraShake(DeltaTime, CurShakeAlpha, InOutPOV);
			}
		}

		// Delete any obsolete shakes
        // 删除所有过时的震动
		for (int32 i = ActiveShakes.Num() - 1; i >= 0; i--)
		{
			FActiveCameraShakeInfo ShakeInfo = ActiveShakes[i]; // Copy struct, we're going to maybe delete it.
			if (ShakeInfo.ShakeInstance == nullptr || ShakeInfo.ShakeInstance->IsFinished() || ShakeInfo.ShakeSource.IsStale())
			{
				if (ShakeInfo.ShakeInstance != nullptr)
				{
					ShakeInfo.ShakeInstance->TeardownShake();
				}

				ActiveShakes.RemoveAt(i, 1);
				UE_LOG(LogCameraShake, Verbose, TEXT("UCameraModifier_CameraShake::ModifyCamera Removing obsolete shake %s"), *GetNameSafe(ShakeInfo.ShakeInstance));

				SaveShakeInExpiredPoolIfPossible(ShakeInfo);
			}
		}
	}
	// If ModifyCamera returns true, exit loop
	// Allows high priority things to dictate if they are
	// the last modifier to be applied
	// Returning true causes to stop adding another modifier! 
	// Returning false is the right behavior since this is not high priority modifier.
	return false;
}

其中参数列表中的DeltaTime表示两次调用间隔的时间,震动实例会以这个时间来确定当前应该如何调整参数。

这个函数主要做了两件事情:

  1. 遍历ActiveShake数组,调用所有激活的震动实例的UpdateAndApplyCameraShake函数,修改InOutPOV
  2. 删除所有过期的震动

UpdateAndApplyCameraShake我们等下再讲。

在遍历数组的过程中,会根据相机与震动源的距离计算震动的衰减系数,从而影响到最终的结果。

值得一提的是,在此处的实现中,如果存在多个震动实例,它们会根据在数组中的先后顺序依次对InOutPOV进行修改,因此不同的震动实例会存在叠加的情况。

关于返回值,从ModifyCamera的调用处(PlayerCameraManager)可以看出,如果返回值为true,则不再执行后面其他的modifier:

void APlayerCameraManager::ApplyCameraModifiers(float DeltaTime, FMinimalViewInfo& InOutPOV)
{
	ClearCachedPPBlends();

	// Loop through each camera modifier
	for (int32 ModifierIdx = 0; ModifierIdx < ModifierList.Num(); ++ModifierIdx)
	{
		// Apply camera modification and output into DesiredCameraOffset/DesiredCameraRotation
		if ((ModifierList[ModifierIdx] != NULL) && !ModifierList[ModifierIdx]->IsDisabled())
		{
			// If ModifyCamera returns true, exit loop
			// Allows high priority things to dictate if they are
			// the last modifier to be applied
			if (ModifierList[ModifierIdx]->ModifyCamera(DeltaTime, InOutPOV))
			{
				break;
			}
		}
	}
}

这里简单的提一下,通常情况都是返回false。

UCameraShakeBase::UpdateAndApplyCameraShake(float DeltaTime, float Alpha, FMinimalViewInfo& InOutPOV)

void UCameraShakeBase::UpdateAndApplyCameraShake(float DeltaTime, float Alpha, FMinimalViewInfo& InOutPOV)
{

	// Update our state, and early out if we have gone over the duration of the shake.
	float BlendingWeight = State.Update(DeltaTime);
	if (!State.IsActive())
	{
		return;
	}

	// Make the sub-class do the actual work.
	FCameraShakeUpdateParams Params(InOutPOV);
	Params.DeltaTime = DeltaTime;
	Params.ShakeScale = ShakeScale;
	Params.DynamicScale = Alpha;
	Params.BlendingWeight = BlendingWeight;
	
	// Result object is initialized with zero values since the default flags make us handle it
	// as an additive offset.
    // Result是偏移量,最后会被加到InOutPov中
	FCameraShakeUpdateResult Result;

	if (RootShakePattern)
	{
		RootShakePattern->UpdateShakePattern(Params, Result);
	}

	// Apply the result to the given view info.
	FCameraShakeApplyResultParams ApplyParams;
	ApplyParams.Scale = Params.GetTotalScale();
	ApplyParams.PlaySpace = PlaySpace;
	ApplyParams.UserPlaySpaceMatrix = UserPlaySpaceMatrix;
	ApplyParams.CameraManager = CameraManager;
	ApplyResult(ApplyParams, Result, InOutPOV);
}

这个函数做了两件事:

  1. 计算相机的偏移(UpdateShakePattern),并将偏移量保存到Result中返回
  2. 应用偏移(ApplyResult),将偏移量应用到InOutPOV中

ULegacyCameraShake::DoUpdateShake(const FCameraShakeUpdateParams& Params, FCameraShakeUpdateResult& OutResult)

由于UpdateShakePatternImpl里直接原封不动地调用了DoUpdateShake,这里就不再赘述UpdateShakePattern相关的东西了。

实际上,这个函数并没有什么复杂的东西,就是根据delta时间更新各种参数,并计算出偏移值记录在OutResult中,有兴趣的可以自行翻阅源码细细品尝。

但有时候还是会好奇这底层的细节是怎样实现的,这里截取部分更新参数的片段,与诸位共享:

if (bOscillationFinished == false)
	{
		// calculate blend weight. calculating separately and taking the minimum handles overlapping blends nicely.
		float const BlendInWeight = (bBlendingIn) ? (CurrentBlendInTime / OscillationBlendInTime) : 1.f;
		float const BlendOutWeight = (bBlendingOut) ? (1.f - CurrentBlendOutTime / OscillationBlendOutTime) : 1.f;
		float const CurrentBlendWeight = FMath::Min(BlendInWeight, BlendOutWeight);

		// this is the oscillation scale, which includes oscillation fading
		// we'll apply the general shake scale, along with the current frame's dynamic scale, a bit later.
		float const OscillationScale = CurrentBlendWeight;

		if (OscillationScale > 0.f)
		{
			// View location offset, compute sin wave value for each component
			FVector	LocOffset = FVector(0);
			LocOffset.X = FFOscillator::UpdateOffset(LocOscillation.X, LocSinOffset.X, DeltaTime);
			LocOffset.Y = FFOscillator::UpdateOffset(LocOscillation.Y, LocSinOffset.Y, DeltaTime);
			LocOffset.Z = FFOscillator::UpdateOffset(LocOscillation.Z, LocSinOffset.Z, DeltaTime);
			LocOffset *= OscillationScale;

			OutResult.Location = LocOffset;
....

这个片段展示了根据Blend时间计算混合权值,然后根据DeltaTime计算坐标的偏移量,最终与权值相乘得到一个新的偏移坐标。

这里调用了FFOscillator::UpdateOffset,其内部使用了三角函数的曲线对坐标的偏移进行计算,具体的细节就不展示了,总之最后会得到一个当前偏移坐标。

UCameraShakeBase::ApplyResult(const FCameraShakeApplyResultParams& ApplyParams, const FCameraShakeUpdateResult& InResult, FMinimalViewInfo& InOutPOV)

在执行完DoUpdateShake后,我们获得了一个记录着参数偏移量的Result。

为了让Result发挥作用,在上文讲解的UpdateAndApplyCameraShake的最后,调用了UCameraShakeBase::ApplyResult,将这些偏移量应用到InOutPOV上面去,从而得到最终的结果。

这个函数并不是虚函数,意味着所有CameraShake最终都会调用到这个函数。

void UCameraShakeBase::ApplyResult(const FCameraShakeApplyResultParams& ApplyParams, const FCameraShakeUpdateResult& InResult, FMinimalViewInfo& InOutPOV)
{
	FCameraShakeUpdateResult TempResult(InResult);

	// If the sub-class gave us a delta-transform, we can help with some of the basic functionality
	// of a camera shake... namely: apply shake scaling, system limits, and play space transformation.
	if (!EnumHasAnyFlags(TempResult.Flags, ECameraShakeUpdateResultFlags::ApplyAsAbsolute))
	{
		if (!EnumHasAnyFlags(TempResult.Flags, ECameraShakeUpdateResultFlags::SkipAutoScale))
		{
			ApplyScale(ApplyParams.Scale, TempResult);
		}

		ApplyLimits(InOutPOV, TempResult);

		if (!EnumHasAnyFlags(TempResult.Flags, ECameraShakeUpdateResultFlags::SkipAutoPlaySpace))
		{
			ApplyPlaySpace(ApplyParams.PlaySpace, ApplyParams.UserPlaySpaceMatrix, InOutPOV, TempResult);
		}
	}

	// Now we can apply the shake to the camera matrix.
	if (EnumHasAnyFlags(TempResult.Flags, ECameraShakeUpdateResultFlags::ApplyAsAbsolute))
	{
		InOutPOV.Location = TempResult.Location;
		InOutPOV.Rotation = TempResult.Rotation;
		InOutPOV.FOV = TempResult.FOV;
	}
	else
	{
		InOutPOV.Location += TempResult.Location;
		InOutPOV.Rotation += TempResult.Rotation;
		InOutPOV.FOV += TempResult.FOV;
	}

	// It's weird but the post-process settings go directly on the camera manager, not on the view info.
	if (ApplyParams.CameraManager.IsValid() && TempResult.PostProcessBlendWeight > 0.f)
	{
		ApplyParams.CameraManager->AddCachedPPBlend(TempResult.PostProcessSettings, TempResult.PostProcessBlendWeight);
	}
}

通常情况下,我们只需要将偏移量与InOutPOV相加即可得到我们想要的结果。当然代码里还考虑到很多不同的选项,目前并不是我们考虑的重点。

下面是Apply过程中会调用到的函数,可以看到基本上都是基础的矩阵运算:

void FCameraAnimationHelper::ApplyOffset(const FMinimalViewInfo& InPOV, const FCameraAnimationHelperOffset& InOffset, FVector& OutLocation, FRotator& OutRotation)
{
	const FRotationMatrix CameraRot(InPOV.Rotation);
	const FRotationMatrix OffsetRot(InOffset.Rotation);

	// Apply translation offset in the camera's local space.
	OutLocation = InPOV.Location + CameraRot.TransformVector(InOffset.Location);

	// Apply rotation offset to camera's local orientation.
	OutRotation = (OffsetRot * CameraRot).Rotator();
}

到这里为止,震动过程的参数变化基本上就告一段落了。

再强调一遍,CameraModifier只是对相机会用到的参数进行了修改,并不直接对相机进行操作。之所以要这样强调,是为了说明CameraModifier并不一定要与PlayerCameraManager绑定使用,只要理解其原理,我们甚至可以在编辑器视口下调用CameraModifier。

结束震动

最后解决一个问题:震动实例是如何停止的?

其实在之前的代码分析中,我们就多次看到了停止相关的代码逻辑,比较重要的是在UCameraModifier_CameraShake::ModifyCamera函数中:

		// Delete any obsolete shakes
        // 删除所有过时的震动
		for (int32 i = ActiveShakes.Num() - 1; i >= 0; i--)
		{
			FActiveCameraShakeInfo ShakeInfo = ActiveShakes[i]; // Copy struct, we're going to maybe delete it.
			if (ShakeInfo.ShakeInstance == nullptr || ShakeInfo.ShakeInstance->IsFinished() || ShakeInfo.ShakeSource.IsStale())
			{
				if (ShakeInfo.ShakeInstance != nullptr)
				{
					ShakeInfo.ShakeInstance->TeardownShake();
				}

				ActiveShakes.RemoveAt(i, 1);
				UE_LOG(LogCameraShake, Verbose, TEXT("UCameraModifier_CameraShake::ModifyCamera Removing obsolete shake %s"), *GetNameSafe(ShakeInfo.ShakeInstance));

				SaveShakeInExpiredPoolIfPossible(ShakeInfo);
			}
		}

在ModifyCamera函数每次调用时,都会检查当前是否有已经结束的震动实例,如果有实例已经结束了,就会调用TeardownShake()函数进行一些收尾工作,并将其从ActiveShakes中移除,然后调用SaveShakeInExpiredPoolIfPossible尝试在之后复用这个震动实例。

那么如何判断实例已经结束了呢?

在UpdateAndApplyCameraShake函数中,会调用FCameraShakeState::Update(DeltaTime)检查震动实例是否超时,如果超时,就将其的bIsActive成员设置成false,这样在别的地方就可以很轻松的判断这个震动实例是否应该被删除掉了。

posted @ 2023-11-01 17:21  仇白  阅读(402)  评论(0编辑  收藏  举报