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,216 @@
using System.Numerics;
using AcDream.Core.Vfx;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Vfx;
/// <summary>
/// Update-thread registry of final root and indexed part poses for particles,
/// lights, and other DAT-driven entity effects.
/// </summary>
/// <remarks>
/// Retail composes particle anchors from the current physics-object/part frame
/// in <c>Particle::Init</c> (<c>0x0051C930</c>) and refreshes parent-local
/// particles from those frames in <c>ParticleEmitter::UpdateParticles</c>
/// (<c>0x0051D180</c>). This registry is the modern, read-only seam exposing
/// those same final frames without coupling Core effects to the renderer.
/// </remarks>
public sealed class EntityEffectPoseRegistry : IEntityEffectPoseSource, IEntityEffectCellSource
{
private sealed class PoseRecord
{
public Matrix4x4 RootWorld;
public Matrix4x4[] PartLocal = Array.Empty<Matrix4x4>();
public bool[] PartAvailable = Array.Empty<bool>();
public uint CellId;
}
private readonly Dictionary<uint, PoseRecord> _poses = new();
public int Count => _poses.Count;
public void Publish(WorldEntity entity, IReadOnlyList<Matrix4x4> partLocal)
{
Publish(entity, partLocal, availability: null);
}
public void Publish(
WorldEntity entity,
IReadOnlyList<Matrix4x4> partLocal,
IReadOnlyList<bool>? availability)
{
ArgumentNullException.ThrowIfNull(entity);
Publish(
entity.Id,
Matrix4x4.CreateFromQuaternion(entity.Rotation)
* Matrix4x4.CreateTranslation(entity.Position),
partLocal,
entity.ParentCellId ?? 0u,
availability);
}
/// <summary>
/// Publish the exact indexed part transforms already installed on an
/// entity. Appearance replacement uses this overload so a PhysicsScript
/// delivered later in the same update observes the new parts immediately.
/// </summary>
public void PublishMeshRefs(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
Matrix4x4 rootWorld = Matrix4x4.CreateFromQuaternion(entity.Rotation)
* Matrix4x4.CreateTranslation(entity.Position);
if (!_poses.TryGetValue(entity.Id, out PoseRecord? record))
{
record = new PoseRecord();
_poses.Add(entity.Id, record);
}
record.RootWorld = rootWorld;
record.CellId = entity.ParentCellId ?? 0u;
if (entity.IndexedPartTransforms.Count > 0)
{
CopyParts(
record,
entity.IndexedPartTransforms,
entity.IndexedPartAvailable);
}
else
{
if (record.PartLocal.Length != entity.MeshRefs.Count)
record.PartLocal = new Matrix4x4[entity.MeshRefs.Count];
if (record.PartAvailable.Length != entity.MeshRefs.Count)
record.PartAvailable = new bool[entity.MeshRefs.Count];
for (int i = 0; i < entity.MeshRefs.Count; i++)
{
record.PartLocal[i] = entity.MeshRefs[i].PartTransform;
record.PartAvailable[i] = true;
}
}
}
public void Publish(
uint localEntityId,
Matrix4x4 rootWorld,
IReadOnlyList<Matrix4x4> partLocal,
uint cellId,
IReadOnlyList<bool>? availability = null)
{
if (localEntityId == 0)
return;
ArgumentNullException.ThrowIfNull(partLocal);
if (!_poses.TryGetValue(localEntityId, out PoseRecord? record))
{
record = new PoseRecord();
_poses.Add(localEntityId, record);
}
record.RootWorld = rootWorld;
record.CellId = cellId;
CopyParts(record, partLocal, availability);
}
/// <summary>Refresh only the moving root while retaining current part poses.</summary>
public bool UpdateRoot(WorldEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
if (!_poses.TryGetValue(entity.Id, out PoseRecord? record))
return false;
record.RootWorld = Matrix4x4.CreateFromQuaternion(entity.Rotation)
* Matrix4x4.CreateTranslation(entity.Position);
record.CellId = entity.ParentCellId ?? 0u;
return true;
}
public bool Remove(uint localEntityId) => _poses.Remove(localEntityId);
public void Clear() => _poses.Clear();
public bool TryGetRootPose(uint localEntityId, out Matrix4x4 rootWorld)
{
if (_poses.TryGetValue(localEntityId, out PoseRecord? record))
{
rootWorld = record.RootWorld;
return true;
}
rootWorld = default;
return false;
}
public bool TryGetPartPose(uint localEntityId, int partIndex, out Matrix4x4 partLocal)
{
if (partIndex >= 0
&& _poses.TryGetValue(localEntityId, out PoseRecord? record)
&& partIndex < record.PartLocal.Length
&& record.PartAvailable[partIndex])
{
partLocal = record.PartLocal[partIndex];
return true;
}
partLocal = default;
return false;
}
/// <summary>
/// Borrow the current indexed part-pose snapshot for synchronous
/// update-thread composition. Callers must not retain or mutate it.
/// </summary>
public bool TryGetPartPoses(
uint localEntityId,
out IReadOnlyList<Matrix4x4> partLocal)
{
if (_poses.TryGetValue(localEntityId, out PoseRecord? record))
{
partLocal = record.PartLocal;
return true;
}
partLocal = Array.Empty<Matrix4x4>();
return false;
}
public bool TryGetPartPoseSnapshot(
uint localEntityId,
out IReadOnlyList<Matrix4x4> partLocal,
out IReadOnlyList<bool> availability)
{
if (_poses.TryGetValue(localEntityId, out PoseRecord? record))
{
partLocal = record.PartLocal;
availability = record.PartAvailable;
return true;
}
partLocal = Array.Empty<Matrix4x4>();
availability = Array.Empty<bool>();
return false;
}
public bool TryGetCellId(uint localEntityId, out uint cellId)
{
if (_poses.TryGetValue(localEntityId, out PoseRecord? record))
{
cellId = record.CellId;
return true;
}
cellId = 0;
return false;
}
private static void CopyParts(
PoseRecord record,
IReadOnlyList<Matrix4x4> partLocal,
IReadOnlyList<bool>? availability)
{
if (availability is not null && availability.Count != partLocal.Count)
throw new ArgumentException("Part pose and availability counts must match.");
if (record.PartLocal.Length != partLocal.Count)
record.PartLocal = new Matrix4x4[partLocal.Count];
if (record.PartAvailable.Length != partLocal.Count)
record.PartAvailable = new bool[partLocal.Count];
for (int i = 0; i < partLocal.Count; i++)
{
record.PartLocal[i] = partLocal[i];
record.PartAvailable[i] = availability is null || availability[i];
}
}
}