mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 07:10:51 +02:00
keep a sloped roof's surface off its own outline cap
The hip ends of the gabled house and Oak's lab come out as black teeth marching down the slope instead of battens running with it. The depth->row map is authored for columns the drawing paints edge to edge. A tapered column starts further down the band, and the surface was clamped into its first DRAWN row to stay inside the silhouette -- but a column's first drawn row is its silhouette CAP, and the cap is outline black. On a tapered column the map spends most of the roof's depth above that cap (seven of the gabled house's fifty depth voxels land on it outright), so the clamp painted one outline pixel the length of the slope and roofCycle beat against it: black punctuated by the real batten colour every 4 rows on the house, every 8 on the lab. Lift to the column's first PAINTED row instead -- the same refusal to let outline black stand as a face that measure() already makes for the side faces, which the roof band was never given. Geometry is untouched: voxel, shell and recess counts are identical for all 50 templates. verify_roof now asserts no roof surface samples its column's cap, and tests/roof_cap_shots.lua shoots the two Pallet Town drawings A/B. The version bump and CHANGELOG entry are left out deliberately, so this does not collide with the release flow.
This commit is contained in:
+28
-5
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user