feat(vfx): bind effects to live animated poses
This commit is contained in:
parent
96ddfdf175
commit
542dcfc384
41 changed files with 3246 additions and 741 deletions
|
|
@ -0,0 +1,166 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Vfx;
|
||||
|
||||
public sealed class EntityEffectPoseRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Publish_PreservesIndexedParts_AndUpdateRootDoesNotReplaceThem()
|
||||
{
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
WorldEntity entity = Entity(7u, new Vector3(1, 2, 3));
|
||||
poses.Publish(entity, new[]
|
||||
{
|
||||
Matrix4x4.Identity,
|
||||
Matrix4x4.CreateTranslation(0, 0, 4),
|
||||
});
|
||||
|
||||
entity.SetPosition(new Vector3(10, 20, 30));
|
||||
Assert.True(poses.UpdateRoot(entity));
|
||||
|
||||
Assert.True(poses.TryGetRootPose(7u, out Matrix4x4 root));
|
||||
Assert.Equal(new Vector3(10, 20, 30), root.Translation);
|
||||
Assert.True(poses.TryGetPartPose(7u, 1, out Matrix4x4 part));
|
||||
Assert.Equal(new Vector3(0, 0, 4), part.Translation);
|
||||
Assert.False(poses.TryGetPartPose(7u, -1, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublishMeshRefs_ReplacesPartSnapshotImmediately()
|
||||
{
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
WorldEntity entity = Entity(8u, Vector3.Zero);
|
||||
poses.Publish(entity, new[] { Matrix4x4.Identity });
|
||||
entity.MeshRefs = new[]
|
||||
{
|
||||
new MeshRef(0x01000001u, Matrix4x4.CreateTranslation(1, 2, 3)),
|
||||
new MeshRef(0x01000002u, Matrix4x4.CreateTranslation(4, 5, 6)),
|
||||
};
|
||||
|
||||
poses.PublishMeshRefs(entity);
|
||||
|
||||
Assert.True(poses.TryGetPartPoses(8u, out var parts));
|
||||
Assert.Equal(2, parts.Count);
|
||||
Assert.Equal(new Vector3(4, 5, 6), parts[1].Translation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Publish_MissingMiddlePartRetainsIndicesButCannotBeResolved()
|
||||
{
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
WorldEntity entity = Entity(81u, Vector3.Zero);
|
||||
|
||||
poses.Publish(
|
||||
entity,
|
||||
[
|
||||
Matrix4x4.CreateTranslation(1, 0, 0),
|
||||
Matrix4x4.CreateTranslation(2, 0, 0),
|
||||
Matrix4x4.CreateTranslation(3, 0, 0),
|
||||
],
|
||||
[true, false, true]);
|
||||
|
||||
Assert.False(poses.TryGetPartPose(81u, 1, out _));
|
||||
Assert.True(poses.TryGetPartPose(81u, 2, out Matrix4x4 third));
|
||||
Assert.Equal(3f, third.Translation.X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublishMeshRefs_PrefersStableIndexedEntityPosesOverCompactedDrawables()
|
||||
{
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
WorldEntity entity = Entity(82u, Vector3.Zero);
|
||||
entity.MeshRefs =
|
||||
[
|
||||
new MeshRef(1u, Matrix4x4.CreateTranslation(1, 0, 0)),
|
||||
new MeshRef(3u, Matrix4x4.CreateTranslation(3, 0, 0)),
|
||||
];
|
||||
entity.SetIndexedPartPoses(
|
||||
[
|
||||
Matrix4x4.CreateTranslation(1, 0, 0),
|
||||
Matrix4x4.CreateTranslation(2, 0, 0),
|
||||
Matrix4x4.CreateTranslation(3, 0, 0),
|
||||
],
|
||||
[true, false, true]);
|
||||
|
||||
poses.PublishMeshRefs(entity);
|
||||
|
||||
Assert.False(poses.TryGetPartPose(82u, 1, out _));
|
||||
Assert.True(poses.TryGetPartPose(82u, 2, out Matrix4x4 third));
|
||||
Assert.Equal(3f, third.Translation.X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnimationHookQueue_DrainsAgainstPosePublishedAfterCapture()
|
||||
{
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
WorldEntity entity = Entity(9u, Vector3.Zero);
|
||||
poses.Publish(entity, Array.Empty<Matrix4x4>());
|
||||
var router = new AnimationHookRouter();
|
||||
var sink = new RecordingSink();
|
||||
router.Register(sink);
|
||||
var queue = new AnimationHookFrameQueue(router, poses);
|
||||
var sequencer = new AnimationSequencer(
|
||||
new Setup(),
|
||||
new MotionTable(),
|
||||
new NullAnimationLoader());
|
||||
|
||||
queue.Capture(9u, sequencer, new AnimationHook[] { new SoundHook() });
|
||||
entity.SetPosition(new Vector3(4, 5, 6));
|
||||
poses.UpdateRoot(entity);
|
||||
queue.Drain();
|
||||
|
||||
Assert.Single(sink.Calls);
|
||||
Assert.Equal(new Vector3(4, 5, 6), sink.Calls[0].Position);
|
||||
Assert.Equal(0, queue.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnimationHookQueue_CompletesMotionStateEvenWhenPoseWasRemoved()
|
||||
{
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var queue = new AnimationHookFrameQueue(new AnimationHookRouter(), poses);
|
||||
var sequencer = new AnimationSequencer(
|
||||
new Setup(),
|
||||
new MotionTable(),
|
||||
new NullAnimationLoader());
|
||||
sequencer.Manager.AddToQueue(0x10000001u, ticks: 1u);
|
||||
|
||||
queue.Capture(
|
||||
11u,
|
||||
sequencer,
|
||||
new AnimationHook[] { new AnimationDoneHook() });
|
||||
queue.Drain();
|
||||
|
||||
Assert.Empty(sequencer.Manager.PendingAnimations);
|
||||
Assert.Equal(0, queue.Count);
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(uint id, Vector3 position) => new()
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = id,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = position,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
ParentCellId = 0x01010001u,
|
||||
};
|
||||
|
||||
private sealed class RecordingSink : IAnimationHookSink
|
||||
{
|
||||
public List<(uint Id, Vector3 Position)> Calls { get; } = new();
|
||||
|
||||
public void OnHook(uint entityId, Vector3 entityWorldPosition, AnimationHook hook) =>
|
||||
Calls.Add((entityId, entityWorldPosition));
|
||||
}
|
||||
|
||||
private sealed class NullAnimationLoader : IAnimationLoader
|
||||
{
|
||||
public Animation? LoadAnimation(uint animationId) => null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue