Files
DramaticShapeVoxelMod/lib/Buildings.lua
T
DramaticShape 752653e243 update oak's pc
2026-08-01 16:20:04 -04:00

919 lines
33 KiB
Lua

-- Voxel world mode: a building voxelized from its own sprite.
--
-- A Game Boy overworld building is a fake-3D projection that packs several
-- different 3D facings into one flat drawing: the roof is drawn as if seen
-- from above, the facade as if seen face-on, and the sloped ends as
-- diagonal silhouettes. Raising the whole footprint as one box (what the
-- generic volume path does) folds all three into a wall, so a house comes
-- out as a cube wearing its own elevation.
--
-- This module does the other thing: it classifies each BAND of the drawing
-- by the surface it depicts and applies the matching operation per band --
-- the pipeline written up in assets/docs/buidling_to_voxel/. Two rules govern it:
--
-- 1. Every visible voxel colour is a real texel of the drawing. Nothing
-- is invented but the geometry the sprite implies and never paints
-- (undersides, the depth behind the facade), and those wear the
-- drawing's own four shades.
-- 2. The sprite is ground truth, not the tile grid. The silhouette, the
-- taper rate, the eave height and every window are MEASURED off the
-- pixels; the profile only says which rows are roof and which are
-- facade.
--
-- The pipeline, per template (see data/voxel_heights.lua `buildings`):
--
-- read composite the building out of the atlas and flood its
-- silhouette in from the border through light pixels only --
-- the black outline and the #555 shading together are the
-- boundary, and a "not black" test eats the shaded flanks.
-- measure the topmost drawn row of each column IS the roof's elevation
-- profile (the drawn taper is the slope); the facade's panes
-- are the non-black regions its black frames seal off.
-- build facade rows extrude straight back over the footprint, the
-- awning band juts past them, panes sink one voxel, and the
-- roof lays the top-facing rows flat -- level over the
-- plateau, stepping down the drawn taper at the ends -- then
-- overwrites the walls it intersects.
-- emit cull to the shell and merge runs of texel-adjacent faces into
-- single quads, so a 90k-voxel house ships as ~2k quads.
--
-- One model is built per template and stamped at every placement: Red's
-- and Blue's houses are the same seven-placement drawing, so they cost one
-- build between them. mods/DRAMATIC_SHAPE/tools/building_voxels.py is the
-- reference implementation of the same algorithm and prints the voxel and
-- shell counts this one must agree with.
--
-- Purely presentational, like everything else in the mod: the tiles a
-- building claims keep the collision, warps and triggers they always had.
-- the mod namespace (see main.lua): V.data loads a shipped data file
local V = ...
local Budget = V.require("BuildBudget")
local Buildings = {}
-- The four GB shades, lightest first (same cutoffs as Structures.shadeClass,
-- which reasons about the same art).
local WHITE, GREY, DARK, BLACK = 0, 1, 2, 3
-- A pane is a window or a doorway: a non-black region the drawing seals
-- off behind its own black frame. Anything wider or taller than this is a
-- band of the facade itself -- a siding course, the awning's grey field --
-- and must stay flush.
local RECESS_MAX = 24
-- Face shades, matching the rest of the mod's objects: the south face is
-- the drawing itself and draws at full brightness.
local SHADE = { top = 0.95, south = 1.0, north = 0.68,
side = 0.78, bottom = 0.5 }
local function keyOf(tx, ty)
return (ty + 64) * 4096 + (tx + 64)
end
local function shadeOf(r, g, b, a)
if a == 0 then return WHITE end
local v = math.min(r, g, b)
if v <= 0.25 then return BLACK end
if v <= 0.55 then return DARK end
if v <= 0.85 then return GREY end
return WHITE
end
-- The shape profile ships with the mod; absent or broken simply means no
-- building templates, and every building falls back to the volume path.
local spec = nil
local function profile()
if spec == nil then
local ok, s = pcall(V.data, "voxel_heights")
spec = (ok and type(s) == "table") and s or false
end
return spec or nil
end
local models = {} -- "<tileset>:<index>" -> prebuilt local quads
-- ------------------------------------------------------------------ read --
-- Composite the template out of the atlas and flood the silhouette in from
-- the border. Returns flat arrays indexed y * W + x.
--
-- `topRows`, when a template carries it, is extra drawing rows composited
-- ABOVE the matched grid: rows of the same drawing that are not on the
-- map this template places on. The Pokemon Tower is the case that needs
-- it -- the drawing straddles the LAVENDER_TOWN / ROUTE_10 boundary, its
-- roof band and top window courses standing in the route's last rows, so
-- no single map's grid holds the whole building. The matcher never sees
-- topRows (placement is still by `tiles` alone); they exist so the MODEL
-- is built from the complete drawing and the tower rises to its real
-- height instead of folding as two half-buildings.
local function read(t, data, perRow)
local tiles = t.tiles
if t.topRows then
tiles = {}
for _, row in ipairs(t.topRows) do tiles[#tiles + 1] = row end
for _, row in ipairs(t.tiles) do tiles[#tiles + 1] = row end
end
local bh, bw = #tiles, #t.tiles[1]
local W, H = bw * 8, bh * 8
local col, ax, ay = {}, {}, {}
for sy = 0, H - 1 do
Budget.tick()
local row = tiles[math.floor(sy / 8) + 1]
for sx = 0, W - 1 do
local tile = row[math.floor(sx / 8) + 1]
local px = (tile % perRow) * 8 + sx % 8
local py = math.floor(tile / perRow) * 8 + sy % 8
local i = sy * W + sx
ax[i], ay[i] = px, py
local r, g, b, a = data:getPixel(px, py)
col[i] = shadeOf(r, g, b, a)
end
end
local outside = {}
local queue, n = {}, 0
local function seed(x, y)
local i = y * W + x
if not outside[i] and col[i] <= GREY then
outside[i] = true
n = n + 1
queue[n] = i
end
end
-- The flood comes in from the border, which assumes the drawing is
-- bounded by its own outline on every side. A drawing trimmed flush to
-- its art -- one whose base course is a row of brick rather than the
-- black threshold every other building stands on -- names the sides it
-- runs off in `seal`, and the flood does not seed there. Without it the
-- flood climbs in through the light mortar and hollows the wall out.
local seal = t.seal or ""
local function sealed(side) return string.find(seal, side, 1, true) ~= nil end
for x = 0, W - 1 do
if not sealed("n") then seed(x, 0) end
if not sealed("s") then seed(x, H - 1) end
end
for y = 0, H - 1 do
if not sealed("w") then seed(0, y) end
if not sealed("e") then seed(W - 1, y) end
end
while n > 0 do
local i = queue[n]
n = n - 1
local x, y = i % W, math.floor(i / W)
if x + 1 < W then seed(x + 1, y) end
if x > 0 then seed(x - 1, y) end
if y + 1 < H then seed(x, y + 1) end
if y > 0 then seed(x, y - 1) end
end
local inside = {}
for i = 0, W * H - 1 do inside[i] = not outside[i] end
return { W = W, H = H, col = col, ax = ax, ay = ay, inside = inside }
end
-- --------------------------------------------------------------- measure --
local function measure(sp, t)
local W, H = sp.W, sp.H
local roofRows = t.roofRows
-- The drawn taper IS the slope: the first drawn row of a column is how
-- far the roof has stepped down by the time it reaches that column.
local top = {}
for x = 0, W - 1 do
local r = roofRows
for y = 0, roofRows - 1 do
if sp.inside[y * W + x] then r = y break end
end
top[x] = r
end
-- The drawing's own ground line: the row after the last drawn one. A
-- building ends on the black threshold row it stands on (ground == H),
-- but furniture is drawn standing on open floor -- the lab table's
-- legs stop two rows short of its grid -- and extruding against H
-- would float it that far above its own plot.
local ground = roofRows
for sy = H - 1, roofRows, -1 do
local drawn = false
for sx = 0, W - 1 do
if sp.inside[sy * W + sx] then drawn = true break end
end
if drawn then
ground = sy + 1
break
end
end
local wallH = ground - roofRows
local ytop = wallH - 1 + t.slab
-- Side faces must not come out as slabs of outline black: where the
-- drawing's own pixel is the outline, walk inward for the first painted
-- colour, which is what the flanks of the real thing would show.
local interior = {}
for sy = roofRows, H - 1 do
for sx = 0, W - 1 do
local i = sy * W + sx
local src = i
if sp.inside[i] and sp.col[i] == BLACK then
local step = sx < W / 2 and 1 or -1
for d = 1, 3 do
local nx = sx + step * d
if nx >= 0 and nx < W then
local ni = sy * W + nx
if sp.inside[ni] and sp.col[ni] ~= BLACK then
src = ni
break
end
end
end
end
interior[i] = src
end
end
-- Panes: the facade's non-black pixels split into regions across the
-- black frames, and a region small enough to be a window or a doorway
-- sinks a voxel. Frames stay proud, so the pane behind them reads as
-- glass set into the wall -- and a nested frame (the door's own little
-- window) layers for free.
local recess, seen = {}, {}
for sy = roofRows, H - 1 do
for sx = 0, W - 1 do
local i0 = sy * W + sx
if not seen[i0] and sp.inside[i0] and sp.col[i0] ~= BLACK then
local cells, stack = {}, { i0 }
seen[i0] = true
local x0, x1, y0, y1 = sx, sx, sy, sy
local function step(nx, ny)
if nx < 0 or nx >= W or ny < roofRows or ny >= H then return end
local ni = ny * W + nx
if not seen[ni] and sp.inside[ni] and sp.col[ni] ~= BLACK then
seen[ni] = true
stack[#stack + 1] = ni
end
end
while #stack > 0 do
local i = table.remove(stack)
cells[#cells + 1] = i
local cx, cy = i % W, math.floor(i / W)
if cx < x0 then x0 = cx end
if cx > x1 then x1 = cx end
if cy < y0 then y0 = cy end
if cy > y1 then y1 = cy end
step(cx + 1, cy)
step(cx - 1, cy)
step(cx, cy + 1)
step(cx, cy - 1)
end
if x1 - x0 < RECESS_MAX and y1 - y0 < RECESS_MAX then
for _, i in ipairs(cells) do recess[i] = true end
end
end
end
end
-- One representative texel per shade, taken from the building's own art:
-- the roof's fascia and its undersides are geometry the drawing implies
-- but never paints, and they must still wear its palette (and pick up
-- whatever SGB recolouring the atlas carries).
local shadeTexel = {}
for i = 0, sp.W * sp.H - 1 do
if sp.inside[i] and not shadeTexel[sp.col[i]] then
shadeTexel[sp.col[i]] = i
end
end
for s = WHITE, BLACK do
shadeTexel[s] = shadeTexel[s] or shadeTexel[BLACK] or 0
end
-- Depth is the MATCHED footprint, not the sprite height. The two are
-- the same number for every whole-drawing template (the sprite is
-- built from `tiles` alone), but a template with `topRows` has a
-- sprite taller than its footprint -- the tower's 16-row drawing
-- stands on the 8 rows of it that are actually on the map, and D = H
-- would have pushed its body 64px south into the town plaza.
-- `depth` (in tile rows) names the plot when the grid runs PAST it
-- onto ground the drawing merely stands its legs on: the lab table's
-- third row is the walkable cell the player faces it from, and the
-- full-grid depth would stand the model in their path.
return { top = top, ytop = ytop, D = (t.depth or #t.tiles) * 8,
ground = ground,
recess = recess, interior = interior, shadeTexel = shadeTexel }
end
-- ----------------------------------------------------------------- build --
-- A desk with separately-classified objects on it (a template's `parts`
-- list): the methodology's region classification at part granularity.
-- Upright parts anchor their drawn bottom row to the desk's top plane
-- and wear their own drawn tops as lids; flat parts (a keyboard, a
-- sheet of paper) lie one voxel proud at drawn row = depth row -- the
-- same 1:1 the tabletop itself is drawn with, so an object's height ON
-- the drawing is its position ON the desk. The desk is the lab-table
-- slab + base; its lid is the one synthesized surface in the model
-- (the objects cover every drawn pixel of the tabletop), continued
-- from the sibling tables' pattern in the drawing's own shades.
-- tools/building_voxels.py `build_desk_set` is the reference twin.
local function deskSetModel(sp, pr, t)
local W, H, D = sp.W, sp.H, pr.D
local ground = pr.ground
local col, inside = sp.col, sp.inside
local vox = {}
local function key(x, y, z) return (y * D + z) * W + x end
local function put(x, y, z, i) vox[key(x, y, z)] = i end
-- de-outline walk bounded to the part, so a part's side faces show
-- its own material and never the neighbour's (the sprite-wide walk
-- the facade path uses would cross the black seam between units)
local function interiorAt(sx, sy, lo, hi)
local i = sy * W + sx
if col[i] ~= BLACK then return sx end
local step = sx < math.floor((lo + hi) / 2) and 1 or -1
for d = 1, 3 do
local nx = sx + step * d
if nx >= lo and nx <= hi then
local ni = sy * W + nx
if inside[ni] and col[ni] ~= BLACK then return nx end
end
end
return sx
end
local f0, f1 = t.desk.fascia[1], t.desk.fascia[2]
local b0, b1 = t.desk.base[1], t.desk.base[2]
local plane = (b1 - b0 + 1) + (f1 - f0 + 1)
-- the base band, extruded exactly like every lab table's
for sy = b0, b1 do
Budget.tick()
local y = ground - 1 - sy
for sx = 0, W - 1 do
if inside[sy * W + sx] then
local ix = interiorAt(sx, sy, 0, W - 1)
for z = 0, D - 1 do
local px = (z == 0 or z == D - 1) and sx or ix
put(sx, y, z, sy * W + px)
end
end
end
end
for i in pairs(pr.recess) do
local sy = math.floor(i / W)
if sy >= b0 and sy <= b1 then
vox[key(i % W, ground - 1 - sy, D - 1)] = nil
end
end
-- the slab: fascia rows wrap every side; the lid continues the
-- sibling tables' top -- black rim, white highlight courses along
-- the north and west, grey field
for sy = f0, f1 do
Budget.tick()
local y = plane - 1 - (sy - f0)
for sx = 0, W - 1 do
for z = 0, D - 1 do put(sx, y, z, sy * W + sx) end
end
end
for sx = 0, W - 1 do
for z = 0, D - 1 do
local shade = GREY
if sx == 0 or sx == W - 1 or z == 0 or z == D - 1 then
shade = BLACK
elseif sx == 1 or z == 1 then
shade = WHITE
end
put(sx, plane - 1, z, pr.shadeTexel[shade])
end
end
local ytop = plane
for _, p in ipairs(t.parts) do
Budget.tick()
local x0, x1 = p.x[1], p.x[2]
if p.kind == "flat" then
for sy = p.rows[1], p.rows[2] do
if sy >= 0 and sy < D then
for sx = x0, x1 do
if inside[sy * W + sx] then put(sx, plane, sy, sy * W + sx) end
end
end
end
else
local tr0, tr1 = p.top[1], p.top[2]
local fr0, fr1 = p.facade[1], p.facade[2]
local pd = p.depth
local ytp = plane + (fr1 - fr0)
if ytp > ytop then ytop = ytp end
for sx = x0, x1 do
-- the lid: the part's drawn top laid across its depth from the
-- back, last row continuing forward; the front lid row is the
-- facade's own top row -- the drawn front-top edge
for z = 0, pd - 1 do
local front = z == pd - 1
local sy = front and fr0 or math.min(tr0 + z, tr1)
while sy <= tr1 and not inside[sy * W + sx] do sy = sy + 1 end
local ok = sy <= tr1 or (front and inside[fr0 * W + sx])
if ok then
put(sx, ytp, z, (front and fr0 or sy) * W + sx)
end
end
-- the body: facade rows anchored to the desk's top plane
for sy = fr0 + 1, fr1 do
local y = plane + (fr1 - sy)
local i = sy * W + sx
if inside[i] then
local ix = interiorAt(sx, sy, x0, x1)
for z = 0, pd - 1 do
if z == pd - 1 then
if not pr.recess[i] then put(sx, y, z, i) end
elseif z == 0 then
put(sx, y, z, i)
else
put(sx, y, z, sy * W + ix)
end
end
end
end
end
end
end
return { at = function(x, y, z)
if x < 0 or x >= W or y < 0 or z < 0 or z >= D then return nil end
return vox[key(x, y, z)]
end,
W = W, ytop = ytop, zmin = 0, zmax = D - 1 }
end
-- The voxel model as a lookup: `at(x, y, z)` is the index of the sprite
-- pixel that voxel wears, or nil. Build ORDER is expressed as lookup
-- order -- roof first, so it overwrites the walls it intersects, and walls
-- are trimmed to its underside so nothing pokes through the surface.
local function model(sp, pr, t)
if t.parts then return deskSetModel(sp, pr, t) end
local W, H, D = sp.W, sp.H, pr.D
local slab, roofRows = t.slab, t.roofRows
local top, ytop, ground = pr.top, pr.ytop, pr.ground
-- The roof's drawn span. A sprite inset from its box (B03) leaves outer
-- columns undrawn in the roof band; they carry no roof at all, and the
-- rim treatment belongs to the outermost drawn columns instead of the
-- box edge.
local x0d, x1d
for x = 0, W - 1 do
if top[x] < roofRows then
x0d = x0d or x
x1d = x
end
end
local ledge0, ledge1 = nil, nil
if t.ledge then ledge0, ledge1 = t.ledge[1], t.ledge[2] end
local rz0, rz1 = 0, D - 1 + (t.frontEave or 0)
local back, front = t.roofBack, t.roofFront
local cyc0, cyc1 = t.roofCycle[1], t.roofCycle[2]
local cycN = cyc1 - cyc0 + 1
-- Which drawn row lies at depth z. The drawing looks at the roof from
-- the north, so its top rows ARE the far edge and its bottom rows the
-- eave over the facade. The band is shallower than the building, so the
-- rims map one row per voxel and the middle cycles a run whose period is
-- the course rhythm -- picked up where the north rim left off, which
-- continues both the course lines and the roof texture seamlessly.
local roofSy = {}
for z = rz0, rz1 do
local df, db = z - rz0, rz1 - z -- from the north / south edge
if df < back then
roofSy[z] = df
elseif db < front then
roofSy[z] = roofRows - 1 - db
else
roofSy[z] = cyc0 + (df - cyc0) % cycN
end
end
local T = {}
for x = 0, W - 1 do T[x] = ytop - top[x] end
local function at(x, y, z)
if x < 0 or x >= W then return nil end
local tx = T[x]
-- roof: a solid of constant thickness following the elevation profile
if top[x] < roofRows
and y > tx - slab and y <= tx and z >= rz0 and z <= rz1 then
if y == tx and x > x0d and x < x1d and z > rz0 and z < rz1 then
-- the surface itself. Clamping the row into the column's first
-- drawn row keeps the flank battens running down the slope
-- instead of falling off the silhouette.
local sy = roofSy[z]
if sy < top[x] then sy = top[x] end
return sy * W + x
end
-- The rim reproduces the eave the drawing itself paints under the
-- roof: a black outline, a shaded fascia, closed by the outline
-- again. (A GREY fascia band -- what the first cut had -- comes out
-- WHITE once the atlas is recoloured and turns every sloped end
-- into a black-and-white zip.) Under the surface it is all shadow.
local outer = x == x0d or x == x1d or z == rz0 or z == rz1
if not outer then return pr.shadeTexel[DARK] end
if y == tx or y == tx - slab + 1 then return pr.shadeTexel[BLACK] end
return pr.shadeTexel[DARK]
end
-- trimmed: under the slope. A column with no roof over it has no
-- underside to trim to, and must not be cut away by a profile the
-- drawing never set.
if top[x] < roofRows and y > tx - slab then return nil end
-- the awning: the band juts two voxels past the walls, front and back
if ledge0 and (z == -2 or z == -1 or z == D or z == D + 1) then
local sy = ground - 1 - y
if sy >= ledge0 and sy <= ledge1 and sp.inside[sy * W + x] then
return sy * W + x
end
return nil
end
-- the facade, extruded straight back over the footprint. Rows map
-- against the measured ground line, not the grid's last row: the two
-- differ only for furniture standing on open floor (see measure).
if z < 0 or z >= D then return nil end
local sy = ground - 1 - y
local i = sy * W + x
if y == 0 and not sp.inside[i] and sy > 0 and sp.inside[i - W] then
-- the drawing's last row is the ground the building stands on, so
-- its base course is one row up; without this the walls float a
-- voxel over their own plot
sy, i = sy - 1, i - W
end
if not sp.inside[i] then return nil end
if z == D - 1 then
if pr.recess[i] then return nil end
return i
end
if z == 0 then return i end
return pr.interior[i]
end
return { at = at, W = W, ytop = ytop,
zmin = ledge0 and -2 or 0,
zmax = math.max(rz1, ledge0 and (D + 1) or 0) }
end
-- ------------------------------------------------------------------ emit --
-- Cull to the shell and merge. A run of faces collapses into one quad when
-- its texels are the SAME (a flat-coloured strip, which is most of a side
-- face) or ADJACENT IN THE ATLAS along the run (the drawing continuing
-- across the face, which is most of a front face or a roof top). Both keep
-- every texel exactly where the sprite put it.
local function emit(m, sp, atlasW, atlasH)
local W = m.W
local quads = { voxels = 0, shell = 0 }
local cell = {} -- (y, z, x) -> sprite pixel index
local zmin, zmax, ytop = m.zmin, m.zmax, m.ytop
local zn = zmax - zmin + 1
local function ci(x, y, z)
if x < 0 or x >= W or y < 0 or y > ytop or z < zmin or z > zmax then
return nil
end
return cell[(y * zn + (z - zmin)) * W + x]
end
for y = 0, ytop do
Budget.tick()
for z = zmin, zmax do
local base = (y * zn + (z - zmin)) * W
for x = 0, W - 1 do
local v = m.at(x, y, z)
cell[base + x] = v
if v then quads.voxels = quads.voxels + 1 end
end
end
end
-- the shell: what survives hidden-face culling. Counted here rather than
-- derived from the quads because it is the number
-- tools/building_voxels.py checks this build against.
for y = 0, ytop do
Budget.tick()
for z = zmin, zmax do
for x = 0, W - 1 do
if ci(x, y, z) and not (ci(x + 1, y, z) and ci(x - 1, y, z)
and ci(x, y + 1, z) and ci(x, y - 1, z)
and ci(x, y, z + 1) and ci(x, y, z - 1)) then
quads.shell = quads.shell + 1
end
end
end
end
-- u/v of a run: `n` texels starting at sprite pixel `i`, stepping along
-- the atlas when the run is a strip and standing still when it is flat.
local function uvOf(i, strip, n)
local x0 = sp.ax[i]
local y0 = sp.ay[i]
local x1 = strip and (x0 + n) or (x0 + 1)
return (x0 + 0.05) / atlasW, (x1 - 0.05) / atlasW,
(y0 + 0.05) / atlasH, (y0 + 1 - 0.05) / atlasH
end
local function put(c1, c2, c3, c4, uv, shade)
quads[#quads + 1] = { c1, c2, c3, c4, uv = uv, shade = shade }
end
-- How far a run of exposed faces reaches from `x`, and whether it is a
-- strip (texels marching along the atlas) or flat (one texel repeated).
local function runX(y, z, dx, dy, dz, x)
local i0 = ci(x, y, z)
local strip, n = nil, 1
while true do
local nx = x + n
local i = ci(nx, y, z)
if not i or ci(nx + dx, y + dy, z + dz) then break end
local prev = ci(nx - 1, y, z)
if sp.ay[i] ~= sp.ay[prev] then break end
local d = sp.ax[i] - sp.ax[prev]
if d == 1 then
if strip == false then break end
strip = true
elseif d == 0 then
if strip == true then break end
strip = false
else
break
end
n = n + 1
end
return i0, strip == true, n
end
-- ---- faces along +-Z (the facade, the roof's rims): merge along x ----
for _, d in ipairs({ 1, -1 }) do
local shade = d == 1 and SHADE.south or SHADE.north
for y = 0, ytop do
Budget.tick()
for z = zmin, zmax do
local x = 0
while x < W do
if ci(x, y, z) and not ci(x, y, z + d) then
local i, strip, n = runX(y, z, 0, 0, d, x)
local u0, u1, v0, v1 = uvOf(i, strip, n)
local zf = d == 1 and (z + 1) or z
if d == 1 then
put({ x, y, zf }, { x + n, y, zf },
{ x + n, y + 1, zf }, { x, y + 1, zf },
{ { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, shade)
else
put({ x + n, y, zf }, { x, y, zf },
{ x, y + 1, zf }, { x + n, y + 1, zf },
{ { u1, v1 }, { u0, v1 }, { u0, v0 }, { u1, v0 } }, shade)
end
x = x + n
else
x = x + 1
end
end
end
end
end
-- ---- faces along +-Y (roof surfaces, undersides): merge along x ----
for _, d in ipairs({ 1, -1 }) do
local shade = d == 1 and SHADE.top or SHADE.bottom
for y = 0, ytop do
Budget.tick()
-- the underside of the bottom layer is the ground it stands on
if not (d == -1 and y == 0) then
for z = zmin, zmax do
local x = 0
while x < W do
if ci(x, y, z) and not ci(x, y + d, z) then
local i, strip, n = runX(y, z, 0, d, 0, x)
local u0, u1, v0, v1 = uvOf(i, strip, n)
local yf = d == 1 and (y + 1) or y
if d == 1 then
put({ x, yf, z }, { x + n, yf, z },
{ x + n, yf, z + 1 }, { x, yf, z + 1 },
{ { u0, v0 }, { u1, v0 }, { u1, v1 }, { u0, v1 } }, shade)
else
put({ x, yf, z + 1 }, { x + n, yf, z + 1 },
{ x + n, yf, z }, { x, yf, z },
{ { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, shade)
end
x = x + n
else
x = x + 1
end
end
end
end
end
end
-- ---- faces along +-X (the flanks): merge along z, one texel each ----
for _, d in ipairs({ 1, -1 }) do
for y = 0, ytop do
for x = 0, W - 1 do
local z = zmin
while z <= zmax do
local i = ci(x, y, z)
if i and not ci(x + d, y, z) then
local n = 1
while z + n <= zmax do
local j = ci(x, y, z + n)
if j ~= i or ci(x + d, y, z + n) then break end
n = n + 1
end
local u0, u1, v0, v1 = uvOf(i, false, n)
local xf = d == 1 and (x + 1) or x
if d == 1 then
put({ xf, y, z + n }, { xf, y, z },
{ xf, y + 1, z }, { xf, y + 1, z + n },
{ { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } },
SHADE.side)
else
put({ xf, y, z }, { xf, y, z + n },
{ xf, y + 1, z + n }, { xf, y + 1, z },
{ { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } },
SHADE.side)
end
z = z + n
else
z = z + 1
end
end
end
end
end
return quads
end
-- ------------------------------------------------------------- placement --
-- Does the template's tile grid sit at (tx, ty)?
local function matches(S, t, tx, ty)
local tiles = t.tiles
for r = 1, #tiles do
local row = tiles[r]
for c = 1, #row do
if S.tileAt[keyOf(tx + c - 1, ty + r - 1)] ~= row[c] then
return false
end
end
end
return true
end
-- Find every placement of every template for this map's tileset, build one
-- model per template, and stamp it. Returns nothing; the quads land in
-- S.objectQuads and the tiles are claimed so the volume path never boxes a
-- building this module has already modelled.
function Buildings.build(S, map, data, perRow)
if not data then return end
local tileset = map.tileset
local s = profile()
local list = s and s.buildings and s.buildings[tileset.id]
if not list then return end
local atlasW = tileset.imageWidth or 128
local atlasH = tileset.imageHeight or 48
local tw, th = map.def.width * 4, map.def.height * 4
local quads = S.objectQuads
for index, t in ipairs(list) do
if type(t.tiles) == "table" and #t.tiles > 0 then
local bh, bw = #t.tiles, #t.tiles[1]
local first = t.tiles[1][1]
local built = nil
for ty = 0, th - bh do
Budget.tick()
for tx = 0, tw - bw do
-- A placement never stamps into cells another template already
-- claimed. Templates are matched independently, and one
-- drawing can satisfy two grids: the Pokemon Tower's upper
-- twelve rows on ROUTE_10 are a standard 6-cell block tile for
-- tile, so `gabled_block_6x6` matched there and stood a whole
-- second building behind the tower. First claim wins, so the
-- list order below is the priority order -- the tower's own
-- templates come first precisely so they take those cells.
local free = S.tileAt[keyOf(tx, ty)] == first
if free then
for r = 0, bh - 1 do
for c = 0, bw - 1 do
if S.skip[keyOf(tx + c, ty + r)] then
free = false
break
end
end
if not free then break end
end
end
if free and matches(S, t, tx, ty) then
if not built then
local key = tileset.id .. ":" .. index
if not models[key] then
if t.claimOnly then
-- claim the cells, stamp nothing: the drawing here is
-- the off-map half of a building another map models in
-- full (the tower's roof rows on ROUTE_10 -- Lavender's
-- placement composites them via topRows). Left to the
-- detector they stood as a second half-building.
models[key] = {}
else
local sp = read(t, data, perRow)
local pr = measure(sp, t)
models[key] = emit(model(sp, pr, t), sp, atlasW, atlasH)
end
end
built = models[key]
end
Buildings.stamp(S, map, built, tx, ty, bw, bh)
end
end
end
end
end
end
-- One placement: claim its tiles (so the detector leaves them alone and
-- the mesher paints ground under them) and copy the model into place.
function Buildings.stamp(S, map, quads, tx, ty, bw, bh)
local shape = { class = "building", h = 0, art = "building",
flat = false, authored = true }
-- the ground the building stands on: the commonest flat tile around its
-- feet, so a house on a path keeps its path
local votes, best, bestN = {}, nil, 0
local function vote(x, y)
local k = keyOf(x, y)
local ns = S.shapeAt[k]
if ns and ns.flat and ns.class ~= "void" then
local tile = S.tileAt[k]
votes[tile] = (votes[tile] or 0) + 1
if votes[tile] > bestN then best, bestN = tile, votes[tile] end
end
end
for c = 0, bw - 1 do
vote(tx + c, ty - 1)
vote(tx + c, ty + bh)
end
for r = 0, bh - 1 do
vote(tx - 1, ty + r)
vote(tx + bw, ty + r)
end
for r = 0, bh - 1 do
for c = 0, bw - 1 do
local k = keyOf(tx + c, ty + r)
S.shapeAt[k] = shape
S.skip[k] = true
S.ground[k] = best or false
end
end
local mx, mz = tx * 8, ty * 8
local out = S.objectQuads
for _, q in ipairs(quads) do
out[#out + 1] = {
{ q[1][1] + mx, q[1][2], q[1][3] + mz },
{ q[2][1] + mx, q[2][2], q[2][3] + mz },
{ q[3][1] + mx, q[3][2], q[3][3] + mz },
{ q[4][1] + mx, q[4][2], q[4][3] + mz },
uv = q.uv, shade = q.shade,
-- placements only ever scan the BODY, so a building is always this
-- map's own structure: the mesher's edge keep-rules must not eat
-- the parts that poke past the boundary (an edge-row house's eave
-- juts frontEave voxels into the neighbour's airspace, and the
-- neighbour-body mask read that overhang as a ring scrap -- which
-- opened the roof rim into the sky from across the seam)
own = true,
}
end
end
-- What the models built so far cost, keyed "<tileset>:<index>": the voxel
-- and shell counts tools/building_voxels.py checks this implementation
-- against (Stage 5 of the methodology), and the quad count that ships.
function Buildings.stats()
local out = {}
for key, quads in pairs(models) do
out[key] = { voxels = quads.voxels, shell = quads.shell,
quads = #quads }
end
return out
end
-- Drop the prebuilt models (hot reload, or a mod shadowing the profile).
function Buildings.invalidate()
spec = nil
models = {}
end
return Buildings