Altering Animation Results
Rukhanka animation calculation engine works as two-phase process:
- The animation calculation phase is performed by
AnimationProcessSystem
. - The animation application phase is performed by
AnimationApplicationSystem
.
Between the execution of these two systems, there is a point when animations are already calculated, but not applied to destination rigs yet. At this point any modifications of result animation data are possible, and performed changes will be applied to animated skeletons.
For convenience, a ComponentSystemGroup
named RukhankaAnimationInjectionSystemGroup
was created, and placed at this execution point. All systems willing to modify animation results should use the [UpdateInGroup(typeof(RukhankaAnimationInjectionSystemGroup))]
attribute at their declaration.
Bone Data Modification System
To show how animation data can be modified, we will make a simple modification system. Our goal will be to change the world position of specific bone with data contained in another component.
- Suppose, there is a component with required data:
struct BonePositionOverrideComponent: IComponentData
{
public int boneIndex; // We want to modify bone identified by this index
public float3 newWorldPose; // World position that should be given to target bone
}
- Create a system with proper declaration:
[UpdateInGroup(typeof(RukhankaAnimationInjectionSystemGroup))]
[RequireMatchingQueriesForUpdate]
public partial struct SimpleBonePositionOverride: ISystem
{
...
}
- Processing job needs operate on bone transform data. We will get it before job initialization, in
OnUpdate
function:
public void OnUpdate(ref SystemState ss)
{
...
ref var runtimeData = ref SystemAPI.GetSingletonRW<RuntimeAnimationData>().ValueRW;
...
}
- To access bone transform data of specific rig, a helper
AnimationStream
structure was introduced:
void Execute(Entity entity, RigDefinitionComponent rigDef) // Execute function of IJobEntity
{
...
using var animStream = AnimationStream.Create(runtimeData, entity, rigDef);
...
}
AnimationStream
has functions to get and set the world and local poses of every animated rig bone. We change signature ofIJobEntity.Execute
function to getBonePositionOverrideComponent
data, which is used as source for bone transform modifications:
void Execute(Entity entity, RigDefinitionComponent rigDef, BonePositionOverrideComponent bonePosOverride)
{
...
animStream.SetWorldPosition(bonePosOverride.boneIndex, bonePosOverride.newWorldPose);
...
}
Rukhanka
has delayed mechanism of all children's bone poses recalculation. DuringGet
calls required bone position data will be recalculated automatically. Any outdated bone data that is left at the end of theAnimationStream
object lifetime will be refreshed during theDispose()
call (or useusing
statement);
Final system and work job will look like:
struct BonePositionOverrideComponent: IComponentData
{
public int boneIndex;
public float3 newWorldPose;
}
/////////////////////////////////////////////////////////////////////////////////
[UpdateInGroup(typeof(RukhankaAnimationInjectionSystemGroup))]
[RequireMatchingQueriesForUpdate]
public partial struct SimpleBonePositionOverride: ISystem
{
[BurstCompile]
partial struct BonePositionOverrideJob : IJobEntity
{
[NativeDisableContainerSafetyRestriction]
public RuntimeAnimationData runtimeData;
void Execute(Entity entity, RigDefinitionComponent rigDef, BonePositionOverrideComponent bonePosOverride)
{
using var animStream = AnimationStream.Create(runtimeData, entity, rigDef);
animStream.SetWorldPosition(bonePosOverride.boneIndex, bonePosOverride.newWorldPose);
// Implicit animationStream.Dispose() call is here
}
}
/////////////////////////////////////////////////////////////////////////////////
[BurstCompile]
public void OnUpdate(ref SystemState ss)
{
ref var runtimeData = ref SystemAPI.GetSingletonRW<RuntimeAnimationData>().ValueRW;
var job = new BonePositionOverrideJob()
{
runtimeData = runtimeData,
};
job.ScheduleParallel();
}
}