2021-10-22

本文详细解读了2021年的SC@SDUSC项目中的Animation类,涉及动画对象的属性、构造、加载与清空缓存,以及AnimationGraph和SkinnedModel的结构与操作。重点介绍了动画时长、帧率、关键帧计算及模型LOD管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2021SC@SDUSC

Class Animation 

Animation.h

  

API_CLASS(NoSpawn) class FLAXENGINE_API Animation : public BinaryAsset
{

本质是一个继承的二进制流对象。

/// Length of the animation in seconds.
/// </summary>
API_FIELD() float Length;

/// <summary>
/// Amount of animation frames (some curve tracks may use less keyframes).
/// </summary>
API_FIELD() int32 FramesCount;

/// <summary>
/// Amount of animation channel tracks.
/// </summary>
API_FIELD() int32 ChannelsCount;

乱七八糟一堆属性。

看文档。

构造器Animation()

Properties

Duration get动画的持续时间,以帧为单位 类型是single

FramesperSecond 返回动画的帧率 single类型

Info 返回动画片段的信息  类型是InfoData

Length get动画长度,以秒为单位 single

Methods

LoadTimeLine() 获取作为序列化时间线数据的动画。用于在编辑器中显示它。

SaveTimeline(Byte[]) 保存时间轴

参数就是时间线data的字节流,返回一个表示操作成功与否的布尔值

然后是Animation.cpp

ScopeLock lock(Locker);
InfoData info;
info.MemoryUsage = sizeof(Animation);
if (IsLoaded())
{
    info.Length = Data.GetLength();
    info.FramesCount = (int32)Data.Duration;
    info.ChannelsCount = Data.Channels.Count();
    info.KeyframesCount = Data.GetKeyframesCount();
    info.MemoryUsage += Data.Channels.Capacity() * sizeof(NodeAnimationData);
    for (auto& e : Data.Channels)
    {
        info.MemoryUsage += (e.NodeName.Length() + 1) * sizeof(Char);
        info.MemoryUsage += e.Position.GetKeyframes().Capacity() * sizeof(LinearCurveKeyframe<Vector3>);
        info.MemoryUsage += e.Rotation.GetKeyframes().Capacity() * sizeof(LinearCurveKeyframe<Quaternion>);
        info.MemoryUsage += e.Scale.GetKeyframes().Capacity() * sizeof(LinearCurveKeyframe<Vector3>);
    }
}

加载目标Animation的信息。如果还没有加载,全部置零。

void Animation::ClearCache()
{
    ScopeLock lock(Locker);

    // Unlink events
    for (auto i = MappingCache.Begin(); i.IsNotEnd(); ++i)
    {
        i->Key->OnUnloaded.Unbind<Animation, &Animation::OnSkinnedModelUnloaded>(this);
        i->Key->OnReloading.Unbind<Animation, &Animation::OnSkinnedModelUnloaded>(this);
    }

    // Free memory
    MappingCache.Clear();
    MappingCache.Cleanup();
}

动画清缓存;分为两步,第一步清理事件,第二步清理内存。

Asset::LoadResult Animation::load()

加载动画。

const auto dataChunk = GetChunk(0);
if (dataChunk == nullptr)
    return LoadResult::MissingDataChunk;
MemoryReadStream stream(dataChunk->Get(), dataChunk->Size());

拿到动画的数据流。

stream.ReadInt32(&headerVersion);
stream.ReadDouble(&Data.Duration);
stream.ReadDouble(&Data.FramesPerSecond);
Data.EnableRootMotion = stream.ReadBool();
stream.ReadString(&Data.RootNodeName, 13);

读动画的信息流。

if (Data.Duration < ZeroTolerance || Data.FramesPerSecond < ZeroTolerance)
{
    LOG(Warning, "Invalid animation info");
    return LoadResult::Failed;
}

读数据的时候,如果你的信息不合法,返回失败。

int32 animationsCount;
stream.ReadInt32(&animationsCount);
Data.Channels.Resize(animationsCount, false);
for (int32 i = 0; i < animationsCount; i++)
{
    auto& anim = Data.Channels[i];

    stream.ReadString(&anim.NodeName, 172);
    bool failed = Serialization::Deserialize(stream, anim.Position);
    failed |= Serialization::Deserialize(stream, anim.Rotation);
    failed |= Serialization::Deserialize(stream, anim.Scale);

    if (failed)
    {
        LOG(Warning, "Failed to deserialize the animation curve data.");
        return LoadResult::Failed;
    }
}

装载动画。对应报错。

void Animation::unload(bool isReloading)
{
    ClearCache();
    Data.Dispose();
}

卸载。

然后上面提到了Animation的infoData,跳过去看一下吧。

结构体:Animation.InfoData

Fields:

ChannelsCount

通道数,类型Int32

FramesCount

帧数,int32

KeyframesCount

关键帧计数,int32.这个关键帧在视频编码技术中常用。比如一个大容量的视频文件,如果直接剪辑切开,势必需要二次编码;而采用关键帧分割,就可以避免二次编码。

Length 长度。按秒计数。Single

MemoryUsage

总内存占用,int32

然后看一下文档:

Class AnimationGraph

动画图像,用于评估当前帧的动画模型的最终姿态。这个翻译挺费解的。后面再看看能不能搞清楚它。

构造器:AnimationGraph()

Properties:

BaseModel Gets the base model asset used for the animation preview and the skeleton layout source.

获取用于动画预览和骨架布局的基础模型。类型是SkinnedModel。

Class AnimationGraphFunction

动画的函数,包含动画中可重用的部分。

Methods

GetSignature(out String[], out String[])

获取 Visject Surface 编辑器的函数签名。两个参数,第一个是类型,第二个是名字。

LoadSurface()

加载图像

SaveSurface(Byte[])

更新表面(保存新图表,放弃缓存的数据,重新加载)

Asset::LoadResult AnimationGraphFunction::load()

整个的用来加载图像。

const auto surfaceChunk = GetChunk(0);
if (!surfaceChunk || !surfaceChunk->IsLoaded())
    return LoadResult::MissingDataChunk;
GraphData.Swap(surfaceChunk->Data);

从chunk加载图,如果丢失进入对应的事件。

MemoryReadStream stream(GraphData.Get(), GraphData.Length());
AnimGraph graph(this, true);
if (graph.Load(&stream, false))
    return LoadResult::Failed;

加载图。可以看到对应的方法和错误处理。

if (Inputs.Count() >= 16 || Outputs.Count() >= 16)
{
    LOG(Error, "Too many function inputs/outputs in '{0}'. The limit is max 16 inputs and max 16 outputs.", ToString());
}

输入控制。

void AnimationGraphFunction::unload(bool isReloading)
{
    GraphData.Release();
    Inputs.Clear();
    Outputs.Clear();
}

卸载。

Class SkinnedModel

蒙皮模型。包含网格构成的模型,可以使用骨骼蒙皮渲染。

构造器:SkinnedModel()

Fields

MaxBones 最大骨骼支架可允许数量 默认256  Int32

Properties

BlendShapes

获取蒙皮模型网格所使用的混合形状名称。返回一个字符串数组。

Bones 获取骨骼层级结构。直接返回一个骨骼类型数组。

跳过去看一下这个SkeletonBone。

结构体:SkeletonBone

描述单个骨骼数据。由运行时使用。也就是骨骼节点集合的子集。

Fields

LocalTransform

骨骼相对于父骨在绑定姿势上的局部转变。

类型是一个Transform。我们再跳进Transform看一下。

结构体:Transform

描述三维空间中的变形。

构造器就不看了。直接看Fields。

Fields:

Identity 一个默认的恒等变换。

Orientation 旋转。类型是Quaternion。再看Quaternion。

结构体:Quaternion

表示扭转的结构。按照俯仰,横向摆动,滚转的顺序(x,y,z)。

结束父类之后我们再细说这个Quaternion。先往回递归。

Scale 变换的比例向量。类型是vector3.

Sizeinbytes 大小,没什么好说的。类型int32

Translation translation的向量。这个翻译很模糊,如果不实际在编辑器里试一下,我就搞不懂它的作用了。Vector3类型。

Properties:

Backward 向后的向量。

Down  向下。

Forward 向前。

Isidentity get一个布尔值,看这个变换是不是恒等的。

Left 向左

Right 向右。

Up 向上。

上面这些全都是方向向量vector3

Methods:

Add(Transform, Transform)

把两个变形叠加。输入两个transform类型,输出一个transform。

Equals(transform)

给一个布尔值,判断两个变换是否恒等。

然后还有两个判断相等的函数,参数不同。

Gethashcode

拿到变换的哈希码。重写。

GetRotation 拿到旋转的矩阵(这个矩阵是从Orientation中导出的。)

GetWorld 拿到一个全局的四维矩阵,等价于这一变换。

Lerp(transform,transform,Single)

在两个transform之间执行一次线性插值。

LocalToWorld(Transform)

在局部空间中执行给定变换的变换。

然后下面是一大堆局部和全局、vector、矩阵、transform之间相互变换的函数。

NearEqual(Transform, Transform, Single)

得到一个布尔值,判断两个变换是否非严格地相似。

SetRotation(Matrix)

设置旋转。参数给一个矩阵。

Subtract(transform,transform)

两个变换相减。

之后下面是重写基本的几个函数,toString,然后是重载操作符。

NodeIndex

骨骼节点的索引。Int32

OffsetMatrix

绑定姿势中从网格空间变换到骨骼空间的矩阵。

ParentIndex

父骨骼的节点。根骨骼默认是-1.

LoadedLODS

获取获取加载的模型LOD。这个LoD是什么呢?我们往下看。

LODS

模型细节的等级。所以这个lod就是level of details的缩写。

那表示精细程度总得有个结构吧。

往下一看确实有。

Class SkinnedModelLOD

表示蒙皮模型的细节级别。

构造器:SkinnedModelLoD()

Properties:

Box

获取此模型 LOD 中所有网格的包围盒组合。类型是个BoundingBox。

进去看。

结构体:BoundingBox

表示三维空间中轴对齐的边界框。

构造器。BoundingBox(vector3,vector3)用两个向量构造。两个向量表示顶点的最大值、最小值。

后面的太多了,之后一起单独拿出来看。

Meshes

网格阵列。类型是Class SkinnedMesh。这个也后面拿出来看(因为相当于是另一个类了,太长了递归容易回不来)

ScreenSize

呈现此 LOD 的模型屏幕大小的下限。Single类型。

看一下SkinnedModel.cpp

   if (buffer->IsValidFor(this) == false) \
{ \
   LOG(Warning, "Invalid Skinned Model Instance Buffer size {0} for Skinned Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), ToString(), MaterialSlots.Count()); \
   buffer->Setup(this); \
}

判断是否有效。

BytesContainer data;
model->GetLODData(_lodIndex, data);
if (data.IsInvalid())
{
    LOG(Warning, "Missing data chunk");
    return true;
}
MemoryReadStream stream(data.Get(), data.Length());

// Note: this is running on thread pool task so we must be sure that updated LOD is not used at all (for rendering)

// Load model LOD (initialize vertex and index buffers)
if (model->LODs[_lodIndex].Load(stream))
{
    LOG(Warning, "Cannot load LOD{1} for model \'{0}\'", model->ToString(), _lodIndex);
    return true;
}

同样的错误判断。

Array<String> SkinnedModel::GetBlendShapes()
{
    Array<String> result;
    if (LODs.HasItems())
    {
        for (auto& mesh : LODs[0].Meshes)
        {
            for (auto& blendShape : mesh.BlendShapes)
            {
                if (!result.Contains(blendShape.Name))
                    result.Add(blendShape.Name);
            }
        }
    }
    return result;
}

上面看到过的。

BoundingBox SkinnedModel::GetBox(int32 lodIndex) const
{
    return LODs[lodIndex].GetBox();
}

拿边界箱。可以看到都是从LOD里面拿。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值