fix(animation): retain trailing PartArray poses

Port CPartArray::UpdateParts minimum-count behavior: preserve the prior visual and rigid transforms for setup parts absent from a short authored AnimFrame, including the initial hydrated rest pose.

Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-21 09:41:15 +02:00
parent f004ac5562
commit a2fd61684a
6 changed files with 326 additions and 59 deletions

View file

@ -39,6 +39,7 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
public readonly Frame RootFrameScratch = new();
public readonly List<MeshRef> MeshRefs = new();
public readonly List<Matrix4x4> PartPoses = new();
public readonly List<Matrix4x4> VisualPartPoses = new();
}
private readonly IAnimationLoader _animationLoader;
@ -389,8 +390,8 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
private static void Compose(Owner owner, IReadOnlyList<PartTransform> frames)
{
EnsureRetainedPoses(owner);
owner.MeshRefs.Clear();
owner.PartPoses.Clear();
Matrix4x4 objectScale = owner.Entity.Scale == 1f
? Matrix4x4.Identity
: Matrix4x4.CreateScale(owner.Entity.Scale);
@ -398,28 +399,27 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
int partCount = owner.Setup.Parts.Count;
for (int i = 0; i < partCount; i++)
{
Vector3 origin = i < frames.Count ? frames[i].Origin : Vector3.Zero;
Quaternion orientation = i < frames.Count
? frames[i].Orientation
: Quaternion.Identity;
Vector3 defaultScale = i < owner.Setup.DefaultScale.Count
? owner.Setup.DefaultScale[i]
: Vector3.One;
if (i < frames.Count)
{
Vector3 origin = frames[i].Origin;
Quaternion orientation = frames[i].Orientation;
Vector3 defaultScale = i < owner.Setup.DefaultScale.Count
? owner.Setup.DefaultScale[i]
: Vector3.One;
Matrix4x4 visual =
Matrix4x4.CreateScale(defaultScale)
* Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(origin);
if (owner.Entity.Scale != 1f)
visual *= objectScale;
owner.PartPoses.Add(
Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(origin * owner.Entity.Scale));
Matrix4x4 visual = Matrix4x4.CreateScale(defaultScale)
* Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(origin);
if (owner.Entity.Scale != 1f)
visual *= objectScale;
owner.VisualPartPoses[i] = visual;
owner.PartPoses[i] = Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(origin * owner.Entity.Scale);
}
if (!owner.PartAvailable[i])
continue;
owner.MeshRefs.Add(new MeshRef(owner.PartGfxIds[i], visual)
owner.MeshRefs.Add(new MeshRef(owner.PartGfxIds[i], owner.VisualPartPoses[i])
{
SurfaceOverrides = owner.SurfaceOverrides[i],
});
@ -429,6 +429,49 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
owner.Entity.SetIndexedPartPoses(owner.PartPoses, owner.PartAvailable);
}
private static void EnsureRetainedPoses(Owner owner)
{
int partCount = owner.Setup.Parts.Count;
if (owner.VisualPartPoses.Count == partCount
&& owner.PartPoses.Count == partCount)
{
return;
}
owner.VisualPartPoses.Clear();
owner.PartPoses.Clear();
bool[] consumed = new bool[owner.Entity.MeshRefs.Count];
for (int i = 0; i < partCount; i++)
{
Matrix4x4 rigid = i < owner.Entity.IndexedPartTransforms.Count
? owner.Entity.IndexedPartTransforms[i]
: Matrix4x4.Identity;
owner.PartPoses.Add(rigid);
Matrix4x4 visual = default;
bool found = false;
for (int meshIndex = 0; meshIndex < owner.Entity.MeshRefs.Count; meshIndex++)
{
MeshRef mesh = owner.Entity.MeshRefs[meshIndex];
if (consumed[meshIndex] || mesh.GfxObjId != owner.PartGfxIds[i])
continue;
consumed[meshIndex] = true;
visual = mesh.PartTransform;
found = true;
break;
}
if (!found)
{
Vector3 defaultScale = i < owner.Setup.DefaultScale.Count
? owner.Setup.DefaultScale[i]
: Vector3.One;
visual = Matrix4x4.CreateScale(defaultScale * owner.Entity.Scale)
* rigid;
}
owner.VisualPartPoses.Add(visual);
}
}
private static uint[] BuildPartGfxIds(Setup setup, WorldEntity entity)
{
var result = new uint[setup.Parts.Count];