feat(rendering): journal live scene projections
Mirror exact live-root and equipped-child lifetimes behind the non-drawing render-scene boundary. Ready, visibility, final-pose, rebucket, removal, and resource teardown edges retain canonical LiveEntityRuntime identity while active-only synchronization avoids resident-static scans. Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
parent
5d19c56d15
commit
58e7c2eb99
10 changed files with 949 additions and 148 deletions
|
|
@ -59,6 +59,17 @@ internal sealed class LivePresentationRuntimeBindings : IDisposable
|
|||
() => source.ProjectionPoseReady -= handler));
|
||||
}
|
||||
|
||||
public void BindProjectionRemoved(
|
||||
EquippedChildRenderController source,
|
||||
Action<uint> handler)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
source.ProjectionRemoved += handler;
|
||||
Adopt("equipped-child projection removal", new DelegateBinding(
|
||||
() => source.ProjectionRemoved -= handler));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_deactivationStarted && _bindings.Count == 0)
|
||||
|
|
|
|||
266
src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs
Normal file
266
src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
using AcDream.App.World;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
internal interface ILiveRenderProjectionSink
|
||||
{
|
||||
bool OnEntityReady(LiveEntityReadyCandidate candidate);
|
||||
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
|
||||
void OnProjectionPoseReady(uint serverGuid);
|
||||
void OnProjectionRemoved(uint localEntityId);
|
||||
void OnResourceUnregister(WorldEntity entity);
|
||||
void SynchronizeActiveSources();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change-driven render projection for canonical live records. It retains
|
||||
/// exact record references and local presentation IDs only; server GUID lookup
|
||||
/// remains exclusively in <see cref="LiveEntityRuntime"/>.
|
||||
/// </summary>
|
||||
internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||
{
|
||||
private const byte LiveProjectionDomain = 3;
|
||||
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly RenderProjectionJournal _journal;
|
||||
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
|
||||
private readonly List<TrackedProjection> _activeScratch = [];
|
||||
|
||||
public LiveRenderProjectionJournal(
|
||||
LiveEntityRuntime runtime,
|
||||
RenderProjectionJournal journal)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_journal = journal ?? throw new ArgumentNullException(nameof(journal));
|
||||
}
|
||||
|
||||
public int ProjectionCount => _byLocalId.Count;
|
||||
|
||||
public bool OnEntityReady(LiveEntityReadyCandidate candidate)
|
||||
{
|
||||
if (!candidate.IsCurrent(_runtime)
|
||||
|| candidate.WorldEntity is not { } entity
|
||||
|| !candidate.Record.ResourcesRegistered)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Upsert(candidate.Record, entity, candidate.Record.IsSpatiallyVisible);
|
||||
return candidate.IsCurrent(_runtime);
|
||||
}
|
||||
|
||||
public void OnProjectionVisibilityChanged(
|
||||
LiveEntityRecord record,
|
||||
bool visible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
if (!_runtime.IsCurrentRecord(record)
|
||||
|| record.WorldEntity is not { } entity
|
||||
|| !_byLocalId.ContainsKey(entity.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Upsert(record, entity, visible);
|
||||
}
|
||||
|
||||
public void OnProjectionPoseReady(uint serverGuid)
|
||||
{
|
||||
if (!_runtime.TryGetRecord(serverGuid, out LiveEntityRecord record)
|
||||
|| record.WorldEntity is not { } entity
|
||||
|| !_byLocalId.ContainsKey(entity.Id)
|
||||
|| record.ProjectionKind is not LiveEntityProjectionKind.Attached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Upsert(record, entity, record.IsSpatiallyVisible);
|
||||
}
|
||||
|
||||
public void OnProjectionRemoved(uint localEntityId)
|
||||
{
|
||||
if (_byLocalId.TryGetValue(
|
||||
localEntityId,
|
||||
out TrackedProjection? tracked)
|
||||
&& tracked.Projection.ProjectionClass is
|
||||
RenderProjectionClass.EquippedChild)
|
||||
{
|
||||
Remove(tracked);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnResourceUnregister(WorldEntity entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
if (_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked)
|
||||
&& ReferenceEquals(tracked.Entity, entity))
|
||||
{
|
||||
Remove(tracked);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors only registered live/attached sources after simulation,
|
||||
/// animation, and attachment pose composition have committed. Resident
|
||||
/// statics are never scanned.
|
||||
/// </summary>
|
||||
public void SynchronizeActiveSources()
|
||||
{
|
||||
_activeScratch.Clear();
|
||||
foreach (TrackedProjection tracked in _byLocalId.Values)
|
||||
{
|
||||
if (tracked.Record.IsSpatiallyProjected)
|
||||
_activeScratch.Add(tracked);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _activeScratch.Count; i++)
|
||||
{
|
||||
TrackedProjection tracked = _activeScratch[i];
|
||||
if (!_runtime.IsCurrentRecord(tracked.Record)
|
||||
|| !ReferenceEquals(tracked.Record.WorldEntity, tracked.Entity)
|
||||
|| !_byLocalId.TryGetValue(
|
||||
tracked.Entity.Id,
|
||||
out TrackedProjection? current)
|
||||
|| !ReferenceEquals(current, tracked))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Upsert(
|
||||
tracked.Record,
|
||||
tracked.Entity,
|
||||
tracked.Record.IsSpatiallyVisible);
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetTracking()
|
||||
{
|
||||
_byLocalId.Clear();
|
||||
_activeScratch.Clear();
|
||||
}
|
||||
|
||||
internal bool TryGet(
|
||||
uint localEntityId,
|
||||
out RenderProjectionRecord record)
|
||||
{
|
||||
if (_byLocalId.TryGetValue(
|
||||
localEntityId,
|
||||
out TrackedProjection? tracked))
|
||||
{
|
||||
record = tracked.Projection;
|
||||
return true;
|
||||
}
|
||||
|
||||
record = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static RenderProjectionId ProjectionId(uint localEntityId) =>
|
||||
RenderProjectionId.FromRaw(
|
||||
((ulong)LiveProjectionDomain << 56) | localEntityId);
|
||||
|
||||
private void Upsert(
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
bool spatiallyVisible)
|
||||
{
|
||||
RenderProjectionRecord projected = Project(
|
||||
record,
|
||||
entity,
|
||||
spatiallyVisible);
|
||||
if (!_byLocalId.TryGetValue(entity.Id, out TrackedProjection? tracked))
|
||||
{
|
||||
_journal.Register(in projected);
|
||||
_byLocalId.Add(
|
||||
entity.Id,
|
||||
new TrackedProjection(record, entity, projected));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(tracked.Record, record)
|
||||
|| !ReferenceEquals(tracked.Entity, entity)
|
||||
|| tracked.Projection.OwnerIncarnation
|
||||
!= projected.OwnerIncarnation
|
||||
|| tracked.Projection.ProjectionClass
|
||||
!= projected.ProjectionClass)
|
||||
{
|
||||
_journal.Register(in projected);
|
||||
_byLocalId[entity.Id] =
|
||||
new TrackedProjection(record, entity, projected);
|
||||
return;
|
||||
}
|
||||
|
||||
projected = projected with
|
||||
{
|
||||
PreviousTransform = new PreviousRenderTransform(
|
||||
tracked.Projection.Transform.LocalToWorld),
|
||||
};
|
||||
_journal.AppendDifference(tracked.Projection, projected);
|
||||
tracked.Projection = projected;
|
||||
}
|
||||
|
||||
private RenderProjectionRecord Project(
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
bool spatiallyVisible)
|
||||
{
|
||||
uint fullCellId = record.FullCellId != 0
|
||||
? record.FullCellId
|
||||
: entity.ParentCellId ?? 0;
|
||||
uint ownerLandblockId = record.CanonicalLandblockId != 0
|
||||
? record.CanonicalLandblockId
|
||||
: fullCellId == 0
|
||||
? 0
|
||||
: Canonicalize(fullCellId);
|
||||
return RenderProjectionRecordFactory.ProjectEntity(
|
||||
ProjectionId(entity.Id),
|
||||
record.ProjectionKind is LiveEntityProjectionKind.Attached
|
||||
? RenderProjectionClass.EquippedChild
|
||||
: RenderProjectionClass.LiveDynamicRoot,
|
||||
RenderOwnerIncarnation.FromRaw(
|
||||
((ulong)record.Generation << 32) | entity.Id),
|
||||
ownerLandblockId,
|
||||
fullCellId,
|
||||
entity,
|
||||
spatiallyVisible);
|
||||
}
|
||||
|
||||
private void Remove(TrackedProjection tracked)
|
||||
{
|
||||
if (!_byLocalId.Remove(tracked.Entity.Id))
|
||||
return;
|
||||
_journal.Unregister(
|
||||
tracked.Projection.Id,
|
||||
tracked.Projection.OwnerIncarnation);
|
||||
}
|
||||
|
||||
private static uint Canonicalize(uint cellOrLandblockId) =>
|
||||
(cellOrLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
|
||||
private sealed class TrackedProjection(
|
||||
LiveEntityRecord record,
|
||||
WorldEntity entity,
|
||||
RenderProjectionRecord projection)
|
||||
{
|
||||
public LiveEntityRecord Record { get; } = record;
|
||||
public WorldEntity Entity { get; } = entity;
|
||||
public RenderProjectionRecord Projection { get; set; } = projection;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LiveRenderProjectionResourceLifecycle(
|
||||
ILiveRenderProjectionSink sink) : ILiveEntityResourceLifecycle
|
||||
{
|
||||
private readonly ILiveRenderProjectionSink _sink =
|
||||
sink ?? throw new ArgumentNullException(nameof(sink));
|
||||
|
||||
public void Register(WorldEntity entity)
|
||||
{
|
||||
// The exact EntityReady edge follows all resource registration and is
|
||||
// the first point at which the projection may enter the journal.
|
||||
}
|
||||
|
||||
public void Unregister(WorldEntity entity) =>
|
||||
_sink.OnResourceUnregister(entity);
|
||||
}
|
||||
|
|
@ -43,6 +43,45 @@ internal sealed class RenderProjectionJournal
|
|||
id,
|
||||
incarnation));
|
||||
|
||||
public void AppendDifference(
|
||||
in RenderProjectionRecord prior,
|
||||
in RenderProjectionRecord current)
|
||||
{
|
||||
if (prior.Id != current.Id
|
||||
|| prior.OwnerIncarnation != current.OwnerIncarnation
|
||||
|| prior.ProjectionClass != current.ProjectionClass)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"A channel update cannot change projection identity, incarnation, or class.",
|
||||
nameof(current));
|
||||
}
|
||||
|
||||
if (prior.Transform != current.Transform
|
||||
|| prior.Bounds != current.Bounds
|
||||
|| prior.SortKey != current.SortKey
|
||||
|| prior.Source.TransformFingerprint
|
||||
!= current.Source.TransformFingerprint)
|
||||
{
|
||||
Update(RenderProjectionDeltaKind.UpdateTransform, in current);
|
||||
}
|
||||
|
||||
if (prior.MeshSet != current.MeshSet
|
||||
|| prior.Material != current.Material
|
||||
|| prior.DegradeState != current.DegradeState
|
||||
|| prior.Source.GeometryFingerprint
|
||||
!= current.Source.GeometryFingerprint
|
||||
|| prior.Source.AppearanceFingerprint
|
||||
!= current.Source.AppearanceFingerprint)
|
||||
{
|
||||
Update(RenderProjectionDeltaKind.UpdateAppearance, in current);
|
||||
}
|
||||
|
||||
if (prior.Residency != current.Residency)
|
||||
Update(RenderProjectionDeltaKind.Rebucket, in current);
|
||||
if (prior.Flags != current.Flags)
|
||||
Update(RenderProjectionDeltaKind.UpdateFlags, in current);
|
||||
}
|
||||
|
||||
public RenderDeltaApplyResult DrainTo(IRenderScene scene)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scene);
|
||||
|
|
|
|||
130
src/AcDream.App/Rendering/Scene/RenderProjectionRecordFactory.cs
Normal file
130
src/AcDream.App/Rendering/Scene/RenderProjectionRecordFactory.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
/// <summary>
|
||||
/// Pure conversion from an accepted <see cref="WorldEntity"/> presentation
|
||||
/// snapshot into the packed render-scene record. This method never mutates
|
||||
/// source AABB state or acquires gameplay identity.
|
||||
/// </summary>
|
||||
internal static class RenderProjectionRecordFactory
|
||||
{
|
||||
private const float DefaultAabbRadius = 5.0f;
|
||||
|
||||
public static RenderProjectionRecord ProjectEntity(
|
||||
RenderProjectionId id,
|
||||
RenderProjectionClass projectionClass,
|
||||
RenderOwnerIncarnation incarnation,
|
||||
uint ownerLandblockId,
|
||||
uint fullCellId,
|
||||
WorldEntity entity,
|
||||
bool spatiallyVisible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
CurrentRenderProjectionFingerprint fingerprint =
|
||||
CurrentRenderSceneOracle.CreateProjectionFingerprint(
|
||||
ownerLandblockId,
|
||||
entity);
|
||||
RenderProjectionFlags flags = RenderProjectionFlags.Selectable;
|
||||
if (spatiallyVisible
|
||||
&& entity.IsDrawVisible
|
||||
&& entity.IsAncestorDrawVisible)
|
||||
{
|
||||
flags |= RenderProjectionFlags.Draw;
|
||||
}
|
||||
if (!entity.IsDrawVisible)
|
||||
flags |= RenderProjectionFlags.Hidden;
|
||||
if (!entity.IsAncestorDrawVisible)
|
||||
flags |= RenderProjectionFlags.AncestorHidden;
|
||||
|
||||
RenderTransform transform = RenderTransform.FromRoot(
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
entity.Scale);
|
||||
(Vector3 minimum, Vector3 maximum) = CalculateBounds(entity);
|
||||
return new RenderProjectionRecord(
|
||||
id,
|
||||
projectionClass,
|
||||
incarnation,
|
||||
transform,
|
||||
new PreviousRenderTransform(transform.LocalToWorld),
|
||||
new RenderMeshSet(
|
||||
RenderAssetHandle.FromRaw(
|
||||
fingerprint.Geometry.Low ^ fingerprint.Geometry.High),
|
||||
entity.MeshRefs.Count,
|
||||
0),
|
||||
new RenderMaterialVariant(
|
||||
fingerprint.Appearance.Low,
|
||||
fingerprint.Appearance.High,
|
||||
1.0f),
|
||||
new RenderSpatialResidency(
|
||||
RenderSpatialBucket.FromRaw(fullCellId),
|
||||
ownerLandblockId,
|
||||
fullCellId),
|
||||
new RenderWorldBounds(minimum, maximum),
|
||||
flags,
|
||||
default,
|
||||
id.ToSortKey(),
|
||||
RenderDirtyMask.All,
|
||||
new RenderSourceMetadata(
|
||||
entity.Id,
|
||||
entity.ServerGuid,
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
entity.ParentCellId ?? 0,
|
||||
entity.EffectCellId ?? 0,
|
||||
entity.BuildingShellAnchorCellId ?? 0,
|
||||
fingerprint.Transform,
|
||||
fingerprint.Geometry,
|
||||
fingerprint.Appearance));
|
||||
}
|
||||
|
||||
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
||||
WorldEntity entity)
|
||||
{
|
||||
Vector3 position = entity.Position;
|
||||
if (entity.HasLocalBounds)
|
||||
{
|
||||
Vector3 localMin = entity.LocalBoundMin;
|
||||
Vector3 localMax = entity.LocalBoundMax;
|
||||
Vector3 minimum = default;
|
||||
Vector3 maximum = default;
|
||||
for (int cornerIndex = 0; cornerIndex < 8; cornerIndex++)
|
||||
{
|
||||
Vector3 corner = new(
|
||||
(cornerIndex & 1) == 0 ? localMin.X : localMax.X,
|
||||
(cornerIndex & 2) == 0 ? localMin.Y : localMax.Y,
|
||||
(cornerIndex & 4) == 0 ? localMin.Z : localMax.Z);
|
||||
Vector3 transformed =
|
||||
Vector3.Transform(corner, entity.Rotation);
|
||||
if (cornerIndex == 0)
|
||||
{
|
||||
minimum = transformed;
|
||||
maximum = transformed;
|
||||
}
|
||||
else
|
||||
{
|
||||
minimum = Vector3.Min(minimum, transformed);
|
||||
maximum = Vector3.Max(maximum, transformed);
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 margin = new(DefaultAabbRadius);
|
||||
return (
|
||||
position + minimum - margin,
|
||||
position + maximum + margin);
|
||||
}
|
||||
|
||||
float radius = DefaultAabbRadius;
|
||||
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||
{
|
||||
radius = MathF.Max(
|
||||
radius,
|
||||
DefaultAabbRadius
|
||||
+ entity.MeshRefs[i].PartTransform.Translation.Length());
|
||||
}
|
||||
|
||||
Vector3 extent = new(radius);
|
||||
return (position - extent, position + extent);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.World;
|
||||
|
|
@ -25,7 +24,6 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
{
|
||||
private const byte StaticEntityDomain = 1;
|
||||
private const byte EnvCellShellDomain = 2;
|
||||
private const float DefaultAabbRadius = 5.0f;
|
||||
|
||||
private readonly RenderProjectionJournal _journal;
|
||||
private readonly Dictionary<uint, Dictionary<RenderProjectionId, TrackedProjection>>
|
||||
|
|
@ -115,7 +113,7 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
|
||||
if (accepted != retained.Record)
|
||||
{
|
||||
AppendUpdates(retained.Record, accepted);
|
||||
_journal.AppendDifference(retained.Record, accepted);
|
||||
current[candidate.Id] = new TrackedProjection(accepted);
|
||||
}
|
||||
continue;
|
||||
|
|
@ -217,61 +215,18 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
uint landblockId,
|
||||
WorldEntity entity)
|
||||
{
|
||||
CurrentRenderProjectionFingerprint fingerprint =
|
||||
CurrentRenderSceneOracle.CreateProjectionFingerprint(
|
||||
landblockId,
|
||||
entity);
|
||||
RenderProjectionFlags flags = RenderProjectionFlags.Selectable;
|
||||
if (entity.IsDrawVisible && entity.IsAncestorDrawVisible)
|
||||
flags |= RenderProjectionFlags.Draw;
|
||||
if (!entity.IsDrawVisible)
|
||||
flags |= RenderProjectionFlags.Hidden;
|
||||
if (!entity.IsAncestorDrawVisible)
|
||||
flags |= RenderProjectionFlags.AncestorHidden;
|
||||
|
||||
RenderTransform transform = RenderTransform.FromRoot(
|
||||
entity.Position,
|
||||
entity.Rotation,
|
||||
entity.Scale);
|
||||
(Vector3 minimum, Vector3 maximum) = CalculateBounds(entity);
|
||||
RenderProjectionId id = StaticEntityId(landblockId, entity.Id);
|
||||
uint fullCellId = entity.ParentCellId ?? landblockId;
|
||||
return new RenderProjectionRecord(
|
||||
return RenderProjectionRecordFactory.ProjectEntity(
|
||||
id,
|
||||
InteriorEntityPartition.IsIndoorCellId(entity.ParentCellId)
|
||||
? RenderProjectionClass.IndoorCellStatic
|
||||
: RenderProjectionClass.OutdoorStatic,
|
||||
default,
|
||||
transform,
|
||||
new PreviousRenderTransform(transform.LocalToWorld),
|
||||
new RenderMeshSet(
|
||||
RenderAssetHandle.FromRaw(
|
||||
fingerprint.Geometry.Low ^ fingerprint.Geometry.High),
|
||||
entity.MeshRefs.Count,
|
||||
0),
|
||||
new RenderMaterialVariant(
|
||||
fingerprint.Appearance.Low,
|
||||
fingerprint.Appearance.High,
|
||||
1.0f),
|
||||
new RenderSpatialResidency(
|
||||
RenderSpatialBucket.FromRaw(fullCellId),
|
||||
landblockId,
|
||||
fullCellId),
|
||||
new RenderWorldBounds(minimum, maximum),
|
||||
flags,
|
||||
default,
|
||||
id.ToSortKey(),
|
||||
RenderDirtyMask.All,
|
||||
new RenderSourceMetadata(
|
||||
entity.Id,
|
||||
entity.ServerGuid,
|
||||
entity.SourceGfxObjOrSetupId,
|
||||
entity.ParentCellId ?? 0,
|
||||
entity.EffectCellId ?? 0,
|
||||
entity.BuildingShellAnchorCellId ?? 0,
|
||||
fingerprint.Transform,
|
||||
fingerprint.Geometry,
|
||||
fingerprint.Appearance));
|
||||
landblockId,
|
||||
fullCellId,
|
||||
entity,
|
||||
spatiallyVisible: true);
|
||||
}
|
||||
|
||||
private static RenderProjectionRecord ProjectEnvCellShell(
|
||||
|
|
@ -332,95 +287,6 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
RenderSceneHash128.Empty));
|
||||
}
|
||||
|
||||
private void AppendUpdates(
|
||||
in RenderProjectionRecord prior,
|
||||
in RenderProjectionRecord current)
|
||||
{
|
||||
if (prior.Transform != current.Transform
|
||||
|| prior.Bounds != current.Bounds
|
||||
|| prior.SortKey != current.SortKey
|
||||
|| prior.Source.TransformFingerprint
|
||||
!= current.Source.TransformFingerprint)
|
||||
{
|
||||
_journal.Update(
|
||||
RenderProjectionDeltaKind.UpdateTransform,
|
||||
in current);
|
||||
}
|
||||
|
||||
if (prior.MeshSet != current.MeshSet
|
||||
|| prior.Material != current.Material
|
||||
|| prior.DegradeState != current.DegradeState
|
||||
|| prior.Source.GeometryFingerprint != current.Source.GeometryFingerprint
|
||||
|| prior.Source.AppearanceFingerprint != current.Source.AppearanceFingerprint)
|
||||
{
|
||||
_journal.Update(
|
||||
RenderProjectionDeltaKind.UpdateAppearance,
|
||||
in current);
|
||||
}
|
||||
|
||||
if (prior.Residency != current.Residency)
|
||||
{
|
||||
_journal.Update(
|
||||
RenderProjectionDeltaKind.Rebucket,
|
||||
in current);
|
||||
}
|
||||
|
||||
if (prior.Flags != current.Flags)
|
||||
{
|
||||
_journal.Update(
|
||||
RenderProjectionDeltaKind.UpdateFlags,
|
||||
in current);
|
||||
}
|
||||
}
|
||||
|
||||
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
||||
WorldEntity entity)
|
||||
{
|
||||
Vector3 position = entity.Position;
|
||||
if (entity.HasLocalBounds)
|
||||
{
|
||||
Vector3 localMin = entity.LocalBoundMin;
|
||||
Vector3 localMax = entity.LocalBoundMax;
|
||||
Vector3 minimum = default;
|
||||
Vector3 maximum = default;
|
||||
for (int cornerIndex = 0; cornerIndex < 8; cornerIndex++)
|
||||
{
|
||||
Vector3 corner = new(
|
||||
(cornerIndex & 1) == 0 ? localMin.X : localMax.X,
|
||||
(cornerIndex & 2) == 0 ? localMin.Y : localMax.Y,
|
||||
(cornerIndex & 4) == 0 ? localMin.Z : localMax.Z);
|
||||
Vector3 transformed = Vector3.Transform(corner, entity.Rotation);
|
||||
if (cornerIndex == 0)
|
||||
{
|
||||
minimum = transformed;
|
||||
maximum = transformed;
|
||||
}
|
||||
else
|
||||
{
|
||||
minimum = Vector3.Min(minimum, transformed);
|
||||
maximum = Vector3.Max(maximum, transformed);
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 margin = new(DefaultAabbRadius);
|
||||
return (
|
||||
position + minimum - margin,
|
||||
position + maximum + margin);
|
||||
}
|
||||
|
||||
float radius = DefaultAabbRadius;
|
||||
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||
{
|
||||
radius = MathF.Max(
|
||||
radius,
|
||||
DefaultAabbRadius
|
||||
+ entity.MeshRefs[i].PartTransform.Translation.Length());
|
||||
}
|
||||
|
||||
Vector3 extent = new(radius);
|
||||
return (position - extent, position + extent);
|
||||
}
|
||||
|
||||
private RenderOwnerIncarnation NextIncarnation()
|
||||
{
|
||||
if (_nextIncarnation == ulong.MaxValue)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Settings;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -136,6 +137,7 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
|||
_animatedEntities;
|
||||
private readonly EquippedChildRenderController _equippedChildren;
|
||||
private readonly LiveEffectFrameController _effects;
|
||||
private readonly ILiveRenderProjectionSink? _renderProjections;
|
||||
|
||||
public LiveObjectFrameController(
|
||||
RetailInboundEventDispatcher inboundEvents,
|
||||
|
|
@ -149,7 +151,8 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
|||
LiveEntityAnimationPresenter animationPresenter,
|
||||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animatedEntities,
|
||||
EquippedChildRenderController equippedChildren,
|
||||
LiveEffectFrameController effects)
|
||||
LiveEffectFrameController effects,
|
||||
ILiveRenderProjectionSink? renderProjections = null)
|
||||
{
|
||||
_inboundEvents = inboundEvents ?? throw new ArgumentNullException(nameof(inboundEvents));
|
||||
_localPlayerFrame = localPlayerFrame
|
||||
|
|
@ -168,6 +171,7 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
|||
_equippedChildren = equippedChildren
|
||||
?? throw new ArgumentNullException(nameof(equippedChildren));
|
||||
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
|
||||
_renderProjections = renderProjections;
|
||||
}
|
||||
|
||||
public void Tick(float deltaSeconds) =>
|
||||
|
|
@ -202,6 +206,7 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
|||
if (_animatedEntities.Count > 0)
|
||||
_animationPresenter.Present(schedules);
|
||||
_equippedChildren.Tick();
|
||||
_renderProjections?.SynchronizeActiveSources();
|
||||
// Retail CPhysicsObj::animate_static_object @ 0x00513DF0 runs
|
||||
// Script -> Particle -> process_hooks. acdream intentionally retains
|
||||
// the TS-51 static-order difference here: ProcessHooks precedes the
|
||||
|
|
@ -221,24 +226,28 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
|
|||
private readonly EquippedChildRenderController _equippedChildren;
|
||||
private readonly ParticleHookSink _particleSink;
|
||||
private readonly LiveEntityLightController _lights;
|
||||
private readonly ILiveRenderProjectionSink? _renderProjections;
|
||||
|
||||
public LiveSpatialPresentationReconciler(
|
||||
EntityEffectController entityEffects,
|
||||
EquippedChildRenderController equippedChildren,
|
||||
ParticleHookSink particleSink,
|
||||
LiveEntityLightController lights)
|
||||
LiveEntityLightController lights,
|
||||
ILiveRenderProjectionSink? renderProjections = null)
|
||||
{
|
||||
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
|
||||
_equippedChildren = equippedChildren
|
||||
?? throw new ArgumentNullException(nameof(equippedChildren));
|
||||
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
|
||||
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
||||
_renderProjections = renderProjections;
|
||||
}
|
||||
|
||||
public void Reconcile()
|
||||
{
|
||||
_entityEffects.RefreshLiveOwnerPoses();
|
||||
_equippedChildren.Tick();
|
||||
_renderProjections?.SynchronizeActiveSources();
|
||||
_particleSink.RefreshAttachedEmitters();
|
||||
_lights.Refresh();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Net;
|
||||
|
|
@ -80,16 +81,19 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
|||
private readonly Func<LiveEntityRecord, bool> _prepareEffects;
|
||||
private readonly Func<LiveEntityRecord, bool> _presentState;
|
||||
private readonly Func<LiveEntityRecord, bool> _replayEffects;
|
||||
private readonly ILiveRenderProjectionSink? _renderProjection;
|
||||
|
||||
public LiveEntityReadyPublisher(
|
||||
LiveEntityRuntime runtime,
|
||||
EntityEffectController effects,
|
||||
LiveEntityPresentationController presentation)
|
||||
LiveEntityPresentationController presentation,
|
||||
ILiveRenderProjectionSink? renderProjection = null)
|
||||
: this(
|
||||
runtime,
|
||||
record => effects.PrepareLiveEntityOwner(record.ServerGuid),
|
||||
record => presentation.OnLiveEntityReady(record.ServerGuid),
|
||||
record => effects.ReplayPendingForLiveEntity(record.ServerGuid))
|
||||
record => effects.ReplayPendingForLiveEntity(record.ServerGuid),
|
||||
renderProjection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(effects);
|
||||
ArgumentNullException.ThrowIfNull(presentation);
|
||||
|
|
@ -99,7 +103,8 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
|||
LiveEntityRuntime runtime,
|
||||
Func<LiveEntityRecord, bool> prepareEffects,
|
||||
Func<LiveEntityRecord, bool> presentState,
|
||||
Func<LiveEntityRecord, bool> replayEffects)
|
||||
Func<LiveEntityRecord, bool> replayEffects,
|
||||
ILiveRenderProjectionSink? renderProjection = null)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_prepareEffects = prepareEffects
|
||||
|
|
@ -108,6 +113,7 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
|||
?? throw new ArgumentNullException(nameof(presentState));
|
||||
_replayEffects = replayEffects
|
||||
?? throw new ArgumentNullException(nameof(replayEffects));
|
||||
_renderProjection = renderProjection;
|
||||
}
|
||||
|
||||
public bool Publish(LiveEntityReadyCandidate candidate)
|
||||
|
|
@ -125,7 +131,13 @@ internal sealed class LiveEntityReadyPublisher : ILiveEntityReadyPublisher
|
|||
return false;
|
||||
}
|
||||
|
||||
return _replayEffects(expectedRecord)
|
||||
if (!_replayEffects(expectedRecord)
|
||||
|| !candidate.IsCurrent(_runtime))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (_renderProjection?.OnEntityReady(candidate) ?? true)
|
||||
&& candidate.IsCurrent(_runtime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue