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
|
|
@ -13,7 +13,7 @@ namespace AcDream.Core.Lighting;
|
|||
/// <para>
|
||||
/// Retail <see cref="LightInfo"/> fields (r13 §1):
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>ViewSpaceLocation</c>: local Frame relative to the owning part.</description></item>
|
||||
/// <item><description><c>ViewSpaceLocation</c>: local Frame relative to the owning PartArray root.</description></item>
|
||||
/// <item><description><c>Color</c>: packed ARGB. Alpha is ignored; channels go through <c>/255</c>.</description></item>
|
||||
/// <item><description><c>Intensity</c>: multiplies color for final diffuse.</description></item>
|
||||
/// <item><description><c>Falloff</c>: world metres — acts as the <see cref="LightSource.Range"/> hard cutoff.</description></item>
|
||||
|
|
@ -24,13 +24,10 @@ namespace AcDream.Core.Lighting;
|
|||
public static class LightInfoLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Extract all lights from a Setup, positioned in the entity's
|
||||
/// world frame (via <paramref name="entityPosition"/> +
|
||||
/// <paramref name="entityRotation"/>). The dat's per-light Frame is
|
||||
/// treated as a local offset relative to the entity root; acdream
|
||||
/// doesn't yet transform through the animated part chain (retail's
|
||||
/// hand-held torches), so held lights render at the entity root
|
||||
/// until the animation hook layer handles per-part placement.
|
||||
/// Extract all lights from a Setup and retain each light's complete local
|
||||
/// frame. Retail Setup lights belong to the PartArray root rather than an
|
||||
/// indexed mesh part; moving held objects therefore follow their child
|
||||
/// physics-object root after <c>CPhysicsObj::UpdateChild</c> composes it.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<LightSource> Load(
|
||||
Setup setup,
|
||||
|
|
@ -38,7 +35,8 @@ public static class LightInfoLoader
|
|||
Vector3 entityPosition,
|
||||
Quaternion entityRotation,
|
||||
bool isDynamic = false,
|
||||
uint cellId = 0)
|
||||
uint cellId = 0,
|
||||
bool tracksOwnerPose = false)
|
||||
{
|
||||
var results = new List<LightSource>();
|
||||
if (setup?.Lights is null || setup.Lights.Count == 0) return results;
|
||||
|
|
@ -64,12 +62,15 @@ public static class LightInfoLoader
|
|||
info.ViewSpaceLocation.Orientation.W);
|
||||
}
|
||||
|
||||
// Transform local offset into world space via the entity's
|
||||
// rotation + translation. No per-part chain yet — held
|
||||
// torches track the entity's root for now.
|
||||
Vector3 worldPos = entityPosition + Vector3.Transform(localOffset, entityRotation);
|
||||
Quaternion worldRot = entityRotation * localRot;
|
||||
Vector3 forward = Vector3.Transform(Vector3.UnitY, worldRot);
|
||||
Matrix4x4 localPose = Matrix4x4.CreateFromQuaternion(localRot)
|
||||
* Matrix4x4.CreateTranslation(localOffset);
|
||||
Matrix4x4 rootWorld = Matrix4x4.CreateFromQuaternion(entityRotation)
|
||||
* Matrix4x4.CreateTranslation(entityPosition);
|
||||
Matrix4x4 lightWorld = localPose * rootWorld;
|
||||
Vector3 worldPos = lightWorld.Translation;
|
||||
Vector3 forward = Vector3.TransformNormal(Vector3.UnitY, lightWorld);
|
||||
if (forward.LengthSquared() > 1e-8f)
|
||||
forward = Vector3.Normalize(forward);
|
||||
|
||||
var light = new LightSource
|
||||
{
|
||||
|
|
@ -93,6 +94,8 @@ public static class LightInfoLoader
|
|||
CellId = cellId, // owning cell — scopes the per-frame visible-cell pool (A7 #176/#177)
|
||||
IsLit = true,
|
||||
IsDynamic = isDynamic,
|
||||
TracksOwnerPose = tracksOwnerPose,
|
||||
LocalPose = localPose,
|
||||
};
|
||||
results.Add(light);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,20 @@ public sealed class LightSource
|
|||
public bool IsLit = true; // SetLightHook latch
|
||||
public bool IsDynamic; // #143: true = D3D hardware path (1/d att, range×1.5);
|
||||
// false = static dat-baked bake (1/d³, range×1.3)
|
||||
/// <summary>
|
||||
/// True when this light belongs to a live physics object whose root can
|
||||
/// move. DAT-static lights keep their hydration-time world frame and are
|
||||
/// deliberately excluded from the per-frame pose refresh.
|
||||
/// </summary>
|
||||
public bool TracksOwnerPose;
|
||||
|
||||
/// <summary>
|
||||
/// Setup-local light frame retained from <c>LIGHTINFO::view_space_location</c>.
|
||||
/// Object-borne lights compose it with their owner's current root each
|
||||
/// frame, matching <c>CPartArray::SetFrame</c> (<c>0x00519310</c>) calling
|
||||
/// <c>LIGHTLIST::set_frame</c> (<c>0x00517C60</c>).
|
||||
/// </summary>
|
||||
public Matrix4x4 LocalPose = Matrix4x4.Identity;
|
||||
|
||||
// Cached each frame by LightManager.
|
||||
public float DistSq;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Vfx;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Lighting;
|
||||
|
|
@ -24,14 +25,27 @@ namespace AcDream.Core.Lighting;
|
|||
public sealed class LightingHookSink : IAnimationHookSink
|
||||
{
|
||||
private readonly LightManager _lights;
|
||||
private readonly IEntityEffectPoseSource _poses;
|
||||
private readonly IEntityEffectCellSource? _cells;
|
||||
|
||||
// Index owner → the set of LightSource instances they registered.
|
||||
// Maintained lazily — populated on first RegisterLight for that owner.
|
||||
private readonly Dictionary<uint, List<LightSource>> _byOwner = new();
|
||||
private readonly Dictionary<uint, List<LightSource>> _trackedByOwner = new();
|
||||
private readonly Dictionary<uint, bool> _enabledByOwner = new();
|
||||
|
||||
public LightingHookSink(LightManager lights)
|
||||
/// <summary>
|
||||
/// Raised after retail's <c>set_lights</c> state changes. App-layer live
|
||||
/// owners use this to create/destroy their Setup light presentation;
|
||||
/// DAT-static owners are already registered and simply consume IsLit.
|
||||
/// </summary>
|
||||
public event Action<uint, bool>? OwnerLightingChanged;
|
||||
|
||||
public LightingHookSink(LightManager lights, IEntityEffectPoseSource poses)
|
||||
{
|
||||
_lights = lights ?? throw new System.ArgumentNullException(nameof(lights));
|
||||
_poses = poses ?? throw new System.ArgumentNullException(nameof(poses));
|
||||
_cells = poses as IEntityEffectCellSource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -41,6 +55,8 @@ public sealed class LightingHookSink : IAnimationHookSink
|
|||
public void RegisterOwnedLight(LightSource light)
|
||||
{
|
||||
System.ArgumentNullException.ThrowIfNull(light);
|
||||
if (_enabledByOwner.TryGetValue(light.OwnerId, out bool enabled))
|
||||
light.IsLit = enabled;
|
||||
_lights.Register(light);
|
||||
if (!_byOwner.TryGetValue(light.OwnerId, out var list))
|
||||
{
|
||||
|
|
@ -48,14 +64,54 @@ public sealed class LightingHookSink : IAnimationHookSink
|
|||
_byOwner[light.OwnerId] = list;
|
||||
}
|
||||
list.Add(light);
|
||||
if (light.TracksOwnerPose)
|
||||
{
|
||||
if (!_trackedByOwner.TryGetValue(light.OwnerId, out List<LightSource>? tracked))
|
||||
{
|
||||
tracked = new List<LightSource>();
|
||||
_trackedByOwner[light.OwnerId] = tracked;
|
||||
}
|
||||
tracked.Add(light);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Drop every light tagged to this owner (despawn / unload).</summary>
|
||||
public void UnregisterOwner(uint ownerId)
|
||||
public void UnregisterOwner(uint ownerId, bool forgetState = true)
|
||||
{
|
||||
if (!_byOwner.TryGetValue(ownerId, out var list)) return;
|
||||
foreach (var l in list) _lights.Unregister(l);
|
||||
_byOwner.Remove(ownerId);
|
||||
if (_byOwner.TryGetValue(ownerId, out var list))
|
||||
{
|
||||
foreach (var l in list) _lights.Unregister(l);
|
||||
_byOwner.Remove(ownerId);
|
||||
}
|
||||
_trackedByOwner.Remove(ownerId);
|
||||
if (forgetState)
|
||||
_enabledByOwner.Remove(ownerId);
|
||||
}
|
||||
|
||||
public void InitializeOwnerLighting(uint ownerId, bool enabled) =>
|
||||
_enabledByOwner.TryAdd(ownerId, enabled);
|
||||
|
||||
public bool IsOwnerLightingEnabled(uint ownerId) =>
|
||||
!_enabledByOwner.TryGetValue(ownerId, out bool enabled) || enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors <c>CPhysicsObj::set_lights</c> (0x0050FCF0): update the owner's
|
||||
/// Lighting state and its currently materialized lights as one operation.
|
||||
/// </summary>
|
||||
public void SetOwnerLighting(uint ownerId, bool enabled)
|
||||
{
|
||||
if (_enabledByOwner.TryGetValue(ownerId, out bool current)
|
||||
&& current == enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_enabledByOwner[ownerId] = enabled;
|
||||
if (_byOwner.TryGetValue(ownerId, out var list))
|
||||
{
|
||||
foreach (LightSource light in list)
|
||||
light.IsLit = enabled;
|
||||
}
|
||||
OwnerLightingChanged?.Invoke(ownerId, enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -67,12 +123,36 @@ public sealed class LightingHookSink : IAnimationHookSink
|
|||
return _byOwner.TryGetValue(ownerId, out var list) ? list : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recompose every object-borne light from the current physics-object root.
|
||||
/// Setup <c>LIGHTINFO</c> has no part index; equipped-object animation is
|
||||
/// represented by publishing that child's composed root.
|
||||
/// </summary>
|
||||
public void RefreshAttachedLights()
|
||||
{
|
||||
foreach ((uint ownerId, List<LightSource> lights) in _trackedByOwner)
|
||||
{
|
||||
if (!_poses.TryGetRootPose(ownerId, out Matrix4x4 rootWorld))
|
||||
continue;
|
||||
|
||||
uint cellId = 0;
|
||||
_cells?.TryGetCellId(ownerId, out cellId);
|
||||
for (int i = 0; i < lights.Count; i++)
|
||||
{
|
||||
LightSource light = lights[i];
|
||||
Matrix4x4 lightWorld = light.LocalPose * rootWorld;
|
||||
light.WorldPosition = lightWorld.Translation;
|
||||
Vector3 forward = Vector3.TransformNormal(Vector3.UnitY, lightWorld);
|
||||
if (forward.LengthSquared() > 1e-8f)
|
||||
light.WorldForward = Vector3.Normalize(forward);
|
||||
light.CellId = cellId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnHook(uint entityId, Vector3 entityWorldPosition, AnimationHook hook)
|
||||
{
|
||||
if (hook is not SetLightHook slh) return;
|
||||
if (!_byOwner.TryGetValue(entityId, out var list)) return;
|
||||
|
||||
foreach (var light in list)
|
||||
light.IsLit = slh.LightsOn;
|
||||
SetOwnerLighting(entityId, slh.LightsOn);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue