Skip to main content

Altering Animation Results

Rukhanka animation calculation engine works as two-phase process:

  1. The animation calculation phase is performed by AnimationProcessSystem.
  2. 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.

  1. 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
}
  1. Create a system with proper declaration:
[UpdateInGroup(typeof(RukhankaAnimationInjectionSystemGroup))]
[RequireMatchingQueriesForUpdate]
public partial struct SimpleBonePositionOverride: ISystem
{
...
}
  1. 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;
...
}
  1. 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);
...
}

  1. AnimationStream has functions to get and set the world and local poses of every animated rig bone. We change signature of IJobEntity.Execute function to get BonePositionOverrideComponent data, which is used as source for bone transform modifications:
void Execute(Entity entity, RigDefinitionComponent rigDef, BonePositionOverrideComponent bonePosOverride)
{
...
animStream.SetWorldPosition(bonePosOverride.boneIndex, bonePosOverride.newWorldPose);
...
}
  1. Rukhanka has delayed mechanism of all children's bone poses recalculation. During Get calls required bone position data will be recalculated automatically. Any outdated bone data that is left at the end of the AnimationStream object lifetime will be refreshed during the Dispose() call (or use using 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();
}
}