Unreal Engine开发:虚拟摄像机开发_虚拟摄像机的动画与关键帧控制

虚拟摄像机的动画与关键帧控制

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 使用动画序列编辑器创建动画曲线

  1. 创建动画序列

    • 在内容浏览器(Content Browser)中右键点击,选择“动画”下的“创建动画序列”(Create Animation Sequence)。

    • 设置动画序列的名称和持续时间。

  2. 添加动画曲线

    • 打开创建的动画序列,进入动画序列编辑器。

    • 在“曲线编辑器”(Curve Editor)中点击“添加曲线”(Add Curve)按钮。

    • 选择曲线类型(如浮点曲线、向量曲线、旋转曲线)并命名。

  3. 编辑关键帧

    • 在时间轴上点击添加关键帧的位置。

    • 在曲线编辑器中设置关键帧的属性值。

    • 通过拖动关键帧来调整时间点。

2.2 使用蓝图创建动画曲线

  1. 创建蓝图

    • 在内容浏览器中右键点击,选择“蓝图类”(Blueprint Class)。

    • 选择一个合适的父类(如ActorCharacter)并命名。

  2. 添加动画曲线变量

    • 打开创建的蓝图,进入事件图表(Event Graph)。

    • 在变量列表中点击“添加变量”(Add Variable)按钮。

    • 选择“浮点曲线”(Float Curve)或其他类型的曲线,并命名。

  3. 设置关键帧

    • 在事件图表中添加“设置曲线关键帧”(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轴位置:

  1. 创建动画曲线变量

    • 在蓝图中添加一个浮点曲线变量,命名为CameraPositionX
  2. 设置关键帧

    • 在事件图表中添加“设置曲线关键帧”(Set Curve Key)节点。

    • 连接曲线变量和关键帧节点,设置关键帧的时间和值。

  3. 应用动画曲线

    • 在每一帧更新时,使用“获取曲线值”(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):

  1. 创建动画曲线变量

    • 在蓝图中添加一个浮点曲线变量,命名为CameraFOV
  2. 设置关键帧

    • 在事件图表中添加“设置曲线关键帧”(Set Curve Key)节点。

    • 连接曲线变量和关键帧节点,设置关键帧的时间和值。

  3. 应用动画曲线

    • 在每一帧更新时,使用“获取曲线值”(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 预计算动画曲线

预计算动画曲线可以在游戏启动时或加载时进行,将动画曲线的计算结果保存到内存中,以减少运行时的计算量。以下是一个蓝图示例,展示如何预计算动画曲线并将其保存到数组中:

  1. 创建数组变量

    • 在蓝图中添加一个浮点数组变量,命名为CachedPositionValues

    • 再添加一个浮点数组变量,命名为CachedRotationValues

  2. 预计算动画曲线

    • 在事件图表中添加一个“事件开始播放”(Event Begin Play)节点。

    • 在“事件开始播放”节点中调用“预计算曲线”(Precompute Curve)函数,将曲线值保存到数组中。

  3. 应用预计算结果

    • 在每一帧更新时,使用数组中的值来更新摄像机的位置和旋转。

// 包含必要的头文件

#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的官方文档或社区资源。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/chenlz2007/article/details/147031630
今日推荐