docs(phase-2): refine design after DatReaderWriter spike
Verified the real GfxObj data shape against DatReaderWriter's generated types and WorldBuilder's ObjectMeshManager. Three corrections: - GfxObj.Surfaces is a list and Polygon.PosSurface indexes into it; multiple surfaces per GfxObj are the norm. GfxObjMesh.Build now returns IReadOnlyList<GfxObjSubMesh> — one sub-mesh per referenced surface. StaticMeshRenderer draws entities by grouping entities by GfxObj and then by sub-mesh within each GfxObj. Matches the approach WorldBuilder takes. - LandBlockInfo has both Objects (Stabs — small decorations, rocks, trees) AND Buildings (BuildingInfo). LandblockLoader loads both lists and maps them uniformly to WorldEntity since both types share the Frame shape. - Stab.Frame and BuildingInfo.Frame carry position+orientation only, no scale component. WorldEntity.Scale dropped. WorldEntitySnapshot loses its Scale field too. Phase 3+ can add it back if needed. Closes the one open design question flagged in the original doc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7e601073d4
commit
b72851a3ee
1 changed files with 36 additions and 16 deletions
|
|
@ -166,7 +166,6 @@ public sealed class WorldEntity
|
|||
public required uint SourceGfxObjOrSetupId { get; init; }
|
||||
public required Vector3 Position { get; init; }
|
||||
public required Quaternion Rotation { get; init; }
|
||||
public required Vector3 Scale { get; init; }
|
||||
public required IReadOnlyList<MeshRef> MeshRefs { get; init; }
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +174,9 @@ public readonly record struct MeshRef(uint GfxObjId, Matrix4x4 PartTransform);
|
|||
|
||||
`Id` is a monotonic counter assigned at load time (no wire protocol yet). `SourceGfxObjOrSetupId` is the dat id that this entity was built from; used for deduplication in rendering.
|
||||
|
||||
**`LandblockLoader`** — static class with `Load(DatCollection dats, uint landblockId) → LoadedLandblock`. Returns the heightmap `LandBlock` + the flat list of `WorldEntity` from `LandBlockInfo`. No GPU state. Testable.
|
||||
**No `Scale` field:** `Stab` and `BuildingInfo` both store a `Frame` with `Origin: Vector3` and `Orientation: Quaternion` — no scale component. Phase 2 renders at unit scale. If Phase 3+ needs scale (e.g., for loot drops or resized objects), it can be added then.
|
||||
|
||||
**`LandblockLoader`** — static class with `Load(DatCollection dats, uint landblockId) → LoadedLandblock`. Returns the heightmap `LandBlock` + the flat list of `WorldEntity` from `LandBlockInfo`. Loads **both** `LandBlockInfo.Objects` (`Stab` list — small decorations, barrels, rocks, trees) **and** `LandBlockInfo.Buildings` (`BuildingInfo` list — actual buildings). Both types share the same `Frame` shape so they map to `WorldEntity` uniformly. No GPU state. Testable.
|
||||
|
||||
```csharp
|
||||
public sealed record LoadedLandblock(
|
||||
|
|
@ -201,14 +202,31 @@ Computes neighbor ids from the center: the high 16 bits of a landblock id are an
|
|||
|
||||
### `AcDream.Core/Meshing/`
|
||||
|
||||
**`GfxObjMesh.Build(GfxObj) → LandblockMeshData`** — walks `GfxObj.VertexArray` + `GfxObj.Polygons` into a vertex+index mesh. This is the hardest piece of Phase 2.
|
||||
**`GfxObjMesh.Build(GfxObj) → IReadOnlyList<GfxObjSubMesh>`** — walks `GfxObj.VertexArray` + `GfxObj.Polygons` into one sub-mesh **per referenced `Surface`**. This is the hardest piece of Phase 2.
|
||||
|
||||
Key challenges:
|
||||
- AC's GfxObj uses per-polygon vertex indexing with separate UV indices per vertex per polygon. A single vertex position may appear multiple times with different UVs — we need to either duplicate vertices or build a `(posIdx, uvIdx)` → output-vertex-idx map.
|
||||
- Polygons can have more than 3 vertices (4, 5, ...). Simple fan triangulation is fine for convex polygons.
|
||||
- Polygons reference a `Surface` index; for Phase 2, we record that as vertex data so the shader knows which texture to sample. For now, we assume each GfxObj uses one surface — if multiple, we pick the first (Phase 3 handles multi-surface properly).
|
||||
```csharp
|
||||
public readonly record struct GfxObjSubMesh(
|
||||
uint SurfaceId, // from GfxObj.Surfaces[Polygon.PosSurface]
|
||||
Vertex[] Vertices,
|
||||
uint[] Indices);
|
||||
```
|
||||
|
||||
We reuse the `Vertex` struct from Phase 1 (`Position`, `Normal`, `TexCoord`) even though `Normal` will be flat `UnitZ` for now.
|
||||
Key facts (verified in DatReaderWriter and cross-checked against WorldBuilder's `ObjectMeshManager`):
|
||||
- `GfxObj.Surfaces` is a `List<QualifiedDataId<Surface>>` — **multiple surfaces per GfxObj are the norm, not the exception.**
|
||||
- `Polygon.PosSurface` is a `short` index into `GfxObj.Surfaces`. Different polygons in the same GfxObj can use different surfaces.
|
||||
- `GfxObj.VertexArray.Vertices` is a `Dictionary<ushort, SWVertex>` — sparse indexing.
|
||||
- `SWVertex.UVs` is a `List<Vec2Duv>` — a single position may have multiple UV sets.
|
||||
- `Polygon.VertexIds` is a `List<short>` — position indices for this polygon's vertices in order.
|
||||
- `Polygon.PosUVIndices` is a `List<byte>` — for each vertex in this polygon, which of the `SWVertex.UVs[]` slots to use.
|
||||
|
||||
So the real rule is: **for each polygon, group by `PosSurface`, emit output vertices `(swVertex.Origin, swVertex.Normal, swVertex.UVs[PosUVIndices[i]])` for each `VertexIds[i]`, deduplicate by `(posIdx, uvIdx)` tuple, fan-triangulate the polygon's output vertices into sub-mesh indices.** The result is one `GfxObjSubMesh` per unique surface.
|
||||
|
||||
Other challenges:
|
||||
- Polygons can have more than 3 vertices (4, 5, ...). Simple fan triangulation works for convex polygons (AC's polygons are convex).
|
||||
- `Polygon.CullMode` could mean we need to render both sides or one — for Phase 2 we ignore it and render both (no backface culling).
|
||||
- `Polygon.NegSurface` and `NegUVIndices` support double-sided polys with different textures per face — ignored for Phase 2, same reason.
|
||||
|
||||
We reuse the `Vertex` struct from Phase 1 (`Position`, `Normal`, `TexCoord`) and take `Normal` straight from `SWVertex.Normal` (AC already bakes per-vertex normals).
|
||||
|
||||
**`SetupMesh.Flatten(Setup, DatCollection) → IReadOnlyList<(uint GfxObjId, Matrix4x4 PartTransform)>`** — walks a `Setup`'s parts and placement frames, yields the flat list of (GfxObjId, transform) tuples for a single `WorldEntity` to reference.
|
||||
|
||||
|
|
@ -282,17 +300,18 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
|||
{
|
||||
public StaticMeshRenderer(GL gl, Shader meshShader, TextureCache textures);
|
||||
|
||||
// Upload a mesh for a given GfxObj id (idempotent; cached).
|
||||
public void EnsureUploaded(uint gfxObjId, LandblockMeshData meshData, uint surfaceId);
|
||||
// Upload all sub-meshes for a given GfxObj id (idempotent; cached).
|
||||
public void EnsureUploaded(uint gfxObjId, IReadOnlyList<GfxObjSubMesh> subMeshes);
|
||||
|
||||
// Draw all entities grouped by GfxObj for batched state changes.
|
||||
// Draw all entities grouped by GfxObj, then within each GfxObj by sub-mesh
|
||||
// (each sub-mesh has its own surface/texture).
|
||||
public void Draw(ICamera camera, IEnumerable<WorldEntity> entities);
|
||||
|
||||
public void Dispose();
|
||||
}
|
||||
```
|
||||
|
||||
Internally holds a `Dictionary<uint, GfxObjGpu>` where `GfxObjGpu` bundles VAO + index count + surface id.
|
||||
Internally holds a `Dictionary<uint, GfxObjGpu>` where `GfxObjGpu` bundles a list of `SubMeshGpu` records — one VAO/VBO/EBO per sub-mesh, each with its own `SurfaceId`. The draw loop iterates entities, and for each entity iterates that entity's GfxObj's sub-meshes, switching texture between sub-meshes.
|
||||
|
||||
**`TextureCache`** — App-side GL texture ownership:
|
||||
|
||||
|
|
@ -334,10 +353,11 @@ public readonly record struct WorldEntitySnapshot(
|
|||
uint Id,
|
||||
uint SourceId,
|
||||
Vector3 Position,
|
||||
Quaternion Rotation,
|
||||
Vector3 Scale);
|
||||
Quaternion Rotation);
|
||||
```
|
||||
|
||||
(No `Scale` field — Phase 2 renders at unit scale because `Stab.Frame` and `BuildingInfo.Frame` don't carry a scale component.)
|
||||
|
||||
**`IEvents`** — new interface:
|
||||
|
||||
```csharp
|
||||
|
|
@ -435,7 +455,7 @@ Plugins get snapshots, not live `WorldEntity` references — this protects core
|
|||
|
||||
## Open questions (deferred, not blocking)
|
||||
|
||||
- Does `GfxObj` always reference one `Surface`, or do we need per-vertex surface indexing? If the latter, Phase 2 might need per-polygon draw batching. Defer investigation to Task 2.
|
||||
- ~~Does `GfxObj` always reference one `Surface`?~~ **Answered during design spike:** multiple surfaces per GfxObj are the norm. Mesh builder produces one sub-mesh per referenced surface. WorldBuilder's `ObjectMeshManager` confirms this is the canonical approach.
|
||||
- Terrain atlas size — 16 layers enough, or more? Deferred to Task 11 once we know the retail terrain type count.
|
||||
- Should `WorldEntitySnapshot` be sent as a single list or as an `IEnumerable` (lazy)? Probably list — plugins want indexed access. Locked: list.
|
||||
- Should `WorldEntitySnapshot` be sent as a single list or as an `IEnumerable` (lazy)? Plugins want indexed access. Locked: list.
|
||||
- Collision, animation, hot reload of plugins — all Phase 3+.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue