fix(maps): v0.5.6 — derive bitmask from cell's own corner paint-tiles
compute_cell_bitmask_v3 now reads the 8-bit neighbour bitmask from the render-cell's own 4 corner paint-tiles (cardinal bit set iff at least one of the 2 paint-tiles on that edge is painted; diagonal bit set iff the corner paint-tile is painted), not from the any-corner material status of the 8 neighbour render-cells. Pre-0.5.6 used cell-neighbour-material, which let two cells share connectivity across an empty paint-tile gap whenever any unrelated corner of either was painted — producing connected blob shapes where two visually separated 2x2 islands were expected. Atlas, paint storage, override sublayer, public API and the slot lookup table all unchanged. Doc comment and README painting-model section rewritten to describe the dual-grid offset explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
34
README.md
34
README.md
@@ -5,7 +5,7 @@ Multi-layer tile-grid map implementation with vertex-painted autotile
|
|||||||
loading + UV resolution, walkability + sight-blocking queries, and a
|
loading + UV resolution, walkability + sight-blocking queries, and a
|
||||||
v3 multi-layer render pipeline with opaque-ceiling cache.
|
v3 multi-layer render pipeline with opaque-ceiling cache.
|
||||||
|
|
||||||
**Version:** 0.5.4
|
**Version:** 0.5.6
|
||||||
**Lib-ID:** lib-core.maps
|
**Lib-ID:** lib-core.maps
|
||||||
**Requires:** (none)
|
**Requires:** (none)
|
||||||
**Tags:** maps, tile-grid, multi-layer, vertex-painting, autotile, blob-14, override, walkability, tilemap
|
**Tags:** maps, tile-grid, multi-layer, vertex-painting, autotile, blob-14, override, walkability, tilemap
|
||||||
@@ -18,19 +18,35 @@ v3 multi-layer render pipeline with opaque-ceiling cache.
|
|||||||
| v2 | Multi-atlas + multi-layer with packed-u32 GIDs (atlas_idx + tile_id + rotation). `set_cell_gid`, `save_to_disk`. |
|
| v2 | Multi-atlas + multi-layer with packed-u32 GIDs (atlas_idx + tile_id + rotation). `set_cell_gid`, `save_to_disk`. |
|
||||||
| v3 | Per-layer `material` (atlas-alias), optional `vertices` ((W+1)·(H+1) grid), optional `overrides` ({"x:y": int OR {slot, rot, flip}}). Renderer derives slot/rot/flip from neighbour-bitmask blob-gating; overrides force-place specific orientations. |
|
| v3 | Per-layer `material` (atlas-alias), optional `vertices` ((W+1)·(H+1) grid), optional `overrides` ({"x:y": int OR {slot, rot, flip}}). Renderer derives slot/rot/flip from neighbour-bitmask blob-gating; overrides force-place specific orientations. |
|
||||||
|
|
||||||
## Painting model (v3 autotile)
|
## Painting model (v3 autotile, dual-grid)
|
||||||
|
|
||||||
- **Vertex grid**: (W+1)·(H+1) cells. Any-corner rule: cell (x,y) is
|
Two grids offset by half a tile. The PAINT grid (called `vertices` in
|
||||||
material iff any of its 4 corner vertices is painted.
|
storage, "map-tiles" in design docs) is what users paint. The RENDER
|
||||||
- **Blob-gating + SLOT_LOOKUP**: each material cell's 8-bit
|
grid (called `cells`) is offset by (+0.5, +0.5) tile and is where
|
||||||
neighbour-bitmask (clockwise from N) is blob-gated (diagonals zeroed
|
sprites sit. Each render-cell spans 4 surrounding paint-tiles which
|
||||||
unless both adjacent cardinals are set), then mapped to one of 14
|
act as its 4 corners (TL, TR, BL, BR).
|
||||||
canonical slots × {0,1,2,3} rotation × {0,1} flip via a D4-orbit
|
|
||||||
table. 47 distinct gated bitmasks total.
|
- **Paint grid**: (W+1)·(H+1) cells. Any-corner rule: render-cell
|
||||||
|
(x,y) is material iff any of its 4 corner paint-tiles is painted.
|
||||||
|
- **Bitmask + SLOT_LOOKUP** (0.5.6 dual-grid native): each material
|
||||||
|
render-cell's 8-bit neighbour-bitmask (clockwise from N) is derived
|
||||||
|
from its own 4 corner paint-tiles — cardinal bit set iff ≥1 of the
|
||||||
|
edge's 2 paint-tiles painted, diagonal bit set iff the corner
|
||||||
|
paint-tile is painted. Blob-gating then zeroes diagonals whose 2
|
||||||
|
adjacent cardinals are not both set. The gated bitmask maps to one
|
||||||
|
of 14 canonical slots × {0,1,2,3} rotation × {0,1} flip via a
|
||||||
|
D4-orbit table.
|
||||||
- **Override sublayer**: sparse `{"x:y": slot}` or `{"x:y": {slot, rot,
|
- **Override sublayer**: sparse `{"x:y": slot}` or `{"x:y": {slot, rot,
|
||||||
flip}}`. Takes precedence over the bitmask-derived slot AND forces
|
flip}}`. Takes precedence over the bitmask-derived slot AND forces
|
||||||
material-presence on the cell.
|
material-presence on the cell.
|
||||||
|
|
||||||
|
**0.5.6 fix:** pre-0.5.6 derived the bitmask from the material status
|
||||||
|
of the 8 neighbour render-cells, which violated dual-grid semantics —
|
||||||
|
two cells whose shared edge had no painted paint-tiles still saw each
|
||||||
|
other as material whenever any unrelated corner of either was painted,
|
||||||
|
producing connected blobs across visually empty paint-tile gaps. See
|
||||||
|
plan `2026-05-29-painting-model-rethink`.
|
||||||
|
|
||||||
## Topology
|
## Topology
|
||||||
|
|
||||||
<!-- topology:start (auto-generated; do not edit) -->
|
<!-- topology:start (auto-generated; do not edit) -->
|
||||||
|
|||||||
74
init.lua
74
init.lua
@@ -140,21 +140,36 @@ local LAYER_ORDER_TOP_DOWN = {
|
|||||||
"topsurface", "surface", "subsurface", "foundation",
|
"topsurface", "surface", "subsurface", "foundation",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- 47-Blob Autotile (Schema-v3 vertex-painted layers, 0.5.0c)
|
-- Dual-Grid Autotile (Schema-v3 vertex-painted layers, 0.5.0c)
|
||||||
--
|
--
|
||||||
-- The cell-state for an autotile cell is derived from the layer's
|
-- Two grids, offset by half a tile. The PAINT grid (called `vertices`
|
||||||
-- vertex grid using the "any-corner" rule: cell (x, y) is material
|
-- in code, "map-tiles" in design docs) stores material flags at
|
||||||
-- iff any of its 4 corner vertices (x, y), (x+1, y), (x, y+1), or
|
-- integer positions. The RENDER grid (called `cells`) is offset by
|
||||||
-- (x+1, y+1) is painted. A single painted vertex thus produces a
|
-- (+0.5, +0.5) tile from the paint grid and is where sprites sit.
|
||||||
-- 2x2 mini-blob of material cells centred on the vertex.
|
-- Each render-cell (x, y) spans 4 surrounding paint-tiles, which act
|
||||||
|
-- as its 4 corner vertices: TL=(x,y), TR=(x+1,y), BL=(x,y+1),
|
||||||
|
-- BR=(x+1,y+1).
|
||||||
--
|
--
|
||||||
-- Given the derived per-cell material-state, each cell computes its
|
-- Material rule ("any-corner"): render-cell (x, y) is material iff
|
||||||
-- 8-bit neighbour bitmask (clockwise from north, LSB-first): N, NE,
|
-- any of its 4 corner paint-tiles is painted. A single painted tile
|
||||||
-- E, SE, S, SW, W, NW. The blob-gating rule then zeroes a diagonal
|
-- thus produces a 2x2 of material render-cells centred on it.
|
||||||
-- bit unless both adjacent cardinal bits are set. The resulting
|
--
|
||||||
-- gated bitmask resolves through SLOT_LOOKUP into one of the 14
|
-- Bitmask rule (0.5.6, dual-grid native): each render-cell's 8-bit
|
||||||
-- canonical S-V2E2-RM-Blob slots plus the rotation + flip that
|
-- neighbour bitmask (clockwise from N: N, NE, E, SE, S, SW, W, NW)
|
||||||
-- transforms the canonical sprite into the rendered one.
|
-- is derived from its own 4 corner paint-tiles. A cardinal bit is
|
||||||
|
-- set iff at least one of the 2 paint-tiles on that edge is painted;
|
||||||
|
-- a diagonal bit is set iff the corner paint-tile in that direction
|
||||||
|
-- is painted. Then blob-gating zeroes any diagonal bit whose 2
|
||||||
|
-- adjacent cardinal bits are not both set. The gated bitmask
|
||||||
|
-- resolves through SLOT_LOOKUP into one of the 14 canonical
|
||||||
|
-- S-V2E2-RM-Blob slots plus the rotation + flip that transforms the
|
||||||
|
-- canonical sprite into the rendered one.
|
||||||
|
--
|
||||||
|
-- Pre-0.5.6 derived the bitmask from the material status of the 8
|
||||||
|
-- neighbour render-cells, which violated dual-grid semantics: two
|
||||||
|
-- render-cells with an empty shared edge could see each other as
|
||||||
|
-- material whenever any unrelated corner of either was painted,
|
||||||
|
-- producing connected blobs across visually empty paint-tile gaps.
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
|
|
||||||
-- Canonical 14 slots, keyed by their lowest-numbered representative
|
-- Canonical 14 slots, keyed by their lowest-numbered representative
|
||||||
@@ -259,16 +274,33 @@ end
|
|||||||
-- Returns the blob-gated 8-bit neighbour bitmask for cell (x, y) given
|
-- Returns the blob-gated 8-bit neighbour bitmask for cell (x, y) given
|
||||||
-- the layer's vertex grid + map size. Bit layout (clockwise from N):
|
-- the layer's vertex grid + map size. Bit layout (clockwise from N):
|
||||||
-- 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW.
|
-- 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW.
|
||||||
|
--
|
||||||
|
-- 0.5.6: bits derive from the render-cell's own 4 corner map-tiles
|
||||||
|
-- (= the 4 surrounding vertices in dual-grid terms), not from the
|
||||||
|
-- material status of the 8 neighbour cells. A cardinal bit is set iff
|
||||||
|
-- at least one of the 2 vertices on that edge is painted; a diagonal
|
||||||
|
-- bit is set iff the corner vertex in that direction is painted.
|
||||||
|
-- Pre-0.5.6 used cell-neighbour-material, which made two cells with a
|
||||||
|
-- shared unpainted edge see each other as material whenever any other
|
||||||
|
-- corner of either was painted — producing connected blobs across
|
||||||
|
-- visually-empty map-tile gaps. See plan
|
||||||
|
-- 2026-05-29-painting-model-rethink for the discovery.
|
||||||
local function compute_cell_bitmask_v3(vertices, w, h, x, y)
|
local function compute_cell_bitmask_v3(vertices, w, h, x, y)
|
||||||
|
local vw = w + 1
|
||||||
|
local vh = h + 1
|
||||||
|
local TL = vertex_at(vertices, vw, vh, x, y ) ~= 0
|
||||||
|
local TR = vertex_at(vertices, vw, vh, x + 1, y ) ~= 0
|
||||||
|
local BL = vertex_at(vertices, vw, vh, x, y + 1) ~= 0
|
||||||
|
local BR = vertex_at(vertices, vw, vh, x + 1, y + 1) ~= 0
|
||||||
local b = 0
|
local b = 0
|
||||||
if cell_has_material(vertices, w, h, x, y - 1) then b = b | 0x01 end -- N
|
if TL or TR then b = b | 0x01 end -- N edge: TL or TR painted
|
||||||
if cell_has_material(vertices, w, h, x + 1, y - 1) then b = b | 0x02 end -- NE
|
if TR then b = b | 0x02 end -- NE corner: TR painted
|
||||||
if cell_has_material(vertices, w, h, x + 1, y ) then b = b | 0x04 end -- E
|
if TR or BR then b = b | 0x04 end -- E edge: TR or BR painted
|
||||||
if cell_has_material(vertices, w, h, x + 1, y + 1) then b = b | 0x08 end -- SE
|
if BR then b = b | 0x08 end -- SE corner: BR painted
|
||||||
if cell_has_material(vertices, w, h, x, y + 1) then b = b | 0x10 end -- S
|
if BL or BR then b = b | 0x10 end -- S edge: BL or BR painted
|
||||||
if cell_has_material(vertices, w, h, x - 1, y + 1) then b = b | 0x20 end -- SW
|
if BL then b = b | 0x20 end -- SW corner: BL painted
|
||||||
if cell_has_material(vertices, w, h, x - 1, y ) then b = b | 0x40 end -- W
|
if TL or BL then b = b | 0x40 end -- W edge: TL or BL painted
|
||||||
if cell_has_material(vertices, w, h, x - 1, y - 1) then b = b | 0x80 end -- NW
|
if TL then b = b | 0x80 end -- NW corner: TL painted
|
||||||
return apply_blob_gating(b)
|
return apply_blob_gating(b)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"id":"lib-core.maps","version":"0.5.5","api_min":"0.1"}
|
{"id":"lib-core.maps","version":"0.5.6","api_min":"0.1"}
|
||||||
Reference in New Issue
Block a user