Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 102 additions & 61 deletions Common/Model/Animation/ClientAnimator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public static ClientAnimator CreateForEntity(Entity entity, List<ElementPose> ro
entity.AnimManager.TriggerAnimationStopped,
entity.AnimManager.ShouldPlaySound
);
} else
}
else
{
return new ClientAnimator(
() => 1,
Expand Down Expand Up @@ -164,9 +165,9 @@ protected virtual void initMatrices(int maxJointId)
// Matrices are only required on client side; and from 1.20.5 limited to the MaxJointId (x16) in length to avoid wasting RAM
var identMat = ClientAnimator.identMat;
var defaultPoseMatrices = new float[16 * maxJointId];
for (int i = 0; i < defaultPoseMatrices.Length; i++)
for (int i = 0; i < defaultPoseMatrices.Length; i += 16)
{
defaultPoseMatrices[i] = identMat[i % 16];
Buffer.BlockCopy(identMat, 0, defaultPoseMatrices, i * sizeof(float), 16 * sizeof(float));
}
TransformationMatricesDefaultPose = defaultPoseMatrices;
TransformationMatrices = new float[defaultPoseMatrices.Length];
Expand All @@ -189,7 +190,8 @@ protected virtual void LoadAttachmentPoints(List<ElementPose> cachedPoses)
for (int j = 0; j < attachmentPoints.Length; j++)
{
AttachmentPoint apoint = attachmentPoints[j];
AttachmentPointByCode[apoint.Code] = new AttachmentPointAndPose() {
AttachmentPointByCode[apoint.Code] = new AttachmentPointAndPose()
{
AttachPoint = apoint,
CachedPose = elem
};
Expand Down Expand Up @@ -219,7 +221,8 @@ protected virtual void LoadPosesAndAttachmentPoints(ShapeElement[] elements, Lis
for (int j = 0; j < elem.AttachmentPoints.Length; j++)
{
AttachmentPoint apoint = elem.AttachmentPoints[j];
AttachmentPointByCode[apoint.Code] = new AttachmentPointAndPose() {
AttachmentPointByCode[apoint.Code] = new AttachmentPointAndPose()
{
AttachPoint = apoint,
CachedPose = pose
};
Expand Down Expand Up @@ -321,38 +324,54 @@ public override void OnFrame(Dictionary<string, AnimationMetaData> activeAnimati
base.OnFrame(activeAnimationsByAnimCode, dt);
}

// Override to calculate transformation matrices on each frame
protected override void calculateMatrices(float dt)
{
if (!CalculateMatrices) return;
// If matrix calculation is disabled, exit the method
if (!CalculateMatrices)
return;

// Clear the set of processed joints
jointsDone.Clear();

// Initialize animation version (used for caching transformations)
int animVersion = 0;

for (int j = 0; j < activeAnimCount; j++)
// Limit the number of animations to process
int safeAnimCount = Math.Min(activeAnimCount, MaxConcurrentAnimations);

// Process each active animation
for (int j = 0; j < safeAnimCount; j++)
{
RunningAnimation anim = CurAnims[j];

// Set element weights for the current animation
weightsByAnimationAndElement[0][j] = anim.ElementWeights;

// Track maximum animation version among active ones
animVersion = Math.Max(animVersion, anim.Animation.Version);

// Get previous and next keyframes for current time
AnimationFrame[] prevNextFrame = anim.Animation.PrevNextKeyFrameByFrame[(int)anim.CurrentFrame % anim.Animation.QuantityFrames];
frameByDepthByAnimation[0][j] = prevNextFrame[0].RootElementTransforms;
prevFrame[j] = prevNextFrame[0].FrameNumber;

// Determine next frame based on animation end handling type
if (anim.Animation.OnAnimationEnd == EnumEntityAnimationEndHandling.Hold && (int)anim.CurrentFrame + 1 == anim.Animation.QuantityFrames)
{
// If animation should hold on the last frame
nextFrameTransformsByAnimation[0][j] = prevNextFrame[0].RootElementTransforms;
nextFrame[j] = prevNextFrame[0].FrameNumber;
}
else
{
// Otherwise move to next frame for interpolation
nextFrameTransformsByAnimation[0][j] = prevNextFrame[1].RootElementTransforms;
nextFrame[j] = prevNextFrame[1].FrameNumber;
}
}

// Recursively calculate transformation matrices for all hierarchy elements
calculateMatrices(
animVersion,
dt,
Expand All @@ -364,144 +383,166 @@ protected override void calculateMatrices(float dt)
0
);

// Initialize transformation matrices for joints not used in animations
var TransformationMatrices = this.TransformationMatrices;
if (TransformationMatrices != null)
{
for (int jointidMul16 = 0; jointidMul16 < TransformationMatrices.Length; jointidMul16 += 16) // radfast 20.2.25: jointIdMul16 is (jointId * 16); I think this is the highest performance way to loop through this, taking account of the many array bounds checks
// Iterate through joint matrices (each matrix takes 16 elements)
for (int jointidMul16 = 0; jointidMul16 < TransformationMatrices.Length; jointidMul16 += 16)
{
if (jointsById.ContainsKey(jointidMul16 / 16)) continue;
// Skip joints that are active in animation
if (jointsById.ContainsKey(jointidMul16 / 16))
continue;

for (int j = 0; j < 16; j++)
{
TransformationMatrices[jointidMul16 + j] = identMat[j];
}
// Copy identity matrix for unused joints
Buffer.BlockCopy(identMat, 0, TransformationMatrices, jointidMul16 * sizeof(float), 16 * sizeof(float));
}
}

// Synchronize model matrices for attachment points
foreach (var val in AttachmentPointByCode)
{
var cachedMatrix = val.Value.CachedPose.AnimModelMatrix;
var animMatrix = val.Value.AnimModelMatrix;
for (int i = 0; i < 16; i++)
{
animMatrix[i] = cachedMatrix[i];
}
// Copy current transformation matrix to attachment point animation matrix
Buffer.BlockCopy(cachedMatrix, 0, animMatrix, 0, 16 * sizeof(float));
}
}



// Careful when changing around stuff in here, this is a recursively called method
// Recursive calculation of transformation matrices for hierarchy elements
private void calculateMatrices(
int animVersion,
float dt,
List<ElementPose> outFrame,
ShapeElementWeights[][] weightsByAnimationAndElement,
float[] modelMatrix,
List<ElementPose>[] nowKeyFrameByAnimation,
List<ElementPose>[] nextInKeyFrameByAnimation,
int depth
)
int animVersion,
float dt,
List<ElementPose> outFrame,
ShapeElementWeights[][] weightsByAnimationAndElement,
float[] modelMatrix,
List<ElementPose>[] nowKeyFrameByAnimation,
List<ElementPose>[] nextInKeyFrameByAnimation,
int depth
)
{
// Increment recursion depth
depth++;
List<ElementPose>[] nowChildKeyFrameByAnimation = this.frameByDepthByAnimation[depth];
List<ElementPose>[] nextChildKeyFrameByAnimation = this.nextFrameTransformsByAnimation[depth];
ShapeElementWeights[][] childWeightsByAnimationAndElement = this.weightsByAnimationAndElement[depth];

localTransformMatrix ??= Mat4f.Create();

// Process each child element in current pose
for (int childPoseIndex = 0; childPoseIndex < outFrame.Count; childPoseIndex++)
{
ElementPose outFramePose = outFrame[childPoseIndex];
ShapeElement elem = outFramePose.ForElement;

// Set model matrix for current element
outFramePose.SetMat(modelMatrix);
// Initialize local transformation matrix with identity matrix
Mat4f.Identity(localTransformMatrix);

// Clear pose before calculating weighted transformations
outFramePose.Clear();

float weightSum = 0f;

#if DEBUG
StringBuilder sb = null;
if (EleWeightDebug) sb = new StringBuilder();
// Debug information about element weights
StringBuilder sb = null;
if (EleWeightDebug) sb = new StringBuilder();
#endif
for (int animIndex = 0; animIndex < activeAnimCount; animIndex++)

// Limit the number of animations to process
int safeAnimCount = Math.Min(activeAnimCount, MaxConcurrentAnimations);

// First pass: calculate total weight of all influencing animations
for (int animIndex = 0; animIndex < safeAnimCount; animIndex++)
{
RunningAnimation anim = CurAnims[animIndex];
if (childPoseIndex >= weightsByAnimationAndElement[animIndex].Length)
continue;

RunningAnimation anim = CurAnims[animIndex];
ShapeElementWeights sew = weightsByAnimationAndElement[animIndex][childPoseIndex];

// Only blending modes other than "Add" participate in weight normalization
if (sew.BlendMode != EnumAnimationBlendMode.Add)
{
weightSum += sew.Weight * anim.EasingFactor;
}

#if DEBUG
if (EleWeightDebug) sb.Append(string.Format("{0:0.0} from {1} (blendmode {2}), ", sew.Weight * anim.EasingFactor, anim.Animation.Code, sew.BlendMode));
if (EleWeightDebug) sb.Append(string.Format("{0:0.0} from {1} (blendmode {2}), ", sew.Weight * anim.EasingFactor, anim.Animation.Code, sew.BlendMode));
#endif
}
#if DEBUG
if (EleWeightDebug)
{
if (eleWeights.ContainsKey(elem.Name)) eleWeights[elem.Name] += sb.ToString();
else eleWeights[elem.Name] = sb.ToString();
}
if (EleWeightDebug)
{
if (eleWeights.ContainsKey(elem.Name)) eleWeights[elem.Name] += sb.ToString();
else eleWeights[elem.Name] = sb.ToString();
}
#endif

for (int animIndex = 0; animIndex < activeAnimCount; animIndex++)
// Second pass: blend transformations of all active animations
for (int animIndex = 0; animIndex < safeAnimCount; animIndex++)
{
if (childPoseIndex >= weightsByAnimationAndElement[animIndex].Length)
continue;

RunningAnimation anim = CurAnims[animIndex];
ShapeElementWeights sew = weightsByAnimationAndElement[animIndex][childPoseIndex];
// Calculate final weighted coefficient for current animation
anim.CalcBlendedWeight(weightSum / sew.Weight, sew.BlendMode);

ElementPose nowFramePose = nowKeyFrameByAnimation[animIndex][childPoseIndex];
ElementPose nextFramePose = nextInKeyFrameByAnimation[animIndex][childPoseIndex];

int prevFrame = this.prevFrame[animIndex];
int nextFrame = this.nextFrame[animIndex];

// May loop around, so nextFrame can be smaller than prevFrame
// Calculate distance between keyframes (accounting for cyclic animations)
float keyFrameDist = nextFrame > prevFrame ? (nextFrame - prevFrame) : (anim.Animation.QuantityFrames - prevFrame + nextFrame);
// Calculate distance from previous frame to current animation frame
float curFrameDist = anim.CurrentFrame >= prevFrame ? (anim.CurrentFrame - prevFrame) : (anim.Animation.QuantityFrames - prevFrame + anim.CurrentFrame);
// Calculate interpolation coefficient (0-1) between keyframes
float lerp = keyFrameDist < 1e-6f ? 0f : curFrameDist / keyFrameDist;

float lerp = curFrameDist / keyFrameDist;

// Linear interpolation between current and next frame with animation weight applied
outFramePose.Add(nowFramePose, nextFramePose, lerp, anim.BlendedWeight);

// Prepare data for recursive child element processing
nowChildKeyFrameByAnimation[animIndex] = nowFramePose.ChildElementPoses;
childWeightsByAnimationAndElement[animIndex] = sew.ChildElements;

nextChildKeyFrameByAnimation[animIndex] = nextFramePose.ChildElementPoses;
}

// Get and apply local transformation matrix for element
elem.GetLocalTransformMatrix(animVersion, localTransformMatrix, outFramePose);
Mat4f.Mul(outFramePose.AnimModelMatrix, outFramePose.AnimModelMatrix, localTransformMatrix);

if (TransformationMatrices != null) // It is null on a server, non-null on a client
// If this is client (joint matrices are calculated), update joint transformation matrices
if (TransformationMatrices != null)
{
// If element has a joint and this joint hasn't been processed yet
if (elem.JointId > 0 && !jointsDone.Contains(elem.JointId))
{
var tmpMatrixLocal = tmpMatrix ??= Mat4f.Create();
// Apply inverse model transformation to get final joint matrix
Mat4f.Mul(tmpMatrixLocal, outFramePose.AnimModelMatrix, elem.inverseModelTransform);

int index = 16 * elem.JointId;
var transformationMatrices = TransformationMatrices;
if (index + 16 > transformationMatrices.Length) // Check we have space for this joint: we normally should, if MaxJointId was correct, but a mod could have modified the shape or something

// Check if there's space in the matrix array (in case a mod extended the hierarchy)
if (index + 16 > transformationMatrices.Length)
{
var transformationMatricesDefault = this.TransformationMatricesDefaultPose;
initMatrices(elem.JointId + 1); // This replaces the matrices in both fields, but our local references are still the old populated matrices
Array.Copy(transformationMatrices, this.TransformationMatrices, transformationMatrices.Length);
Array.Copy(transformationMatricesDefault, this.TransformationMatricesDefaultPose, transformationMatricesDefault.Length);
// Re-initialize matrices with larger size
initMatrices(elem.JointId + 1);
// Copy previously calculated matrices to new array
Buffer.BlockCopy(transformationMatrices, 0, this.TransformationMatrices, 0, transformationMatrices.Length * sizeof(float));
Buffer.BlockCopy(transformationMatricesDefault, 0, this.TransformationMatricesDefaultPose, 0, transformationMatricesDefault.Length * sizeof(float));
transformationMatrices = this.TransformationMatrices;
}
for (int i = 0; i < 16; i++)
{
transformationMatrices[index + i] = tmpMatrixLocal[i];
}

// Copy joint matrix to global transformation matrices array
Buffer.BlockCopy(tmpMatrix, 0, transformationMatrices, index * sizeof(float), 16 * sizeof(float));
// Mark joint as processed
jointsDone.Add(elem.JointId);
}
}

// Recursively process child elements
if (outFramePose.ChildElementPoses != null)
{
calculateMatrices(
Expand Down
Loading