feat(vfx): bind effects to live animated poses

This commit is contained in:
Erik 2026-07-14 10:56:01 +02:00
parent 96ddfdf175
commit 542dcfc384
41 changed files with 3246 additions and 741 deletions

View file

@ -0,0 +1,177 @@
using System.Numerics;
using AcDream.App.Rendering;
namespace AcDream.App.Tests.Rendering;
public sealed class AttachmentUpdateOrderTests
{
[Fact]
public void ReinsertedParentStillUpdatesBeforeGrandchild()
{
const uint parent = 10u;
const uint grandchild = 20u;
// Dictionary order models B being removed/re-added while C remained.
uint[] insertionOrder = [grandchild, parent];
var parents = new Dictionary<uint, uint?>
{
[grandchild] = parent,
[parent] = null,
};
var calls = new List<uint>();
var children = insertionOrder.ToDictionary(id => id, id => parents[id]);
new AttachmentUpdateOrder<uint?>().ForEachParentFirst(
children,
static parentId => parentId,
id =>
{
calls.Add(id);
return true;
});
Assert.Equal([parent, grandchild], calls);
}
[Fact]
public void FailedParentSuppressesAndReportsDescendantsAfterTraversal()
{
const uint parent = 10u;
const uint grandchild = 20u;
var children = new Dictionary<uint, uint?>
{
[grandchild] = parent,
[parent] = null,
};
var calls = new List<uint>();
IReadOnlyList<uint> failed = new AttachmentUpdateOrder<uint?>().ForEachParentFirst(
children,
static parentId => parentId,
id =>
{
calls.Add(id);
return id != parent;
});
Assert.Equal([parent], calls);
Assert.Equal([parent, grandchild], failed);
Assert.Equal(2, children.Count);
}
[Fact]
public void NestedDrawableUsesImmediateParentsComposedWorldRoot()
{
Quaternion turn = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f);
Matrix4x4 parentWorld = Matrix4x4.CreateFromQuaternion(turn)
* Matrix4x4.CreateTranslation(100f, 0f, 0f);
var child = new AcDream.Core.World.WorldEntity
{
Id = 3u,
SourceGfxObjOrSetupId = 0x02000003u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs =
[
new AcDream.Core.World.MeshRef(
0x01000003u,
Matrix4x4.CreateTranslation(2f, 0f, 0f)),
],
};
Assert.True(EquippedChildRenderController.ApplyParentWorldPose(child, parentWorld));
Matrix4x4 installedRoot = Matrix4x4.CreateFromQuaternion(child.Rotation)
* Matrix4x4.CreateTranslation(child.Position);
Vector3 renderedOrigin = Vector3.Transform(
Vector3.Zero,
child.MeshRefs[0].PartTransform * installedRoot);
Assert.Equal(new Vector3(100f, 2f, 0f), renderedOrigin, new Vector3ToleranceComparer());
}
[Fact]
public void AncestorTeardownCollectsEntireAttachmentSubtreePostOrder()
{
var children = new Dictionary<uint, uint?>
{
[20u] = 10u,
[30u] = 20u,
};
var order = new AttachmentUpdateOrder<uint?>().CollectSubtreePostOrder(
children,
10u,
static parentId => parentId);
Assert.Equal([30u, 20u, 10u], order);
}
[Fact]
public void PoseRecoveryRealizesCompleteDescendantChainParentFirst()
{
var waitingByParent = new Dictionary<uint, IReadOnlyList<uint>>
{
[10u] = [20u],
[20u] = [30u],
};
var realized = new List<uint>();
new AttachmentUpdateOrder<uint?>().RealizeDescendants(
10u,
parent => waitingByParent.TryGetValue(parent, out var waiting)
? waiting
: Array.Empty<uint>(),
child =>
{
realized.Add(child);
return true;
});
Assert.Equal([20u, 30u], realized);
}
[Fact]
public void PoseRecoveryDoesNotTraverseThroughUnrealizedIntermediateParent()
{
var waitingByParent = new Dictionary<uint, IReadOnlyList<uint>>
{
[10u] = [20u],
[20u] = [30u],
};
var attempted = new List<uint>();
new AttachmentUpdateOrder<uint?>().RealizeDescendants(
10u,
parent => waitingByParent.TryGetValue(parent, out var waiting)
? waiting
: Array.Empty<uint>(),
child =>
{
attempted.Add(child);
return false;
});
Assert.Equal([20u], attempted);
}
[Fact]
public void DrawableRootRejectsVisualScaleInRigidPoseChannel()
{
var child = new AcDream.Core.World.WorldEntity
{
Id = 4u,
SourceGfxObjOrSetupId = 0x02000004u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<AcDream.Core.World.MeshRef>(),
};
Assert.False(EquippedChildRenderController.ApplyParentWorldPose(
child,
Matrix4x4.CreateScale(2f) * Matrix4x4.CreateTranslation(10, 0, 0)));
}
private sealed class Vector3ToleranceComparer : IEqualityComparer<Vector3>
{
public bool Equals(Vector3 x, Vector3 y) => Vector3.DistanceSquared(x, y) < 1e-8f;
public int GetHashCode(Vector3 obj) => obj.GetHashCode();
}
}