虚拟摄像机的动画与关键帧控制
1. 动画曲线与关键帧基础
在Unreal Engine中,虚拟摄像机的动画和关键帧控制是实现流畅、自然摄像机运动的关键技术。通过动画曲线和关键帧,可以精确控制摄像机在不同时间点的位置、旋转和其他属性。本节将介绍动画曲线和关键帧的基本概念,以及如何在Unreal Engine中使用它们来创建复杂的摄像机动画。
1.1 动画曲线
动画曲线(Animation Curves)是一种通过曲线来表示属性随时间变化的数据结构。在Unreal Engine中,动画曲线可以用于控制摄像机的平移、旋转、缩放等属性。动画曲线通常由一系列关键帧(Keyframes)组成,每个关键帧定义了特定时间点的属性值,曲线则通过插值算法在关键帧之间生成平滑的过渡。
1.2 关键帧
关键帧(Keyframes)是动画曲线上的特定时间点,每个关键帧包含一个或多个属性值。在Unreal Engine中,关键帧可以手动添加和编辑,也可以通过脚本自动生成。关键帧的添加和编辑可以通过动画序列编辑器(Animation Sequence Editor)或蓝图(Blueprint)来完成。
1.3 动画曲线的类型
Unreal Engine支持多种类型的动画曲线,包括但不限于:
-
浮点曲线(Float Curve):用于表示一个浮点数随时间的变化。
-
向量曲线(Vector Curve):用于表示一个向量(如位置、旋转)随时间的变化。
-
旋转曲线(Rotator Curve):用于表示一个旋转值随时间的变化。
1.4 动画曲线的插值
插值(Interpolation)是动画曲线中关键帧之间生成平滑过渡的过程。Unreal Engine提供了多种插值算法,包括线性插值(Linear Interpolation)、样条插值(Spline Interpolation)等。选择合适的插值算法可以显著影响动画的流畅性和自然效果。
2. 创建和编辑动画曲线
在Unreal Engine中,创建和编辑动画曲线可以通过以下步骤完成:
2.1 使用动画序列编辑器创建动画曲线
-
创建动画序列:
-
在内容浏览器(Content Browser)中右键点击,选择“动画”下的“创建动画序列”(Create Animation Sequence)。
-
设置动画序列的名称和持续时间。
-
-
添加动画曲线:
-
打开创建的动画序列,进入动画序列编辑器。
-
在“曲线编辑器”(Curve Editor)中点击“添加曲线”(Add Curve)按钮。
-
选择曲线类型(如浮点曲线、向量曲线、旋转曲线)并命名。
-
-
编辑关键帧:
-
在时间轴上点击添加关键帧的位置。
-
在曲线编辑器中设置关键帧的属性值。
-
通过拖动关键帧来调整时间点。
-
2.2 使用蓝图创建动画曲线
-
创建蓝图:
-
在内容浏览器中右键点击,选择“蓝图类”(Blueprint Class)。
-
选择一个合适的父类(如
Actor
、Character
)并命名。
-
-
添加动画曲线变量:
-
打开创建的蓝图,进入事件图表(Event Graph)。
-
在变量列表中点击“添加变量”(Add Variable)按钮。
-
选择“浮点曲线”(Float Curve)或其他类型的曲线,并命名。
-
-
设置关键帧:
-
在事件图表中添加“设置曲线关键帧”(Set Curve Key)节点。
-
连接曲线变量和关键帧节点,设置关键帧的时间和值。
-
2.3 代码示例:添加关键帧
以下是一个C++代码示例,展示如何在动画序列中添加关键帧:
// 包含必要的头文件
#include "Animation/AnimSequenceBase.h"
#include "Curves/CurveFloat.h"
#include "Engine/World.h"
#include "Kismet/KismetMathLibrary.h"
void AddKeyframesToAnimationSequence(UAnimSequenceBase* AnimSequence, const TArray<FFloatCurve::FKeyHandle>& KeyHandles)
{
// 检查动画序列是否有效
if (!IsValid(AnimSequence))
{
UE_LOG(LogTemp, Warning, TEXT("动画序列无效"));
return;
}
// 获取动画序列中的浮点曲线
FFloatCurve* FloatCurve = AnimSequence->FindOrAddFloatCurve("CameraPositionX");
// 添加关键帧
for (int32 i = 0; i < KeyHandles.Num(); ++i)
{
float Time = i * 0.5f; // 每个关键帧间隔0.5秒
float Value = i * 100.0f; // 每个关键帧的值递增100
// 添加关键帧
FFloatCurve::FKeyHandle KeyHandle = FloatCurve->AddKey(Time, Value);
// 输出关键帧信息
UE_LOG(LogTemp, Log, TEXT("添加关键帧: 时间 %f, 值 %f, 关键帧句柄 %d"), Time, Value, KeyHandle.GetKeyIndex());
}
// 保存动画序列
AnimSequence->MarkPackageDirty();
}
2.4 代码示例:编辑关键帧
以下是一个C++代码示例,展示如何编辑已存在的关键帧:
void EditKeyframesInAnimationSequence(UAnimSequenceBase* AnimSequence, const TArray<FFloatCurve::FKeyHandle>& KeyHandles)
{
// 检查动画序列是否有效
if (!IsValid(AnimSequence))
{
UE_LOG(LogTemp, Warning, TEXT("动画序列无效"));
return;
}
// 获取动画序列中的浮点曲线
FFloatCurve* FloatCurve = AnimSequence->FindFloatCurve("CameraPositionX");
// 检查曲线是否存在
if (!FloatCurve)
{
UE_LOG(LogTemp, Warning, TEXT("未找到曲线 CameraPositionX"));
return;
}
// 编辑关键帧
for (int32 i = 0; i < KeyHandles.Num(); ++i)
{
float NewTime = i * 0.75f; // 新的时间间隔0.75秒
float NewValue = i * 150.0f; // 新的值递增150
// 编辑关键帧
FloatCurve->SetKeyTime(KeyHandles[i], NewTime);
FloatCurve->SetKeyValue(KeyHandles[i], NewValue);
// 输出关键帧信息
UE_LOG(LogTemp, Log, TEXT("编辑关键帧: 时间 %f, 值 %f, 关键帧句柄 %d"), NewTime, NewValue, KeyHandles[i].GetKeyIndex());
}
// 保存动画序列
AnimSequence->MarkPackageDirty();
}
3. 动画曲线的应用
动画曲线可以应用于虚拟摄像机的多种属性,包括位置、旋转、缩放等。通过合理设置动画曲线,可以实现复杂的摄像机运动效果,如跟随角色移动、平滑过渡、镜头抖动等。
3.1 摄像机位置动画
通过动画曲线控制摄像机的位置,可以实现摄像机沿着预定路径移动的效果。以下是一个蓝图示例,展示如何使用浮点曲线控制摄像机的X轴位置:
-
创建动画曲线变量:
- 在蓝图中添加一个浮点曲线变量,命名为
CameraPositionX
。
- 在蓝图中添加一个浮点曲线变量,命名为
-
设置关键帧:
-
在事件图表中添加“设置曲线关键帧”(Set Curve Key)节点。
-
连接曲线变量和关键帧节点,设置关键帧的时间和值。
-
-
应用动画曲线:
-
在每一帧更新时,使用“获取曲线值”(Get Curve Value)节点获取当前时间点的X轴位置值。
-
将获取的值应用于摄像机的位置。
-
3.2 摄像机旋转动画
通过动画曲线控制摄像机的旋转,可以实现摄像机视角的变化。以下是一个C++代码示例,展示如何使用旋转曲线控制摄像机的旋转:
// 包含必要的头文件
#include "GameFramework/Actor.h"
#include "Curves/CurveRotator.h"
#include "Components/CameraComponent.h"
#include "TimerManager.h"
class AMyCamera : public AActor
{
public:
// 摄像机组件
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
UCameraComponent* CameraComponent;
// 旋转曲线
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
UCurveRotator* RotationCurve;
// 动画时间
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
float AnimationTime;
// 动画播放速度
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
float AnimationSpeed;
// 动画播放计时器
float CurrentTime;
// 开始播放动画
void StartAnimation();
// 每帧更新
virtual void Tick(float DeltaTime) override;
};
void AMyCamera::StartAnimation()
{
// 重置计时器
CurrentTime = 0.0f;
// 启动计时器
GetWorld()->GetTimerManager().SetTimer(AnimationTimer, this, &AMyCamera::Tick, 1.0f / AnimationSpeed, true);
}
void AMyCamera::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 更新计时器
CurrentTime += DeltaTime;
// 获取当前时间点的旋转值
FRotator CurrentRotation = RotationCurve->GetFloatValue(CurrentTime);
// 应用旋转值到摄像机
if (CameraComponent)
{
CameraComponent->SetWorldRotation(CurrentRotation);
}
// 检查是否动画结束
if (CurrentTime >= AnimationTime)
{
GetWorld()->GetTimerManager().ClearTimer(AnimationTimer);
}
}
3.3 摄像机缩放动画
通过动画曲线控制摄像机的缩放,可以实现摄像机的拉近和拉远效果。以下是一个蓝图示例,展示如何使用浮点曲线控制摄像机的FOV(Field of View):
-
创建动画曲线变量:
- 在蓝图中添加一个浮点曲线变量,命名为
CameraFOV
。
- 在蓝图中添加一个浮点曲线变量,命名为
-
设置关键帧:
-
在事件图表中添加“设置曲线关键帧”(Set Curve Key)节点。
-
连接曲线变量和关键帧节点,设置关键帧的时间和值。
-
-
应用动画曲线:
-
在每一帧更新时,使用“获取曲线值”(Get Curve Value)节点获取当前时间点的FOV值。
-
将获取的值应用于摄像机的FOV。
-
4. 动画曲线的插值算法
Unreal Engine提供了多种插值算法,选择合适的插值算法可以显著提升动画的流畅性和自然效果。以下是一些常见的插值算法及其应用场景:
4.1 线性插值
线性插值(Linear Interpolation)是最简单的插值算法,它在两个关键帧之间生成一条直线。线性插值适用于需要快速、直接过渡的场景。
4.2 样条插值
样条插值(Spline Interpolation)通过样条曲线在关键帧之间生成平滑的过渡。样条插值适用于需要平滑、自然过渡的场景,如摄像机的路径动画。
4.3 自定义插值
在某些情况下,可能需要自定义插值算法来实现特定的动画效果。以下是一个C++代码示例,展示如何实现自定义插值算法:
// 包含必要的头文件
#include "Curves/CurveFloat.h"
#include "Kismet/KismetMathLibrary.h"
float CustomInterpolation(float Time, float TimeA, float TimeB, float ValueA, float ValueB)
{
float t = FMath::Clamp((Time - TimeA) / (TimeB - TimeA), 0.0f, 1.0f);
// 自定义插值公式
float InterpolatedValue = FMath::Lerp(ValueA, ValueB, t * t * (3.0f - 2.0f * t));
return InterpolatedValue;
}
void ApplyCustomInterpolation(UAnimSequenceBase* AnimSequence, float CurrentTime, UCameraComponent* CameraComponent)
{
// 检查动画序列是否有效
if (!IsValid(AnimSequence))
{
UE_LOG(LogTemp, Warning, TEXT("动画序列无效"));
return;
}
// 获取动画序列中的浮点曲线
FFloatCurve* PositionCurve = AnimSequence->FindFloatCurve("CameraPositionX");
FFloatCurve* RotationCurve = AnimSequence->FindFloatCurve("CameraRotationY");
// 检查曲线是否存在
if (!PositionCurve || !RotationCurve)
{
UE_LOG(LogTemp, Warning, TEXT("未找到所需的曲线"));
return;
}
// 获取当前时间点的前后关键帧
FFloatCurve::FKeyHandle PrevPositionKey, NextPositionKey;
FFloatCurve::FKeyHandle PrevRotationKey, NextRotationKey;
PositionCurve->GetNearKeys(CurrentTime, PrevPositionKey, NextPositionKey);
RotationCurve->GetNearKeys(CurrentTime, PrevRotationKey, NextRotationKey);
// 获取前后关键帧的值
float PrevPositionValue = PositionCurve->GetKey(PrevPositionKey).Value;
float NextPositionValue = PositionCurve->GetKey(NextPositionKey).Value;
float PrevRotationValue = RotationCurve->GetKey(PrevRotationKey).Value;
float NextRotationValue = RotationCurve->GetKey(NextRotationKey).Value;
// 获取前后关键帧的时间
float PrevPositionTime = PositionCurve->GetKey(PrevPositionKey).Time;
float NextPositionTime = PositionCurve->GetKey(NextPositionKey).Time;
float PrevRotationTime = RotationCurve->GetKey(PrevRotationKey).Time;
float NextRotationTime = RotationCurve->GetKey(NextRotationKey).Time;
// 应用自定义插值算法
float InterpolatedPosition = CustomInterpolation(CurrentTime, PrevPositionTime, NextPositionTime, PrevPositionValue, NextPositionValue);
float InterpolatedRotation = CustomInterpolation(CurrentTime, PrevRotationTime, NextRotationTime, PrevRotationValue, NextRotationValue);
// 应用插值结果到摄像机
if (CameraComponent)
{
CameraComponent->SetWorldLocation(FVector(InterpolatedPosition, 0.0f, 0.0f));
CameraComponent->SetWorldRotation(FRotator(0.0f, InterpolatedRotation, 0.0f));
}
}
5. 动画曲线的优化
在开发虚拟摄像机的动画时,优化动画曲线的性能是非常重要的。以下是一些常见的优化技巧:
5.1 减少关键帧数量
过多的关键帧会增加计算负荷,影响性能。通过减少关键帧数量,可以在保持动画效果的同时提高性能。可以使用曲线编辑器中的“简化曲线”(Simplify Curve)功能来减少关键帧。
5.2 使用缓存
缓存动画曲线的计算结果可以显著提高性能。以下是一个C++代码示例,展示如何使用缓存来优化动画曲线的计算:
// 包含必要的头文件
#include "Curves/CurveFloat.h"
#include "Kismet/KismetMathLibrary.h"
#include "Engine/World.h"
class AMyCamera : public AActor
{
public:
// 摄像机组件
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
UCameraComponent* CameraComponent;
// 位置曲线
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
UCurveFloat* PositionCurve;
// 旋转曲线
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
UCurveFloat* RotationCurve;
// 动画时间
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
float AnimationTime;
// 动画播放速度
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
float AnimationSpeed;
// 动画播放计时器
float CurrentTime;
// 缓存关键帧
TArray<FFloatCurve::FKeyHandle> CachedPositionKeys;
TArray<FFloatCurve::FKeyHandle> CachedRotationKeys;
// 开始播放动画
void StartAnimation();
// 每帧更新
virtual void Tick(float DeltaTime) override;
// 缓存关键帧
void CacheKeyframes();
};
void AMyCamera::StartAnimation()
{
// 重置计时器
CurrentTime = 0.0f;
// 缓存关键帧
CacheKeyframes();
// 启动计时器
GetWorld()->GetTimerManager().SetTimer(AnimationTimer, this, &AMyCamera::Tick, 1.0f / AnimationSpeed, true);
}
void AMyCamera::CacheKeyframes()
{
// 检查曲线是否有效
if (!PositionCurve || !RotationCurve)
{
UE_LOG(LogTemp, Warning, TEXT("曲线无效"));
return;
}
// 缓存位置曲线的关键帧
for (int32 i = 0; i < PositionCurve->GetNumKeys(); ++i)
{
FFloatCurve::FKeyHandle KeyHandle = PositionCurve->GetKeyHandle(i);
CachedPositionKeys.Add(KeyHandle);
}
// 缓存旋转曲线的关键帧
for (int32 i = 0; i < RotationCurve->GetNumKeys(); ++i)
{
FFloatCurve::FKeyHandle KeyHandle = RotationCurve->GetKeyHandle(i);
CachedRotationKeys.Add(KeyHandle);
}
}
void AMyCamera::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 更新计时器
CurrentTime += DeltaTime;
// 获取当前时间点的前后关键帧
FFloatCurve::FKeyHandle PrevPositionKey, NextPositionKey;
FFloatCurve::FKeyHandle PrevRotationKey, NextRotationKey;
PositionCurve->GetNearKeys(CurrentTime, PrevPositionKey, NextPositionKey);
RotationCurve->GetNearKeys(CurrentTime, PrevRotationKey, NextRotationKey);
// 获取前后关键帧的值
float PrevPositionValue = PositionCurve->GetKey(PrevPositionKey).Value;
float NextPositionValue = PositionCurve->GetKey(NextPositionKey).Value;
float PrevRotationValue = RotationCurve->GetKey(PrevRotationKey).Value;
float NextRotationValue = RotationCurve->GetKey(NextRotationKey).Value;
// 获取前后关键帧的时间
float PrevPositionTime = PositionCurve->GetKey(PrevPositionKey).Time;
float NextPositionTime = PositionCurve->GetKey(NextPositionKey).Time;
float PrevRotationTime = RotationCurve->GetKey(PrevRotationKey).Time;
float NextRotationTime = RotationCurve->GetKey(NextRotationKey).Time;
// 应用自定义插值算法
float InterpolatedPosition = CustomInterpolation(CurrentTime, PrevPositionTime, NextPositionTime, PrevPositionValue, NextPositionValue);
float InterpolatedRotation = CustomInterpolation(CurrentTime, PrevRotationTime, NextRotationTime, PrevRotationValue, NextRotationValue);
// 应用插值结果到摄像机
if (CameraComponent)
{
CameraComponent->SetWorldLocation(FVector(InterpolatedPosition, 0.0f, 0.0f));
CameraComponent->SetWorldRotation(FRotator(0.0f, InterpolatedRotation, 0.0f));
}
// 检查是否动画结束
if (CurrentTime >= AnimationTime)
{
GetWorld()->GetTimerManager().ClearTimer(AnimationTimer);
}
}
float CustomInterpolation(float Time, float TimeA, float TimeB, float ValueA, float ValueB)
{
float t = FMath::Clamp((Time - TimeA) / (TimeB - TimeA), 0.0f, 1.0f);
// 自定义插值公式
float InterpolatedValue = FMath::Lerp(ValueA, ValueB, t * t * (3.0f - 2.0f * t));
return InterpolatedValue;
}
5.3 使用低分辨率曲线
在某些情况下,可以使用低分辨率的动画曲线来减少计算负荷。低分辨率曲线通过减少关键帧的数量来实现性能优化,但可能会牺牲一些动画的平滑度。可以通过曲线编辑器中的“简化曲线”功能来生成低分辨率曲线。
5.4 预计算动画曲线
预计算动画曲线可以在游戏启动时或加载时进行,将动画曲线的计算结果保存到内存中,以减少运行时的计算量。以下是一个蓝图示例,展示如何预计算动画曲线并将其保存到数组中:
-
创建数组变量:
-
在蓝图中添加一个浮点数组变量,命名为
CachedPositionValues
。 -
再添加一个浮点数组变量,命名为
CachedRotationValues
。
-
-
预计算动画曲线:
-
在事件图表中添加一个“事件开始播放”(Event Begin Play)节点。
-
在“事件开始播放”节点中调用“预计算曲线”(Precompute Curve)函数,将曲线值保存到数组中。
-
-
应用预计算结果:
- 在每一帧更新时,使用数组中的值来更新摄像机的位置和旋转。
// 包含必要的头文件
#include "Curves/CurveFloat.h"
#include "Kismet/KismetMathLibrary.h"
#include "Engine/World.h"
class AMyCamera : public AActor
{
public:
// 摄像机组件
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
UCameraComponent* CameraComponent;
// 位置曲线
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
UCurveFloat* PositionCurve;
// 旋转曲线
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
UCurveFloat* RotationCurve;
// 动画时间
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
float AnimationTime;
// 动画播放速度
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
float AnimationSpeed;
// 动画播放计时器
float CurrentTime;
// 缓存的动画值
TArray<float> CachedPositionValues;
TArray<float> CachedRotationValues;
// 开始播放动画
void StartAnimation();
// 每帧更新
virtual void Tick(float DeltaTime) override;
// 预计算动画曲线
void PrecomputeCurves();
};
void AMyCamera::BeginPlay()
{
Super::BeginPlay();
// 预计算动画曲线
PrecomputeCurves();
}
void AMyCamera::PrecomputeCurves()
{
// 检查曲线是否有效
if (!PositionCurve || !RotationCurve)
{
UE_LOG(LogTemp, Warning, TEXT("曲线无效"));
return;
}
// 预计算位置曲线
for (float Time = 0.0f; Time <= AnimationTime; Time += 1.0f / AnimationSpeed)
{
float PositionValue = PositionCurve->GetFloatValue(Time);
CachedPositionValues.Add(PositionValue);
}
// 预计算旋转曲线
for (float Time = 0.0f; Time <= AnimationTime; Time += 1.0f / AnimationSpeed)
{
float RotationValue = RotationCurve->GetFloatValue(Time);
CachedRotationValues.Add(RotationValue);
}
}
void AMyCamera::StartAnimation()
{
// 重置计时器
CurrentTime = 0.0f;
// 启动计时器
GetWorld()->GetTimerManager().SetTimer(AnimationTimer, this, &AMyCamera::Tick, 1.0f / AnimationSpeed, true);
}
void AMyCamera::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 更新计时器
CurrentTime += DeltaTime;
// 获取当前时间点的索引
int32 Index = FMath::Clamp(int32(CurrentTime * AnimationSpeed), 0, CachedPositionValues.Num() - 1);
// 应用预计算结果到摄像机
if (CameraComponent && CachedPositionValues.IsValidIndex(Index) && CachedRotationValues.IsValidIndex(Index))
{
float InterpolatedPosition = CachedPositionValues[Index];
float InterpolatedRotation = CachedRotationValues[Index];
CameraComponent->SetWorldLocation(FVector(InterpolatedPosition, 0.0f, 0.0f));
CameraComponent->SetWorldRotation(FRotator(0.0f, InterpolatedRotation, 0.0f));
}
// 检查是否动画结束
if (CurrentTime >= AnimationTime)
{
GetWorld()->GetTimerManager().ClearTimer(AnimationTimer);
}
}
5.5 使用多线程
在高负载的场景中,可以考虑使用多线程来计算动画曲线。多线程可以将计算任务分配到多个CPU核心上,从而提高性能。以下是一个C++代码示例,展示如何使用多线程来计算动画曲线:
// 包含必要的头文件
#include "Curves/CurveFloat.h"
#include "Kismet/KismetMathLibrary.h"
#include "Engine/World.h"
#include "Async/TaskGraphInterfaces.h"
class AMyCamera : public AActor
{
public:
// 摄像机组件
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
UCameraComponent* CameraComponent;
// 位置曲线
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
UCurveFloat* PositionCurve;
// 旋转曲线
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
UCurveFloat* RotationCurve;
// 动画时间
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
float AnimationTime;
// 动画播放速度
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
float AnimationSpeed;
// 动画播放计时器
float CurrentTime;
// 缓存的动画值
TArray<float> CachedPositionValues;
TArray<float> CachedRotationValues;
// 开始播放动画
void StartAnimation();
// 每帧更新
virtual void Tick(float DeltaTime) override;
// 预计算动画曲线
void PrecomputeCurves();
};
void AMyCamera::BeginPlay()
{
Super::BeginPlay();
// 在多线程中预计算动画曲线
FGraphEventRef Task = FFunctionGraphTask::CreateAndDispatchWhenReady([this]()
{
PrecomputeCurves();
}, TStatId(), nullptr, ENamedThreads::AnyBackgroundThreadNormalTask);
FFunctionGraphTask::CreateAndDispatchWhenReady([this, Task]()
{
// 等待预计算任务完成
Task->Wait();
// 开始播放动画
StartAnimation();
}, TStatId(), nullptr, ENamedThreads::GameThread);
}
void AMyCamera::PrecomputeCurves()
{
// 检查曲线是否有效
if (!PositionCurve || !RotationCurve)
{
UE_LOG(LogTemp, Warning, TEXT("曲线无效"));
return;
}
// 预计算位置曲线
for (float Time = 0.0f; Time <= AnimationTime; Time += 1.0f / AnimationSpeed)
{
float PositionValue = PositionCurve->GetFloatValue(Time);
CachedPositionValues.Add(PositionValue);
}
// 预计算旋转曲线
for (float Time = 0.0f; Time <= AnimationTime; Time += 1.0f / AnimationSpeed)
{
float RotationValue = RotationCurve->GetFloatValue(Time);
CachedRotationValues.Add(RotationValue);
}
}
void AMyCamera::StartAnimation()
{
// 重置计时器
CurrentTime = 0.0f;
// 启动计时器
GetWorld()->GetTimerManager().SetTimer(AnimationTimer, this, &AMyCamera::Tick, 1.0f / AnimationSpeed, true);
}
void AMyCamera::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 更新计时器
CurrentTime += DeltaTime;
// 获取当前时间点的索引
int32 Index = FMath::Clamp(int32(CurrentTime * AnimationSpeed), 0, CachedPositionValues.Num() - 1);
// 应用预计算结果到摄像机
if (CameraComponent && CachedPositionValues.IsValidIndex(Index) && CachedRotationValues.IsValidIndex(Index))
{
float InterpolatedPosition = CachedPositionValues[Index];
float InterpolatedRotation = CachedRotationValues[Index];
CameraComponent->SetWorldLocation(FVector(InterpolatedPosition, 0.0f, 0.0f));
CameraComponent->SetWorldRotation(FRotator(0.0f, InterpolatedRotation, 0.0f));
}
// 检查是否动画结束
if (CurrentTime >= AnimationTime)
{
GetWorld()->GetTimerManager().ClearTimer(AnimationTimer);
}
}
6. 总结
虚拟摄像机的动画和关键帧控制是Unreal Engine中实现流畅、自然摄像机运动的关键技术。通过动画曲线和关键帧,可以精确控制摄像机在不同时间点的位置、旋转和其他属性。本教程介绍了动画曲线和关键帧的基本概念,以及如何在Unreal Engine中使用它们来创建复杂的摄像机动画。此外,还探讨了多种插值算法及其应用场景,以及如何通过减少关键帧数量、使用缓存、预计算和多线程等技术来优化动画曲线的性能。
希望这些内容能够帮助你在Unreal Engine中更好地实现和优化虚拟摄像机的动画效果。如果你有任何问题或需要进一步的帮助,请参考Unreal Engine的官方文档或社区资源。