iterate version

This commit is contained in:
DramaticShape
2026-08-01 16:41:03 -04:00
parent 752653e243
commit 47363b8d23
10 changed files with 644 additions and 21 deletions
+61
View File
@@ -1,5 +1,66 @@
# Changelog
## 1.4.1
### Added
- **Furniture through the building pipeline.** The band-table voxelizer
that models whole buildings from their own drawings (lib/Buildings.lua)
now reads interior furniture too, and the first four drawings are in:
- **F01, the starter-ball table in Oak's lab** -- the tabletop's 16
drawn rows lay flat over a 16px plot (1:1, the first template that
never cycles), the black/#555/black edge band folds into the slab's
own rim, and the base extrudes with its corner feet. Six voxels
tall, exactly the drawn elevation.
- **F03, the empty north table beside it** -- the same band table on a
grid two tiles narrower.
- **F02, the lab's computer desk** -- the first DESK-SET template: the
drawing segments into PARTS, each classified by the surface it
depicts. The monitor and the computer tower stand upright on the
desk wearing their own drawn tops as lids; the keyboards and the
mouse lie flat in front of them; the sheet of paper on the right
lies flat across the desk. Flat parts keep the drawing's own rule --
drawn row IS depth row, the same 1:1 the tabletop is drawn with --
so an object's height on the drawing is its position on the desk.
The Hall of Fame's recording machine is this drawing tile for tile
on the GYM atlas, and models identically for free.
- **F04, the Center PC** -- the desk-set read again: a Mac-style unit
with its screen and drive slot in relief, standing at the back of a
low white-topped desk with its keyboard lying at the front edge.
Eleven Pokemon Centers, plus the Indigo Plateau lobby, whose MART
tileset shares the atlas.
Two measurements had to stop being assumptions for furniture to fit
the pipeline: the GROUND LINE is now read off the drawing (a building
ends on the black threshold row it stands on; a table's legs stop two
rows short of theirs, and extruding against the grid floated them in
the air), and a template may name its PLOT (`depth`) when the matched
grid runs past it onto the walkable floor the legs merely stand on.
Both are identities for every existing building.
- **The Center couch has a backrest.** The couch is drawn from above --
back-and-arm strip down the west side, cushions and seams on the east
-- and rendered as one seat-high box. The new `backrest` class raises
the drawn back strip to 12px over the 8px seat, in every Center and
the Celadon Hotel. The man sitting on it keeps his seat: the figure
anchor now scans under his card for the tallest authored upright (his
cushion) instead of reading the corner tile, which is the backrest
now.
### Changed
- **Sprites ride at the height the art actually stands.** Class heights
can now be overridden per tileset (a tileset entry's `heights`), and
DOJO's lab tables use it: they are drawn 6px tall, not the default
table's 12, so the starter balls sit exactly on the modelled tabletop
-- and the volume-built north tables drop to the same height, keeping
every table in the room level.
- The Center PC's old rendering -- a 12px table box with the unit as a
flat standee on it -- retires wherever the F04 template stamps; the
pins stay only as the degradation path when the shape profile is
absent.
## 1.4.0
### Added
+76 -6
View File
@@ -110,6 +110,9 @@ return {
bed = 7,
stool = 8,
counter = 8,
-- the raised back band of low seating (the Center couch's back and
-- arm strip): half again the 8px seat it rises over
backrest = 12,
table = 12,
desk = 24,
prop = 16,
@@ -705,14 +708,25 @@ return {
-- one clean band, so the drawn front panel stands up and the
-- counter top stays on top; at 12 they read as wall stubs
counter = { 8, 10, 24, 25, 56, 90, 91,
-- and the lounge couch with the man sitting on it.
-- Same half-cell box: its bottom row (42/43, the front
-- base) stands up as the couch's front, and the three
-- rows above it -- cushion (38/39) and the man
-- (36/37 head, 52/53 face) -- ride the top face in
-- and the lounge couch's SEAT column with the man
-- sitting on it. Same half-cell box: its bottom row
-- (43, the front base) stands up as the couch's
-- front, and the rows above it -- cushion (39) and
-- the man (37 head, 53 face) -- ride the top face in
-- drawn order, each exactly once. See the note below
-- on why he cannot be stood upright.
36, 37, 38, 39, 42, 43, 52, 53 },
37, 39, 43, 53 },
-- The couch's WEST tile column: the drawing's left strip is the
-- couch's back and arm running north-south (the seat's cushions
-- and seams fill the east column), so it rises over the 8px seat
-- the way a couch back does instead of lying flush in the same
-- box. Per-tile granularity puts the drawn ~6px strip plus a
-- 2px sliver of cushion on the raised band -- invisible at tile
-- scale, and the alternative is no backrest at all. The figure
-- anchor scans for the tallest authored UPRIGHT under the man
-- (see Structures.buildFigure), so he keeps sitting at seat
-- height beside it.
backrest = { 36, 38, 42, 52 },
-- standing per-pixel props, black-outline segmented: the healing
-- machines' screen tops (58/59/74/75, drawn above the pinned
-- bodies so they stand ON them) and the PC (66/70/82/86), which
@@ -3320,6 +3334,62 @@ return {
},
},
POKECENTER = {
-- F04: the PC in every Center's northeast corner (11
-- placements; the Indigo Plateau lobby's twin is registered
-- under MART below). The lab desk-set read again: a Mac-style
-- unit drawn face-on -- white top band (rows 0-3), bezel, screen
-- and drive slot (4-14) -- standing at the back of a low desk
-- whose front face is rows 20-23; the drawn top around the unit
-- is WHITE, which is what `lid` carries. The keyboard rows 17-19
-- are drawn below the desk's 16px top span, so the flat part's
-- `z` puts it at the desk's front edge. The old billboard pins
-- for these tiles (66/70/82/86, desk 9/88) stay as the
-- degradation path -- the claim neutralizes them wherever this
-- template stamps.
{
id = "center_pc",
tiles = {
{ 66, 70 },
{ 82, 86 },
{ 9, 88 },
},
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
slab = 0, frontEave = 0, ledge = nil, depth = 2,
desk = { fascia = { 20, 21 }, base = { 22, 23 }, lid = "white" },
parts = {
{ kind = "upright", x = { 2, 13 }, top = { 0, 3 },
facade = { 4, 14 }, depth = 6 }, -- the unit
{ kind = "flat", x = { 2, 13 }, rows = { 17, 19 },
z = 13 }, -- keyboard
},
},
},
MART = {
-- F04 again: the Indigo Plateau lobby's PC (cell 15,7) -- the
-- MART tileset shares the POKECENTER atlas, so this is the same
-- drawing tile for tile. Same part table as the POKECENTER entry
-- above.
{
id = "center_pc",
tiles = {
{ 66, 70 },
{ 82, 86 },
{ 9, 88 },
},
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
slab = 0, frontEave = 0, ledge = nil, depth = 2,
desk = { fascia = { 20, 21 }, base = { 22, 23 }, lid = "white" },
parts = {
{ kind = "upright", x = { 2, 13 }, top = { 0, 3 },
facade = { 4, 14 }, depth = 6 }, -- the unit
{ kind = "flat", x = { 2, 13 }, rows = { 17, 19 },
z = 13 }, -- keyboard
},
},
},
GYM = {
-- F02 again: the Hall of Fame's recording machine is the lab's
-- computer desk drawing, tile for tile, on the GYM atlas (one
+244
View File
@@ -0,0 +1,244 @@
-- Voxel world mode: anti-aliasing, by supersampling.
--
-- Everything else in this mod is flat art blitted at whole pixels; this one
-- pass is real geometry seen through a perspective camera, and a polygon
-- edge that lands at an angle across the pixel grid is the one place in the
-- game where a hard stair-step is not a stylistic choice. A roof ridge, a
-- ledge lip, a tree's silhouette against the sky and the leaning card of a
-- character are all cut by an edge that has no reason to line up with
-- anything, and at the shallow rungs -- where the diorama reads most like a
-- photograph of a model -- they crawl as the camera drifts.
--
-- SUPERSAMPLING, not MSAA and not a filter over the finished frame, for two
-- reasons that both come out of what the pass already is:
--
-- MSAA would take the water with it. The reflections read the frame's own
-- DEPTH buffer as a texture (Voxel3D.beginWater), and a multisampled depth
-- attachment is not a thing a fragment shader in this dialect can sample.
-- The row would have quietly switched the other row off.
--
-- An edge filter (FXAA and its relatives) works from the finished colour
-- alone, and would be GUESSING where the edges are out of one sample per
-- pixel -- inventing detail it never rendered, and unable to tell a
-- geometry edge from the boundary between two texels of a tileset.
--
-- Rendering the pass larger and folding it back down has neither problem:
-- the depth buffer stays an ordinary texture, every pass in the frame keeps
-- working in the canvas it was handed, and the fold is an average of samples
-- that were each rendered honestly. It antialiases everything at once --
-- geometry, the alpha-cut outline of a sprite card, the wireframe, the
-- water's ray march -- because none of them know it is happening.
--
-- Be clear about what "everything" means: the artwork softens too. A tileset
-- texel out here is not a screen pixel, it is a quad in a perspective view,
-- and its boundary crosses the pixel grid at the same arbitrary angle a roof
-- ridge does -- so the fold averages across it exactly as it averages across
-- the ridge. That is what an honest extra sample says about that pixel, and
-- it is also the trade the row IS: the diorama comes out smoother, not
-- sharper. Which is why this is a row and not something that is simply on.
--
-- What it costs is pixels, which is the whole of why this is a row and not
-- something that is simply on: 2X is half again as many in each direction,
-- 4X is twice, and the scene pass is the most expensive thing in the frame.
-- the mod namespace (see main.lua): V.require loads a sibling module
local V = ...
local ModSetting = V.require("ModSetting")
local AntiAlias = {}
-- the key under options.modOptions.DRAMATIC_SHAPE, shared by the row in
-- OPTIONS and the mod manager's own settings page for this mod
AntiAlias.KEY = "aa"
AntiAlias.LABEL = "AA"
-- The ladder is SAMPLES PER DISPLAY PIXEL, which is how an AA setting reads
-- everywhere else, and the canvas scale each rung costs is its square root:
-- 2 samples is a canvas 1.41x wider and taller, 4 is one exactly twice the
-- size. OFF is the default -- this is a cost knob, and a mod should not
-- quietly spend four times the fill rate of the machine it lands on.
AntiAlias.setting = ModSetting.new(AntiAlias.KEY, AntiAlias.LABEL,
{ 0, 2, 4 }, { "OFF", "2X", "4X" })
-- The scale the pass currently open was actually expanded by (see expand).
-- 1 while there is no supersampling in force, which is also what every
-- reader gets on a frame that never opened a pass at all.
local live = 1
function AntiAlias.samples()
return tonumber(AntiAlias.setting:get()) or 0
end
-- What the row ASKS for. The scale in force is `factor()`, which is this
-- clamped to what the driver will actually allocate.
local function wanted()
local n = AntiAlias.samples()
if n <= 1 then return 1 end
return math.sqrt(n)
end
-- The biggest canvas this driver admits to, or nil where it will not say.
-- A 4K window at 4X asks for 7680 across, which is past the limit on plenty
-- of hardware and every phone -- and a refused canvas is not a softer
-- diorama, it is beginScene returning false and the whole mode falling back
-- to the flat 2D path.
local function textureLimit()
if not (love.graphics and love.graphics.getSystemLimits) then return nil end
local ok, limits = pcall(love.graphics.getSystemLimits)
return (ok and limits and limits.texturesize) or nil
end
-- The size to render `w` x `h` display pixels at, and the size everything
-- inside the pass then measures itself in.
--
-- Also where `live` is set, which is why this must be called once per pass
-- immediately before beginScene: the wireframe's line width and the FX
-- overlay's sprite scale are both quoted in DISPLAY pixels and have to be
-- multiplied up into canvas ones, and the honest multiplier is the one this
-- returned rather than the one the row asked for.
function AntiAlias.expand(w, h)
local s = wanted()
local max = textureLimit()
if max and max > 0 then
-- clamped rather than abandoned: a window too big for 4X can usually
-- still carry some of it, and half a rung of smoothing is worth more
-- than a row that silently does nothing at that size
s = math.min(s, max / math.max(1, w), max / math.max(1, h))
end
if not (s > 1.01) then
live = 1
return w, h
end
local ew, eh = math.floor(w * s + 0.5), math.floor(h * s + 0.5)
live = ew / math.max(1, w)
return ew, eh
end
-- The scale the open pass was expanded by; 1 when it was not.
function AntiAlias.factor()
return live
end
-- ------- the fold
--
-- One target per pass (the free-roam world and the battle's arena are alive
-- at different moments but reallocating on every battle entry and exit is
-- what the scene canvas's own slots exist to avoid), reallocated only when
-- that pass's DISPLAY size changes -- a window resize, or the row itself
-- moving, which changes the source and not this.
local targets = {}
local function targetFor(slot, w, h)
local t = targets[slot]
if not (t and t.w == w and t.h == h) then
local ok, c = pcall(love.graphics.newCanvas, w, h)
if not (ok and c) then return nil end
-- nearest, like the canvas it stands in for: this one is composited a
-- canvas pixel to a display pixel, and the smoothing has already happened
pcall(c.setFilter, c, "nearest", "nearest")
if t and t.canvas and t.canvas.release then pcall(t.canvas.release, t.canvas) end
t = { canvas = c, w = w, h = h }
targets[slot] = t
end
return t.canvas
end
-- The box filter, and the whole of why it is a shader rather than a scaled
-- draw with linear filtering on.
--
-- The void this pass renders into is cleared to a TRANSPARENT BLACK, and at
-- the rungs below FULL a good deal of the frame is still that. Averaging a
-- straight-alpha edge against it drags the result toward black as well as
-- toward transparent, and then the engine's own composite multiplies by that
-- alpha a second time -- so every silhouette against the void would come out
-- ringed with a dark fringe, which is exactly the artefact the row is here to
-- remove.
--
-- So the taps are premultiplied before they are averaged and divided back out
-- after, which is the arithmetic that makes an edge pixel mean "half covered
-- by this colour" instead of "covered by half of this colour".
--
-- Four taps, half a source texel from the destination centre. At 4X those
-- land dead on the four texel centres the destination pixel covers, so it is
-- an exact 2x2 box; at 2X the source grid does not divide, and the bilinear
-- fetch under each tap widens the box a little rather than missing samples.
local SHADER = [[
uniform vec2 tap; // half a SOURCE texel, in uv
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {
vec4 a = Texel(tex, tc + vec2(-tap.x, -tap.y));
vec4 b = Texel(tex, tc + vec2( tap.x, -tap.y));
vec4 c = Texel(tex, tc + vec2(-tap.x, tap.y));
vec4 d = Texel(tex, tc + vec2( tap.x, tap.y));
float al = (a.a + b.a + c.a + d.a) * 0.25;
if (al <= 0.0) return vec4(0.0);
vec3 sum = a.rgb * a.a + b.rgb * b.a + c.rgb * c.a + d.rgb * d.a;
return vec4(sum * 0.25 / al, al) * color;
}
]]
local shader = nil -- nil = untried, false = unavailable
local function getShader()
if shader == nil then
local ok, sh = pcall(love.graphics.newShader, SHADER)
shader = (ok and sh) or false
end
return shader or nil
end
-- Fold `canvas` down to `w` x `h` and hand back the result.
--
-- Returns the input untouched when there is nothing to fold -- the row is
-- off, or the canvas already IS that size -- so a caller can run it
-- unconditionally, and so can a headless test run. A target that would not
-- allocate is the same answer: the pass is lost either way if this hands back
-- something the wrong size, so it hands back the input and the frame draws at
-- the size it was rendered.
function AntiAlias.resolve(canvas, w, h, slot)
if not canvas then return canvas end
local ok, cw, ch = pcall(canvas.getDimensions, canvas)
if not ok or (cw == w and ch == h) then return canvas end
local target = targetFor(slot or "world", w, h)
if not target then return canvas end
local sh = getShader()
local prevBlend, prevAlpha = love.graphics.getBlendMode()
-- the scene canvas filters nearest for its usual 1:1 blit; the taps want
-- linear, put back below so every other pass finds what it expects
pcall(canvas.setFilter, canvas, "linear", "linear")
love.graphics.setColor(1, 1, 1, 1)
-- replace, not alpha-blend: this is an image-processing copy, and the alpha
-- the shader worked out has to land as itself rather than be composited
-- against whatever the target held
love.graphics.setBlendMode("replace", "premultiplied")
if sh then
love.graphics.setShader(sh)
pcall(sh.send, sh, "tap", { 0.5 / cw, 0.5 / ch })
end
local drew = pcall(function()
love.graphics.setCanvas(target)
love.graphics.clear(0, 0, 0, 0)
love.graphics.draw(canvas, 0, 0, 0, w / cw, h / ch)
end)
love.graphics.setCanvas()
love.graphics.setShader()
love.graphics.setBlendMode(prevBlend or "alpha", prevAlpha)
pcall(canvas.setFilter, canvas, "nearest", "nearest")
return drew and target or canvas
end
-- Drop the GPU objects (window resize, hot reload).
function AntiAlias.invalidate()
for slot, t in pairs(targets) do
if t.canvas and t.canvas.release then pcall(t.canvas.release, t.canvas) end
targets[slot] = nil
end
end
function AntiAlias.row()
return AntiAlias.setting:row()
end
return AntiAlias
+11 -4
View File
@@ -378,9 +378,10 @@ local function deskSetModel(sp, pr, t)
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 = GREY
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
@@ -395,10 +396,16 @@ local function deskSetModel(sp, pr, t)
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
-- 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, sy, sy * W + sx) end
if inside[sy * W + sx] then put(sx, plane, z, sy * W + sx) end
end
end
end
+13 -5
View File
@@ -2253,14 +2253,22 @@ local function buildFigure(S, map, fig, tx, ty, perRow)
-- He stands ON the furniture he was drawn into -- the same lift a pinned
-- prop above a pinned box takes (see buildObject), and gated the same
-- way: a thing set down on furniture occupies a BLOCKED cell, while a
-- seat you merely walk up to is in a walkable one.
-- seat you merely walk up to is in a walkable one. The row under his
-- card is SCANNED for the tallest authored upright rather than read at
-- its west corner: the corner tile can be furniture that is not his
-- seat (the couch's raised backrest column stands there, `top` art and
-- taller than the cushion he actually sits on).
local baseY = 0
local bs = S.shapeAt[keyOf(tx, ty + fig.h)]
local blocked = not map:isWalkableCell(math.floor(tx / 2),
math.floor((ty + fig.h - 1) / 2))
if blocked and bs and bs.authored and bs.art == "upright"
and (bs.h or 0) > 0 then
baseY = bs.h
if blocked then
for dx = 0, fig.w - 1 do
local bs = S.shapeAt[keyOf(tx + dx, ty + fig.h)]
if bs and bs.authored and bs.art == "upright"
and (bs.h or 0) > baseY then
baseY = bs.h
end
end
end
local atlasW = map.tileset.imageWidth or 128
+7
View File
@@ -83,6 +83,10 @@ local FALLBACK_HEIGHTS = {
bed = 7,
stool = 8,
counter = 8,
-- the raised back band of low seating: the Center couch's west strip
-- is drawn from above like the rest of the couch, but depicts the
-- back and arm rising over the 8px seat
backrest = 12,
table = 12,
desk = 24,
prop = 16,
@@ -145,6 +149,9 @@ local ART = {
-- profile archetype Structures builds real steps for -- rising flights
-- for stairs leading up, sunken stairwells for stairs leading down
bed = "top",
-- a backrest's art is the couch seen from above, so like the bed it
-- rides the top face of its taller box
backrest = "top",
stool = "billboard",
-- half-cell furniture: a service counter, a low couch. One 8px band,
-- so exactly the drawing's bottom row stands up as the front and
+1 -1
View File
@@ -890,7 +890,7 @@ mod.hooks:wrap("world.tod", function(next, tod, ctx)
return DayNight.tod()
end)
mod.exports.version = "1.4.0"
mod.exports.version = "1.4.1"
-- exposed so a companion mod can pin its own tiles' shapes or read the
-- camera without reaching into this mod's file layout
mod.exports.lib = V
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "DRAMATIC_SHAPE",
"name": "Dramatic Shape Voxel Mod",
"version": "1.4.0",
"version": "1.4.1",
"api": 2,
"entry": "main.lua",
"profile": "content",
+195
View File
@@ -0,0 +1,195 @@
-- Driver: one scene, once per rung of the AA row.
--
-- The AA row is the one setting in this mod whose whole effect is a pixel
-- wide, so it is also the one that cannot be judged from a description. This
-- renders the SAME frame at each rung and writes one PNG per rung; put two of
-- them side by side, magnified, and the row is either doing something or it
-- is not.
--
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/aa_shots.lua \
-- SHOT_DIR=<dir> lovec.exe .
--
-- knobs (env):
-- SHOT_DIR output directory (created if missing) (default "shots/aa")
-- AA_MAP map id (default VIRIDIAN_CITY)
-- AA_SPOT "x,y[,facing]" (default 20,26,up)
-- AA_RUNG the voxel camera rung (default 5, the 75 one)
--
-- The scene defaults to a town at the LOW camera on purpose: roof ridges, the
-- diagonal of a fence and a tree's silhouette against the sky are the edges
-- that stair-step, and 75 degrees is the rung that puts the most of them at an
-- angle to the pixel grid.
--
-- Determinism matters here for the same reason it does in voxel_shots_ab: the
-- three shots differ ONLY by the row under test, or comparing them means
-- nothing. The clock is pinned, the animated tile slots are frozen, the
-- townsfolk are stopped where they stand, and the tilt-shift is held at zero
-- (a gaussian over the frame would smear away the very edges being looked at).
--
-- Nothing here writes the player's options: the row is moved with
-- ModSetting:sync, which moves the cached index and persists nothing.
return function(game)
local U = dofile("tests/drivers/util.lua")
local Pipelines = require("src.render.Pipelines")
local OverworldState = require("src.world.OverworldController")
local ROOT = os.getenv("SHOT_DIR") or "shots/aa"
local handle = game.mods.exports["DRAMATIC_SHAPE"]
if not (handle and handle.lib) then
print("[aa] DRAMATIC_SHAPE mod not loaded -- nothing to shoot")
return
end
local V = handle.lib
local DayNight = V.require("DayNight")
local AntiAlias = V.require("AntiAlias")
local ChunkMesher = V.require("ChunkMesher")
local Voxel = V.require("VoxelState")
local ShadowMap = V.require("ShadowMap")
local MAP = os.getenv("AA_MAP") or "VIRIDIAN_CITY"
local SPOT = os.getenv("AA_SPOT") or "20,26,up"
local RUNG = math.floor(tonumber(os.getenv("AA_RUNG")) or 5)
local sx, sy, sf = SPOT:match("^(%-?%d+),%s*(%-?%d+),?%s*(%a*)$")
sx, sy = tonumber(sx) or 20, tonumber(sy) or 26
if sf == "" then sf = "up" end
OverworldState.rollEncounter = function() return nil end
local NPC = require("src.world.NPC")
if not NPC.dramaticShapeAaFreeze then
local inner = NPC.update
function NPC:update(...)
self.frozen = true
return inner(self, ...)
end
NPC.dramaticShapeAaFreeze = true
end
pcall(love.math.setRandomSeed, 20260801)
local TileRenderer = require("src.render.TileRenderer")
TileRenderer.tick = function() end
TileRenderer.animFrame = function() return 0 end
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 cameraStill()
local o = game.overworld
local c = o and o.camera
if not c then return true end
local lx, ly, held = nil, nil, 0
for _ = 1, 300 do
if c.x == lx and c.y == ly then
held = held + 1
if held >= 10 then return true end
else
held = 0
lx, ly = c.x, c.y
end
U.wait(1)
end
return false
end
-- The camera PITCH, which is the one that caught this driver out. The tween
-- runs on wall-clock dt and Voxel.t reaching 1 is not the same instant the
-- angle stops moving, so the first shot of a run came out at 67 degrees
-- while the two after it were at 75 -- three frames that differ by the
-- camera, in a comparison whose entire subject is a pixel.
local function angleStill()
local last, held = nil, 0
for _ = 1, 600 do
if Voxel.angle == last then
held = held + 1
if held >= 10 then return true end
else
held = 0
last = Voxel.angle
end
U.wait(1)
end
return false
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
angleStill()
cameraStill()
-- the sun map is only redrawn when its inputs move, and the AA row is not
-- one of them -- so force one pass at the settled camera rather than
-- comparing a frame against a map fitted a few hundredths of a pixel ago
if ShadowMap.forget then ShadowMap.forget() end
U.wait(20)
end
DayNight.setting:sync("day")
U.teleport(game, MAP, sx, sy, sf)
Pipelines.setLevel("voxel", RUNG)
Pipelines.setLevel("tiltshift", 0)
-- Warm up before the FIRST shot, not just between them.
--
-- Neighbour maps are requested from inside the render itself
-- (VoxelScene.prefetch), so an empty build queue right after a teleport
-- means "nothing has been asked for yet", not "everything is here". The
-- first capture of a run came out with the map beyond Viridian missing --
-- a whole tree line absent from one frame of a three-way comparison, which
-- looks exactly like the row under test doing something enormous. Settling
-- twice lets the first render request the neighbourhood and the second
-- drain it.
settle()
settle()
local shots, missed = 0, 0
for _, samples in ipairs({ 0, 2, 4 }) do
AntiAlias.setting:sync(samples)
settle()
-- AA_TRACE=1 prints the state each shot was taken in. When two shots of a
-- run disagree by more than the row could account for, this is what says
-- which input moved -- it is how the camera-tween and the neighbour-mesh
-- settles above were both found.
if os.getenv("AA_TRACE") == "1" then
local Voxel3D = V.require("Voxel3D")
local o = game.overworld
local cw, chh = Voxel3D.size()
print(("[aa] trace samples=%d angle=%.6f fov=%.6f cell=%.4f canvas=%dx%d cam=(%.3f,%.3f) eye=(%.2f,%.2f,%.2f) factor=%.4f")
:format(samples, Voxel.angle or -1, Voxel3D.fovY or -1,
Voxel3D.cell or -1, cw or 0, chh or 0,
o and o.camera and o.camera.x or -1,
o and o.camera and o.camera.y or -1,
(Voxel3D.eye or {})[1] or 0, (Voxel3D.eye or {})[2] or 0,
(Voxel3D.eye or {})[3] or 0, AntiAlias.factor()))
end
local path = ("%s/aa_%d.png"):format(ROOT, samples)
game.capturePath = path
U.wait(6)
local f = io.open(path, "rb")
if f then
f:close()
shots = shots + 1
print(("[aa] %s samples=%d"):format(path, samples))
else
missed = missed + 1
print("[aa] capture did not reach disk: " .. path)
end
end
-- left where it was found, so a run cannot leak a rung into the next one
AntiAlias.setting:sync(0)
print(("[aa] %d shots into %s (%d failed to reach disk)")
:format(shots, ROOT, missed))
end
+35 -4
View File
@@ -614,6 +614,31 @@ TEMPLATES = {
roof_rows=19, roof_back=16, roof_front=0, roof_cycle=(2, 13),
slab=3, front_eave=0, ledge=None, tileset="gym", depth=2,
),
# F04: the Pokemon Center's PC -- every Center's northeast corner
# (11 placements) plus the Indigo Plateau lobby, whose MART tileset
# shares this atlas. The lab desk-set read again: a
# Mac-style unit drawn face-on -- white top band (rows 0-3), bezel,
# screen and drive slot (4-14) -- standing at the back of a low
# desk whose front face is rows 20-23 and whose drawn top (the
# white sliver of row 15 and the margins beside the unit) names the
# lid shade. The keyboard rows 17-19 lie BELOW the desk's 16px top
# span, so the flat part carries an authored z origin: it lies at
# the desk's front edge, in front of the unit.
"center_pc": dict(
tiles=[
[66, 70],
[82, 86],
[9, 88],
],
roof_rows=0, roof_back=0, roof_front=0, roof_cycle=(0, 0),
slab=0, front_eave=0, ledge=None, tileset="pokecenter", depth=2,
desk=dict(fascia=(20, 21), base=(22, 23), lid="white"),
parts=[
dict(kind="upright", x=(2, 13), top=(0, 3), facade=(4, 14),
depth=6), # the unit
dict(kind="flat", x=(2, 13), rows=(17, 19), z=13), # keyboard
],
),
# B23: the Victory Road entrance on Route 23: a rock face with two
# barred doors. The roof band is the pale cliff top seen from above.
"victory_road_gate": dict(
@@ -815,6 +840,7 @@ def build_desk_set(sp, pr, t):
for sx in range(W):
for z in range(D):
put(sx, y, z, sx, sy)
field = WHITE if t["desk"].get("lid") == "white" else GREY
for sx in range(W):
for z in range(D):
if sx in (0, W - 1) or z in (0, D - 1):
@@ -822,7 +848,7 @@ def build_desk_set(sp, pr, t):
elif sx == 1 or z == 1:
shade = WHITE
else:
shade = GREY
shade = field
px = shade_px.get(shade) or shade_px[BLACK]
put(sx, plane - 1, z, px[0], px[1])
@@ -831,11 +857,15 @@ def build_desk_set(sp, pr, t):
if p["kind"] == "flat":
r0, r1 = p["rows"]
for sy in range(r0, r1 + 1):
if not 0 <= sy < D:
# 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)
z = p.get("z", r0) + (sy - r0)
if not 0 <= z < D:
continue
for sx in range(x0, x1 + 1):
if inside(sx, sy):
put(sx, plane, sy, sx, sy)
put(sx, plane, z, sx, sy)
continue
tr0, tr1 = p["top"]
fr0, fr1 = p["facade"]
@@ -1048,8 +1078,9 @@ def verify_desk_set(vox, pr, t):
x0, x1 = p["x"]
if p["kind"] == "flat":
r0, r1 = p["rows"]
z0 = p.get("z", r0)
for x in range(x0, x1 + 1):
for z in range(max(r0, 0), min(r1, D - 1) + 1):
for z in range(max(z0, 0), min(z0 + r1 - r0, D - 1) + 1):
tops[(x, z)] = max(tops.get((x, z), 0), plane)
else:
fr0, fr1 = p["facade"]