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

@ -7,48 +7,35 @@ using DatReaderWriter.Types;
namespace AcDream.Core.Meshing;
/// <summary>
/// Compute the per-part static transforms for a Setup using its
/// PlacementFrames. For each part <c>i</c>, the returned matrix takes a
/// point in part-local space and yields a point in setup-local space at
/// the Setup's resting pose.
///
/// <para>
/// Mirrors <see cref="SetupMesh.Flatten"/>'s pose-source priority —
/// <c>PlacementFrames[Resting]</c> → <c>[Default]</c> → first available
/// — so that a particle anchor at part <c>i</c> matches the part's
/// visible rest position. If renderer and resolver ever drift on this
/// priority, particles will visibly drift relative to their parent
/// mesh; keep them in lockstep.
/// </para>
///
/// <para>
/// Returns an empty list when the Setup has no PlacementFrames. The
/// caller (e.g. <c>ParticleHookSink.SpawnFromHook</c>) should then fall
/// back to <see cref="Matrix4x4.Identity"/> per part, which is the
/// pre-C.1.5b behavior.
/// </para>
///
/// <para>
/// For animated entities, per-part transforms vary per animation frame
/// and live in <c>AnimatedEntityState</c>; a future "animated
/// DefaultScript" path would publish those each tick via the same
/// <c>SetEntityPartTransforms</c> seam. Out of scope here.
/// </para>
/// Computes retail's rigid per-part frames for attachments and effects.
/// Visual GfxObj scale is deliberately excluded.
/// </summary>
/// <remarks>
/// Pose selection matches <see cref="SetupMesh.Flatten"/>: an explicit motion
/// frame, then Resting, Default, then the first placement frame. Retail
/// <c>CPartArray::UpdateParts</c> (<c>0x005190F0</c>) scales only the frame
/// origin by the object's scale. <c>CPartArray::SetScaleInternal</c>
/// (<c>0x00518A00</c>) stores Setup DefaultScale separately on the visual
/// <c>CPhysicsPart::gfxobj_scale</c>, so it never enters this rigid frame.
/// </remarks>
public static class SetupPartTransforms
{
public static IReadOnlyList<Matrix4x4> Compute(Setup setup)
public static IReadOnlyList<Matrix4x4> Compute(
Setup setup,
AnimationFrame? motionFrameOverride = null,
float objectScale = 1.0f)
{
AnimationFrame? source = null;
if (setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting))
ArgumentNullException.ThrowIfNull(setup);
AnimationFrame? source = motionFrameOverride;
if (source is null && setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting))
{
source = resting;
}
else if (setup.PlacementFrames.TryGetValue(Placement.Default, out var def))
else if (source is null && setup.PlacementFrames.TryGetValue(Placement.Default, out var def))
{
source = def;
}
else
else if (source is null)
{
foreach (var kvp in setup.PlacementFrames)
{
@ -57,7 +44,8 @@ public static class SetupPartTransforms
}
}
if (source is null) return System.Array.Empty<Matrix4x4>();
if (source is null)
return Array.Empty<Matrix4x4>();
int partCount = setup.Parts.Count;
var result = new Matrix4x4[partCount];
@ -66,10 +54,8 @@ public static class SetupPartTransforms
Frame frame = i < source.Frames.Count
? source.Frames[i]
: new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity };
Vector3 scale = i < setup.DefaultScale.Count ? setup.DefaultScale[i] : Vector3.One;
result[i] = Matrix4x4.CreateScale(scale)
* Matrix4x4.CreateFromQuaternion(frame.Orientation)
* Matrix4x4.CreateTranslation(frame.Origin);
result[i] = Matrix4x4.CreateFromQuaternion(frame.Orientation)
* Matrix4x4.CreateTranslation(frame.Origin * objectScale);
}
return result;
}