Add **Animation Events**

This commit is contained in:
Wojtek Figat
2022-01-08 15:06:02 +01:00
parent 79200a784b
commit 82a43dea28
11 changed files with 720 additions and 3 deletions

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#include "AnimEvent.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Scripting/ManagedSerialization.h"
#include "Engine/Serialization/SerializationFwd.h"
#include "Engine/Serialization/Serialization.h"
AnimEvent::AnimEvent(const SpawnParams& params)
: ScriptingObject(params)
{
}
void AnimEvent::Serialize(SerializeStream& stream, const void* otherObj)
{
SERIALIZE_GET_OTHER_OBJ(AnimEvent);
#if !COMPILE_WITHOUT_CSHARP
// Handle C# objects data serialization
if (Flags & ObjectFlags::IsManagedType)
{
stream.JKEY("V");
if (other)
{
ManagedSerialization::SerializeDiff(stream, GetOrCreateManagedInstance(), other->GetOrCreateManagedInstance());
}
else
{
ManagedSerialization::Serialize(stream, GetOrCreateManagedInstance());
}
}
#endif
// Handle custom scripting objects data serialization
if (Flags & ObjectFlags::IsCustomScriptingType)
{
stream.JKEY("D");
_type.Module->SerializeObject(stream, this, other);
}
}
void AnimEvent::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
#if !COMPILE_WITHOUT_CSHARP
// Handle C# objects data serialization
if (Flags & ObjectFlags::IsManagedType)
{
auto* const v = SERIALIZE_FIND_MEMBER(stream, "V");
if (v != stream.MemberEnd() && v->value.IsObject() && v->value.MemberCount() != 0)
{
ManagedSerialization::Deserialize(v->value, GetOrCreateManagedInstance());
}
}
#endif
// Handle custom scripting objects data serialization
if (Flags & ObjectFlags::IsCustomScriptingType)
{
auto* const v = SERIALIZE_FIND_MEMBER(stream, "D");
if (v != stream.MemberEnd() && v->value.IsObject() && v->value.MemberCount() != 0)
{
_type.Module->DeserializeObject(v->value, this, modifier);
}
}
}
AnimContinuousEvent::AnimContinuousEvent(const SpawnParams& params)
: AnimEvent(params)
{
}

View File

@@ -0,0 +1,72 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ScriptingObject.h"
#include "Engine/Core/ISerializable.h"
#if USE_EDITOR
#include "Engine/Core/Math/Color.h"
#endif
class AnimatedModel;
class Animation;
/// <summary>
/// The animation notification event triggered during animation playback.
/// </summary>
API_CLASS(Abstract) class FLAXENGINE_API AnimEvent : public ScriptingObject, public ISerializable
{
DECLARE_SCRIPTING_TYPE(AnimEvent);
#if USE_EDITOR
/// <summary>
/// Event display color in the Editor.
/// </summary>
API_FIELD(Attributes="HideInEditor, NoSerialize") Color Color = Color::White;
#endif
/// <summary>
/// Animation event notification.
/// </summary>
/// <param name="actor">The animated model actor instance.</param>
/// <param name="anim">The source animation.</param>
/// <param name="time">The current animation time (in seconds).</param>
/// <param name="deltaTime">The current animation tick delta time (in seconds).</param>
API_FUNCTION() virtual void OnEvent(AnimatedModel* actor, Animation* anim, float time, float deltaTime)
{
}
// [ISerializable]
void Serialize(SerializeStream& stream, const void* otherObj) override;
void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
};
/// <summary>
/// The animation notification event (with duration) triggered during animation playback that contains begin and end (event notification is received as a tick).
/// </summary>
API_CLASS(Abstract) class FLAXENGINE_API AnimContinuousEvent : public AnimEvent
{
DECLARE_SCRIPTING_TYPE(AnimContinuousEvent);
/// <summary>
/// Animation notification called before the first event.
/// </summary>
/// <param name="actor">The animated model actor instance.</param>
/// <param name="anim">The source animation.</param>
/// <param name="time">The current animation time (in seconds).</param>
/// <param name="deltaTime">The current animation tick delta time (in seconds).</param>
API_FUNCTION() virtual void OnBegin(AnimatedModel* actor, Animation* anim, float time, float deltaTime)
{
}
/// <summary>
/// Animation notification called after the last event (guaranteed to be always called).
/// </summary>
/// <param name="actor">The animated model actor instance.</param>
/// <param name="anim">The source animation.</param>
/// <param name="time">The current animation time (in seconds).</param>
/// <param name="deltaTime">The current animation tick delta time (in seconds).</param>
API_FUNCTION() virtual void OnEnd(AnimatedModel* actor, Animation* anim, float time, float deltaTime)
{
}
};

