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
176
src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs
Normal file
176
src/AcDream.App/Rendering/Vfx/LiveEntityLightController.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
using AcDream.App.World;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Rendering.Vfx;
|
||||
|
||||
/// <summary>
|
||||
/// Projects Setup lights for live top-level and attached entities, while the
|
||||
/// shared pose registry supplies their current root on each update frame.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail <c>CPartArray::SetFrame</c> (<c>0x00519310</c>) calls
|
||||
/// <c>LIGHTLIST::set_frame</c> (<c>0x00517C60</c>) whenever the owning physics
|
||||
/// object's frame changes. Logical ownership remains in
|
||||
/// <see cref="LiveEntityRuntime"/>; leaving the world removes only this
|
||||
/// cell-scoped presentation and re-entry registers it again.
|
||||
/// </remarks>
|
||||
public sealed class LiveEntityLightController : IDisposable
|
||||
{
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
private readonly EntityEffectPoseRegistry _poses;
|
||||
private readonly LightingHookSink _lighting;
|
||||
private readonly Func<uint, Setup?> _loadSetup;
|
||||
private readonly Dictionary<uint, uint> _serverGuidByOwner = new();
|
||||
private readonly HashSet<uint> _presentOwners = new();
|
||||
|
||||
public LiveEntityLightController(
|
||||
LiveEntityRuntime liveEntities,
|
||||
EntityEffectPoseRegistry poses,
|
||||
LightingHookSink lighting,
|
||||
Func<uint, Setup?> loadSetup)
|
||||
{
|
||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_poses = poses ?? throw new ArgumentNullException(nameof(poses));
|
||||
_lighting = lighting ?? throw new ArgumentNullException(nameof(lighting));
|
||||
_loadSetup = loadSetup ?? throw new ArgumentNullException(nameof(loadSetup));
|
||||
_lighting.OwnerLightingChanged += OnOwnerLightingChanged;
|
||||
_liveEntities.ProjectionVisibilityChanged += OnProjectionVisibilityChanged;
|
||||
}
|
||||
|
||||
public bool Register(uint serverGuid)
|
||||
{
|
||||
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
|| record.WorldEntity is not { } entity
|
||||
|| !record.IsSpatiallyProjected
|
||||
|| !record.IsSpatiallyVisible)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_serverGuidByOwner[entity.Id] = serverGuid;
|
||||
_presentOwners.Add(entity.Id);
|
||||
_lighting.UnregisterOwner(entity.Id, forgetState: false);
|
||||
_lighting.InitializeOwnerLighting(
|
||||
entity.Id,
|
||||
(record.FinalPhysicsState & PhysicsStateFlags.Lighting) != 0);
|
||||
if ((entity.SourceGfxObjOrSetupId & 0xFF000000u) != 0x02000000u)
|
||||
return false;
|
||||
|
||||
if (record.ProjectionKind is LiveEntityProjectionKind.World)
|
||||
{
|
||||
if (!_poses.UpdateRoot(entity))
|
||||
_poses.PublishMeshRefs(entity);
|
||||
}
|
||||
|
||||
Setup? setup = _loadSetup(entity.SourceGfxObjOrSetupId);
|
||||
if (setup is null
|
||||
|| setup.Lights.Count == 0
|
||||
|| !_lighting.IsOwnerLightingEnabled(entity.Id))
|
||||
return false;
|
||||
|
||||
bool isDynamic = (record.FinalPhysicsState & PhysicsStateFlags.Static) == 0;
|
||||
IReadOnlyList<LightSource> loaded = LightInfoLoader.Load(
|
||||
setup,
|
||||
entity.Id,
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
isDynamic,
|
||||
entity.ParentCellId ?? 0u,
|
||||
tracksOwnerPose: true);
|
||||
for (int i = 0; i < loaded.Count; i++)
|
||||
_lighting.RegisterOwnedLight(loaded[i]);
|
||||
return loaded.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>Withdraw cell-scoped presentation but retain logical light state.</summary>
|
||||
public void Unregister(uint ownerLocalId)
|
||||
{
|
||||
_presentOwners.Remove(ownerLocalId);
|
||||
_lighting.UnregisterOwner(ownerLocalId, forgetState: false);
|
||||
}
|
||||
|
||||
/// <summary>Apply a fresh authoritative PhysicsState Lighting transition.</summary>
|
||||
public void OnStateChanged(uint serverGuid)
|
||||
{
|
||||
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
|| record.WorldEntity is not { } entity)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lighting.SetOwnerLighting(
|
||||
entity.Id,
|
||||
(record.FinalPhysicsState & PhysicsStateFlags.Lighting) != 0);
|
||||
}
|
||||
|
||||
/// <summary>End logical ownership after leave-world presentation teardown.</summary>
|
||||
public void Forget(uint ownerLocalId)
|
||||
{
|
||||
_presentOwners.Remove(ownerLocalId);
|
||||
_serverGuidByOwner.Remove(ownerLocalId);
|
||||
_lighting.UnregisterOwner(ownerLocalId);
|
||||
}
|
||||
|
||||
public void Refresh() => _lighting.RefreshAttachedLights();
|
||||
|
||||
/// <summary>
|
||||
/// Attached projection visibility can become true before its holding-part
|
||||
/// composition is published. The attachment owner calls this barrier only
|
||||
/// after the composed child root is current.
|
||||
/// </summary>
|
||||
public void OnAttachedPoseReady(uint serverGuid)
|
||||
{
|
||||
if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
|| record.ProjectionKind is not LiveEntityProjectionKind.Attached
|
||||
|| record.WorldEntity is not { } entity
|
||||
|| _presentOwners.Contains(entity.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Register(serverGuid);
|
||||
}
|
||||
|
||||
private void OnOwnerLightingChanged(uint ownerLocalId, bool enabled)
|
||||
{
|
||||
if (!_presentOwners.Contains(ownerLocalId)
|
||||
|| !_serverGuidByOwner.TryGetValue(ownerLocalId, out uint serverGuid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
_lighting.UnregisterOwner(ownerLocalId, forgetState: false);
|
||||
return;
|
||||
}
|
||||
|
||||
Register(serverGuid);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_lighting.OwnerLightingChanged -= OnOwnerLightingChanged;
|
||||
_liveEntities.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged;
|
||||
foreach (uint ownerId in _serverGuidByOwner.Keys.ToArray())
|
||||
Forget(ownerId);
|
||||
}
|
||||
|
||||
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
|
||||
{
|
||||
if (record.WorldEntity is not { } entity)
|
||||
return;
|
||||
if (record.ProjectionKind is LiveEntityProjectionKind.Attached)
|
||||
{
|
||||
// The true edge precedes attachment pose publication. False is
|
||||
// safe and must immediately withdraw the cell-scoped light.
|
||||
if (!visible)
|
||||
Unregister(entity.Id);
|
||||
return;
|
||||
}
|
||||
if (visible)
|
||||
Register(record.ServerGuid);
|
||||
else
|
||||
Unregister(entity.Id);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue