Add entity-owned dynamic fog fields
This commit is contained in:
parent
004c05d350
commit
04c0b77c94
7 changed files with 224 additions and 9 deletions
|
|
@ -3413,6 +3413,17 @@ static AetheriaRuntimeEntitySnapshotCommit ToRuntimeEntity(
|
|||
DockingBayAssignments = (entity.DockingBayAssignments ?? Array.Empty<int>()).ToArray(),
|
||||
Visibility = entity.Visibility,
|
||||
VisibilitySourceCount = entity.VisibilitySourceCount,
|
||||
FogFieldEmitters = (entity.FogFieldEmitters ?? Array.Empty<AetheriaFogFieldEmitterSnapshot>())
|
||||
.Select(emitter => new AetheriaRuntimeFogFieldEmitterCommit
|
||||
{
|
||||
Enabled = emitter.Enabled,
|
||||
Radius = emitter.Radius,
|
||||
Density = emitter.Density,
|
||||
OffsetX = emitter.OffsetX,
|
||||
OffsetZ = emitter.OffsetZ,
|
||||
FalloffExponent = emitter.FalloffExponent
|
||||
})
|
||||
.ToArray(),
|
||||
Contacts = (entity.Contacts ?? Array.Empty<AetheriaEntityContactSnapshot>())
|
||||
.Select(contact => ToRuntimeContact(contact, entityIndices))
|
||||
.Where(contact => contact.TargetEntityIndex >= 0)
|
||||
|
|
|
|||
|
|
@ -682,6 +682,31 @@ public sealed class AetheriaEntitySnapshot
|
|||
|
||||
[Key(38)]
|
||||
public string AssignedAgentTaskId { get; set; } = "";
|
||||
|
||||
[Key(39)]
|
||||
public AetheriaFogFieldEmitterSnapshot[] FogFieldEmitters { get; set; } = [];
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaFogFieldEmitterSnapshot
|
||||
{
|
||||
[Key(0)]
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
[Key(1)]
|
||||
public double Radius { get; set; }
|
||||
|
||||
[Key(2)]
|
||||
public double Density { get; set; } = 1;
|
||||
|
||||
[Key(3)]
|
||||
public double OffsetX { get; set; }
|
||||
|
||||
[Key(4)]
|
||||
public double OffsetZ { get; set; }
|
||||
|
||||
[Key(5)]
|
||||
public double FalloffExponent { get; set; } = 2;
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
|
|||
|
|
@ -1675,6 +1675,71 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.That(bodySplats.All(index => Math.Abs(document.Splats.CenterY[index] + 45) < 0.0001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EntityOwnedFogFieldFollowsItsSoaEntityPose()
|
||||
{
|
||||
var entity = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 4,
|
||||
EntityId = "global:aetheria.run_state.run-dynamic-fog.zone.2.entity.4.v1",
|
||||
IsActive = true,
|
||||
PositionX = 30,
|
||||
PositionZ = -10,
|
||||
FogFieldEmitters = new[]
|
||||
{
|
||||
new AetheriaRuntimeFogFieldEmitterCommit
|
||||
{
|
||||
Radius = 24,
|
||||
Density = 0.75,
|
||||
OffsetX = 3,
|
||||
OffsetZ = -2,
|
||||
FalloffExponent = 4
|
||||
}
|
||||
}
|
||||
};
|
||||
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
||||
{
|
||||
ZoneIndex = 2,
|
||||
Entities = new[] { entity }
|
||||
};
|
||||
var frame = new AetheriaRuntimeDaemonFrameDocument
|
||||
{
|
||||
FrameId = 9,
|
||||
Run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
RunId = "run-dynamic-fog",
|
||||
CurrentZoneIndex = zone.ZoneIndex,
|
||||
Zones = new[] { zone }
|
||||
}
|
||||
};
|
||||
|
||||
var document = AetheriaRuntimeGameDocuments.RenderSplatsViewport(
|
||||
frame,
|
||||
new AetheriaRuntimeViewportBounds { MinX = 0, MinY = -50, MaxX = 80, MaxY = 30 });
|
||||
var expectedSource = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(
|
||||
frame.Run.RunId,
|
||||
zone.ZoneIndex,
|
||||
entity.EntityIndex) + ":fog-field:0";
|
||||
var splatIndex = Enumerable.Range(0, document.Splats.Count)
|
||||
.Single(index => string.Equals(document.Splats.SourceKey[index], expectedSource, StringComparison.Ordinal));
|
||||
var layer = document.Layers[document.Splats.LayerIndex[splatIndex]];
|
||||
|
||||
Assert.AreEqual(AetheriaRuntimeRenderSplatLayerKeys.FogPatch, layer.LayerKey);
|
||||
Assert.AreEqual(33, document.Splats.CenterX[splatIndex], 0.0001);
|
||||
Assert.AreEqual(-12, document.Splats.CenterY[splatIndex], 0.0001);
|
||||
Assert.AreEqual(24, document.Splats.HalfExtentX[splatIndex], 0.0001);
|
||||
Assert.AreEqual(0.75, document.Splats.ValueR[splatIndex], 0.0001);
|
||||
Assert.AreEqual(4, document.Splats.FalloffExponent[splatIndex], 0.0001);
|
||||
|
||||
using var publisher = new AetheriaRuntimeDaemonSoaFramePublisher(9001);
|
||||
using var soaFrame = publisher.BuildCurrentZoneEntities(frame);
|
||||
Assert.IsNotNull(soaFrame);
|
||||
var identity = soaFrame.View.Identities.Single(value => value.EntityIndex == entity.EntityIndex);
|
||||
Assert.AreEqual(entity.EntityId, identity.EntityId);
|
||||
Assert.IsFalse(identity.Selectable, "An undetected field owner remains in SoA without becoming targetable.");
|
||||
Assert.IsFalse(identity.Controllable);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DaemonRenderQueriesPublishBodyViewsFromZoneSnapshot()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
_demand = demand;
|
||||
}
|
||||
|
||||
public CultMeshBodyDemandPlan? CurrentDemandPlan() => _demand?.Plan(BodyId);
|
||||
|
||||
public AetheriaRuntimeDaemonSoaFrame? BuildCurrentZoneEntities(
|
||||
AetheriaRuntimeDaemonFrameDocument frame,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog = null,
|
||||
|
|
@ -103,8 +105,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
throw new ArgumentNullException(nameof(frame));
|
||||
var demand = _demand?.Plan(BodyId);
|
||||
if (demand != null &&
|
||||
(!demand.HasConsumers || !demand.RequiresSharedMemory) &&
|
||||
!realtimeDemand)
|
||||
(!demand.HasConsumers ||
|
||||
(!demand.RequiresSharedMemory && !demand.RequiresSharedFileMapping && !realtimeDemand)))
|
||||
return null;
|
||||
|
||||
var run = frame.Run ?? new AetheriaRuntimeRunCheckpointCommit();
|
||||
|
|
@ -129,8 +131,14 @@ namespace GameCult.Aetheria.State.Verse
|
|||
.ToHashSet();
|
||||
if (dockParent != null)
|
||||
visibleEntityIndices.Add(dockParent.EntityIndex);
|
||||
var fieldContributorIndices = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.Where(AetheriaRuntimeGameDocuments.HasActiveFogFieldEmitter)
|
||||
.Select(entity => entity.EntityIndex)
|
||||
.ToHashSet();
|
||||
var entities = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.Where(entity => entity != null && visibleEntityIndices.Contains(entity.EntityIndex))
|
||||
.Where(entity => entity != null &&
|
||||
(visibleEntityIndices.Contains(entity.EntityIndex) ||
|
||||
fieldContributorIndices.Contains(entity.EntityIndex)))
|
||||
.OrderBy(entity => entity.EntityIndex)
|
||||
.ToArray();
|
||||
var pickups = (zone?.DroppedPickups ?? Array.Empty<AetheriaRuntimeDroppedPickupCommit>())
|
||||
|
|
@ -180,7 +188,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
var bytes = write.Span[..checked((int)layout.TotalByteLength)];
|
||||
bytes.Clear();
|
||||
WriteEntities(bytes, layout, entities);
|
||||
WriteEntities(bytes, layout, entities, visibleEntityIndices);
|
||||
WritePickups(bytes, layout, pickups, pickupEntityIds, syntheticEntityIndices, entities.Length);
|
||||
WritePayloads(bytes, layout, payloads, payloadEntityIds, syntheticEntityIndices, entities.Length + pickups.Length);
|
||||
WriteCelestialBodies(
|
||||
|
|
@ -226,8 +234,10 @@ namespace GameCult.Aetheria.State.Verse
|
|||
Kind = entity.Kind,
|
||||
Label = entity.Name,
|
||||
Faction = entity.FactionKey,
|
||||
Selectable = entity.EntityIndex != controlled?.EntityIndex,
|
||||
Controllable = entity.EntityIndex == controlled?.EntityIndex,
|
||||
Selectable = visibleEntityIndices.Contains(entity.EntityIndex) &&
|
||||
entity.EntityIndex != controlled?.EntityIndex,
|
||||
Controllable = visibleEntityIndices.Contains(entity.EntityIndex) &&
|
||||
entity.EntityIndex == controlled?.EntityIndex,
|
||||
AssetRef = AetheriaRuntimeAssets.ResolveEntityPrefabAssetRef(entity, catalog)
|
||||
})
|
||||
.Concat(pickups.Select((pickup, pickupRow) => new AetheriaRuntimeDaemonSoaIdentityDocument
|
||||
|
|
@ -428,7 +438,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
private static void WriteEntities(
|
||||
Span<byte> bytes,
|
||||
EntityHotSlabLayout layout,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities)
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
ISet<int> visibleEntityIndices)
|
||||
{
|
||||
for (var index = 0; index < entities.Count; index++)
|
||||
{
|
||||
|
|
@ -443,7 +454,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
WriteFloat(bytes, layout.PhysicsBodyMass, index, 1.0);
|
||||
WriteFloat(bytes, layout.PhysicsBodyInverseMass, index, 1.0);
|
||||
WriteFloat(bytes, layout.RenderScale, index, 1.0);
|
||||
bytes[checked((int)(layout.RenderVisibility + index * ByteStride))] = (byte)(entity.IsActive ? 1 : 0);
|
||||
bytes[checked((int)(layout.RenderVisibility + index * ByteStride))] =
|
||||
(byte)(entity.IsActive && visibleEntityIndices.Contains(entity.EntityIndex) ? 1 : 0);
|
||||
WriteInt32(bytes, layout.RenderLod + index * IntStride, 0);
|
||||
WriteUInt32(bytes, layout.RenderGroupId + index * IntStride, EntityRenderGroupId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -300,6 +300,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
foreach (var entity in zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
{
|
||||
var entityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(
|
||||
context.RunId,
|
||||
zone.ZoneIndex,
|
||||
entity.EntityIndex);
|
||||
AddEntityFogFieldEmitters(splats, layerIndices, entity, entityKey, normalizedViewport);
|
||||
|
||||
if (!IsPlayerControlled(entity))
|
||||
continue;
|
||||
|
||||
|
|
@ -324,7 +330,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
1,
|
||||
1,
|
||||
1,
|
||||
AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(context.RunId, zone.ZoneIndex, entity.EntityIndex));
|
||||
entityKey);
|
||||
}
|
||||
|
||||
return new EveFieldsSplatsDocument
|
||||
|
|
@ -454,6 +460,64 @@ namespace GameCult.Aetheria.State.Verse
|
|||
falloffExponent: 2);
|
||||
}
|
||||
|
||||
private static void AddEntityFogFieldEmitters(
|
||||
RenderSplatBuilder splats,
|
||||
IReadOnlyDictionary<string, int> layerIndices,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
string entityKey,
|
||||
AetheriaRuntimeViewportBounds viewport)
|
||||
{
|
||||
if (!HasActiveFogFieldEmitter(entity))
|
||||
return;
|
||||
|
||||
var emitters = entity.FogFieldEmitters ?? Array.Empty<AetheriaRuntimeFogFieldEmitterCommit>();
|
||||
for (var emitterIndex = 0; emitterIndex < emitters.Count; emitterIndex++)
|
||||
{
|
||||
var emitter = emitters[emitterIndex];
|
||||
if (emitter == null || !emitter.Enabled || !double.IsFinite(emitter.Radius) || emitter.Radius <= 0)
|
||||
continue;
|
||||
|
||||
var centerX = entity.PositionX + emitter.OffsetX;
|
||||
var centerY = entity.PositionZ + emitter.OffsetZ;
|
||||
if (centerX + emitter.Radius < viewport.MinX ||
|
||||
centerX - emitter.Radius > viewport.MaxX ||
|
||||
centerY + emitter.Radius < viewport.MinY ||
|
||||
centerY - emitter.Radius > viewport.MaxY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
splats.Add(
|
||||
layerIndices[AetheriaRuntimeRenderSplatLayerKeys.FogPatch],
|
||||
centerX,
|
||||
centerY,
|
||||
emitter.Radius,
|
||||
emitter.Radius,
|
||||
AetheriaRuntimeRenderSplatChannels.Tint,
|
||||
EveFieldsSplatFalloffs.PowerPulse,
|
||||
double.IsFinite(emitter.Density) ? Math.Max(0, emitter.Density) : 0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
$"{entityKey}:fog-field:{emitterIndex}",
|
||||
falloffScale: 1,
|
||||
falloffExponent: double.IsFinite(emitter.FalloffExponent)
|
||||
? Math.Max(0.0001, emitter.FalloffExponent)
|
||||
: 2);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasActiveFogFieldEmitter(AetheriaRuntimeEntitySnapshotCommit entity)
|
||||
{
|
||||
return entity != null &&
|
||||
entity.IsActive &&
|
||||
(entity.FogFieldEmitters ?? Array.Empty<AetheriaRuntimeFogFieldEmitterCommit>())
|
||||
.Any(emitter => emitter != null &&
|
||||
emitter.Enabled &&
|
||||
double.IsFinite(emitter.Radius) &&
|
||||
emitter.Radius > 0);
|
||||
}
|
||||
|
||||
private static void AddFossilSimplexBrush(
|
||||
RenderSplatBuilder splats,
|
||||
int layerIndex,
|
||||
|
|
|
|||
|
|
@ -1167,6 +1167,32 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
[Key(56)]
|
||||
public double SecurityRadius { get; set; }
|
||||
|
||||
[Key(57)]
|
||||
public IReadOnlyList<AetheriaRuntimeFogFieldEmitterCommit> FogFieldEmitters { get; set; } =
|
||||
Array.Empty<AetheriaRuntimeFogFieldEmitterCommit>();
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeFogFieldEmitterCommit
|
||||
{
|
||||
[Key(0)]
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
[Key(1)]
|
||||
public double Radius { get; set; }
|
||||
|
||||
[Key(2)]
|
||||
public double Density { get; set; } = 1;
|
||||
|
||||
[Key(3)]
|
||||
public double OffsetX { get; set; }
|
||||
|
||||
[Key(4)]
|
||||
public double OffsetZ { get; set; }
|
||||
|
||||
[Key(5)]
|
||||
public double FalloffExponent { get; set; } = 2;
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
|
|||
|
|
@ -1814,6 +1814,18 @@ edges with one worker and exactly two start/transfer/completion event triplets.
|
|||
`fog.surface_height` is the positive visual displacement the fossil
|
||||
`PowerBrush` camera supplied to `_NebulaSurfaceHeight`. Both derive from the
|
||||
same daemon-owned radial brushes, but neither may impersonate the other.
|
||||
- Entity-owned fog cover: a durable or runtime entity may own typed fog-field
|
||||
emitter components containing radius, density, local XZ offset, and falloff.
|
||||
The owning entity remains an ordinary live SoA entity. The daemon derives each
|
||||
demanded `fog.patch` splat from that entity's current pose and stamps the
|
||||
canonical entity record key into the splat source identity. Every active field
|
||||
contributor retains a row in the live entity SoA even when the observer has
|
||||
not detected it; such a row is render-invisible and non-selectable until normal
|
||||
contact visibility reveals the owner. Splat position is not stored independently
|
||||
and generic clients cannot move, create, or preserve a fog field after its
|
||||
owner or component disappears. No generated world uses
|
||||
these components by default; gameplay commands may attach them later without
|
||||
changing the rendering contract.
|
||||
- Fossil producer map: layer 9 coarse displacement and layer 10 fine waves
|
||||
compose `fog.surface_height`; those same producers plus layer 12 patch
|
||||
displacement compose `fog.patch_height`; layer 11 simplex/cellular density
|
||||
|
|
|
|||
Loading…
Reference in a new issue