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:
parent
f004ac5562
commit
a2fd61684a
6 changed files with 326 additions and 59 deletions
|
|
@ -194,6 +194,29 @@ InterpolationManager.adjust_offset(delta, dt):
|
||||||
relative.frame.set_heading(0)
|
relative.frame.set_heading(0)
|
||||||
delta = relative.frame // replaces both origin and orientation
|
delta = relative.frame // replaces both origin and orientation
|
||||||
|
|
||||||
|
## Part-count retention (`CPartArray::UpdateParts` @ `0x005190F0`)
|
||||||
|
|
||||||
|
```text
|
||||||
|
count = min(partArray.num_parts, animationFrame.num_parts)
|
||||||
|
for i in 0 .. count - 1:
|
||||||
|
part[i].frame = combine_scaled(animationFrame.part[i], partArray.scale)
|
||||||
|
|
||||||
|
// No loop touches [count .. partArray.num_parts).
|
||||||
|
// Those CPhysicsPart frames therefore retain their complete prior state.
|
||||||
|
```
|
||||||
|
|
||||||
|
The retained state has two presentation channels in acdream:
|
||||||
|
|
||||||
|
- the visible mesh transform retains Setup `DefaultScale`, animated frame, and
|
||||||
|
object `ObjScale`;
|
||||||
|
- the indexed rigid/effect pose retains animated orientation and the
|
||||||
|
ObjScale-adjusted origin, without Setup `DefaultScale`.
|
||||||
|
|
||||||
|
On the first presentation, "prior" means the already-hydrated Setup/rest
|
||||||
|
frames installed by PartArray construction. A short later AnimFrame retains
|
||||||
|
the most recently presented transform in both channels. Resetting missing
|
||||||
|
parts to origin/identity is not retail behavior.
|
||||||
|
|
||||||
CPhysicsObj.SetPositionInternal(transition):
|
CPhysicsObj.SetPositionInternal(transition):
|
||||||
if transition.current_cell is null:
|
if transition.current_cell is null:
|
||||||
prepare_to_leave_visibility()
|
prepare_to_leave_visibility()
|
||||||
|
|
|
||||||
|
|
@ -181,29 +181,36 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
{
|
{
|
||||||
int partCount = animation.PartTemplate.Count;
|
int partCount = animation.PartTemplate.Count;
|
||||||
EmitPartDiagnostics(record, animation, sequenceFrames, partCount);
|
EmitPartDiagnostics(record, animation, sequenceFrames, partCount);
|
||||||
|
EnsureRetainedPoses(animation);
|
||||||
List<MeshRef> meshRefs = animation.MeshRefsScratch;
|
List<MeshRef> meshRefs = animation.MeshRefsScratch;
|
||||||
meshRefs.Clear();
|
meshRefs.Clear();
|
||||||
List<Matrix4x4> rigidPoses = animation.EffectPartPosesScratch;
|
List<Matrix4x4> rigidPoses = animation.EffectPartPosesScratch;
|
||||||
rigidPoses.Clear();
|
List<Matrix4x4> visualPoses = animation.VisualPartPosesScratch;
|
||||||
Matrix4x4 scale = animation.Scale == 1f
|
Matrix4x4 scale = animation.Scale == 1f
|
||||||
? Matrix4x4.Identity
|
? Matrix4x4.Identity
|
||||||
: Matrix4x4.CreateScale(animation.Scale);
|
: Matrix4x4.CreateScale(animation.Scale);
|
||||||
|
|
||||||
for (int i = 0; i < partCount; i++)
|
for (int i = 0; i < partCount; i++)
|
||||||
{
|
{
|
||||||
ResolvePartFrame(animation, sequenceFrames, i, out Vector3 origin, out Quaternion orientation);
|
if (TryResolvePartFrame(
|
||||||
Vector3 defaultScale = i < animation.Setup.DefaultScale.Count
|
animation,
|
||||||
? animation.Setup.DefaultScale[i]
|
sequenceFrames,
|
||||||
: Vector3.One;
|
i,
|
||||||
Matrix4x4 visual = Matrix4x4.CreateScale(defaultScale)
|
out Vector3 origin,
|
||||||
* Matrix4x4.CreateFromQuaternion(orientation)
|
out Quaternion orientation))
|
||||||
* Matrix4x4.CreateTranslation(origin);
|
{
|
||||||
if (animation.Scale != 1f)
|
Vector3 defaultScale = i < animation.Setup.DefaultScale.Count
|
||||||
visual *= scale;
|
? animation.Setup.DefaultScale[i]
|
||||||
|
: Vector3.One;
|
||||||
Matrix4x4 rigid = Matrix4x4.CreateFromQuaternion(orientation)
|
Matrix4x4 visual = Matrix4x4.CreateScale(defaultScale)
|
||||||
* Matrix4x4.CreateTranslation(origin * animation.Scale);
|
* Matrix4x4.CreateFromQuaternion(orientation)
|
||||||
rigidPoses.Add(rigid);
|
* Matrix4x4.CreateTranslation(origin);
|
||||||
|
if (animation.Scale != 1f)
|
||||||
|
visual *= scale;
|
||||||
|
visualPoses[i] = visual;
|
||||||
|
rigidPoses[i] = Matrix4x4.CreateFromQuaternion(orientation)
|
||||||
|
* Matrix4x4.CreateTranslation(origin * animation.Scale);
|
||||||
|
}
|
||||||
|
|
||||||
LiveAnimationPartTemplate template = animation.PartTemplate[i];
|
LiveAnimationPartTemplate template = animation.PartTemplate[i];
|
||||||
if (!template.IsDrawable
|
if (!template.IsDrawable
|
||||||
|
|
@ -211,7 +218,7 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
meshRefs.Add(new MeshRef(template.GfxObjId, visual)
|
meshRefs.Add(new MeshRef(template.GfxObjId, visualPoses[i])
|
||||||
{
|
{
|
||||||
SurfaceOverrides = template.SurfaceOverrides,
|
SurfaceOverrides = template.SurfaceOverrides,
|
||||||
});
|
});
|
||||||
|
|
@ -226,7 +233,7 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
_effectPoses.Publish(entity, rigidPoses, animation.PartAvailability);
|
_effectPoses.Publish(entity, rigidPoses, animation.PartAvailability);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ResolvePartFrame(
|
private static bool TryResolvePartFrame(
|
||||||
LiveEntityAnimationState animation,
|
LiveEntityAnimationState animation,
|
||||||
IReadOnlyList<PartTransform>? sequenceFrames,
|
IReadOnlyList<PartTransform>? sequenceFrames,
|
||||||
int partIndex,
|
int partIndex,
|
||||||
|
|
@ -239,13 +246,11 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
{
|
{
|
||||||
origin = sequenceFrames[partIndex].Origin;
|
origin = sequenceFrames[partIndex].Origin;
|
||||||
orientation = sequenceFrames[partIndex].Orientation;
|
orientation = sequenceFrames[partIndex].Orientation;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
else
|
origin = default;
|
||||||
{
|
orientation = default;
|
||||||
origin = Vector3.Zero;
|
return false;
|
||||||
orientation = Quaternion.Identity;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int frameIndex = (int)Math.Floor(animation.CurrFrame);
|
int frameIndex = (int)Math.Floor(animation.CurrFrame);
|
||||||
|
|
@ -270,12 +275,59 @@ internal sealed class LiveEntityAnimationPresenter
|
||||||
var next = partIndex < nextFrames.Count ? nextFrames[partIndex] : first;
|
var next = partIndex < nextFrames.Count ? nextFrames[partIndex] : first;
|
||||||
origin = Vector3.Lerp(first.Origin, next.Origin, t);
|
origin = Vector3.Lerp(first.Origin, next.Origin, t);
|
||||||
orientation = Quaternion.Slerp(first.Orientation, next.Orientation, t);
|
orientation = Quaternion.Slerp(first.Orientation, next.Orientation, t);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
else
|
origin = default;
|
||||||
|
orientation = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureRetainedPoses(LiveEntityAnimationState animation)
|
||||||
|
{
|
||||||
|
int partCount = animation.PartTemplate.Count;
|
||||||
|
if (animation.PresentationPosesInitialized
|
||||||
|
&& animation.VisualPartPosesScratch.Count == partCount
|
||||||
|
&& animation.EffectPartPosesScratch.Count == partCount)
|
||||||
{
|
{
|
||||||
origin = Vector3.Zero;
|
return;
|
||||||
orientation = Quaternion.Identity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Matrix4x4> visual = animation.VisualPartPosesScratch;
|
||||||
|
List<Matrix4x4> rigid = animation.EffectPartPosesScratch;
|
||||||
|
visual.Clear();
|
||||||
|
rigid.Clear();
|
||||||
|
bool[] consumed = new bool[animation.Entity.MeshRefs.Count];
|
||||||
|
for (int i = 0; i < partCount; i++)
|
||||||
|
{
|
||||||
|
Matrix4x4 rigidRest = i < animation.Entity.IndexedPartTransforms.Count
|
||||||
|
? animation.Entity.IndexedPartTransforms[i]
|
||||||
|
: Matrix4x4.Identity;
|
||||||
|
rigid.Add(rigidRest);
|
||||||
|
|
||||||
|
LiveAnimationPartTemplate template = animation.PartTemplate[i];
|
||||||
|
Matrix4x4 visualRest = default;
|
||||||
|
bool found = false;
|
||||||
|
for (int meshIndex = 0; meshIndex < animation.Entity.MeshRefs.Count; meshIndex++)
|
||||||
|
{
|
||||||
|
MeshRef mesh = animation.Entity.MeshRefs[meshIndex];
|
||||||
|
if (consumed[meshIndex] || mesh.GfxObjId != template.GfxObjId)
|
||||||
|
continue;
|
||||||
|
consumed[meshIndex] = true;
|
||||||
|
visualRest = mesh.PartTransform;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!found)
|
||||||
|
{
|
||||||
|
Vector3 defaultScale = i < animation.Setup.DefaultScale.Count
|
||||||
|
? animation.Setup.DefaultScale[i]
|
||||||
|
: Vector3.One;
|
||||||
|
visualRest = Matrix4x4.CreateScale(defaultScale * animation.Scale)
|
||||||
|
* rigidRest;
|
||||||
|
}
|
||||||
|
visual.Add(visualRest);
|
||||||
|
}
|
||||||
|
animation.PresentationPosesInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EmitSequenceDiagnostics(
|
private void EmitSequenceDiagnostics(
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
||||||
public readonly Frame RootFrameScratch = new();
|
public readonly Frame RootFrameScratch = new();
|
||||||
public readonly List<MeshRef> MeshRefs = new();
|
public readonly List<MeshRef> MeshRefs = new();
|
||||||
public readonly List<Matrix4x4> PartPoses = new();
|
public readonly List<Matrix4x4> PartPoses = new();
|
||||||
|
public readonly List<Matrix4x4> VisualPartPoses = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IAnimationLoader _animationLoader;
|
private readonly IAnimationLoader _animationLoader;
|
||||||
|
|
@ -389,8 +390,8 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
||||||
|
|
||||||
private static void Compose(Owner owner, IReadOnlyList<PartTransform> frames)
|
private static void Compose(Owner owner, IReadOnlyList<PartTransform> frames)
|
||||||
{
|
{
|
||||||
|
EnsureRetainedPoses(owner);
|
||||||
owner.MeshRefs.Clear();
|
owner.MeshRefs.Clear();
|
||||||
owner.PartPoses.Clear();
|
|
||||||
Matrix4x4 objectScale = owner.Entity.Scale == 1f
|
Matrix4x4 objectScale = owner.Entity.Scale == 1f
|
||||||
? Matrix4x4.Identity
|
? Matrix4x4.Identity
|
||||||
: Matrix4x4.CreateScale(owner.Entity.Scale);
|
: Matrix4x4.CreateScale(owner.Entity.Scale);
|
||||||
|
|
@ -398,28 +399,27 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
||||||
int partCount = owner.Setup.Parts.Count;
|
int partCount = owner.Setup.Parts.Count;
|
||||||
for (int i = 0; i < partCount; i++)
|
for (int i = 0; i < partCount; i++)
|
||||||
{
|
{
|
||||||
Vector3 origin = i < frames.Count ? frames[i].Origin : Vector3.Zero;
|
if (i < frames.Count)
|
||||||
Quaternion orientation = i < frames.Count
|
{
|
||||||
? frames[i].Orientation
|
Vector3 origin = frames[i].Origin;
|
||||||
: Quaternion.Identity;
|
Quaternion orientation = frames[i].Orientation;
|
||||||
Vector3 defaultScale = i < owner.Setup.DefaultScale.Count
|
Vector3 defaultScale = i < owner.Setup.DefaultScale.Count
|
||||||
? owner.Setup.DefaultScale[i]
|
? owner.Setup.DefaultScale[i]
|
||||||
: Vector3.One;
|
: Vector3.One;
|
||||||
|
|
||||||
Matrix4x4 visual =
|
Matrix4x4 visual = Matrix4x4.CreateScale(defaultScale)
|
||||||
Matrix4x4.CreateScale(defaultScale)
|
* Matrix4x4.CreateFromQuaternion(orientation)
|
||||||
* Matrix4x4.CreateFromQuaternion(orientation)
|
* Matrix4x4.CreateTranslation(origin);
|
||||||
* Matrix4x4.CreateTranslation(origin);
|
if (owner.Entity.Scale != 1f)
|
||||||
if (owner.Entity.Scale != 1f)
|
visual *= objectScale;
|
||||||
visual *= objectScale;
|
owner.VisualPartPoses[i] = visual;
|
||||||
|
owner.PartPoses[i] = Matrix4x4.CreateFromQuaternion(orientation)
|
||||||
owner.PartPoses.Add(
|
* Matrix4x4.CreateTranslation(origin * owner.Entity.Scale);
|
||||||
Matrix4x4.CreateFromQuaternion(orientation)
|
}
|
||||||
* Matrix4x4.CreateTranslation(origin * owner.Entity.Scale));
|
|
||||||
if (!owner.PartAvailable[i])
|
if (!owner.PartAvailable[i])
|
||||||
continue;
|
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],
|
SurfaceOverrides = owner.SurfaceOverrides[i],
|
||||||
});
|
});
|
||||||
|
|
@ -429,6 +429,49 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
||||||
owner.Entity.SetIndexedPartPoses(owner.PartPoses, owner.PartAvailable);
|
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)
|
private static uint[] BuildPartGfxIds(Setup setup, WorldEntity entity)
|
||||||
{
|
{
|
||||||
var result = new uint[setup.Parts.Count];
|
var result = new uint[setup.Parts.Count];
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,7 @@ public sealed class AnimationSequencer
|
||||||
// consume the returned IReadOnlyList<PartTransform> synchronously within
|
// consume the returned IReadOnlyList<PartTransform> synchronously within
|
||||||
// the same loop iteration and never cache it across Advance() calls.
|
// the same loop iteration and never cache it across Advance() calls.
|
||||||
private readonly PartTransform[] _partTransformScratch;
|
private readonly PartTransform[] _partTransformScratch;
|
||||||
|
private readonly PartTransformBuffer _partTransformView;
|
||||||
|
|
||||||
// R1-P6: root motion flows through the Advance(dt, Frame) overload
|
// R1-P6: root motion flows through the Advance(dt, Frame) overload
|
||||||
// (retail CSequence::update(Frame*), 0x00525b80) — the old adapter
|
// (retail CSequence::update(Frame*), 0x00525b80) — the old adapter
|
||||||
|
|
@ -279,6 +280,7 @@ public sealed class AnimationSequencer
|
||||||
_mtable = motionTable;
|
_mtable = motionTable;
|
||||||
_loader = loader;
|
_loader = loader;
|
||||||
_partTransformScratch = new PartTransform[_setup.Parts.Count];
|
_partTransformScratch = new PartTransform[_setup.Parts.Count];
|
||||||
|
_partTransformView = new PartTransformBuffer(_partTransformScratch);
|
||||||
_core = new CSequence(loader);
|
_core = new CSequence(loader);
|
||||||
_core.HookObj = new AdapterHookQueue(this);
|
_core.HookObj = new AdapterHookQueue(this);
|
||||||
_table = new CMotionTable(motionTable);
|
_table = new CMotionTable(motionTable);
|
||||||
|
|
@ -714,25 +716,20 @@ public sealed class AnimationSequencer
|
||||||
// instead of allocating a fresh PartTransform[partCount] every call.
|
// instead of allocating a fresh PartTransform[partCount] every call.
|
||||||
// Sized once in the constructor to _setup.Parts.Count, which never
|
// Sized once in the constructor to _setup.Parts.Count, which never
|
||||||
// changes, so partCount here always matches the buffer length.
|
// changes, so partCount here always matches the buffer length.
|
||||||
|
int authoredPartCount = Math.Min(partCount, f0Parts.Count);
|
||||||
var result = _partTransformScratch;
|
var result = _partTransformScratch;
|
||||||
for (int i = 0; i < partCount; i++)
|
for (int i = 0; i < authoredPartCount; i++)
|
||||||
{
|
{
|
||||||
if (i < f0Parts.Count)
|
var p0 = f0Parts[i];
|
||||||
{
|
var p1 = i < f1Parts.Count ? f1Parts[i] : p0;
|
||||||
var p0 = f0Parts[i];
|
|
||||||
var p1 = i < f1Parts.Count ? f1Parts[i] : p0;
|
|
||||||
|
|
||||||
result[i] = new PartTransform(
|
result[i] = new PartTransform(
|
||||||
Vector3.Lerp(p0.Origin, p1.Origin, t),
|
Vector3.Lerp(p0.Origin, p1.Origin, t),
|
||||||
SlerpRetailClient(p0.Orientation, p1.Orientation, t));
|
SlerpRetailClient(p0.Orientation, p1.Orientation, t));
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result[i] = new PartTransform(Vector3.Zero, Quaternion.Identity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
_partTransformView.SetCount(authoredPartCount);
|
||||||
|
return _partTransformView;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<PartTransform> BuildIdentityFrame(int partCount)
|
private IReadOnlyList<PartTransform> BuildIdentityFrame(int partCount)
|
||||||
|
|
@ -742,7 +739,36 @@ public sealed class AnimationSequencer
|
||||||
var result = _partTransformScratch;
|
var result = _partTransformScratch;
|
||||||
for (int i = 0; i < partCount; i++)
|
for (int i = 0; i < partCount; i++)
|
||||||
result[i] = new PartTransform(Vector3.Zero, Quaternion.Identity);
|
result[i] = new PartTransform(Vector3.Zero, Quaternion.Identity);
|
||||||
return result;
|
_partTransformView.SetCount(partCount);
|
||||||
|
return _partTransformView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocation-free view whose Count preserves the authored AnimFrame part
|
||||||
|
/// count. Retail UpdateParts touches only that prefix and retains every
|
||||||
|
/// trailing CPhysicsPart pose.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class PartTransformBuffer : IReadOnlyList<PartTransform>
|
||||||
|
{
|
||||||
|
private readonly PartTransform[] _items;
|
||||||
|
|
||||||
|
public PartTransformBuffer(PartTransform[] items) => _items = items;
|
||||||
|
|
||||||
|
public int Count { get; private set; }
|
||||||
|
public PartTransform this[int index] => index >= 0 && index < Count
|
||||||
|
? _items[index]
|
||||||
|
: throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
|
public void SetCount(int count) => Count = count;
|
||||||
|
|
||||||
|
public IEnumerator<PartTransform> GetEnumerator()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Count; i++)
|
||||||
|
yield return _items[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() =>
|
||||||
|
GetEnumerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
// R2-Q4: IsLocomotionCycleLowByte DELETED with Fix B - the cyclic-to-
|
// R2-Q4: IsLocomotionCycleLowByte DELETED with Fix B - the cyclic-to-
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,90 @@ public sealed class LiveEntityAnimationPresenterTests
|
||||||
Assert.Empty(replacement.EffectPartPosesScratch);
|
Assert.Empty(replacement.EffectPartPosesScratch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortFirstFrame_RetainsDistinctVisualAndRigidRestPoses()
|
||||||
|
{
|
||||||
|
var fixture = Build(partCount: 2, scale: 2f);
|
||||||
|
Matrix4x4 visualRest = Matrix4x4.CreateScale(7f)
|
||||||
|
* Matrix4x4.CreateTranslation(8f, 9f, 10f);
|
||||||
|
Matrix4x4 rigidRest = Matrix4x4.CreateTranslation(11f, 12f, 13f);
|
||||||
|
fixture.Entity.MeshRefs =
|
||||||
|
[
|
||||||
|
new MeshRef(0x01000001u, Matrix4x4.Identity),
|
||||||
|
new MeshRef(0x01000002u, visualRest),
|
||||||
|
];
|
||||||
|
fixture.Entity.SetIndexedPartPoses(
|
||||||
|
[Matrix4x4.Identity, rigidRest],
|
||||||
|
[true, true]);
|
||||||
|
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context());
|
||||||
|
|
||||||
|
presenter.Present(Schedules(fixture,
|
||||||
|
[new PartTransform(Vector3.UnitX, Quaternion.Identity)]));
|
||||||
|
|
||||||
|
Assert.Equal(visualRest, fixture.Entity.MeshRefs[1].PartTransform);
|
||||||
|
Assert.Equal(rigidRest, fixture.Entity.IndexedPartTransforms[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LaterShortFrame_RetainsPreviouslyPresentedTrailingPart()
|
||||||
|
{
|
||||||
|
var fixture = Build(partCount: 2);
|
||||||
|
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context());
|
||||||
|
presenter.Present(Schedules(fixture,
|
||||||
|
[
|
||||||
|
new PartTransform(Vector3.UnitX, Quaternion.Identity),
|
||||||
|
new PartTransform(new Vector3(4f, 5f, 6f), Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f)),
|
||||||
|
]));
|
||||||
|
Matrix4x4 visual = fixture.Entity.MeshRefs[1].PartTransform;
|
||||||
|
Matrix4x4 rigid = fixture.Entity.IndexedPartTransforms[1];
|
||||||
|
|
||||||
|
presenter.Present(Schedules(fixture,
|
||||||
|
[new PartTransform(Vector3.UnitY, Quaternion.Identity)]));
|
||||||
|
|
||||||
|
Assert.Equal(visual, fixture.Entity.MeshRefs[1].PartTransform);
|
||||||
|
Assert.Equal(rigid, fixture.Entity.IndexedPartTransforms[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppearanceShrink_ClearsAllTrailingPresentationState()
|
||||||
|
{
|
||||||
|
var fixture = Build(partCount: 3);
|
||||||
|
var poses = new EntityEffectPoseRegistry();
|
||||||
|
var presenter = Presenter(fixture.Live, poses, new Context());
|
||||||
|
presenter.Present(Schedules(fixture,
|
||||||
|
[
|
||||||
|
new PartTransform(Vector3.UnitX, Quaternion.Identity),
|
||||||
|
new PartTransform(Vector3.UnitY, Quaternion.Identity),
|
||||||
|
new PartTransform(Vector3.UnitZ, Quaternion.Identity),
|
||||||
|
]));
|
||||||
|
var replacementSetup = new Setup();
|
||||||
|
replacementSetup.Parts.Add(0x01000009u);
|
||||||
|
replacementSetup.DefaultScale.Add(Vector3.One);
|
||||||
|
fixture.Entity.MeshRefs = [new MeshRef(0x01000009u, Matrix4x4.Identity)];
|
||||||
|
fixture.Entity.SetIndexedPartPoses([Matrix4x4.Identity], [true]);
|
||||||
|
GameWindow.RebindAnimatedEntityForAppearance(
|
||||||
|
fixture.State,
|
||||||
|
fixture.Entity,
|
||||||
|
replacementSetup,
|
||||||
|
1f,
|
||||||
|
[new LiveAnimationPartTemplate(0x01000009u, null, true)],
|
||||||
|
[true]);
|
||||||
|
|
||||||
|
presenter.Present(Schedules(fixture,
|
||||||
|
[new PartTransform(new Vector3(9f, 0f, 0f), Quaternion.Identity)]));
|
||||||
|
|
||||||
|
Assert.Single(fixture.Entity.MeshRefs);
|
||||||
|
Assert.Single(fixture.Entity.IndexedPartTransforms);
|
||||||
|
Assert.Single(fixture.State.VisualPartPosesScratch);
|
||||||
|
Assert.Single(fixture.State.EffectPartPosesScratch);
|
||||||
|
Assert.True(poses.TryGetPartPoseSnapshot(
|
||||||
|
fixture.Entity.Id,
|
||||||
|
out IReadOnlyList<Matrix4x4> published,
|
||||||
|
out IReadOnlyList<bool> available));
|
||||||
|
Assert.Single(published);
|
||||||
|
Assert.Single(available);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MotionDone_OldComponentCannotResolveReplacementByGuid()
|
public void MotionDone_OldComponentCannotResolveReplacementByGuid()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,45 @@ public sealed class RetailStaticAnimatingObjectSchedulerTests
|
||||||
Assert.Equal(2, posePublishes);
|
Assert.Equal(2, posePublishes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortStaticAnimFrame_RetainsCompleteTrailingPartState()
|
||||||
|
{
|
||||||
|
const uint trailingGfx = GfxId;
|
||||||
|
var loader = new Loader();
|
||||||
|
loader.Add(AnimationId, TwoFrameAnimation());
|
||||||
|
var scheduler = new RetailStaticAnimatingObjectScheduler(
|
||||||
|
loader,
|
||||||
|
(_, _) => { },
|
||||||
|
(_, _, _) => { });
|
||||||
|
Setup setup = MakeSetup();
|
||||||
|
setup.Parts.Add(trailingGfx);
|
||||||
|
setup.DefaultScale.Add(new Vector3(3f, 4f, 5f));
|
||||||
|
Matrix4x4 visualRest = Matrix4x4.CreateScale(6f)
|
||||||
|
* Matrix4x4.CreateTranslation(7f, 8f, 9f);
|
||||||
|
Matrix4x4 rigidRest = Matrix4x4.CreateTranslation(10f, 11f, 12f);
|
||||||
|
WorldEntity entity = MakeEntity();
|
||||||
|
entity.MeshRefs =
|
||||||
|
[
|
||||||
|
entity.MeshRefs[0],
|
||||||
|
new MeshRef(trailingGfx, visualRest),
|
||||||
|
];
|
||||||
|
entity.SetIndexedPartPoses(
|
||||||
|
[Matrix4x4.Identity, rigidRest],
|
||||||
|
[true, true]);
|
||||||
|
scheduler.Register(entity, new ScriptActivationInfo(
|
||||||
|
ScriptId: 0,
|
||||||
|
PartTransforms: entity.IndexedPartTransforms,
|
||||||
|
PartAvailability: entity.IndexedPartAvailable,
|
||||||
|
Setup: setup,
|
||||||
|
DefaultAnimationId: AnimationId,
|
||||||
|
UsesStaticAnimationWorkset: true));
|
||||||
|
|
||||||
|
scheduler.Tick(1f / 60f);
|
||||||
|
|
||||||
|
Assert.Equal(visualRest, entity.MeshRefs[1].PartTransform);
|
||||||
|
Assert.Equal(rigidRest, entity.IndexedPartTransforms[1]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SubEpsilonElapsed_IsDiscardedInsteadOfAccumulated()
|
public void SubEpsilonElapsed_IsDiscardedInsteadOfAccumulated()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue