Merge pull request #111 from Code-Grub/fix/roof-surface-silhouette-cap

keep a sloped roof's surface off its own outline cap
This commit is contained in:
DramaticShape
2026-08-06 01:13:19 -04:00
committed by GitHub
3 changed files with 167 additions and 21 deletions
+28 -5
View File
@@ -264,6 +264,27 @@ local function measure(sp, t)
top[x] = r
end
-- The row a column's roof SURFACE may sink to. `top[x]` is the
-- silhouette cap -- the black the drawing closes its shape with -- and
-- the depth map spends most of a tapered column's depth above it, so
-- clamping onto `top[x]` paints that one outline pixel the length of
-- the slope and the courses beat against it. The surface belongs on the
-- first PAINTED row instead: the same refusal to let the outline stand
-- as a face that the side faces already make below.
local surfaceTop = {}
for x = 0, W - 1 do
local y = top[x]
while y < roofRows and sp.inside[y * W + x]
and sp.col[y * W + x] == BLACK do
y = y + 1
end
if y < roofRows and sp.inside[y * W + x] then
surfaceTop[x] = y
else
surfaceTop[x] = top[x]
end
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
@@ -386,7 +407,7 @@ local function measure(sp, t)
-- 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,
return { top = top, surfaceTop = surfaceTop, ytop = ytop,
D = t.depthPx or ((t.depth or #t.tiles) * 8),
ground = ground,
recess = recess, interior = interior, shadeTexel = shadeTexel }
@@ -882,6 +903,7 @@ local function model(sp, pr, t)
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
local surfaceTop = pr.surfaceTop
-- 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
@@ -931,11 +953,12 @@ local function model(sp, pr, t)
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.
-- the surface itself. Lifting the row into the column's first
-- PAINTED row keeps the flank battens running down the slope
-- instead of falling off the silhouette -- and off its cap, which
-- is outline black and belongs to the rim, not to the surface.
local sy = roofSy[z]
if sy < top[x] then sy = top[x] end
if sy < surfaceTop[x] then sy = surfaceTop[x] end
return sy * W + x
end
-- The rim reproduces the eave the drawing itself paints under the
+83
View File
@@ -0,0 +1,83 @@
-- Driver: the two sloped-roof drawings in Pallet Town, shot at the orbit
-- rungs, to see what the roof surface wears down a TAPERED flank.
--
-- A tapered column's first drawn row is its silhouette cap, and the cap is
-- outline black. The depth map spends most of the roof's depth above that
-- cap, so a surface clamped onto it paints the outline the length of the
-- slope and the course cycle beats against it -- black teeth marching down
-- the hip. The surface belongs on the column's first PAINTED row instead.
--
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/roof_cap_shots.lua \
-- SHOT_DIR=.scratchpad/roofcap AB_TAG=before lovec.exe .
return function(game)
local U = dofile("tests/drivers/util.lua")
local Pipelines = require("src.render.Pipelines")
local ROOT = (os.getenv("SHOT_DIR") or "shots/roofcap")
.. "/" .. (os.getenv("AB_TAG") or "after")
local handle = game.mods.exports["DRAMATIC_SHAPE"]
if not (handle and handle.lib) then
print("[cap] DRAMATIC_SHAPE mod not loaded")
return
end
local V = handle.lib
local DayNight = V.require("DayNight")
local ChunkMesher = V.require("ChunkMesher")
local Voxel = V.require("VoxelState")
require("src.world.OverworldController").rollEncounter = function() return nil end
local TileRenderer = require("src.render.TileRenderer")
TileRenderer.tick = function() end
TileRenderer.animFrame = function() return 0 end
DayNight.setting:sync("day")
pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
local Zoom = require("src.render.Zoom")
pcall(function()
game.save.options.zoom = 1
Zoom.applyOptions(game.save.options)
end)
local function settle()
for _ = 1, 900 do
if ChunkMesher.pending() == 0 then break end
U.wait(1)
end
for _ = 1, 300 do
if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then break end
U.wait(1)
end
U.wait(30)
end
-- B07 (the gabled house: Red's and Blue's) and B31 (Oak's lab) are the
-- two tapered roof bands standing on one map -- 16 drawn rows and 32,
-- the two groups every other sloped building borrows its band table
-- from. Standing south of each puts the hip end across the frame.
local SCENES = {
{ map = "PALLET_TOWN", x = 5, y = 6, face = "up", label = "reds_house" },
{ map = "PALLET_TOWN", x = 13, y = 3, face = "up", label = "blues_house" },
{ map = "PALLET_TOWN", x = 12, y = 12, face = "up", label = "oaks_lab" },
}
local shots = 0
for _, s in ipairs(SCENES) do
for _, rung in ipairs({ 3, 5 }) do
U.teleport(game, s.map, s.x, s.y, s.face)
Pipelines.setLevel("voxel", rung)
Pipelines.setLevel("tiltshift", 0)
settle()
local path = ("%s/%s_v%d.png"):format(ROOT, s.label, rung)
game.capturePath = path
U.wait(8)
local f = io.open(path, "rb")
if f then f:close() shots = shots + 1
else print("[cap] capture missed: " .. path) end
end
end
print(("[cap] %d shots into %s"):format(shots, ROOT))
love.event.quit()
end
+56 -16
View File
@@ -1246,6 +1246,22 @@ def profile(sp, t):
wall_h = ground - t["roof_rows"]
ytop = wall_h - 1 + t["slab"]
# The row a column's roof SURFACE may sink to. `top[x]` is the
# silhouette cap -- the black the drawing closes its shape with -- and
# the depth map spends most of a tapered column's depth above it, so a
# clamp onto `top[x]` paints that one outline pixel across the slope
# and the courses beat against it. The surface belongs on the first
# PAINTED row instead, which is the same thing the side faces already
# do when the drawing's own pixel is the outline.
surface_top = []
for x in range(W):
y = top[x]
while (y < t["roof_rows"] and inside(x, y)
and sp["col"][y][x] == BLACK):
y += 1
surface_top.append(y if y < t["roof_rows"] and inside(x, y)
else top[x])
# Recesses: the panes the art seals behind a black frame. Non-black
# pixels of the facade split into components across the black outline;
# a component small enough to be a window or a doorway sinks one voxel.
@@ -1287,7 +1303,8 @@ def profile(sp, t):
# Depth is the PLOT. For a whole-drawing building that is the grid
# itself; `depth` (in tile rows) names it when the grid runs past the
# plot onto ground the drawing merely stands its legs on.
return dict(top=top, wall_h=wall_h, ytop=ytop, recess=recess,
return dict(top=top, surface_top=surface_top,
wall_h=wall_h, ytop=ytop, recess=recess,
inside=inside,
# `depth` names the plot in TILE ROWS, which is the right
# grain for a building. `depth_px` names it in voxels, for
@@ -1697,6 +1714,29 @@ def build_desk_set(sp, pr, t):
return vox
def roof_surface_row(pr, t):
"""Which drawn row the roof surface wears at column x, depth z.
The drawing looks at the roof from the north, so its rows ARE depth: the
rims map one row per voxel and the middle cycles a run whose period is
the course rhythm. A tapered column's band starts lower down the
drawing, and the row it wears is lifted to stay inside it."""
z0, z1 = 0, pr["D"] - 1 + t["front_eave"]
back, front = t["roof_back"], t["roof_front"]
c0, c1 = t["roof_cycle"]
rows, floor = t["roof_rows"], pr["surface_top"]
def depth_row(z):
df, db = z - z0, z1 - z # from the north / the south edge
if df < back:
return df # north rim: the drawing's top rows
if db < front:
return rows - 1 - db # south rim: fascia and eave course
return c0 + (df - c0) % (c1 - c0 + 1)
return (lambda x, z: max(depth_row(z), floor[x])), z0, z1
def build(sp, pr, t):
"""The voxel model. Order is load-bearing: walls, ledge, recesses, then
the roof solid overwrites what it intersects and the walls are trimmed
@@ -1775,17 +1815,7 @@ def build(sp, pr, t):
vox.pop((sx, pr["ground"] - 1 - sy, D - 1), None)
# ---- roof: flat top over the plateau, stepped diagonal ends
z0, z1 = 0, D - 1 + t["front_eave"]
back, front = t["roof_back"], t["roof_front"]
c0, c1 = t["roof_cycle"]
def roof_sy(z):
df, db = z - z0, z1 - z # from the north / the south edge
if df < back:
return df # north rim: the drawing's top rows
if db < front:
return t["roof_rows"] - 1 - db # south rim: fascia and eave course
return c0 + (df - c0) % (c1 - c0 + 1)
surface_row, z0, z1 = roof_surface_row(pr, t)
shade_px = {}
for sy in range(H):
@@ -1797,10 +1827,7 @@ def build(sp, pr, t):
tt = T(x)
for z in range(z0, z1 + 1):
outer = x == x0d or x == x1d or z == z0 or z == z1
# the slope's texture is the drawing's own: clamping into the
# column's first drawn row keeps flank battens running down the
# slope instead of falling off the silhouette
sy = max(roof_sy(z), pr["top"][x])
sy = surface_row(x, z)
for y in range(tt - slab + 1, tt + 1):
if y == tt and not outer:
put(x, y, z, x, sy)
@@ -2043,6 +2070,19 @@ def verify_roof(vox, pr, t):
# drawing's own corner rounding
assert all(ytop - v <= 1 for v in prof), "the flat roof is not level"
# The surface is surface, not outline. `top[x]` is the column's own
# silhouette cap -- the black the drawing closes the shape with, the
# same black measure() already refuses to let stand as a wall's side
# face -- so a surface that samples it promotes a boundary decoration
# into texture. A tapered column's cap sits well down the band, where
# the depth map spends most of the roof's depth above it.
surface_row, sz0, sz1 = roof_surface_row(pr, t)
for x in roofed[1:-1]:
for z in range(sz0 + 1, sz1):
assert surface_row(x, z) != top[x], \
f"roof surface wears the silhouette cap at {x},{z} " \
f"(row {top[x]})"
# every wall column carries roof over it -- and a column the roof never
# reaches carries nothing at all, rather than being silently trimmed away
over = set(roofed)