iterate version
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user