Phase 2: full-voxel chunk storage, greedy mesher, and Babylon 3D viewer
api: chunk_sections table (per 16x16x16 section, base64-encoded u16 blockStateId array) and mesh_pointers table, additive to Phase 1's column-based chunk_columns/tile_pointers — 2D tile rendering keeps using the cheap column path unchanged. New "sections" WS message (backfill on chunk load + delta resend on flush, same "current state, not a diff" philosophy as columns) reuses the existing dirty-chunk Redis event, so one event now triggers the worker to re-render both the 2D tile and any 3D meshes for that chunk. New mesh-serving routes. worker: a from-scratch greedy mesher (per-axis 2D mask sweep + rectangle merge — the standard voxel-meshing technique, reimplemented from its public description, not copied from any codebase) producing a compact custom binary vertex buffer per non-empty section. Verified with unit tests, including one that specifically checks a uniform section collapses to exactly 6 merged quads rather than one quad per voxel face (the decisive signal that merging, not just per-voxel face emission, is actually happening). frontend: a barebones Babylon.js 3D viewer (/3d) that loads a fixed radius of chunks, parses the mesh binary format, and renders each section as its own mesh (no cross-section merging yet, no camera-based streaming yet — both reasonable follow-ups once there's a reason to optimize). End-to-end verified against live containers, including through the real mod-side Java WS client (see MCMapper-Mod's matching commit): a known half-solid section correctly round-trips to exactly 24 vertices / 36 indices at the mesh-serving endpoint, matching the "6 merged outer faces" the unit tests predict.
This commit is contained in:
+49
-3
@@ -11,9 +11,8 @@ export const servers = pgTable("servers", {
|
||||
});
|
||||
|
||||
// Column-granularity world state: the topmost non-air block per (dimension, x, z), plus its
|
||||
// height. This is deliberately not full per-voxel storage — Phase 1 only needs enough to
|
||||
// rasterize a top-down 2D tile and to derive marker Y later. Full block/section data for 3D
|
||||
// meshing is a Phase 2 extension of this table, not built now (see plan's phased scope).
|
||||
// height. Kept deliberately separate from `chunkSections` below — cheap to write/read for 2D
|
||||
// tile rendering, which never needs full voxel data.
|
||||
export const chunkColumns = pgTable(
|
||||
"chunk_columns",
|
||||
{
|
||||
@@ -31,6 +30,53 @@ export const chunkColumns = pgTable(
|
||||
(table) => [primaryKey({ columns: [table.serverId, table.dimension, table.x, table.z] })],
|
||||
);
|
||||
|
||||
// Full-voxel storage for one 16x16x16 section (sectionY = worldY / 16), for 3D meshing —
|
||||
// additive to `chunkColumns`, not a replacement (see that table's comment). `blocks` is the
|
||||
// same base64 the mod sends over the wire: 4096 little-endian u16 blockStateIds, indexed by
|
||||
// `(ly*16 + lz)*16 + lx` within the section. Stored as base64 text rather than real bytea to
|
||||
// avoid postgres-js/drizzle binary-column plumbing for what's still an MVP — worth revisiting
|
||||
// if storage size ever matters (base64 is ~33% larger than raw bytes).
|
||||
export const chunkSections = pgTable(
|
||||
"chunk_sections",
|
||||
{
|
||||
serverId: uuid("server_id")
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: "cascade" }),
|
||||
dimension: integer("dimension").notNull(),
|
||||
x: integer("x").notNull(),
|
||||
z: integer("z").notNull(),
|
||||
sectionY: integer("section_y").notNull(),
|
||||
blocks: text("blocks").notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.serverId, table.dimension, table.x, table.z, table.sectionY] }),
|
||||
],
|
||||
);
|
||||
|
||||
// Metadata pointer to a rendered mesh buffer in MinIO, mirroring `tilePointers` but for 3D
|
||||
// meshes — one row per rendered section (a chunk with N non-empty sections gets N mesh rows,
|
||||
// each loaded as its own Babylon mesh; see worker/src/mesh/mod.rs for why section boundaries
|
||||
// aren't merged in Phase 2).
|
||||
export const meshPointers = pgTable(
|
||||
"mesh_pointers",
|
||||
{
|
||||
serverId: uuid("server_id")
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: "cascade" }),
|
||||
dimension: integer("dimension").notNull(),
|
||||
x: integer("x").notNull(),
|
||||
z: integer("z").notNull(),
|
||||
sectionY: integer("section_y").notNull(),
|
||||
storageKey: text("storage_key").notNull(),
|
||||
contentHash: text("content_hash").notNull(),
|
||||
renderedAt: timestamp("rendered_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.serverId, table.dimension, table.x, table.z, table.sectionY] }),
|
||||
],
|
||||
);
|
||||
|
||||
// Metadata pointer to a rendered tile PNG in MinIO — the binary itself never touches Postgres.
|
||||
// One row per (server, dimension, zoom, tileX, tileZ); zoom is always 0 until Phase 2 adds
|
||||
// multi-resolution tiles.
|
||||
|
||||
Reference in New Issue
Block a user