acdream/src/AcDream.App/Rendering/LiveEntityAnimationPresenter.cs

481 lines
19 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Vfx;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
/// <summary>
/// Publishes the final visual and rigid part channels after retail object and
/// static-animation scheduling. It advances no time and drains no hooks.
/// </summary>
internal sealed class LiveEntityAnimationPresenter
{
private readonly LiveEntityRuntime _liveEntities;
private readonly ILiveStaticPartFrameSource _staticFrames;
private readonly EntityEffectPoseRegistry _effectPoses;
private readonly ILiveAnimationPresentationContext _context;
private readonly AnimationPresentationDiagnostics _diagnostics;
private readonly int _hidePartIndex;
private readonly List<LiveEntityRecord> _snapshot = new();
private bool _isPresenting;
public LiveEntityAnimationPresenter(
LiveEntityRuntime liveEntities,
ILiveStaticPartFrameSource staticFrames,
EntityEffectPoseRegistry effectPoses,
ILiveAnimationPresentationContext context,
AnimationPresentationDiagnostics diagnostics,
int hidePartIndex)
{
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_staticFrames = staticFrames ?? throw new ArgumentNullException(nameof(staticFrames));
_effectPoses = effectPoses ?? throw new ArgumentNullException(nameof(effectPoses));
_context = context ?? throw new ArgumentNullException(nameof(context));
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_hidePartIndex = hidePartIndex;
}
public void PrepareAnimation(
LiveEntityRecord record,
LiveEntityAnimationState animation)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(animation);
LiveEntityRuntime runtime = _liveEntities;
// The PartArray completion relay is a logical CPhysicsObj component,
// not a render-workset component. Projection hydration can bind it
// while world rendering is intentionally quiesced during login or
// teleport; waiting for spatial visibility would orphan the initial
// StopCompletely pending-motion node before the first visible frame.
if (!runtime.IsCurrentAnimationOwner(record, animation)
|| !ReferenceEquals(record.WorldEntity, animation.Entity)
|| animation.Sequencer is not { MotionDoneTarget: null } sequencer)
{
return;
}
WorldEntity capturedEntity = animation.Entity;
sequencer.MotionDoneTarget = (motion, success) =>
{
LiveEntityRuntime current = _liveEntities;
// CPhysicsObj owns its PartArray and CMotionInterp across
// leave/enter-world projection edges. The callback may therefore
// complete while rendering is intentionally withheld (login or
// teleport reveal), but it must never cross an entity incarnation
// or an animation-component replacement.
if (!current.IsCurrentAnimationOwner(record, animation)
|| !ReferenceEquals(record.WorldEntity, capturedEntity)
|| !ReferenceEquals(animation.Entity, capturedEntity))
{
return;
}
MotionInterpreter? interpreter = _context.ResolveMotionInterpreter(record);
interpreter?.MotionDone(motion, success);
if (_diagnostics.DumpMotionEnabled)
{
Console.WriteLine(
$"[MOTIONDONE] guid={record.ServerGuid:X8} motion=0x{motion:X8} "
+ $"success={success} pending={(interpreter?.MotionsPending() ?? false)}");
}
};
}
public void Present(
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules)
{
ArgumentNullException.ThrowIfNull(schedules);
LiveEntityRuntime runtime = _liveEntities;
if (_isPresenting)
return;
// EffectPoseChanged is synchronous. Presentation is one consuming
// object-frame phase; recursive entry cannot consume legacy elapsed or
// a static prepared frame a second time.
_isPresenting = true;
try
{
runtime.CopySpatialRootObjectRecordsTo(_snapshot);
foreach (LiveEntityRecord record in _snapshot)
{
if (!TryGetCurrent(runtime, record, out WorldEntity entity, out LiveEntityAnimationState animation))
continue;
PrepareAnimation(record, animation);
if (!TryGetCurrent(runtime, record, entity, animation))
continue;
schedules.TryGetValue(entity.Id, out LiveEntityAnimationSchedule schedule);
bool hasOrdinarySchedule = IsCurrentSchedule(runtime, schedule, record, entity, animation);
IReadOnlyList<PartTransform>? sequenceFrames = hasOrdinarySchedule
? schedule.SequenceFrames
: null;
bool composeParts = hasOrdinarySchedule && schedule.ComposeParts;
float legacyAdvanceSeconds = hasOrdinarySchedule
? schedule.LegacyAdvanceSeconds
: 0f;
ulong objectClockEpoch = record.ObjectClockEpoch;
ulong projectionVersion = record.ProjectionMutationVersion;
ulong presentationRevision = animation.PresentationRevision;
if (_staticFrames.TryTakeLivePartFrames(
record,
entity,
animation,
objectClockEpoch,
projectionVersion,
presentationRevision,
out IReadOnlyList<PartTransform> staticPartFrames) == true)
{
if (!TryGetCurrent(
runtime,
record,
entity,
animation,
objectClockEpoch,
projectionVersion,
presentationRevision))
{
continue;
}
sequenceFrames = staticPartFrames;
composeParts = true;
}
if (animation.Sequencer is not null)
{
EmitSequenceDiagnostics(record, animation, sequenceFrames);
}
else
{
int span = animation.HighFrame - animation.LowFrame;
bool hiddenLegacy = (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
if (span <= 0 && !hiddenLegacy)
continue;
if (span > 0 && legacyAdvanceSeconds > 0f)
{
animation.CurrFrame += legacyAdvanceSeconds * animation.Framerate;
if (animation.CurrFrame > animation.HighFrame)
{
float over = animation.CurrFrame - animation.LowFrame;
animation.CurrFrame = animation.LowFrame + (over % (span + 1));
}
else if (animation.CurrFrame < animation.LowFrame)
{
animation.CurrFrame = animation.LowFrame;
}
}
}
if (!composeParts
|| !TryGetCurrent(
runtime,
record,
entity,
animation,
objectClockEpoch,
projectionVersion,
presentationRevision))
{
continue;
}
ComposeAndPublish(runtime, record, entity, animation, sequenceFrames,
objectClockEpoch, projectionVersion, presentationRevision);
}
}
finally
{
_isPresenting = false;
}
}
private void ComposeAndPublish(
LiveEntityRuntime runtime,
LiveEntityRecord record,
WorldEntity entity,
LiveEntityAnimationState animation,
IReadOnlyList<PartTransform>? sequenceFrames,
ulong objectClockEpoch,
ulong projectionVersion,
ulong presentationRevision)
{
int partCount = animation.PartTemplate.Count;
EmitPartDiagnostics(record, animation, sequenceFrames, partCount);
EnsureRetainedPoses(animation);
List<MeshRef> meshRefs = animation.MeshRefsScratch;
meshRefs.Clear();
List<Matrix4x4> rigidPoses = animation.EffectPartPosesScratch;
List<Matrix4x4> visualPoses = animation.VisualPartPosesScratch;
Matrix4x4 scale = animation.Scale == 1f
? Matrix4x4.Identity
: Matrix4x4.CreateScale(animation.Scale);
for (int i = 0; i < partCount; i++)
{
if (TryResolvePartFrame(
animation,
sequenceFrames,
i,
out Vector3 origin,
out Quaternion orientation))
{
Vector3 defaultScale = i < animation.Setup.DefaultScale.Count
? animation.Setup.DefaultScale[i]
: Vector3.One;
Matrix4x4 visual = Matrix4x4.CreateScale(defaultScale)
* Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(origin);
if (animation.Scale != 1f)
visual *= scale;
visualPoses[i] = visual;
rigidPoses[i] = Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(origin * animation.Scale);
}
LiveAnimationPartTemplate template = animation.PartTemplate[i];
if (!template.IsDrawable
|| (_hidePartIndex >= 0 && i == _hidePartIndex && partCount >= 10))
{
continue;
}
meshRefs.Add(new MeshRef(template.GfxObjId, visualPoses[i])
{
SurfaceOverrides = template.SurfaceOverrides,
});
}
if (!TryGetCurrent(runtime, record, entity, animation, objectClockEpoch, projectionVersion, presentationRevision))
return;
entity.MeshRefs = meshRefs;
entity.SetIndexedPartPoses(rigidPoses, animation.PartAvailability);
if (!TryGetCurrent(runtime, record, entity, animation, objectClockEpoch, projectionVersion, presentationRevision))
return;
_effectPoses.Publish(entity, rigidPoses, animation.PartAvailability);
}
private static bool TryResolvePartFrame(
LiveEntityAnimationState animation,
IReadOnlyList<PartTransform>? sequenceFrames,
int partIndex,
out Vector3 origin,
out Quaternion orientation)
{
if (sequenceFrames is not null)
{
if (partIndex < sequenceFrames.Count)
{
origin = sequenceFrames[partIndex].Origin;
orientation = sequenceFrames[partIndex].Orientation;
return true;
}
origin = default;
orientation = default;
return false;
}
int frameIndex = (int)Math.Floor(animation.CurrFrame);
if (frameIndex < animation.LowFrame
|| frameIndex > animation.HighFrame
|| frameIndex >= animation.Animation.PartFrames.Count)
{
frameIndex = animation.LowFrame;
}
int nextIndex = frameIndex + 1;
if (nextIndex > animation.HighFrame
|| nextIndex >= animation.Animation.PartFrames.Count)
{
nextIndex = animation.LowFrame;
}
float t = Math.Clamp(animation.CurrFrame - frameIndex, 0f, 1f);
var frames = animation.Animation.PartFrames[frameIndex].Frames;
var nextFrames = animation.Animation.PartFrames[nextIndex].Frames;
if (partIndex < frames.Count)
{
var first = frames[partIndex];
var next = partIndex < nextFrames.Count ? nextFrames[partIndex] : first;
origin = Vector3.Lerp(first.Origin, next.Origin, t);
orientation = Quaternion.Slerp(first.Orientation, next.Orientation, t);
return true;
}
origin = default;
orientation = default;
return false;
}
private static void EnsureRetainedPoses(LiveEntityAnimationState animation)
{
int partCount = animation.PartTemplate.Count;
if (animation.PresentationPosesInitialized
&& animation.VisualPartPosesScratch.Count == partCount
&& animation.EffectPartPosesScratch.Count == partCount)
{
return;
}
List<Matrix4x4> visual = animation.VisualPartPosesScratch;
List<Matrix4x4> rigid = animation.EffectPartPosesScratch;
visual.Clear();
rigid.Clear();
bool[] consumed = new bool[animation.Entity.MeshRefs.Count];
for (int i = 0; i < partCount; i++)
{
Matrix4x4 rigidRest = i < animation.Entity.IndexedPartTransforms.Count
? animation.Entity.IndexedPartTransforms[i]
: Matrix4x4.Identity;
rigid.Add(rigidRest);
LiveAnimationPartTemplate template = animation.PartTemplate[i];
Matrix4x4 visualRest = default;
bool found = false;
for (int meshIndex = 0; meshIndex < animation.Entity.MeshRefs.Count; meshIndex++)
{
MeshRef mesh = animation.Entity.MeshRefs[meshIndex];
if (consumed[meshIndex] || mesh.GfxObjId != template.GfxObjId)
continue;
consumed[meshIndex] = true;
visualRest = mesh.PartTransform;
found = true;
break;
}
if (!found)
{
Vector3 defaultScale = i < animation.Setup.DefaultScale.Count
? animation.Setup.DefaultScale[i]
: Vector3.One;
visualRest = Matrix4x4.CreateScale(defaultScale * animation.Scale)
* rigidRest;
}
visual.Add(visualRest);
}
animation.PresentationPosesInitialized = true;
}
private void EmitSequenceDiagnostics(
LiveEntityRecord record,
LiveEntityAnimationState animation,
IReadOnlyList<PartTransform>? sequenceFrames)
{
if (!_diagnostics.RemoteVelocityEnabled
|| record.ServerGuid == 0
|| record.ServerGuid == _context.LocalPlayerGuid
|| record.RemoteMotionRuntime is null)
{
return;
}
double now = _diagnostics.Now();
if (now - animation.LastSequenceDiagnosticTime <= 1d)
return;
AnimationSequencer sequencer = animation.Sequencer!;
Console.WriteLine(
$"[SEQSTATE] guid={record.ServerGuid:X8} CurrentMotion=0x{sequencer.CurrentMotion:X8} "
+ $"CurrentSpeedMod={sequencer.CurrentSpeedMod:F3}");
var node = sequencer.CurrentNodeDiag;
int firstHash = sequencer.FirstCyclicAnimRefHash;
Console.WriteLine(
$"[CURRNODE] guid={record.ServerGuid:X8} animRef=0x{node.AnimRefHash:X8} "
+ $"firstCyclicAnimRef=0x{firstHash:X8} "
+ $"isOnCyclic={node.AnimRefHash == firstHash && firstHash != 0} "
+ $"isLooping={node.IsLooping} fr={node.Framerate:F2} "
+ $"frame=[{node.StartFrame}..{node.EndFrame}] "
+ $"pos={node.FramePosition:F2} qCount={node.QueueCount}");
animation.LastSequenceDiagnosticTime = now;
}
private void EmitPartDiagnostics(
LiveEntityRecord record,
LiveEntityAnimationState animation,
IReadOnlyList<PartTransform>? sequenceFrames,
int partCount)
{
if (!_diagnostics.RemoteVelocityEnabled
|| record.ServerGuid == 0
|| record.ServerGuid == _context.LocalPlayerGuid
|| record.RemoteMotionRuntime is null)
{
return;
}
double now = _diagnostics.Now();
if (now - animation.LastPartDiagnosticTime <= 1d)
return;
int animationPartCount = animation.Animation is not null
&& animation.Animation.PartFrames.Count > 0
? animation.Animation.PartFrames[0].Frames.Count
: -1;
double hash = 0d;
if (sequenceFrames is not null)
{
foreach (PartTransform frame in sequenceFrames)
{
hash += frame.Origin.X + frame.Origin.Y + frame.Origin.Z
+ frame.Orientation.X + frame.Orientation.Y
+ frame.Orientation.Z + frame.Orientation.W;
}
}
Console.WriteLine(
$"[PARTSDIAG] guid={record.ServerGuid:X8} pt.Count={partCount} "
+ $"seqFrames.Count={sequenceFrames?.Count ?? -1} "
+ $"setup.Parts.Count={animation.Setup.Parts.Count} "
+ $"anim.PartFrames[0].Frames.Count={animationPartCount} seqHash={hash:F4}");
animation.LastPartDiagnosticTime = now;
}
private static bool IsCurrentSchedule(
LiveEntityRuntime runtime,
LiveEntityAnimationSchedule schedule,
LiveEntityRecord record,
WorldEntity entity,
LiveEntityAnimationState animation) =>
schedule.ComposeParts
&& ReferenceEquals(schedule.Record, record)
&& ReferenceEquals(schedule.Entity, entity)
&& ReferenceEquals(schedule.Animation, animation)
&& TryGetCurrent(
runtime,
record,
entity,
animation,
schedule.ObjectClockEpoch,
schedule.ProjectionMutationVersion,
schedule.PresentationRevision);
private static bool TryGetCurrent(
LiveEntityRuntime runtime,
LiveEntityRecord record,
out WorldEntity entity,
out LiveEntityAnimationState animation)
{
WorldEntity? currentEntity = record.WorldEntity;
LiveEntityAnimationState? currentAnimation =
record.AnimationRuntime as LiveEntityAnimationState;
entity = currentEntity!;
animation = currentAnimation!;
return currentEntity is not null
&& currentAnimation is not null
&& TryGetCurrent(runtime, record, entity, animation);
}
private static bool TryGetCurrent(
LiveEntityRuntime runtime,
LiveEntityRecord record,
WorldEntity entity,
LiveEntityAnimationState animation) =>
runtime.IsCurrentSpatialAnimation(record, animation)
&& ReferenceEquals(record.WorldEntity, entity)
&& ReferenceEquals(animation.Entity, entity);
private static bool TryGetCurrent(
LiveEntityRuntime runtime,
LiveEntityRecord record,
WorldEntity entity,
LiveEntityAnimationState animation,
ulong objectClockEpoch,
ulong projectionVersion,
ulong presentationRevision) =>
TryGetCurrent(runtime, record, entity, animation)
&& record.ObjectClockEpoch == objectClockEpoch
&& record.ProjectionMutationVersion == projectionVersion
&& animation.PresentationRevision == presentationRevision;
}