View File

@@ -2,6 +2,7 @@
#include "AnimGraph.h"
#include "Engine/Animations/Animations.h"
#include "Engine/Animations/AnimEvent.h"
#include "Engine/Content/Assets/SkinnedModel.h"
#include "Engine/Graphics/Models/SkeletonData.h"
#include "Engine/Scripting/Scripting.h"
@@ -91,6 +92,9 @@ void AnimGraphInstanceData::Clear()
State.Resize(0);
NodesPose.Resize(0);
Slots.Resize(0);
for (const auto& e : Events)
((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f);
Events.Resize(0);
}
void AnimGraphInstanceData::ClearState()
@@ -103,6 +107,9 @@ void AnimGraphInstanceData::ClearState()
State.Resize(0);
NodesPose.Resize(0);
Slots.Clear();
for (const auto& e : Events)
((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)Object, e.Anim, 0.0f, 0.0f);
Events.Clear();
}
void AnimGraphInstanceData::Invalidate()
@@ -246,6 +253,8 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
// Initialize buckets
ResetBuckets(context, &_graph);
}
for (auto& e : data.Events)
e.Hit = false;
// Init empty nodes data
context.EmptyNodes.RootMotion = RootMotionData::Identity;
@@ -279,6 +288,19 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
if (animResult == nullptr)
animResult = GetEmptyNodes();
}
if (data.Events.Count() != 0)
{
ANIM_GRAPH_PROFILE_EVENT("Events");
for (int32 i = data.Events.Count() - 1; i >= 0; i--)
{
const auto& e = data.Events[i];
if (!e.Hit)
{
((AnimContinuousEvent*)e.Instance)->OnEnd((AnimatedModel*)context.Data->Object, e.Anim, 0.0f, 0.0f);
data.Events.RemoveAt(i);
}
}
}
// Allow for external override of the local pose (eg. by the ragdoll)
data.LocalPoseOverride(animResult);

View File

@@ -16,6 +16,7 @@
#define ANIM_GRAPH_MULTI_BLEND_2D_MAX_TRIS 32
#define ANIM_GRAPH_MAX_STATE_TRANSITIONS 64
#define ANIM_GRAPH_MAX_CALL_STACK 100
#define ANIM_GRAPH_MAX_EVENTS 64
class AnimGraph;
class AnimSubGraph;
@@ -267,6 +268,7 @@ struct FLAXENGINE_API AnimGraphSlot
/// </summary>
class FLAXENGINE_API AnimGraphInstanceData
{
friend AnimGraphExecutor;
public:
// ---- Quick documentation ----
@@ -402,6 +404,18 @@ public:
/// Invalidates the update timer.
/// </summary>
void Invalidate();
private:
struct Event
{
AnimEvent* Instance;
Animation* Anim;
AnimGraphNode* Node;
bool Hit;
};
Array<Event, InlinedAllocation<8>> Events;
};
/// <summary>

View File

@@ -5,7 +5,9 @@
#include "Engine/Content/Assets/SkeletonMask.h"
#include "Engine/Content/Assets/AnimationGraphFunction.h"
#include "Engine/Animations/AlphaBlend.h"
#include "Engine/Animations/AnimEvent.h"
#include "Engine/Animations/InverseKinematics.h"
#include "Engine/Level/Actors/AnimatedModel.h"
namespace
{
@@ -146,6 +148,7 @@ Variant AnimGraphExecutor::SampleAnimation(AnimGraphNode* node, bool loop, float
if (anim == nullptr || !anim->IsLoaded())
return Value::Null;
PROFILE_CPU_ASSET(anim);
const float oldTimePos = prevTimePos;
// Calculate actual time position within the animation node (defined by length and loop mode)
const float pos = GetAnimPos(newTimePos, startTimePos, loop, length);
@@ -180,6 +183,81 @@ Variant AnimGraphExecutor::SampleAnimation(AnimGraphNode* node, bool loop, float
ExtractRootMotion(mapping, rootNodeIndex, anim, animPos, animPrevPos, nodes->Nodes[rootNodeIndex], nodes->RootMotion);
}
// Collect events
if (anim->Events.Count() != 0)
{
ANIM_GRAPH_PROFILE_EVENT("Events");
auto& context = Context.Get();
float eventTimeMin = animPrevPos;
float eventTimeMax = animPos;
if (loop)
{
// Check if animation looped
const float posNotLooped = startTimePos + oldTimePos;
if (posNotLooped < 0.0f || posNotLooped > length)
{
if (context.DeltaTime * speed < 0)
{
// Playback backwards
Swap(eventTimeMin, eventTimeMax);
}
}
}
const float eventTime = animPos / static_cast<float>(anim->Data.FramesPerSecond);
const float eventDeltaTime = (animPos - animPrevPos) / static_cast<float>(anim->Data.FramesPerSecond);
for (const auto& track : anim->Events)
{
for (const auto& k : track.Second.GetKeyframes())
{
if (!k.Value.Instance)
continue;
const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f;
if (k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration)
{
int32 stateIndex = -1;
if (duration > 1)
{
// Begin for continuous event
for (stateIndex = 0; stateIndex < context.Data->Events.Count(); stateIndex++)
{
const auto& e = context.Data->Events[stateIndex];
if (e.Instance == k.Value.Instance && e.Node == node)
break;
}
if (stateIndex == context.Data->Events.Count())
{
auto& e = context.Data->Events.AddOne();
e.Instance = k.Value.Instance;
e.Anim = anim;
e.Node = node;
ASSERT(k.Value.Instance->Is<AnimContinuousEvent>());
((AnimContinuousEvent*)k.Value.Instance)->OnBegin((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
}
}
// Event
k.Value.Instance->OnEvent((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
if (stateIndex != -1)
context.Data->Events[stateIndex].Hit = true;
}
else if (duration > 1)
{
// End for continuous event
for (int32 i = 0; i < context.Data->Events.Count(); i++)
{
const auto& e = context.Data->Events[i];
if (e.Instance == k.Value.Instance && e.Node == node)
{
((AnimContinuousEvent*)k.Value.Instance)->OnEnd((AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime);
context.Data->Events.RemoveAt(i);
break;
}
}
}
}
}
}
return nodes;
}

View File

@@ -42,6 +42,7 @@ public:
CameraCut = 16,
//AnimationChannel = 17,
//AnimationChannelData = 18,
//AnimationEvent = 19,
};
enum class Flags

View File

@@ -6,10 +6,13 @@
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Animations/CurveSerialization.h"
#include "Engine/Animations/AnimEvent.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Serialization/MemoryReadStream.h"
#if USE_EDITOR
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Level/Level.h"
#endif
REGISTER_BINARY_ASSET(Animation, "FlaxEngine.Animation", false);
@@ -19,6 +22,21 @@ Animation::Animation(const SpawnParams& params, const AssetInfo* info)
{
}
#if USE_EDITOR
void Animation::OnScriptsReloadStart()
{
for (auto& e : Events)
{
for (auto& k : e.Second.GetKeyframes())
{
Level::ScriptsReloadRegisterObject((ScriptingObject*&)k.Value.Instance);
}
}
}
#endif
Animation::InfoData Animation::GetInfo() const
{
ScopeLock lock(Locker);
@@ -127,7 +145,7 @@ void Animation::LoadTimeline(BytesContainer& result) const
const float fpsInv = 1.0f / fps;
stream.WriteFloat(fps);
stream.WriteInt32((int32)Data.Duration);
int32 tracksCount = Data.Channels.Count();
int32 tracksCount = Data.Channels.Count() + Events.Count();
for (auto& channel : Data.Channels)
tracksCount +=
(channel.Position.GetKeyframes().HasItems() ? 1 : 0) +
@@ -214,6 +232,24 @@ void Animation::LoadTimeline(BytesContainer& result) const
trackIndex++;
}
}
for (auto& e : Events)
{
// Animation Event track
stream.WriteByte(19); // Track Type
stream.WriteByte(0); // Track Flags
stream.WriteInt32(-1); // Parent Index
stream.WriteInt32(0); // Children Count
stream.WriteString(e.First, -13); // Name
stream.Write(&Color32::White); // Color
stream.WriteInt32(e.Second.GetKeyframes().Count()); // Events Count
for (const auto& k : e.Second.GetKeyframes())
{
stream.WriteFloat(k.Time);
stream.WriteFloat(k.Value.Duration);
stream.WriteStringAnsi(k.Value.TypeName, 13);
stream.WriteJson(k.Value.Instance);
}
}
result.Copy(stream.GetHandle(), stream.GetPosition());
}
@@ -253,6 +289,7 @@ bool Animation::SaveTimeline(BytesContainer& data)
// Tracks
Data.Channels.Clear();
Events.Clear();
Dictionary<int32, int32> animationChannelTrackIndexToChannelIndex;
animationChannelTrackIndexToChannelIndex.EnsureCapacity(tracksCount * 3);
for (int32 trackIndex = 0; trackIndex < tracksCount; trackIndex++)
@@ -325,6 +362,36 @@ bool Animation::SaveTimeline(BytesContainer& data)
}
break;
}
case 19:
{
// Animation Event
int32 count;
stream.ReadInt32(&count);
auto& eventTrack = Events.AddOne();
eventTrack.First = name;
eventTrack.Second.Resize(count);
for (int32 i = 0; i < count; i++)
{
auto& k = eventTrack.Second.GetKeyframes()[i];
stream.ReadFloat(&k.Time);
stream.ReadFloat(&k.Value.Duration);
stream.ReadStringAnsi(&k.Value.TypeName, 13);
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(k.Value.TypeName);
k.Value.Instance = NewObject<AnimEvent>(typeHandle);
stream.ReadJson(k.Value.Instance);
if (!k.Value.Instance)
{
LOG(Error, "Failed to spawn object of type {0}.", String(k.Value.TypeName));
continue;
}
if (!_registeredForScriptingReload)
{
_registeredForScriptingReload = true;
Level::ScriptsReloadStart.Bind<Animation, &Animation::OnScriptsReloadStart>(this);
}
}
break;
}
default:
LOG(Error, "Unsupported track type {0} for animation.", trackType);
return true;
@@ -364,7 +431,7 @@ bool Animation::Save(const StringView& path)
MemoryWriteStream stream(4096);
// Info
stream.WriteInt32(100);
stream.WriteInt32(101);
stream.WriteDouble(Data.Duration);
stream.WriteDouble(Data.FramesPerSecond);
stream.WriteBool(Data.EnableRootMotion);
@@ -381,6 +448,22 @@ bool Animation::Save(const StringView& path)
Serialization::Serialize(stream, anim.Scale);
}
// Animation events
stream.WriteInt32(Events.Count());
for (int32 i = 0; i < Events.Count(); i++)
{
auto& e = Events[i];
stream.WriteString(e.First, 172);
stream.WriteInt32(e.Second.GetKeyframes().Count());
for (const auto& k : e.Second.GetKeyframes())
{
stream.WriteFloat(k.Time);
stream.WriteFloat(k.Value.Duration);
stream.WriteStringAnsi(k.Value.TypeName, 17);
stream.WriteJson(k.Value.Instance);
}
}
// Set data to the chunk asset
auto chunk0 = GetOrCreateChunk(0);
ASSERT(chunk0 != nullptr);
@@ -432,6 +515,7 @@ Asset::LoadResult Animation::load()
switch (headerVersion)
{
case 100:
case 101:
{
stream.ReadInt32(&headerVersion);
stream.ReadDouble(&Data.Duration);
@@ -471,13 +555,72 @@ Asset::LoadResult Animation::load()
}
}
// Animation events
if (headerVersion >= 101)
{
int32 eventTracksCount;
stream.ReadInt32(&eventTracksCount);
Events.Resize(eventTracksCount, false);
#if !USE_EDITOR
StringAnsi typeName;
#endif
for (int32 i = 0; i < eventTracksCount; i++)
{
auto& e = Events[i];
stream.ReadString(&e.First, 172);
int32 eventsCount;
stream.ReadInt32(&eventsCount);
e.Second.GetKeyframes().Resize(eventsCount);
for (auto& k : e.Second.GetKeyframes())
{
stream.ReadFloat(&k.Time);
stream.ReadFloat(&k.Value.Duration);
#if USE_EDITOR
StringAnsi& typeName = k.Value.TypeName;
#endif
stream.ReadStringAnsi(&typeName, 17);
const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeName);
k.Value.Instance = NewObject<AnimEvent>(typeHandle);
stream.ReadJson(k.Value.Instance);
if (!k.Value.Instance)
{
LOG(Error, "Failed to spawn object of type {0}.", String(typeName));
continue;
}
#if USE_EDITOR
if (!_registeredForScriptingReload)
{
_registeredForScriptingReload = true;
Level::ScriptsReloadStart.Bind<Animation, &Animation::OnScriptsReloadStart>(this);
}
#endif
}
}
}
return LoadResult::Ok;
}
void Animation::unload(bool isReloading)
{
#if USE_EDITOR
if (_registeredForScriptingReload)
{
_registeredForScriptingReload = false;
Level::ScriptsReloadStart.Unbind<Animation, &Animation::OnScriptsReloadStart>(this);
}
#endif
ClearCache();
Data.Dispose();
for (const auto& e : Events)
{
for (const auto& k : e.Second.GetKeyframes())
{
if (k.Value.Instance)
Delete(k.Value.Instance);
}
}
Events.Clear();
}
AssetChunksFlag Animation::getChunksToPreload() const

View File

@@ -7,6 +7,7 @@
#include "Engine/Animations/AnimationData.h"
class SkinnedModel;
class AnimEvent;
/// <summary>
/// Asset that contains an animation spline represented by a set of keyframes, each representing an endpoint of a linear curve.
@@ -48,6 +49,25 @@ DECLARE_BINARY_ASSET_HEADER(Animation, 1);
API_FIELD() int32 MemoryUsage;
};
/// <summary>
/// Contains <see cref="AnimEvent"/> instance.
/// </summary>
struct FLAXENGINE_API AnimEventData
{
float Duration = 0.0f;
AnimEvent* Instance = nullptr;
#if USE_EDITOR
StringAnsi TypeName;
#endif
};
private:
#if USE_EDITOR
bool _registeredForScriptingReload = false;
void OnScriptsReloadStart();
#endif
public:
/// <summary>
@@ -55,6 +75,11 @@ public:
/// </summary>
AnimationData Data;
/// <summary>
/// The animation events (keyframes per named track).
/// </summary>
Array<Pair<String, StepCurve<AnimEventData>>> Events;
/// <summary>
/// Contains the mapping for every skeleton node to the animation data channels.
/// Can be used for a simple lookup or to check if a given node is animated (unused nodes are using -1 index).