mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 09:10:49 +02:00
bills, pewter gym, register, celadon mansion, tables, garbage cans
This commit is contained in:
+899
-78
File diff suppressed because it is too large
Load Diff
+463
-81
@@ -68,6 +68,47 @@ local RECESS_MAX = 24
|
||||
local SHADE = { top = 0.95, south = 1.0, north = 0.68,
|
||||
side = 0.78, bottom = 0.5 }
|
||||
|
||||
-- ------- how far a merged run may reach: the tile lattice
|
||||
--
|
||||
-- Merging is what keeps a 90k-voxel house down to ~2k quads, and under a
|
||||
-- straight projection a run may be as long as it likes -- a straight line
|
||||
-- is a straight line however finely it is cut. THE WORLD CURVE IS NOT
|
||||
-- STRAIGHT. It drops every vertex by the square of its distance from the
|
||||
-- focus (see WorldCurve), so a quad's interior is the CHORD of a parabola
|
||||
-- its neighbours draw the arc of: a run of length L hangs k*L^2/4 below
|
||||
-- the short quads butted against it, and the join tears open.
|
||||
--
|
||||
-- Nothing bounded a run's length before, and the runs that ran away were
|
||||
-- the ones wearing a CONSTANT texel -- the roof's black eave outline, its
|
||||
-- fascia, the shaded underside -- because a flat run has no art to break
|
||||
-- it. Those reached 102px across a gym, which at V-CURVE 3 hangs some
|
||||
-- three world pixels under the roof surface beside it: the eave tore off
|
||||
-- the roof and the drop showed the building's dark interior through the
|
||||
-- slot. (Strip runs, the drawing marching along the atlas, break at the
|
||||
-- tileset's own boundaries and were never the problem.)
|
||||
--
|
||||
-- So a run stops at the next 8px lattice line. Buildings are stamped at
|
||||
-- tx*8 (see stamp), so the model's lattice IS the map's: every quad in the
|
||||
-- scene -- terrain, props, this -- now ends on the same lines, every join
|
||||
-- is vertex-for-vertex, and the bend carries them together. What is left
|
||||
-- is the sag WITHIN one cell, k*64/4, which is under a twentieth of a
|
||||
-- world pixel at any rung.
|
||||
--
|
||||
-- It costs quads on a dense city map (Cerulean's object stream goes from
|
||||
-- 35.7k to 41.6k, and its longest edge from 102px to 8px) and it costs them
|
||||
-- whether the curve is on or not, which is the deliberate trade: the mesh
|
||||
-- is cached per map and built asynchronously over seconds, so meshing for
|
||||
-- the curve's sake only when the curve is on would mean rebuilding every
|
||||
-- live map on a keypress.
|
||||
local CELL = 8
|
||||
|
||||
-- How far a run starting at `a` may go before it crosses the next lattice
|
||||
-- line. Floor-mod, so the awning's negative z lands on the same lines the
|
||||
-- positive side does.
|
||||
local function runCap(a)
|
||||
return CELL - a % CELL
|
||||
end
|
||||
|
||||
local function keyOf(tx, ty)
|
||||
return (ty + 64) * 4096 + (tx + 64)
|
||||
end
|
||||
@@ -170,6 +211,39 @@ local function read(t, data, perRow)
|
||||
|
||||
local inside = {}
|
||||
for i = 0, W * H - 1 do inside[i] = not outside[i] end
|
||||
|
||||
-- `scrub` names pixel rects where the drawing paints an object standing
|
||||
-- ON the surface (Red's potted plant on the dining tabletop). The object
|
||||
-- keeps its own standee -- the template's `keep` leaves its tiles
|
||||
-- unclaimed -- so the band beneath it is the one surface the drawing
|
||||
-- implies but never paints clear: every rect pixel takes the field
|
||||
-- shade, sourced from the first field texel outside the rects, and the
|
||||
-- model's top comes out as the plain surface the object sat on.
|
||||
if t.scrub then
|
||||
local function inRect(x, y)
|
||||
for _, r in ipairs(t.scrub) do
|
||||
if x >= r[1] and x <= r[3] and y >= r[2] and y <= r[4] then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local donor = nil
|
||||
for i = 0, W * H - 1 do
|
||||
if col[i] == GREY and inside[i]
|
||||
and not inRect(i % W, math.floor(i / W)) then
|
||||
donor = i
|
||||
break
|
||||
end
|
||||
end
|
||||
for i = 0, W * H - 1 do
|
||||
if inRect(i % W, math.floor(i / W)) then
|
||||
col[i] = GREY
|
||||
ax[i], ay[i] = ax[donor], ay[donor]
|
||||
inside[i] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return { W = W, H = H, col = col, ax = ax, ay = ay, inside = inside }
|
||||
end
|
||||
|
||||
@@ -276,6 +350,14 @@ local function measure(sp, t)
|
||||
end
|
||||
end
|
||||
|
||||
-- The pane rule reads a LIGHT region the drawing seals behind a BLACK
|
||||
-- frame. A drawing built the other way round -- the healing machine's
|
||||
-- dark screens sealed behind their own white bezels -- inverts under
|
||||
-- it: every lit edge sinks and the black panes stand proud, a black
|
||||
-- lattice a voxel off the face. `panes = false` says the drawing does
|
||||
-- not carry the rule's polarity, so the facade stays flush.
|
||||
if t.panes == false then recess = {} 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
|
||||
@@ -300,7 +382,12 @@ local function measure(sp, t)
|
||||
-- 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,
|
||||
-- `depth` names the plot in TILE ROWS, which is the right grain for a
|
||||
-- building. `depthPx` names it in voxels, for an object whose real
|
||||
-- depth is not a whole tile row -- the Bike Shop toolbox is a box
|
||||
-- standing in the middle of its own cell, not a thing that fills a plot.
|
||||
return { top = top, ytop = ytop,
|
||||
D = t.depthPx or ((t.depth or #t.tiles) * 8),
|
||||
ground = ground,
|
||||
recess = recess, interior = interior, shadeTexel = shadeTexel }
|
||||
end
|
||||
@@ -343,19 +430,308 @@ local function deskSetModel(sp, pr, t)
|
||||
return sx
|
||||
end
|
||||
|
||||
-- The parts list, shared by every base piece: a desk plane or an
|
||||
-- open tray rim alike, `plane` is simply the height they ride.
|
||||
local ytop = 0
|
||||
local function buildParts(plane)
|
||||
for _, p in ipairs(t.parts) do
|
||||
Budget.tick()
|
||||
local x0, x1 = p.x[1], p.x[2]
|
||||
if p.kind == "flat" then
|
||||
-- drawn row = depth row by default; `z` renames the origin when
|
||||
-- the flat sits below the desk's own drawn top span (the Center
|
||||
-- PC's keyboard)
|
||||
local r0 = p.rows[1]
|
||||
local z0 = p.z or r0
|
||||
for sy = r0, p.rows[2] do
|
||||
local z = z0 + (sy - r0)
|
||||
if z >= 0 and z < D then
|
||||
for sx = x0, x1 do
|
||||
if inside[sy * W + sx] then put(sx, plane, z, sy * W + sx) end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif p.kind == "iso" then
|
||||
-- An ISO part is drawn in 2:1 isometric -- a box TURNED 45
|
||||
-- degrees to the map, so one rhombus carries its top, its front
|
||||
-- and its side at once and no band or facade split can reach
|
||||
-- them. Un-projecting it is that projection run backwards: the
|
||||
-- box stands as a real diamond in plan and every voxel wears the
|
||||
-- texel the drawing paints where that voxel projects TO. The
|
||||
-- drawn top lands on the top, the screen on the screen-facing
|
||||
-- side and the flank on the flank, and nothing is segmented by
|
||||
-- hand -- which is the only way to get this right, because the
|
||||
-- three faces meet on a diagonal no rectangle can name.
|
||||
--
|
||||
-- Everything but the depth centre falls out of the drawn rect,
|
||||
-- because the projection fixes it: the half-width is the drawn
|
||||
-- rhombus's x radius, HALF that again its z radius (2:1 is what
|
||||
-- makes it isometric), the near corner's drawn row is the base
|
||||
-- rhombus's front tip, and whatever drawn height is left once
|
||||
-- that rhombus is accounted for is the box's own height. Bill's
|
||||
-- computer: rx 6, rz 3, base centre row 10, and 6 voxels tall --
|
||||
-- which puts its left corner's vertical edge at drawn rows
|
||||
-- 4..10, exactly where the drawing paints one.
|
||||
--
|
||||
-- `plan` is the one thing the drawing CANNOT state: 2:1 is the
|
||||
-- projection, not the object, so reading rz as the plan radius
|
||||
-- too builds a box half as deep as it is wide -- a slab, not the
|
||||
-- cube the drawing depicts. `plan` names the real z radius and
|
||||
-- the drawn row is scaled into it, so a cube is `plan = rx` and
|
||||
-- the drawing still lands on it pixel for pixel.
|
||||
local pr0, pr1 = p.rows[1], p.rows[2]
|
||||
local rx = math.floor((x1 - x0 + 1) / 2)
|
||||
local rz = math.floor(rx / 2)
|
||||
local plan = p.plan or rz
|
||||
local oy = pr1 - rz
|
||||
local h = oy - rz - pr0
|
||||
local ytp = plane + h
|
||||
if ytp > ytop then ytop = ytp end
|
||||
for sx = x0, x1 do
|
||||
-- doubled, so a rect of even width keeps its centre between
|
||||
-- two columns instead of limping one to the left
|
||||
local dx2 = 2 * sx - (x0 + x1)
|
||||
for dz = -plan, plan do
|
||||
local z = p.z + dz
|
||||
local d2 = math.abs(dx2) * plan + 2 * math.abs(dz) * rx
|
||||
if z >= 0 and z < D and d2 <= (2 * rx + 1) * plan then
|
||||
-- the plan row scaled back into the drawn rhombus
|
||||
local dzs = math.floor((2 * dz * rz + plan) / (2 * plan))
|
||||
for y = 0, h do
|
||||
local sy = oy + dzs - y
|
||||
local i = sy * W + sx
|
||||
if sy >= pr0 and sy <= pr1 and inside[i] then
|
||||
put(sx, plane + y, z, i)
|
||||
end
|
||||
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
|
||||
-- `rise` lifts a part off the desk's top plane and `z` names its
|
||||
-- back-most depth row (the field a flat part already carries). An
|
||||
-- object STANDING on a desk needs neither: it starts on the plane
|
||||
-- at the plot's back. The healing machine's console needs both --
|
||||
-- it stands in the FRONT map row of a grid whose back row is the
|
||||
-- wall band it leans against, and its screen head is MOUNTED on
|
||||
-- the console's front two voxels above the body's top. Both come
|
||||
-- off the drawing, not off taste.
|
||||
local base = plane + (p.rise or 0)
|
||||
local pz = p.z or 0
|
||||
local ytp = base + (fr1 - fr0)
|
||||
if ytp > ytop then ytop = ytp end
|
||||
-- `inset` sinks an authored pane one voxel: the pane rule
|
||||
-- applied by hand, for a part whose screen IS sealed behind its
|
||||
-- own black frame while the template's `panes = false` (set for
|
||||
-- the polarity-inverted panel elsewhere in the same drawing)
|
||||
-- blocks the global pass. Same mechanism as a recess: the front
|
||||
-- voxel is simply not placed.
|
||||
local ins = p.inset
|
||||
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. `stretch`
|
||||
-- maps the drawn band over the whole depth instead, the tray's
|
||||
-- rule: for a part authored DEEPER than its drawing (the house
|
||||
-- stool grown past its drawn seat), clamping would print the
|
||||
-- last row as a long smear off the back band's edge.
|
||||
for z = pz, pz + pd - 1 do
|
||||
local front = z == pz + pd - 1
|
||||
local sy
|
||||
if front then
|
||||
sy = fr0
|
||||
elseif p.stretch then
|
||||
sy = math.min(tr0 + math.floor((z - pz) * (tr1 - tr0 + 1)
|
||||
/ (pd - 1)), tr1)
|
||||
else
|
||||
sy = math.min(tr0 + z - pz, tr1)
|
||||
end
|
||||
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 and z >= 0 and z < D then
|
||||
put(sx, ytp, z, (front and fr0 or sy) * W + sx)
|
||||
end
|
||||
end
|
||||
-- the body: facade rows anchored to the part's own base
|
||||
for sy = fr0 + 1, fr1 do
|
||||
local y = base + (fr1 - sy)
|
||||
local i = sy * W + sx
|
||||
if inside[i] then
|
||||
local ix = interiorAt(sx, sy, x0, x1)
|
||||
for z = pz, pz + pd - 1 do
|
||||
if z >= 0 and z < D then
|
||||
if z == pz + pd - 1 then
|
||||
local sunk = ins and sx >= ins.x[1] and sx <= ins.x[2]
|
||||
and sy >= ins.rows[1] and sy <= ins.rows[2]
|
||||
if not sunk and not pr.recess[i] then put(sx, y, z, i) end
|
||||
elseif z == pz then
|
||||
put(sx, y, z, i)
|
||||
else
|
||||
put(sx, y, z, sy * W + ix)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- A TRAY is an open container -- the drawing looks down INTO it, so its
|
||||
-- top-view band is not a lid but the inside of the box, and the model
|
||||
-- has to be hollow. Bands, all measured 1:1 like any other band table:
|
||||
-- `top` is the opening (drawn row -> depth row), `front` the near wall
|
||||
-- seen face-on (drawn row -> elevation), `x` the box's outer span and
|
||||
-- `inner` the opening's, so the difference between them is the wall.
|
||||
-- Four walls stand to the rim, the floor slab lies `floor` voxels thick
|
||||
-- under the opening, and the cavity between them is left as AIR -- which
|
||||
-- is the whole point, and what an extruded facade can never be. Parts (a
|
||||
-- standing lid) then ride the rim like any object on a desk's plane.
|
||||
if t.tray then
|
||||
local tr = t.tray
|
||||
local top0 = tr.top[1]
|
||||
local fr0, fr1 = tr.front[1], tr.front[2]
|
||||
local bx0, bx1 = tr.x[1], tr.x[2]
|
||||
local ix0, ix1 = tr.inner[1], tr.inner[2]
|
||||
local floor = tr.floor or 0
|
||||
local plane = fr1 - fr0 + 1 -- the rim: the wall's height
|
||||
-- Which drawn row lies at depth z. The far rim is the band's first
|
||||
-- row and the near rim the front wall's own, and the drawn inside
|
||||
-- STRETCHES over whatever depth is between them: a box deeper than
|
||||
-- its drawing has rows to spare is the ordinary case once the plot
|
||||
-- stops being the grid, and the alternative -- running out of rows
|
||||
-- and repeating the last one -- would print the wrench twice.
|
||||
local lo, hi = top0 + 1, tr.top[2] - 1 -- the drawn inside
|
||||
local span = math.max(1, D - 3) -- interior depth rows - 1
|
||||
local function trayRow(z)
|
||||
if z == 0 then return top0 end
|
||||
if z == D - 1 then return fr0 end
|
||||
return lo + math.floor((z - 1) * (hi - lo) / span)
|
||||
end
|
||||
for sx = bx0, bx1 do
|
||||
Budget.tick()
|
||||
for z = 0, D - 1 do
|
||||
local hollow = sx >= ix0 and sx <= ix1 and z > 0 and z < D - 1
|
||||
for y = 0, (hollow and floor or plane - 1) do
|
||||
if hollow or y == plane - 1 then
|
||||
-- the opening seen from above: the tray's own floor and
|
||||
-- whatever lies in it -- and the rim is the same band where
|
||||
-- the wall meets it
|
||||
local i = trayRow(z) * W + sx
|
||||
if inside[i] then put(sx, y, z, i) end
|
||||
else
|
||||
-- the wall below the rim: the front band folded up it, the
|
||||
-- drawn face on the front and back layers and the de-outlined
|
||||
-- interior between, exactly as a facade extrudes.
|
||||
--
|
||||
-- NO recess pass here, and it must stay that way: a pane sinks
|
||||
-- by DELETING its front voxel so the one behind becomes the
|
||||
-- pane, and a container's wall is one voxel thick -- there is
|
||||
-- nothing behind it, so the front panel simply opened a hole
|
||||
-- straight into the box and you could see the wrench through it.
|
||||
local sy = fr1 - y
|
||||
local i = sy * W + sx
|
||||
if inside[i] then
|
||||
local px = (z == 0 or z == D - 1) and sx
|
||||
or interiorAt(sx, sy, bx0, bx1)
|
||||
put(sx, y, z, sy * W + px)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if plane > ytop then ytop = plane end
|
||||
buildParts(plane)
|
||||
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
|
||||
|
||||
-- No base piece at all: the drawing IS its parts (the house stool -- a
|
||||
-- seat and its legs, nothing under them but floor). The plane the parts
|
||||
-- anchor to is the ground itself.
|
||||
if not t.desk then
|
||||
buildParts(0)
|
||||
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 desk's top plane. Usually the drawing states it: the fascia and
|
||||
-- base rows it paints below the objects ARE the front face, and their
|
||||
-- row count is the height. Bill's desk paints neither inside its grid
|
||||
-- -- its apron is drawn into the WALKABLE cell in front, and that cell
|
||||
-- is left out on purpose so the chair standing there keeps its own
|
||||
-- tiles -- so `plane` names the height directly and the body below the
|
||||
-- lid is synthesized: the band table's own rim treatment, a shaded box
|
||||
-- closed by the outline where it meets the floor, in the drawing's
|
||||
-- shades via shadeTexel.
|
||||
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 desk's own PLOT, when the grid holds more than the desk. Bill's
|
||||
-- grid runs on into the walkable cell, because the drawing puts the
|
||||
-- desk's apron AND the chair pushed up to it in the same tiles -- so
|
||||
-- the desk box has to stop at its own cell (`depth`) and stand on its
|
||||
-- own ground line rather than the grid's, which the chair's feet set
|
||||
-- eight rows lower. The base band's last row IS that ground line by
|
||||
-- definition, and for every desk drawn inside its own grid it is the
|
||||
-- measured one to the row (lab table, lab computers, Center PC, the
|
||||
-- Bike Shop toolbox), so this changes nothing for them.
|
||||
-- ...and in voxels (`depthPx`) plus a back origin (`z`) when the desk
|
||||
-- is shallower than a tile row and leans against something: the
|
||||
-- healing machine's cabinet is 10 deep -- its drawn top band's 9 rows
|
||||
-- plus the front edge -- standing against the wall band, so its box
|
||||
-- runs z 16..25 of a 32-deep plot.
|
||||
local deskD = t.desk.depthPx or (t.desk.depth and t.desk.depth * 8) or D
|
||||
local dz0 = t.desk.z or 0
|
||||
local dz1 = dz0 + deskD - 1
|
||||
local deskG = b1 + 1
|
||||
|
||||
-- The WALL element: the band the machine backs onto, whose tiles this
|
||||
-- grid claims. The drawing shows it only as the stripe background
|
||||
-- around the tower (the same standing as the potted plants' floor),
|
||||
-- so the block cycles the drawing's own stripe unit -- real pixels of
|
||||
-- column `x`, rows `cycle` -- at wall-band height over the back plot,
|
||||
-- exactly what the neighbouring cells' `wall` pins render.
|
||||
if t.wall then
|
||||
local wl = t.wall
|
||||
local c0, c1 = wl.cycle[1], wl.cycle[2]
|
||||
local cn = c1 - c0 + 1
|
||||
local wx = wl.x or 0
|
||||
for y = 0, wl.h - 1 do
|
||||
Budget.tick()
|
||||
local sy = c0 + (wl.h - 1 - y) % cn
|
||||
for sx = 0, W - 1 do
|
||||
for z = 0, wl.depthPx - 1 do
|
||||
put(sx, y, z, sy * W + wx)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- the base band, extruded exactly like every lab table's
|
||||
for sy = b0, b1 do
|
||||
Budget.tick()
|
||||
local y = ground - 1 - sy
|
||||
local y = deskG - 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
|
||||
for z = dz0, dz1 do
|
||||
local px = (z == dz0 or z == dz1) and sx or ix
|
||||
put(sx, y, z, sy * W + px)
|
||||
end
|
||||
end
|
||||
@@ -364,91 +740,72 @@ local function deskSetModel(sp, pr, t)
|
||||
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
|
||||
vox[key(i % W, deskG - 1 - sy, dz1)] = 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
|
||||
-- the slab: fascia rows wrap every side
|
||||
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
|
||||
local field = t.desk.lid == "white" and WHITE or GREY
|
||||
for sx = 0, W - 1 do
|
||||
for z = 0, D - 1 do
|
||||
local shade = field
|
||||
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])
|
||||
for z = dz0, dz1 do put(sx, y, z, sy * W + sx) end
|
||||
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
|
||||
-- drawn row = depth row by default; `z` renames the origin when
|
||||
-- the flat sits below the desk's own drawn top span (the Center
|
||||
-- PC's keyboard)
|
||||
local r0 = p.rows[1]
|
||||
local z0 = p.z or r0
|
||||
for sy = r0, p.rows[2] do
|
||||
local z = z0 + (sy - r0)
|
||||
if z >= 0 and z < D then
|
||||
for sx = x0, x1 do
|
||||
if inside[sy * W + sx] then put(sx, plane, z, sy * W + sx) end
|
||||
if t.desk.top then
|
||||
-- The lid wears the desk's own drawn top band -- the drawing DOES
|
||||
-- paint this tabletop (the healing machine's white top face with
|
||||
-- its lit west and shaded east strips), so nothing is synthesized
|
||||
-- where it is visible: band rows map back-to-front, the first
|
||||
-- fascia row is the drawn front-top edge, same rule as an upright
|
||||
-- part's lid. Where a part's drawing occludes the band (the monitor
|
||||
-- standing on it), the lid continues the nearest strip BESIDE the
|
||||
-- part -- still the drawing's own pixels, the same sibling-pattern
|
||||
-- rule every synthesized lid follows.
|
||||
local tr0, tr1 = t.desk.top[1], t.desk.top[2]
|
||||
for z = dz0, dz1 do
|
||||
Budget.tick()
|
||||
local sy = z == dz1 and f0 or math.min(tr0 + (z - dz0), tr1)
|
||||
for sx = 0, W - 1 do
|
||||
local px = sx
|
||||
for _, p in ipairs(t.parts) do
|
||||
local px0, px1 = p.x[1], p.x[2]
|
||||
local r0, r1
|
||||
if p.kind == "flat" or p.kind == "iso" then
|
||||
r0, r1 = p.rows[1], p.rows[2]
|
||||
else
|
||||
r0, r1 = p.top[1], p.facade[2]
|
||||
end
|
||||
if sx >= px0 and sx <= px1 and sy >= r0 and sy <= r1 then
|
||||
px = (sx - px0 < px1 - sx) and (px0 - 1) or (px1 + 1)
|
||||
px = math.max(0, math.min(W - 1, px))
|
||||
break
|
||||
end
|
||||
end
|
||||
put(sx, plane - 1, z, sy * W + px)
|
||||
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
|
||||
else
|
||||
-- the lid continues the sibling tables' top -- black rim, white
|
||||
-- highlight courses along the north and west, grey field
|
||||
local field = t.desk.lid == "white" and WHITE or GREY
|
||||
for sx = 0, W - 1 do
|
||||
for z = dz0, dz1 do
|
||||
local shade = field
|
||||
if sx == 0 or sx == W - 1 or z == dz0 or z == dz1 then
|
||||
shade = BLACK
|
||||
elseif sx == 1 or z == dz0 + 1 then
|
||||
shade = WHITE
|
||||
end
|
||||
put(sx, plane - 1, z, pr.shadeTexel[shade])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if plane > ytop then ytop = plane end
|
||||
buildParts(plane)
|
||||
|
||||
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)]
|
||||
@@ -638,7 +995,8 @@ local function emit(m, sp, atlasW, atlasH)
|
||||
local function runX(y, z, dx, dy, dz, x)
|
||||
local i0 = ci(x, y, z)
|
||||
local strip, n = nil, 1
|
||||
while true do
|
||||
local cap = runCap(x)
|
||||
while n < cap 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
|
||||
@@ -730,8 +1088,8 @@ local function emit(m, sp, atlasW, atlasH)
|
||||
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 n, cap = 1, runCap(z)
|
||||
while n < cap and 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
|
||||
@@ -840,7 +1198,7 @@ function Buildings.build(S, map, data, perRow)
|
||||
end
|
||||
built = models[key]
|
||||
end
|
||||
Buildings.stamp(S, map, built, tx, ty, bw, bh)
|
||||
Buildings.stamp(S, map, built, tx, ty, bw, bh, t)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -850,9 +1208,24 @@ 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 }
|
||||
--
|
||||
-- Two template fields alter what a claim means, for a drawing that
|
||||
-- carries a STANDEE on its surface (Red's potted plant on the dining
|
||||
-- table). `keep` names tile ids the stamp must NOT claim: their authored
|
||||
-- pins stay live, so the standee scan still stands the object exactly as
|
||||
-- it always did. `support` is the model's top plane in voxels: the claim
|
||||
-- shape carries it as its height, which is what tells that scan the
|
||||
-- standee's shelf -- a plain claim stays at h = 0, and Structures treats
|
||||
-- a building claim with height as a full model (skip, never a second
|
||||
-- box; see its support branches).
|
||||
function Buildings.stamp(S, map, quads, tx, ty, bw, bh, t)
|
||||
local shape = { class = "building", h = (t and t.support) or 0,
|
||||
art = "building", flat = false, authored = true }
|
||||
local keep = nil
|
||||
if t and t.keep then
|
||||
keep = {}
|
||||
for _, id in ipairs(t.keep) do keep[id] = true end
|
||||
end
|
||||
|
||||
-- the ground the building stands on: the commonest flat tile around its
|
||||
-- feet, so a house on a path keeps its path
|
||||
@@ -878,9 +1251,18 @@ function Buildings.stamp(S, map, quads, tx, ty, bw, bh)
|
||||
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
|
||||
if keep and keep[S.tileAt[k]] then
|
||||
-- unclaimed by request: the tile keeps its pin (the plant's
|
||||
-- cutout pool) and the standee scan finds it there. Only the
|
||||
-- ground is set now, so the scan's own claim of these tiles has
|
||||
-- the building's floor to paint when no flat tile touches a
|
||||
-- cluster ringed by its own furniture.
|
||||
S.ground[k] = best or false
|
||||
else
|
||||
S.shapeAt[k] = shape
|
||||
S.skip[k] = true
|
||||
S.ground[k] = best or false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+1099
-154
File diff suppressed because it is too large
Load Diff
+177
-40
@@ -71,6 +71,21 @@ local FALLBACK_HEIGHTS = {
|
||||
-- body builds from the bark rows and the drawn ellipse projects onto
|
||||
-- the hull's round top
|
||||
stump = 16,
|
||||
-- the same hull cut at both ends, hollowed and tapered: an OPEN bin
|
||||
-- standing on a floor (the Vermilion Gym trash cans). The drawn mouth
|
||||
-- ellipse projects onto the round top and down the well, the drawn base
|
||||
-- ellipse is ground contact rather than body, and the plan narrows toward
|
||||
-- the floor. Height is AUTHORED (the profile's can_height, which this
|
||||
-- pin must be kept equal to so anything riding a can lands on its rim) --
|
||||
-- the drawing's own straight run is only a couple of rows, because a GB
|
||||
-- cell spends most of itself on the opening
|
||||
can = 9,
|
||||
-- round scenery drawn ONE cell wide and TWO cells TALL, standing on one
|
||||
-- cell of plot: the Pokemon Centers' potted plants. Carved as one
|
||||
-- 16x32x16 hull in the SOUTH (pot) cell -- the drawing's upper cell is
|
||||
-- the object's height, not its depth. BOTH cells take the class; the
|
||||
-- group build anchors on the north one (Structures.buildCylinders)
|
||||
planter = 32,
|
||||
billboard = 16,
|
||||
signpost = 16,
|
||||
post = 16,
|
||||
@@ -91,6 +106,10 @@ local FALLBACK_HEIGHTS = {
|
||||
desk = 24,
|
||||
prop = 16,
|
||||
cutout = 16,
|
||||
-- a vehicle drawn SIDE-ON: the showroom bicycles. Standee height like
|
||||
-- every other cutout pool -- what differs is the thickness (see
|
||||
-- Structures' PINNED_DEPTH)
|
||||
bike = 16,
|
||||
console = 16,
|
||||
relief = 3,
|
||||
bookcase = 32,
|
||||
@@ -127,6 +146,8 @@ local ART = {
|
||||
cylinder = "cylinder",
|
||||
canopy = "canopy",
|
||||
stump = "cylinder",
|
||||
can = "cylinder",
|
||||
planter = "planter",
|
||||
billboard = "billboard",
|
||||
-- signposts share the billboard treatment but as their own pool at a
|
||||
-- 2-voxel depth: a sign is a thin plate on a stick, and the standard
|
||||
@@ -165,6 +186,13 @@ local ART = {
|
||||
desk = "upright",
|
||||
prop = "billboard",
|
||||
cutout = "billboard",
|
||||
-- a bicycle is a LINE drawing seen side-on, and its negative space --
|
||||
-- the air inside the frame, between the wheel and the fork -- is what
|
||||
-- makes it read as a bicycle at all. Its own pool at two voxels: any
|
||||
-- thicker and the side faces of neighbouring strokes close those gaps
|
||||
-- from every angle but dead-on, and six of them in a showroom come out
|
||||
-- as one dark lump (which is what the 5px `prop` pool gave)
|
||||
bike = "billboard",
|
||||
-- a machine standing on furniture: the billboard treatment with
|
||||
-- body, plus the one-object contract `cutout` has -- the drawing is
|
||||
-- ringed by the furniture it sits on, and those edges must not be
|
||||
@@ -183,6 +211,7 @@ local ART = {
|
||||
local spec = nil -- the loaded data file, or false when absent
|
||||
local cache = {} -- tileset id -> resolved shape list
|
||||
local figCache = {} -- tileset id -> parsed figure masks, or false
|
||||
local mntCache = {} -- tileset id -> parsed mounted masks, or false
|
||||
local bgCache = {} -- tileset id -> prop background shades, or false
|
||||
|
||||
-- The shape profile ships with the mod (data/voxel_heights.lua) and is read
|
||||
@@ -430,68 +459,158 @@ end
|
||||
-- pixel by pixel (see data/voxel_heights.lua):
|
||||
--
|
||||
-- figures = { { w = <tiles across>,
|
||||
-- depth = <voxels of body; ABSENT for a person>,
|
||||
-- thin = { rows = <top rows>, depth = <voxels> },
|
||||
-- flat = { x = { <lx0>, <lx1> }, rows = { <r0>, <r1> } },
|
||||
-- tiles = { ...w*h tile ids, row-major... },
|
||||
-- under = { ...w*h ids: what each tile wears once the
|
||||
-- figure is lifted off it... },
|
||||
-- pixels = { ...h*8 strings of w*8 chars, "." = not the
|
||||
-- figure... } } }
|
||||
--
|
||||
-- No class: a figure is always a flat sprite card, drawn the way
|
||||
-- SpriteBillboards draws a character (see Structures.buildFigures).
|
||||
-- No class -- what the entry carries instead is a `depth`, or does not:
|
||||
--
|
||||
-- WITHOUT one it is a flat sprite card, drawn the way SpriteBillboards
|
||||
-- draws a character. That is the right reading for a PERSON: a Gen 1
|
||||
-- figure is a face-on 2D icon, and extruding one reconstructs a body
|
||||
-- nobody drew (see Structures.buildFigures).
|
||||
-- WITH one it is an OBJECT and gets the standee treatment every other
|
||||
-- solid here gets -- a per-pixel slab in world space, standing on the
|
||||
-- same furniture the card would have stood on. The Marts' cash
|
||||
-- register is the case: a machine on a counter is a box, not an icon.
|
||||
--
|
||||
-- Two fields say which parts of such a drawing are NOT the extrusion,
|
||||
-- because a solid drawn in one 16x16 GB cell still packs more than one
|
||||
-- facing:
|
||||
--
|
||||
-- `thin` caps the thickness over the mask's top rows, for the part of
|
||||
-- the drawing that is not the machine (the register's receipt curl).
|
||||
-- `flat` names a rect of the mask that is a TOP-VIEW surface rather
|
||||
-- than a face -- the register's keypad, whose keys lie ON its deck.
|
||||
-- The rect lays horizontal one voxel proud of whatever the extrusion
|
||||
-- leaves below it, at the elevation its BOTTOM row would have had,
|
||||
-- with drawn row = depth row 1:1 (the mapping the lab tabletop is
|
||||
-- drawn with). So a drawing whose front elevation is an L reads as
|
||||
-- one: body up the side and along the base, keys lying in the notch.
|
||||
--
|
||||
-- Returned normalized: `mask` as a set keyed by ly * (w * 8) + lx, so
|
||||
-- Structures can read it as a bitmap without re-parsing per position.
|
||||
-- A malformed entry is dropped rather than half-applied -- a typo in a
|
||||
-- mask should leave the couch alone, not carve a hole in it.
|
||||
--
|
||||
-- `mounted` (below) carries the same four fields, so the parse is shared,
|
||||
-- and so are the optional ones that give an authored mask a BODY: `depth`,
|
||||
-- `thin` and `flat` above. `depth` is left nil when unstated, because
|
||||
-- absence is meaningful on a figure: no depth means the flat sprite card a
|
||||
-- person is drawn as.
|
||||
local function authoredMasks(list)
|
||||
local out = {}
|
||||
if type(list) ~= "table" then return out end
|
||||
for _, f in ipairs(list) do
|
||||
local ok = type(f) == "table" and type(f.w) == "number"
|
||||
and type(f.tiles) == "table" and type(f.under) == "table"
|
||||
and type(f.pixels) == "table"
|
||||
local w = ok and math.floor(f.w) or 0
|
||||
local h = (w >= 1) and (#f.tiles / w) or 0
|
||||
ok = ok and w >= 1 and h >= 1 and h == math.floor(h)
|
||||
and #f.under == #f.tiles and #f.pixels == h * 8
|
||||
if ok then
|
||||
for i = 1, h * 8 do
|
||||
local row = f.pixels[i]
|
||||
if type(row) ~= "string" or #row ~= w * 8 then
|
||||
ok = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if ok then
|
||||
local mask, n = {}, 0
|
||||
for ly = 0, h * 8 - 1 do
|
||||
local row = f.pixels[ly + 1]
|
||||
for lx = 0, w * 8 - 1 do
|
||||
if row:sub(lx + 1, lx + 1) ~= "." then
|
||||
mask[ly * (w * 8) + lx] = true
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local depth = tonumber(f.depth)
|
||||
local thin = nil
|
||||
if type(f.thin) == "table" and tonumber(f.thin.rows)
|
||||
and tonumber(f.thin.depth) then
|
||||
thin = { rows = math.floor(tonumber(f.thin.rows)),
|
||||
depth = math.floor(tonumber(f.thin.depth)) }
|
||||
end
|
||||
local flat = nil
|
||||
if type(f.flat) == "table" and type(f.flat.x) == "table"
|
||||
and type(f.flat.rows) == "table" then
|
||||
flat = { x0 = math.floor(f.flat.x[1]), x1 = math.floor(f.flat.x[2]),
|
||||
r0 = math.floor(f.flat.rows[1]),
|
||||
r1 = math.floor(f.flat.rows[2]) }
|
||||
end
|
||||
if n > 0 then
|
||||
out[#out + 1] = { w = w, h = h, n = n, mask = mask,
|
||||
tiles = f.tiles, under = f.under,
|
||||
depth = depth and math.floor(depth) or nil,
|
||||
thin = thin, flat = flat }
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function TileShape.figures(tilesetId)
|
||||
local hit = figCache[tilesetId]
|
||||
if hit ~= nil then return hit or nil end
|
||||
|
||||
local s = load()
|
||||
local entry = s and s.tilesets and s.tilesets[tilesetId]
|
||||
local list = entry and entry.figures
|
||||
local out = {}
|
||||
if type(list) == "table" then
|
||||
for _, f in ipairs(list) do
|
||||
local ok = type(f) == "table" and type(f.w) == "number"
|
||||
and type(f.tiles) == "table" and type(f.under) == "table"
|
||||
and type(f.pixels) == "table"
|
||||
local w = ok and math.floor(f.w) or 0
|
||||
local h = (w >= 1) and (#f.tiles / w) or 0
|
||||
ok = ok and w >= 1 and h >= 1 and h == math.floor(h)
|
||||
and #f.under == #f.tiles and #f.pixels == h * 8
|
||||
if ok then
|
||||
for i = 1, h * 8 do
|
||||
local row = f.pixels[i]
|
||||
if type(row) ~= "string" or #row ~= w * 8 then
|
||||
ok = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if ok then
|
||||
local mask, n = {}, 0
|
||||
for ly = 0, h * 8 - 1 do
|
||||
local row = f.pixels[ly + 1]
|
||||
for lx = 0, w * 8 - 1 do
|
||||
if row:sub(lx + 1, lx + 1) ~= "." then
|
||||
mask[ly * (w * 8) + lx] = true
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if n > 0 then
|
||||
out[#out + 1] = { w = w, h = h, n = n, mask = mask,
|
||||
tiles = f.tiles, under = f.under }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local out = authoredMasks(entry and entry.figures)
|
||||
|
||||
figCache[tilesetId] = (#out > 0) and out or false
|
||||
return figCache[tilesetId] or nil
|
||||
end
|
||||
|
||||
-- Hand-authored MOUNTED objects for one tileset: a thing drawn INTO the
|
||||
-- wall band it hangs on, cut out by an explicit pixel mask and stood
|
||||
-- proud of the wall's face.
|
||||
--
|
||||
-- Same authoring problem as `figures` and the same answer -- a class pin
|
||||
-- resolves a whole 8x8 tile, and the detector cannot segment a drawing
|
||||
-- that has no background margin to flood from. The Bike Shop's two wall
|
||||
-- bicycles are the case: the shop's striped wall panel runs BEHIND them,
|
||||
-- and its #555 stripes are a flood boundary, so a silhouette flood comes
|
||||
-- back with the stripes attached to the bike.
|
||||
--
|
||||
-- Two things differ from a figure, and both follow from the object being
|
||||
-- an object rather than a character:
|
||||
--
|
||||
-- it keeps its DRAWN ELEVATION. A figure stands on its own feet; a
|
||||
-- mounted thing sits where the wall band draws it, so a bicycle hung
|
||||
-- clear of the floor stays hung.
|
||||
-- it has THICKNESS (`depth`, default 2), and it is built in world
|
||||
-- space as a per-pixel slab jutting south of the band -- not as a
|
||||
-- camera-facing sprite card. A bicycle drawn side-on is a plane
|
||||
-- parallel to the wall, not a face-on icon.
|
||||
--
|
||||
-- mounted = { { w = <tiles across>,
|
||||
-- depth = <voxels it juts into the room>,
|
||||
-- tiles = { ...w*h tile ids, row-major... },
|
||||
-- under = { ...w*h ids: what each tile wears once the
|
||||
-- object is lifted off it (the plain panel)... },
|
||||
-- pixels = { ...h*8 strings of w*8 chars, "." = wall... } } }
|
||||
function TileShape.mounted(tilesetId)
|
||||
local hit = mntCache[tilesetId]
|
||||
if hit ~= nil then return hit or nil end
|
||||
|
||||
local s = load()
|
||||
local entry = s and s.tilesets and s.tilesets[tilesetId]
|
||||
local out = authoredMasks(entry and entry.mounted)
|
||||
|
||||
mntCache[tilesetId] = (#out > 0) and out or false
|
||||
return mntCache[tilesetId] or nil
|
||||
end
|
||||
|
||||
-- Which GB shades count as BACKGROUND for a pinned per-pixel prop, per tile
|
||||
-- (a tileset entry's prop_bg). Returns tile id -> set of shade names, or nil.
|
||||
--
|
||||
@@ -564,12 +683,30 @@ function TileShape.bookcaseBackfill(tilesetId)
|
||||
return mode == "above" and mode or nil
|
||||
end
|
||||
|
||||
--- Does this tileset's `bookcase` run carry the measured pane RELIEF on
|
||||
--- its front (a tileset entry's bookcase_relief)? Default yes: the class
|
||||
--- almost always collapses a shelf, a rack or a display case, and every
|
||||
--- one of those seals its contents behind a frame that should stand proud
|
||||
--- of them.
|
||||
---
|
||||
--- A tileset says `bookcase_relief = false` when it borrows the collapse
|
||||
--- for something that is NOT a shelf -- the League's gate walls and
|
||||
--- pilasters, Bill's transporter drums -- where the drawing's light
|
||||
--- regions are the masonry and the barrel, not panes, and sinking them
|
||||
--- carves the surface instead of describing it.
|
||||
function TileShape.bookcaseRelief(tilesetId)
|
||||
local s = load()
|
||||
local entry = s and s.tilesets and s.tilesets[tilesetId]
|
||||
return not (entry and entry.bookcase_relief == false)
|
||||
end
|
||||
|
||||
-- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset
|
||||
-- record needs the next lookup to re-resolve (hot reload, mod toggle).
|
||||
function TileShape.invalidate()
|
||||
spec = nil
|
||||
cache = {}
|
||||
figCache = {}
|
||||
mntCache = {}
|
||||
bgCache = {}
|
||||
end
|
||||
|
||||
|
||||
+61
-9
@@ -381,9 +381,32 @@ Water.EDGE_FADE = 0.14 -- reflection eased off over this much of the fra
|
||||
--
|
||||
-- The scene shader's own vertex path, plus the world position the geometry
|
||||
-- was actually DRAWN at -- after the world curve, because that is the space
|
||||
-- the depth buffer holds and therefore the space the march has to walk in.
|
||||
-- (The curve only ever moves Y, so a fragment's world XZ is the same on both
|
||||
-- sides of it and the ripple can be measured off this one too.)
|
||||
-- the surface the eye MEETS lives in: which wave column a screen pixel is
|
||||
-- looking at is a question about the geometry as drawn, and relief() answers
|
||||
-- it there. (The curve only ever moves Y, so a fragment's world XZ is the
|
||||
-- same on both sides of it and the ripple can be measured off this one too.)
|
||||
--
|
||||
-- WHAT IT REFLECTS is worked out on the other side of the bend, in the FLAT
|
||||
-- world, and this is the same rule the rest of the mode keeps: the curve
|
||||
-- tips the world away and the things standing on it do not lean with it (see
|
||||
-- WorldCurve -- buildings stay upright, shadows are resolved before the bend
|
||||
-- and ride along). A lake is one of those things. Reflect off the bowl the
|
||||
-- bend has made instead and the far half of a pond is a mirror tilted twenty
|
||||
-- degrees: it throws the ray past the vertical, where the sky ramp's own
|
||||
-- measure -- a screen row, through the frame's matrix -- swings from one end
|
||||
-- of the ramp to the other across a single column, and the pond comes out
|
||||
-- with hard-edged patches of the wrong sky stamped into it -- the overhead
|
||||
-- band and the horizon band abutting in the middle of a lake, which reads as
|
||||
-- something other than water showing through. The same tilt sends the
|
||||
-- screen-space march grazing along the bank instead of over it, which is the
|
||||
-- other half: the dock and the roofs smeared across the harbour.
|
||||
--
|
||||
-- So the reflection is taken with the flat view ray about the flat normal,
|
||||
-- exactly as it would be with the curve off -- and the MARCH still has to
|
||||
-- walk the world as drawn, because that is what the depth buffer holds. Both
|
||||
-- at once: the ray is straight in the flat world, and project() bends each
|
||||
-- sample on its way to the screen, which is the same displacement the vertex
|
||||
-- stage applies and therefore lands in the same place the geometry did.
|
||||
local SHADER_SRC = [[
|
||||
varying float vShade;
|
||||
varying vec3 vSun;
|
||||
@@ -435,6 +458,20 @@ uniform vec3 eye;
|
||||
uniform vec2 screen; // the canvas, in pixels
|
||||
uniform float cell; // one diorama pixel, in canvas pixels
|
||||
uniform float pxAngle; // radians of view one screen pixel subtends
|
||||
// The same bend the vertex stage applied. This stage has to undo it to get
|
||||
// back to the flat world it reflects in, and re-apply it on every marched
|
||||
// sample to get back to the screen. Declared in both stages, like `vp`, and
|
||||
// both are highp here.
|
||||
uniform vec3 curve; // xy = the focus in world XZ, z = k; 0 = off
|
||||
|
||||
// How far the bend has pushed the world down at world XZ `q` -- the vertex
|
||||
// stage's own displacement, as a number this stage can add and subtract.
|
||||
// Zero when the curve is off, which is the shader's "skip it" everywhere.
|
||||
float bendDrop(vec2 q) {
|
||||
if (curve.z <= 0.0) return 0.0;
|
||||
vec2 d = q - curve.xy;
|
||||
return dot(d, d) * curve.z;
|
||||
}
|
||||
|
||||
// the sun's own pass, exactly as the scene shader reads it
|
||||
uniform Image sunMap;
|
||||
@@ -658,10 +695,14 @@ vec3 bodyAt(vec3 d, vec3 c, float parity) {
|
||||
// clip-space Y flip is already baked into `vp`, and a canvas texture's v runs
|
||||
// the same way its pixel rows do, so one 0.5x+0.5 answers for both.
|
||||
//
|
||||
// This is why the march walks in the world as DRAWN rather than as authored:
|
||||
// the depth buffer holds the curved world, so a straight line in that space
|
||||
// is the ray, and a straight line in the flat one would bend through it.
|
||||
// The point arrives in the FLAT world -- the space the ray is straight in --
|
||||
// and is bent here, by the same displacement the vertex stage applied, so it
|
||||
// lands exactly where the geometry it is being compared against landed. That
|
||||
// split is the whole trick: the reflection is worked out in a world that has
|
||||
// not been tipped, and every sample of it is tipped on the way to the screen,
|
||||
// so the march reads the depth buffer it actually has.
|
||||
vec4 project(vec3 p) {
|
||||
p.y -= bendDrop(p.xz);
|
||||
vec4 c = vp * vec4(p, 1.0);
|
||||
if (c.w <= 1e-6) return vec4(0.0, 0.0, 0.0, 0.0);
|
||||
return vec4(c.xy / c.w * 0.5 + 0.5, c.z / c.w * 0.5 + 0.5, 1.0);
|
||||
@@ -925,12 +966,23 @@ vec4 effect(mediump vec4 color, Image tex, mediump vec2 tc, mediump vec2 sc) {
|
||||
// drawn at, with no smooth shading anywhere across it. (The depth test
|
||||
// above is the one thing that stays per fragment: that is the hardware's
|
||||
// own question and it is asked in screen space.)
|
||||
vec3 view = normalize(vBent - eye);
|
||||
//
|
||||
// Answered on the FLAT sheet, which is where the bars are a slab of even
|
||||
// thickness over a level plane -- the one thing relief() is built on. The
|
||||
// bend translates every bar straight down by its own column's drop, so the
|
||||
// field keeps its shape and only its height moves; undo that here and the
|
||||
// walk is the walk it was written for. Try it in the world as DRAWN
|
||||
// instead and the slab is a bowl: the backward step up the ray climbs the
|
||||
// bowl's near side as fast as it climbs out of the water, the walk starts
|
||||
// inside the sheet, and it hands back a column a pixel or three off -- per
|
||||
// fragment, differently, which is a patch of noise rather than parallax.
|
||||
vec3 sheet = vec3(vBent.x, vBent.y + bendDrop(vBent.xz), vBent.z);
|
||||
vec3 view = normalize(sheet - eye);
|
||||
vec3 hit;
|
||||
vec2 col;
|
||||
float face;
|
||||
float axis;
|
||||
relief(vBent, view, hit, col, face, axis);
|
||||
relief(sheet, view, hit, col, face, axis);
|
||||
// and the bar's centre, so a column is sampled and reflected from one
|
||||
// place rather than from wherever inside it the fragment happened to land
|
||||
vec3 surf = vec3(col.x + 0.5, hit.y, col.y + 0.5);
|
||||
@@ -1001,7 +1053,7 @@ vec4 effect(mediump vec4 color, Image tex, mediump vec2 tc, mediump vec2 sc) {
|
||||
vec3 rgb = mix(base, refl, clamp(f, 0.0, 1.0));
|
||||
|
||||
#ifdef VOXEL_GRID
|
||||
rgb *= 1.0 - gridDark * columnSeam(hit, vBent, axis);
|
||||
rgb *= 1.0 - gridDark * columnSeam(hit, sheet, axis);
|
||||
#endif
|
||||
return vec4(rgb, 1.0) * color;
|
||||
}
|
||||
|
||||
@@ -1800,9 +1800,21 @@ T.check(plain:find("waveNormal(vec2 q, float tilt)", 1, true) ~= nil
|
||||
and plain:find("waveNormal(col,", 1, true) ~= nil,
|
||||
"still one answer per column, so the surface stays pixel-quantised in "
|
||||
.. "space while the value it reflects with is continuous")
|
||||
T.check(plain:find("relief(vBent, view, hit, col, face, axis)", 1, true) ~= nil,
|
||||
T.check(plain:find("relief(sheet, view, hit, col, face, axis)", 1, true) ~= nil,
|
||||
"and the visible column is found by walking the view ray through the "
|
||||
.. "slab, which is what makes a tall bar hide the short ones behind it")
|
||||
-- ...over the FLAT sheet, which is the one thing that walk is built on: an
|
||||
-- even slab over a level plane. The world curve drops each bar straight down
|
||||
-- by its own column's drop, so undoing that drop hands relief() the field it
|
||||
-- was written for. Walked in the world as DRAWN instead, the slab is a bowl:
|
||||
-- the backward step up the ray climbs the bowl's near side as fast as it
|
||||
-- climbs out of the water, the walk starts inside the sheet, and it returns a
|
||||
-- column a pixel or three off -- differently per fragment, which is a
|
||||
-- hard-edged patch of noise in the middle of a pond.
|
||||
T.check(plain:find("vec3 sheet = vec3(vBent.x, vBent.y + bendDrop(vBent.xz), vBent.z)",
|
||||
1, true) ~= nil,
|
||||
"and it walks the sheet the mesh was AUTHORED as, the bend taken back off, "
|
||||
.. "because a slab walk over a bowl starts inside the water")
|
||||
-- the march's reach grows as one over the ray's descent, so a grazing camera
|
||||
-- asks for hundreds of world pixels of it from a fixed number of samples --
|
||||
-- which stepped over whole crests and smeared the surface into streaks
|
||||
@@ -1830,7 +1842,7 @@ T.check(plain:find("waveUV(tc, col)", 1, true) ~= nil,
|
||||
"and the column is what is handed to it")
|
||||
|
||||
-- the wireframe is ruled on the COLUMNS, not on the flat sheet they stand on
|
||||
T.check(gridded:find("columnSeam(hit, vBent, axis)", 1, true) ~= nil,
|
||||
T.check(gridded:find("columnSeam(hit, sheet, axis)", 1, true) ~= nil,
|
||||
"with V-GRID on, the seams outline the column the ray landed on -- every "
|
||||
.. "voxel of water its own block -- rather than ruling a grid across the "
|
||||
.. "flat quad underneath and ignoring the bars entirely")
|
||||
@@ -1839,6 +1851,25 @@ T.check(gridded:find("vec3 w = fwidth(base);", 1, true) ~= nil,
|
||||
.. "between neighbouring fragments and its own derivative is a step")
|
||||
T.check(plain:find("march(surf, r)", 1, true) ~= nil,
|
||||
"the reflection marches from that column, not from the raw fragment")
|
||||
-- The two halves of the world curve, and they pull opposite ways. WHAT the
|
||||
-- lake reflects is worked out FLAT -- the same rule the rest of the mode
|
||||
-- keeps, that the world tips away and the things standing on it do not lean
|
||||
-- with it. Reflect off the bowl the bend has made instead and the far half
|
||||
-- of a pond is a mirror tilted twenty degrees, throwing the ray past the
|
||||
-- vertical, where the sky ramp's own measure (a screen row, through the
|
||||
-- frame's matrix) swings from one end of the ramp to the other across a
|
||||
-- single column and stamps hard-edged patches of the wrong sky into the
|
||||
-- water. But WHERE it lands has to be found in the world as DRAWN, because
|
||||
-- that is what the depth buffer holds -- so the ray stays straight in the
|
||||
-- flat world and every sample of it is bent on the way to the screen, by the
|
||||
-- vertex stage's own displacement.
|
||||
T.check(plain:find("p.y -= bendDrop(p.xz);", 1, true) ~= nil,
|
||||
"and every marched sample is bent into the world as DRAWN before it is "
|
||||
.. "projected, because that is the world the depth buffer holds")
|
||||
T.check(plain:find("vec3 r = reflect(view, n);", 1, true) ~= nil
|
||||
and plain:find("reflect(view, vec3(0.0, 1.0, 0.0))", 1, true) ~= nil,
|
||||
"while the reflection itself is taken about the FLAT normal, so a curved "
|
||||
.. "world does not tip the lake the way it does not lean the buildings")
|
||||
T.check(plain:find("mod(col.x + col.y, 2.0)", 1, true) ~= nil,
|
||||
"and the dither's checkerboard is cut from the columns too, so a camera "
|
||||
.. "pan slides the world through nothing")
|
||||
|
||||
@@ -214,6 +214,172 @@ Structures.buildFigures(twice, map, 0, 3, 8, 11)
|
||||
T.eq(#twice.figures, 1,
|
||||
"the repaint replaces the pattern, so a rescan cannot match it again")
|
||||
|
||||
-- ------- a figure with a DEPTH is an object, not a card
|
||||
--
|
||||
-- The Marts' cash register: the same authored-mask escape, but a machine
|
||||
-- set down on a counter is a box seen from the front rather than a
|
||||
-- face-on icon, so it builds as a per-pixel solid. Driven over a
|
||||
-- synthetic copy of the counter's east arm, as all nine maps on the MART
|
||||
-- id draw it at cell (1,5):
|
||||
--
|
||||
-- y=9 16 41 the work surface north of it
|
||||
-- y=10 14 15 the register: keypad and receipt curl
|
||||
-- y=11 30 31
|
||||
-- y=12 16 41 the work surface it stands on
|
||||
|
||||
T.check(TileShape.figures("POKECENTER")[1].depth == nil,
|
||||
"the seated man states no depth -- he stays a flat sprite card")
|
||||
|
||||
local regs = TileShape.figures("MART")
|
||||
T.check(type(regs) == "table" and #regs == 1,
|
||||
"MART carries exactly one figure")
|
||||
local reg = regs[1]
|
||||
T.eq(reg.w, 2, "the register is two tiles across")
|
||||
T.eq(reg.h, 2, "and two tall")
|
||||
T.eq(reg.n, 150, "the mask claims 150 pixels of the 256 it spans")
|
||||
T.eq(reg.depth, 12, "its body is 12 voxels deep -- three quarters of the cell")
|
||||
T.check(reg.thin and reg.thin.rows == 4 and reg.thin.depth == 2,
|
||||
"the four rows above its drawn top edge are 2-voxel paper")
|
||||
T.check(reg.flat and reg.flat.x0 == 2 and reg.flat.x1 == 8
|
||||
and reg.flat.r0 == 4 and reg.flat.r1 == 11,
|
||||
"and the keypad is a TOP-VIEW rect, not a face")
|
||||
|
||||
local MART_ROWS = { [9] = { 16, 41 }, [10] = { 14, 15 },
|
||||
[11] = { 30, 31 }, [12] = { 16, 41 } }
|
||||
local martS = { shapeAt = {}, tileAt = {}, figures = {}, skip = {},
|
||||
ground = {}, runs = {}, objectQuads = {} }
|
||||
for ty, row in pairs(MART_ROWS) do
|
||||
for i, tile in ipairs(row) do
|
||||
martS.tileAt[keyOf(1 + i, ty)] = tile
|
||||
martS.shapeAt[keyOf(1 + i, ty)] = COUNTER
|
||||
end
|
||||
end
|
||||
local martMap = {
|
||||
tileset = { id = "MART", tilesPerRow = 16,
|
||||
imageWidth = 128, imageHeight = 48 },
|
||||
isWalkableCell = function() return false end,
|
||||
}
|
||||
Structures.buildFigures(martS, martMap, 2, 3, 9, 12)
|
||||
|
||||
T.eq(#martS.figures, 0, "no card was built -- it is a solid")
|
||||
T.eq(#martS.objectQuads, 351,
|
||||
"and it landed in the standee channel as 351 quads")
|
||||
T.eq(martS.tileAt[keyOf(2, 10)], 16,
|
||||
"its tiles wear the plain work surface now")
|
||||
T.eq(martS.tileAt[keyOf(3, 11)], 41, "all four of them")
|
||||
T.eq(martS.shapeAt[keyOf(2, 10)].class, "counter",
|
||||
"and keep the counter box the machine stands on")
|
||||
|
||||
local rx0, rx1, ry0, ry1, rz0, rz1
|
||||
for _, q in ipairs(martS.objectQuads) do
|
||||
for c = 1, 4 do
|
||||
local p = q[c]
|
||||
rx0 = math.min(rx0 or p[1], p[1]); rx1 = math.max(rx1 or p[1], p[1])
|
||||
ry0 = math.min(ry0 or p[2], p[2]); ry1 = math.max(ry1 or p[2], p[2])
|
||||
rz0 = math.min(rz0 or p[3], p[3]); rz1 = math.max(rz1 or p[3], p[3])
|
||||
end
|
||||
end
|
||||
T.eq(ry0, 8, "it stands ON the counter's 8px top plane, not the floor")
|
||||
T.eq(ry1, 24, "and is its drawn 16px tall")
|
||||
T.eq(rx0, 18, "west edge at the mask's column 2")
|
||||
T.eq(rx1, 30, "east edge at column 13, inside its own cell (16..32)")
|
||||
T.eq(rz1, 96, "its FRONT is the cell's own front edge, where it is drawn")
|
||||
T.eq(rz0, 84, "and it grows north from there, 4 short of the cell's back")
|
||||
|
||||
-- the two thicknesses: the body at 8, the receipt curl at 2, the curl
|
||||
-- centred in the body's own band rather than flush with its front
|
||||
local bands = {}
|
||||
for _, q in ipairs(martS.objectQuads) do
|
||||
for c = 1, 4 do bands[q[c][3]] = true end
|
||||
end
|
||||
for _, z in ipairs({ 84, 89, 91, 96 }) do
|
||||
T.check(bands[z], "the model has a face at z = " .. z)
|
||||
end
|
||||
local curl = {}
|
||||
for _, q in ipairs(martS.objectQuads) do
|
||||
local lo = math.min(q[1][2], q[2][2], q[3][2], q[4][2])
|
||||
if lo >= 21 then for c = 1, 4 do curl[q[c][3]] = true end end
|
||||
end
|
||||
T.check(curl[89] and curl[91] and not curl[84] and not curl[96],
|
||||
"clear of the arm's top only the 2-voxel paper band exists")
|
||||
|
||||
-- THE L. The base band (drawn rows 12-15) stands 4 above the counter and
|
||||
-- the keypad lies on it as a horizontal plate, so the whole machine is
|
||||
-- exactly three surfaces: a foot, an arm, and a deck in the notch.
|
||||
local plate, deckTop = {}, 0
|
||||
for _, q in ipairs(martS.objectQuads) do
|
||||
local flatQuad = q[1][2] == q[2][2] and q[2][2] == q[3][2]
|
||||
and q[3][2] == q[4][2]
|
||||
if flatQuad and q[1][2] == 13 then
|
||||
plate[#plate + 1] = q
|
||||
elseif flatQuad and q[1][2] == 12 then
|
||||
deckTop = deckTop + 1
|
||||
end
|
||||
end
|
||||
T.eq(#plate, 83,
|
||||
"the keypad lies FLAT: one top quad per masked voxel of the deck")
|
||||
T.eq(deckTop, 7,
|
||||
"on the base band's own top, which is 4 voxels up (drawn rows 12-15)")
|
||||
local dz0, dz1
|
||||
for _, q in ipairs(martS.objectQuads) do
|
||||
if q[1][2] == 12 and q[3][2] == 12 then
|
||||
for c = 1, 4 do
|
||||
dz0 = math.min(dz0 or q[c][3], q[c][3])
|
||||
dz1 = math.max(dz1 or q[c][3], q[c][3])
|
||||
end
|
||||
end
|
||||
end
|
||||
T.eq(dz0, 84, "and that deck runs the body's whole depth")
|
||||
T.eq(dz1, 96, "-- plain behind the panel, covered by it in front")
|
||||
|
||||
local px0, px1, pz0, pz1
|
||||
for _, q in ipairs(plate) do
|
||||
for c = 1, 4 do
|
||||
px0 = math.min(px0 or q[c][1], q[c][1]); px1 = math.max(px1 or q[c][1], q[c][1])
|
||||
pz0 = math.min(pz0 or q[c][3], q[c][3]); pz1 = math.max(pz1 or q[c][3], q[c][3])
|
||||
end
|
||||
end
|
||||
T.eq(px0, 18, "the deck spans the mask's columns 2..8")
|
||||
T.eq(px1, 25, "-- the keypad panel and its own black rim")
|
||||
T.eq(pz1, 96, "the deck reaches the body's front edge")
|
||||
T.eq(pz0, 84, "and its back -- 8 drawn rows STRETCHED over 12 voxels")
|
||||
|
||||
-- the stretch is by whole voxels, centre-sampled: 8 drawn rows over 12
|
||||
-- voxels of deck doubles every second one and blurs nothing
|
||||
local perRow16, atlasH16 = 16, 48
|
||||
local depthRow = {}
|
||||
for _, q in ipairs(plate) do
|
||||
local z = math.min(q[1][3], q[2][3], q[3][3], q[4][3])
|
||||
depthRow[z] = math.floor(q.v * atlasH16)
|
||||
end
|
||||
local seen = {}
|
||||
for z = 84, 95 do
|
||||
T.check(depthRow[z] ~= nil, "deck voxel at z = " .. z .. " wears a texel")
|
||||
seen[depthRow[z]] = (seen[depthRow[z]] or 0) + 1
|
||||
end
|
||||
T.eq(depthRow[95], 11, "the front voxel wears the keypad's own bottom rim")
|
||||
T.eq(depthRow[84], 4, "the back one wears its top rim")
|
||||
local doubled = 0
|
||||
for _, n in pairs(seen) do
|
||||
T.check(n == 1 or n == 2, "no drawn row spreads over more than two voxels")
|
||||
if n == 2 then doubled = doubled + 1 end
|
||||
end
|
||||
T.eq(doubled, 4, "exactly four of the eight rows double -- 8 into 12")
|
||||
|
||||
|
||||
-- and the arm still stands its drawn 8 rows above that deck, carrying
|
||||
-- the paper: nothing in the notch reaches higher than the plate
|
||||
local armTop, notchTop = 0, 0
|
||||
for _, q in ipairs(martS.objectQuads) do
|
||||
for c = 1, 4 do
|
||||
if q[c][1] >= 25 then armTop = math.max(armTop, q[c][2])
|
||||
elseif q[c][1] <= 24 then notchTop = math.max(notchTop, q[c][2]) end
|
||||
end
|
||||
end
|
||||
T.eq(armTop, 24, "the arm and its receipt curl reach the drawn 16px")
|
||||
T.eq(notchTop, 23,
|
||||
"and west of it only the keys (13) and the paper overhanging them")
|
||||
|
||||
-- ------- prop_bg: the shades a pinned prop treats as background
|
||||
--
|
||||
-- The potted plants needed this: their pot's olive base is drawn flush on
|
||||
|
||||
+912
-91
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user