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:
+50
-3
@@ -1,6 +1,6 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "./db/client";
|
||||
import { chunkColumns, servers } from "./db/schema";
|
||||
import { chunkColumns, chunkSections, servers } from "./db/schema";
|
||||
import { markChunkDirty } from "./redis";
|
||||
|
||||
// Wire protocol (mod <-> api), one JSON object per WS text frame:
|
||||
@@ -10,13 +10,21 @@ import { markChunkDirty } from "./redis";
|
||||
// {"type":"hello_ack","ok":false,"error":"..."} (connection closed after)
|
||||
//
|
||||
// mod -> api {"type":"columns","dimension":0,"columns":[{"x":..,"z":..,"height":..,"blockId":..,"blockMeta":..}]}
|
||||
// mod -> api {"type":"sections","dimension":0,"chunkX":..,"chunkZ":..,"sections":[{"sectionY":4,"blocks":"<base64>"}]}
|
||||
//
|
||||
// `columns` doubles as both initial backfill (one message per loaded chunk, 256 columns) and
|
||||
// live deltas (one message per flush tick, just the columns that changed) — both are just "here
|
||||
// is the current topmost block + height for these XZ columns", the mod recomputes it from its
|
||||
// own world access rather than the api trying to infer a post-break top block from a raw diff.
|
||||
// Full per-voxel data (needed for Phase 2 3D meshing) is a natural extension of this same
|
||||
// connection once the chunk store grows a full block-data column.
|
||||
//
|
||||
// `sections` is the Phase 2 addition for full-voxel 3D meshing, additive to `columns` (see
|
||||
// db/schema.ts's chunkColumns/chunkSections comments) — one message per loaded chunk at load
|
||||
// time (all non-empty 16x16x16 sections), and again at flush time for chunks touched since the
|
||||
// last flush (the whole section is resent, same "current state, not a diff" philosophy as
|
||||
// columns — see DeltaEvent's javadoc on the mod side). `blocks` is 4096 little-endian u16
|
||||
// blockStateIds, base64-encoded, indexed by `(ly*16 + lz)*16 + lx` within the section.
|
||||
// A "sections" message marks the chunk dirty the same way "columns" does — one dirty-chunk
|
||||
// event now triggers the worker to re-render both the 2D tile and any 3D meshes for that chunk.
|
||||
|
||||
interface Column {
|
||||
x: number;
|
||||
@@ -26,6 +34,11 @@ interface Column {
|
||||
blockMeta: number;
|
||||
}
|
||||
|
||||
interface Section {
|
||||
sectionY: number;
|
||||
blocks: string;
|
||||
}
|
||||
|
||||
interface ConnState {
|
||||
serverId: string;
|
||||
}
|
||||
@@ -107,6 +120,40 @@ export const wsGateway = {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === "sections") {
|
||||
const dimension: number = msg.dimension;
|
||||
const chunkX: number = msg.chunkX;
|
||||
const chunkZ: number = msg.chunkZ;
|
||||
const sections: Section[] = msg.sections ?? [];
|
||||
if (sections.length === 0) return;
|
||||
|
||||
await db
|
||||
.insert(chunkSections)
|
||||
.values(
|
||||
sections.map((s) => ({
|
||||
serverId: state.serverId,
|
||||
dimension,
|
||||
x: chunkX,
|
||||
z: chunkZ,
|
||||
sectionY: s.sectionY,
|
||||
blocks: s.blocks,
|
||||
})),
|
||||
)
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
chunkSections.serverId,
|
||||
chunkSections.dimension,
|
||||
chunkSections.x,
|
||||
chunkSections.z,
|
||||
chunkSections.sectionY,
|
||||
],
|
||||
set: { blocks: sqlExcluded("blocks"), updatedAt: new Date() },
|
||||
});
|
||||
|
||||
await markChunkDirty(state.serverId, dimension, chunkX, chunkZ);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
close(ws: any) {
|
||||
|
||||
Reference in New Issue
Block a user