Phase 13: real non-cube block models via blockstate/model JSON resolution

Adds worker/src/models.rs (parent-chain blockstate/model resolution,
texture-variable substitution reusing Phase 12's atlas keys), routes
non-cube blocks through new per-element mesh emission in mesh.rs while
leaving full-cube blocks on the existing cube mesher, and threads a
per-server ModelContext (vanilla worker-wide + modded per-job) through
main.rs. Modded model JSON is stored in a new block_models Postgres
table and read alongside the existing (previously write-only)
block_registry table. Fixes a pre-existing face-culling bug as a side
effect of excluding non-cube voxels from the cube mesher's input.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
This commit is contained in:
2026-08-10 01:19:13 +02:00
parent 6ad8051eae
commit a7b69bd038
13 changed files with 1069 additions and 15 deletions
+45
View File
@@ -48,6 +48,51 @@ pub(crate) fn texture_atlas() -> Option<&'static crate::atlas::TextureAtlas> {
TEXTURE_ATLAS.get()
}
/// Same `OnceLock`-once-at-startup pattern as `TEXTURE_PALETTE`/`TEXTURE_ATLAS`, for the Phase 13
/// vanilla model registry (see `models.rs`). Worker-wide, not per-server — the same simplification
/// tier already accepted for the texture palette/atlas. `None` until `main.rs` successfully builds
/// one; `mesh.rs` simply skips non-cube resolution entirely when this is `None`.
static VANILLA_MODEL_REGISTRY: OnceLock<crate::models::ModelRegistry> = OnceLock::new();
pub fn set_vanilla_model_registry(registry: crate::models::ModelRegistry) {
let _ = VANILLA_MODEL_REGISTRY.set(registry);
}
pub(crate) fn vanilla_model_registry() -> Option<&'static crate::models::ModelRegistry> {
VANILLA_MODEL_REGISTRY.get()
}
/// Bundles everything `mesh.rs` needs to resolve one voxel's real (possibly non-cube) shape:
/// the worker-wide vanilla registry plus one job's per-server modded registry/id-map, both
/// optional (a server with no modded model data yet, or a worker that never built the vanilla
/// registry, simply resolves nothing — mesh_section falls back to its pre-Phase-13 behavior).
/// Built fresh per chunk-job in `main.rs` (the modded half is genuinely per-server; the vanilla
/// half is just a borrow of the static above) rather than stored globally, since it's cheap and
/// keeps `mesh_section` ignorant of *where* the data came from.
#[derive(Default)]
pub struct ModelContext<'a> {
pub vanilla: Option<&'a crate::models::ModelRegistry>,
pub modded: Option<&'a crate::models::ModelRegistry>,
pub modded_id_to_name: std::collections::HashMap<u16, String>,
}
impl<'a> ModelContext<'a> {
pub fn resolve(&self, block_id: u16, meta: u8) -> Option<crate::models::ResolvedModel> {
crate::models::resolve_for_block(self.vanilla, self.modded, &self.modded_id_to_name, block_id, meta)
}
}
/// Builds a `ModelContext` for one chunk-job, combining the worker-wide vanilla registry (read
/// from the `OnceLock` above — kept private to this module, `main.rs` never reads it directly) with
/// the caller-supplied per-server modded registry/id-map (see `main.rs`'s `fetch_chunk_data`,
/// which queries `block_registry`/`block_models` for the job's `server_id`).
pub fn model_context(
modded: Option<&crate::models::ModelRegistry>,
modded_id_to_name: std::collections::HashMap<u16, String>,
) -> ModelContext<'_> {
ModelContext { vanilla: vanilla_model_registry(), modded, modded_id_to_name }
}
/// One rendered column within a chunk, in chunk-local coordinates (0..16).
pub struct ColumnPixel {
pub local_x: u8,