acdream/src/AcDream.Core/World/WorldEntity.cs
Erik 5dc4140c11 feat(render): Phase A8 — indoor visibility + streaming fixes batch
Lands the working A8 indoor-rendering and streaming fixes accumulated this
session. User has verified these visually to some degree (e.g. lifestone /
translucent meshes confirmed fine under the FrontFace flip; bridge / wall /
collision regressions confirmed fixed after travel); not every path has been
exhaustively gated. The cellar-flap defect remains OPEN and will be solved
the retail-faithful way via a dedicated brainstorm (see handoff docs).

Rendering core (reviewed, high confidence):
- EnvCellRenderer SSBO stride fix: upload packed Matrix4x4[] (64B) instead of
  the 80B CPU InstanceData struct the shader never expected — fixes the
  transform/texture "explosion" for any draw with >1 instance (cells that
  dedupe to a shared cellGeomId). Real root cause.
- WB-style global FrontFace(CW) + per-batch CullMode carried through the MDI
  layout (GroupKey + BuildIndirectArrays + DrawIndirectRange split into
  same-cull runs with absolute uDrawIDOffset per run).
- EntitySet partitioning (IndoorPass / OutdoorScenery / LiveDynamic) +
  WorldEntity.BuildingShellAnchorCellId so building shells scope to their
  dat-derived building cell instead of rendering everywhere.
- RenderOutsideInAcdream (look into buildings from outside) +
  CollectVisiblePortalBuildings frustum cull of portal bounds.
- Sky-when-inside-building + per-cell audit probe + GL-state probe.

Streaming / perf (test-covered; not independently code-reviewed this session):
- Near/far priority queues so near work wins over far; PromoteToNear carries
  full landblock + mesh data; LandblockEntriesWithoutAnimatedIndex avoids
  rebuilding the animated-lookup dict in the hot draw path. Fixes the
  bridge-not-appearing / missing-walls / broken-collision-after-travel
  regressions and improves post-transition FPS.

Tooling + docs:
- tools/A8CellAudit: offline dat cell/portal/building dumper (portals +
  buildings modes) — reproduces the cellar-flap investigation with no launch.
- docs/research cellar-flap root-cause + option-2 handoff (the didInsideStencil
  double-duty finding + the WB-recursive design decision + brainstorm prompt),
  entity-taxonomy, replan, issue-78 visibility investigation.

Diagnostics retained on purpose: ACDREAM_A8_DIAG_* gates, portal_stencil.vert
provisional pos.w clamp, and the probe families are kept (env-var gated, zero
cost when off) because the pending option-2 cellar-flap brainstorm needs them.
Strip in the option-2 ship commit.

Indoor branch stays behind ACDREAM_A8_INDOOR_BRANCH=1 (default off = pre-A8
visual). Build green; App tests + Core (streaming/dispatcher/loader) tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 10:14:50 +02:00

135 lines
6.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
namespace AcDream.Core.World;
public sealed class WorldEntity
{
public required uint Id { get; init; }
/// <summary>
/// Server-assigned GUID (from CreateObject). Zero for dat-hydrated
/// scenery/static entities that don't come from the server.
/// Used by GpuWorldState for persistent-entity rescue on landblock unload.
/// </summary>
public uint ServerGuid { get; init; }
public required uint SourceGfxObjOrSetupId { get; init; }
/// <summary>
/// World-space position. Settable so Phase 6.7 position-update events
/// can reseat an existing entity without rebuilding its meshes.
/// </summary>
public required Vector3 Position { get; set; }
/// <summary>Settable for the same reason as <see cref="Position"/>.</summary>
public required Quaternion Rotation { get; set; }
/// <summary>
/// Per-part mesh references with their root-relative transforms.
/// Mutable so the animation tick can replace it each frame for
/// entities that play a cycle (Phase 6.4); static entities set it
/// once at hydration and never touch it again.
/// </summary>
public required IReadOnlyList<MeshRef> MeshRefs { get; set; }
/// <summary>
/// Optional per-entity palette override (server-specified base +
/// subpalette overlays). When non-null, applies to every palette-
/// indexed texture on this entity. Used for character skin/hair
/// colors, creature recolors (e.g. stone-colored drudge statue),
/// and team colors. Non-palette-indexed textures ignore this field.
/// </summary>
public PaletteOverride? PaletteOverride { get; init; }
/// <summary>
/// EnvCell ID that owns this entity (room geometry or static object inside
/// the cell). Used by portal visibility to filter interior entities — only
/// entities whose ParentCellId appears in the visible set are rendered.
/// Null for outdoor entities (stabs, scenery, live server spawns).
/// </summary>
public uint? ParentCellId { get; init; }
/// <summary>
/// True when this entity originates from <c>LandBlockInfo.Buildings[]</c>
/// (the dat array that carries building shells: cottage walls, smithy walls,
/// inn walls — every solid building enclosure). False for entities from
/// <c>LandBlockInfo.Objects[]</c> (rocks, fences, lampposts, tree clusters —
/// outdoor scenery placeholders). The two arrays are conflated through
/// hydration today but the dat itself carries the distinction; retail
/// (<c>CLandBlock::init_buildings</c>) and WorldBuilder
/// (<c>SceneryInstance.IsBuilding</c>) both preserve it.
///
/// <para>
/// Read at draw time by <c>WbDrawDispatcher</c>'s <c>IndoorPass</c>
/// partition so building shells can render when the camera is inside their
/// own building (they ARE the indoor walls), not stencil-gated as outdoor
/// scenery would be.
/// </para>
/// </summary>
public bool IsBuildingShell { get; init; }
/// <summary>
/// Dat-derived EnvCell anchor for a building shell. Building shells are
/// top-level landblock stabs, so they do not have a real ParentCellId, but
/// the LandBlockInfo.Buildings[] portal list names cells owned by the same
/// building. The indoor renderer uses this anchor only for draw scoping:
/// a shell renders in IndoorPass when its anchor belongs to the camera
/// building's EnvCell set. Collision still treats the shell as an outdoor
/// stab unless ParentCellId is explicitly set.
/// </summary>
public uint? BuildingShellAnchorCellId { get; init; }
/// <summary>
/// Uniform scale applied to this entity's mesh by the scenery pipeline.
/// For scenery objects this is spawn.Scale (typically 0.81.3). For stabs
/// and interior static objects this is 1.0 (no scaling).
///
/// Used by the collision registration path to scale CylSphere / Sphere /
/// Setup.Radius shapes so they match the visually-scaled mesh. Without
/// this, scaled scenery has a collision cylinder that's smaller than the
/// visible trunk, producing "partial passthrough" bugs.
/// </summary>
public float Scale { get; init; } = 1.0f;
/// <summary>
/// Server-sent part-swap overrides from <c>AnimPartChange</c>. Each entry
/// replaces a Setup part's GfxObj with an alternate model (clothing, weapons,
/// helmets). Carried on the entity so <c>EntitySpawnAdapter</c> can populate
/// <c>AnimatedEntityState</c>'s override map at spawn time. Empty for atlas-
/// tier entities.
/// </summary>
public IReadOnlyList<PartOverride> PartOverrides { get; init; } = Array.Empty<PartOverride>();
/// <summary>
/// Bitmask of hidden Setup parts. Bit <c>i</c> set hides part <c>i</c> at
/// draw time. Sourced from the server's <c>CreateObject</c> record when
/// present. Zero (no parts hidden) is the default.
/// </summary>
public ulong HiddenPartsMask { get; init; }
// Per Phase A.5 spec §4.6 Change #2 — cache per-entity AABB so the
// dispatcher's frustum cull is a memory read, not a per-frame recompute.
// AabbDirty starts true so the dispatcher calls RefreshAabb on first read
// (AabbMin/AabbMax are Vector3.Zero until refreshed).
public Vector3 AabbMin { get; private set; }
public Vector3 AabbMax { get; private set; }
public bool AabbDirty { get; private set; } = true;
private const float DefaultAabbRadius = 5.0f;
public void RefreshAabb()
{
var p = Position;
AabbMin = new Vector3(p.X - DefaultAabbRadius, p.Y - DefaultAabbRadius, p.Z - DefaultAabbRadius);
AabbMax = new Vector3(p.X + DefaultAabbRadius, p.Y + DefaultAabbRadius, p.Z + DefaultAabbRadius);
AabbDirty = false;
}
public void SetPosition(Vector3 pos)
{
Position = pos;
AabbDirty = true;
}
}
/// <summary>
/// Lightweight value type for a server-sent <c>AnimPartChange</c> (part index
/// → replacement GfxObj id). Decouples <c>WorldEntity</c> (Core) from the
/// network-layer <c>CreateObject.AnimPartChange</c> type.
/// </summary>
public readonly record struct PartOverride(byte PartIndex, uint GfxObjId);