mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 15:20:51 +02:00
updates to oak's lab
This commit is contained in:
@@ -116,6 +116,72 @@
|
||||
ever lands half-way between two pixels and changing a wavelength moves the
|
||||
speed with it.
|
||||
|
||||
- **AA, a new options row: OFF / 2X / 4X.** Everything else in this game is
|
||||
flat art blitted at whole pixels. This mode's world 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 where a hard stair-step is not a
|
||||
stylistic choice -- a roof ridge, a ledge lip, a tree's silhouette against
|
||||
the sky, the leaning card of a character. At the shallow rungs, where the
|
||||
diorama reads most like a photograph of a model, they crawl as the camera
|
||||
drifts.
|
||||
|
||||
The row is SUPERSAMPLING: the whole pass renders into a canvas larger than
|
||||
the window and is folded back down at the end. The ladder is samples per
|
||||
display pixel, so 2X is a canvas root-two wider and taller and 4X one
|
||||
exactly twice the size -- an honest 2x2 box.
|
||||
|
||||
Two alternatives were tried against what this pass already is, and both
|
||||
lost:
|
||||
|
||||
- **MSAA** would have taken the water with it. The reflections read the
|
||||
frame's own depth buffer as a texture, and a multisampled depth
|
||||
attachment is not something a fragment shader in this dialect can sample.
|
||||
The row would have quietly switched the WATER row off.
|
||||
|
||||
- **An edge filter** (FXAA and its relatives) works from the finished
|
||||
colour alone, so it 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 larger has neither problem, and nothing in the frame had to be
|
||||
taught about it: every pass already measures itself in the canvas it was
|
||||
handed, so the sky's dither, the water's ray march, the shadow lookups and
|
||||
the camera itself come out the same picture at a higher sample rate. It
|
||||
antialiases the geometry, the alpha-cut outline of a sprite card, the
|
||||
wireframe and the reflections at once, because none of them know it is
|
||||
happening.
|
||||
|
||||
And it softens the ARTWORK with them, which is worth saying plainly. 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 it is a row and not something that is simply on.
|
||||
|
||||
Two things are quoted in DISPLAY pixels rather than canvas ones and are
|
||||
multiplied up to match: the voxel wireframe's line width -- left alone it
|
||||
would fold down to half a line, so turning the smoothing up would appear to
|
||||
fade the grid out -- and the scale the overworld's FX closures draw at.
|
||||
|
||||
The fold is a shader rather than a scaled draw, because the void this pass
|
||||
renders into is a transparent BLACK: averaging a straight-alpha edge against
|
||||
it drags the colour toward black as well as toward transparent, and the
|
||||
engine's composite then multiplies by that alpha a second time. Every
|
||||
silhouette against the sky would have come out ringed with a dark fringe --
|
||||
the exact artefact the row exists to remove. So the taps are premultiplied
|
||||
before they are averaged and divided back out after.
|
||||
|
||||
The staged battle gets it too, on its own canvas: the arena is folded back
|
||||
to the window's pixel size before the depth-of-field pass and the HUDs go
|
||||
on, so the world is smoothed and the pics, panels and text box stay the
|
||||
chunky GB art they are.
|
||||
|
||||
OFF by default, and **FULL neither sets it nor takes the row away** -- it
|
||||
is the one row that is not a knob on the look but on what the look COSTS,
|
||||
and only the player knows what their machine can carry. No hotkey, for the
|
||||
same reason: it is set once, not flicked while walking.
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ menu.
|
||||
| `8`, or the **3D-BTL** options row | ON / OFF — fight on the map instead of on a white field |
|
||||
| `9`, or the **WATER** options row | FULL / SKY / OFF — waves and reflections on water. **SKY** gives the surface its pixel-tall wave columns and puts the sky, the sun, the moon and the cast in them; **FULL** adds a screen-space ray march that also reflects the shoreline, the trees and the buildings standing behind it |
|
||||
| the **BACK SPRITES** options row | OFF / ON — keep your own Pokémon on the battle menu, seen from behind in its classic slot, instead of standing it on the map; the foe is still out there. Only on the menu while **3D-BTL** is on, because it decides nothing without it |
|
||||
| the **AA** options row | OFF / 2X / 4X — smooth the stair-stepped edges of the 3D world by rendering the diorama larger than the window and folding it back down. The ladder is samples per display pixel: 2X is a canvas root-two wider and taller, 4X one exactly twice the size. Every edge in the projected picture softens with the silhouettes — the tileset's own texels are quads in a perspective view and cross the pixel grid at the same arbitrary angles — so the diorama reads smoother rather than sharper. The most expensive row in the mod, so it is OFF by default and **FULL** leaves it alone |
|
||||
| the **DAYTIME** options row | SYNC / DAY / NIGHT / DUSK / DAWN / CYCLE — what time it is outdoors, on the diorama *and* on the flat 2D world; held at SYNC (and off the menu) while VOXEL is FULL |
|
||||
|
||||
**3D-BTL** is on by default and is independent of **VOXEL**: battles draw
|
||||
|
||||
@@ -579,6 +579,12 @@ return {
|
||||
-- auto-extracting into standing prisms, and the ball/Pokedex
|
||||
-- sprites ride the table's authored height.
|
||||
table = { 41, 42, 57, 59, 78, 79 },
|
||||
-- These tables are drawn 6px tall (3px slab edge over 3px base --
|
||||
-- see the lab_table entry under `buildings`), not the default
|
||||
-- table's 12: the override keeps the volume-built north tables
|
||||
-- and the band-built starter table one height, and stands the
|
||||
-- ball/Pokedex sprites exactly on the top face of both.
|
||||
heights = { table = 6 },
|
||||
},
|
||||
|
||||
-- Red's room and the Copycat's room (one tileset). The detector reads
|
||||
@@ -3231,5 +3237,89 @@ return {
|
||||
slab = 4, frontEave = 4, ledge = nil,
|
||||
},
|
||||
},
|
||||
|
||||
DOJO = {
|
||||
-- F01: the starter-ball table in Oak's lab (one placement in the
|
||||
-- game: OAKS_LAB cell 6,3) -- the first FURNITURE through the
|
||||
-- band pipeline, and the first drawing whose plot is smaller than
|
||||
-- its grid. Its 24 rows read: 0-15 the tabletop seen from above
|
||||
-- (black rim, white highlight course, grey field); 16-18 the top
|
||||
-- slab's own front edge, black/#555/black -- which is exactly
|
||||
-- what the rim treatment paints, so slab = 3 and those rows fold
|
||||
-- into the roof band instead of extruding under it; 19-21 the
|
||||
-- base band, corner feet and the inset dark panel between them.
|
||||
--
|
||||
-- The legs stand on open FLOOR: the measured ground line lands
|
||||
-- two rows short of the grid (see Buildings measure), and `depth`
|
||||
-- keeps the plot to the blocked cell row -- the grid's third row
|
||||
-- is the walkable cell the player faces the table from, matched
|
||||
-- so the flat leg art is claimed off the floor, not so the model
|
||||
-- stands on it. 16 top rows onto a 16px plot map 1:1: roofBack
|
||||
-- covers the whole depth, nothing cycles, and roofCycle is
|
||||
-- unreachable behind it. The Poke Ball sprites ride the `table`
|
||||
-- pin's height (VoxelScene.groundAt reads the collision tile, not
|
||||
-- this model), so the tileset entry above overrides that height
|
||||
-- to the 6px this drawing actually stands.
|
||||
{
|
||||
id = "lab_table",
|
||||
tiles = {
|
||||
{ 41, 59, 59, 59, 59, 42 },
|
||||
{ 78, 57, 57, 57, 57, 79 },
|
||||
{ 88, 89, 89, 89, 89, 90 },
|
||||
},
|
||||
roofRows = 19, roofBack = 16, roofFront = 0, roofCycle = { 2, 13 },
|
||||
slab = 3, frontEave = 0, ledge = nil, depth = 2,
|
||||
},
|
||||
-- F02: the computer bank on the lab's west table (OAKS_LAB cell
|
||||
-- 0,1) -- the one FACADE-ONLY template: rows 0-14 are machines
|
||||
-- drawn face-on (monitor and keys, the centre console, the right
|
||||
-- cabinet) over the lab-table fascia and base bands, and nothing
|
||||
-- is drawn from above. roofRows = 0, so the whole drawing
|
||||
-- extrudes -- the house facade treatment with no roof band; the
|
||||
-- roof fields are inert and the recess pass sinks every framed
|
||||
-- pane (screens, keys, the cabinet's inset face) into relief on
|
||||
-- its own. Same drawing, same grid, stands in the Hall of Fame
|
||||
-- on the GYM atlas -- registered there below.
|
||||
{
|
||||
id = "lab_computers",
|
||||
tiles = {
|
||||
{ 91, 92, 93, 94 },
|
||||
{ 54, 55, 85, 95 },
|
||||
{ 88, 89, 89, 90 },
|
||||
},
|
||||
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
|
||||
slab = 0, frontEave = 0, ledge = nil, depth = 2,
|
||||
},
|
||||
-- F03: the empty north table beside it (OAKS_LAB cell 2,1): the
|
||||
-- starter table's band table verbatim on a grid two tiles
|
||||
-- narrower.
|
||||
{
|
||||
id = "lab_table_small",
|
||||
tiles = {
|
||||
{ 41, 59, 59, 42 },
|
||||
{ 78, 57, 57, 79 },
|
||||
{ 88, 89, 89, 90 },
|
||||
},
|
||||
roofRows = 19, roofBack = 16, roofFront = 0, roofCycle = { 2, 13 },
|
||||
slab = 3, frontEave = 0, ledge = nil, depth = 2,
|
||||
},
|
||||
},
|
||||
|
||||
GYM = {
|
||||
-- F02 again: the Hall of Fame's recording machine is the lab's
|
||||
-- computer bank drawing, tile for tile, on the GYM atlas (one
|
||||
-- placement: HALL_OF_FAME cell 4,1). Same band table as the DOJO
|
||||
-- entry above.
|
||||
{
|
||||
id = "lab_computers",
|
||||
tiles = {
|
||||
{ 91, 92, 93, 94 },
|
||||
{ 54, 55, 85, 95 },
|
||||
{ 88, 89, 89, 90 },
|
||||
},
|
||||
roofRows = 0, roofBack = 0, roofFront = 0, roofCycle = { 0, 0 },
|
||||
slab = 0, frontEave = 0, ledge = nil, depth = 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+12
-2
@@ -42,6 +42,7 @@ local BattleCam = V.require("BattleCam")
|
||||
local BattleBillboard = V.require("BattleBillboard")
|
||||
local VoxelGrid = V.require("VoxelGrid")
|
||||
local DayNight = V.require("DayNight")
|
||||
local AntiAlias = V.require("AntiAlias")
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local Map = require("src.world.Map")
|
||||
|
||||
@@ -427,7 +428,16 @@ function BattleScene.render(state, arena, textures, token)
|
||||
-- its own canvas slot: this renders at the window's pixel size and the
|
||||
-- free-roam pass does too, but the two are alive at different moments
|
||||
-- and a shared slot would reallocate on every battle entry and exit
|
||||
if not Voxel3D.beginScene(pw, ph, cx, cy, vw, vh, sky, "battle") then
|
||||
--
|
||||
-- AA, if the row asks for it, renders it larger still and folds it back
|
||||
-- to pw x ph below (see AntiAlias). The framing is untouched by that:
|
||||
-- the lens was widened by the window's RATIO to the letterbox and the
|
||||
-- rig solved in the GB's own frame, so a bigger canvas is more samples
|
||||
-- of the identical shot -- which is why the pins below still measure in
|
||||
-- pw and ph, and why the HUDs and the depth of field, drawn onto the
|
||||
-- folded canvas afterwards, stay the chunky GB art they are.
|
||||
local rw, rh = AntiAlias.expand(pw, ph)
|
||||
if not Voxel3D.beginScene(rw, rh, cx, cy, vw, vh, sky, "battle") then
|
||||
return
|
||||
end
|
||||
Voxel3D.draw(terrain, atlasFor(host), nil)
|
||||
@@ -500,7 +510,7 @@ function BattleScene.render(state, arena, textures, token)
|
||||
Mat4.translate(nb.ox, 0, nb.oy), fpull,
|
||||
ShadowMap.snug(Mat4.translate(nb.ox, 0, nb.oy)))
|
||||
end
|
||||
local canvas = Voxel3D.endScene()
|
||||
local canvas = AntiAlias.resolve(Voxel3D.endScene(), pw, ph, "battle")
|
||||
if not canvas then return end
|
||||
|
||||
local vp = Voxel3D.vp
|
||||
|
||||
+30
-6
@@ -190,7 +190,24 @@ local function measure(sp, t)
|
||||
top[x] = r
|
||||
end
|
||||
|
||||
local wallH = H - roofRows
|
||||
-- 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
|
||||
-- legs stop two rows short of its grid -- and extruding against H
|
||||
-- would float it that far above its own plot.
|
||||
local ground = roofRows
|
||||
for sy = H - 1, roofRows, -1 do
|
||||
local drawn = false
|
||||
for sx = 0, W - 1 do
|
||||
if sp.inside[sy * W + sx] then drawn = true break end
|
||||
end
|
||||
if drawn then
|
||||
ground = sy + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local wallH = ground - roofRows
|
||||
local ytop = wallH - 1 + t.slab
|
||||
|
||||
-- Side faces must not come out as slabs of outline black: where the
|
||||
@@ -279,7 +296,12 @@ local function measure(sp, t)
|
||||
-- sprite taller than its footprint -- the tower's 16-row drawing
|
||||
-- stands on the 8 rows of it that are actually on the map, and D = H
|
||||
-- would have pushed its body 64px south into the town plaza.
|
||||
return { top = top, ytop = ytop, D = #t.tiles * 8,
|
||||
-- `depth` (in tile rows) names the plot when the grid runs PAST it
|
||||
-- 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,
|
||||
ground = ground,
|
||||
recess = recess, interior = interior, shadeTexel = shadeTexel }
|
||||
end
|
||||
|
||||
@@ -292,7 +314,7 @@ end
|
||||
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 = pr.top, pr.ytop
|
||||
local top, ytop, ground = pr.top, pr.ytop, pr.ground
|
||||
|
||||
-- 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
|
||||
@@ -367,16 +389,18 @@ local function model(sp, pr, t)
|
||||
|
||||
-- the awning: the band juts two voxels past the walls, front and back
|
||||
if ledge0 and (z == -2 or z == -1 or z == D or z == D + 1) then
|
||||
local sy = H - 1 - y
|
||||
local sy = ground - 1 - y
|
||||
if sy >= ledge0 and sy <= ledge1 and sp.inside[sy * W + x] then
|
||||
return sy * W + x
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- the facade, extruded straight back over the footprint
|
||||
-- the facade, extruded straight back over the footprint. Rows map
|
||||
-- against the measured ground line, not the grid's last row: the two
|
||||
-- differ only for furniture standing on open floor (see measure).
|
||||
if z < 0 or z >= D then return nil end
|
||||
local sy = H - 1 - y
|
||||
local sy = ground - 1 - y
|
||||
local i = sy * W + x
|
||||
if y == 0 and not sp.inside[i] and sy > 0 and sp.inside[i - W] then
|
||||
-- the drawing's last row is the ground the building stands on, so
|
||||
|
||||
@@ -295,6 +295,24 @@ function TileShape.forMap(map)
|
||||
if cache[id] then return cache[id] end
|
||||
|
||||
local heights = TileShape.heights()
|
||||
-- Per-tileset height overrides (a tileset entry's `heights`): the class
|
||||
-- vocabulary is global but the drawings are not -- the DOJO lab tables
|
||||
-- are drawn 6px tall where the default `table` is 12 -- and the height
|
||||
-- a sprite RIDES at (VoxelScene.groundAt) must be the height the art
|
||||
-- actually stands, or the starter balls float over their own table.
|
||||
-- Same gate as the global list: known classes, numbers only.
|
||||
do
|
||||
local s = load()
|
||||
local entry = s and s.tilesets and s.tilesets[id]
|
||||
local over = entry and entry.heights
|
||||
if type(over) == "table" then
|
||||
for class, h in pairs(over) do
|
||||
if type(h) == "number" and FALLBACK_HEIGHTS[class] then
|
||||
heights[class] = h
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local authored = authoredGroups(id, heights)
|
||||
local count = math.floor((tileset.imageWidth or 128) / 8)
|
||||
* math.floor((tileset.imageHeight or 48) / 8)
|
||||
|
||||
+1
-1
@@ -715,7 +715,7 @@ function Voxel3D.beginScene(w, h, cx, cy, vw, vh, sky, slot)
|
||||
pcall(sh.send, sh, "sunTexel", { texel, texel })
|
||||
if grid then
|
||||
pcall(sh.send, sh, "gridDark", VoxelGrid.DARK)
|
||||
pcall(sh.send, sh, "gridWidth", VoxelGrid.WIDTH)
|
||||
pcall(sh.send, sh, "gridWidth", VoxelGrid.width())
|
||||
end
|
||||
-- ordinary shading until the silhouette pass asks for otherwise. Sent
|
||||
-- every frame rather than once, because a scene that opened mid-ghost --
|
||||
|
||||
@@ -44,6 +44,19 @@ VoxelGrid.DARK = 0.45
|
||||
-- 1.0 here is the one-pixel wireframe.
|
||||
VoxelGrid.WIDTH = 1.0
|
||||
|
||||
-- The same width in the CANVAS pixels the shader measures in, which is what
|
||||
-- every sender of it actually wants.
|
||||
--
|
||||
-- The two are the same number until AA renders the pass larger than the
|
||||
-- window (see AntiAlias): there a canvas pixel is a fraction of a display
|
||||
-- one, and a width left at 1.0 would come out a half or a quarter of a line
|
||||
-- after the fold -- the wireframe fading as the smoothing goes up, which
|
||||
-- reads as one row breaking the other. Scaled, it stays a one-pixel seam and
|
||||
-- simply gains the antialiasing everything else in the frame just gained.
|
||||
function VoxelGrid.width()
|
||||
return VoxelGrid.WIDTH * V.require("AntiAlias").factor()
|
||||
end
|
||||
|
||||
-- where it persists and the rows that cycle it (see ModSetting)
|
||||
VoxelGrid.setting = ModSetting.new(VoxelGrid.KEY, VoxelGrid.LABEL,
|
||||
{ false, true }, { "OFF", "ON" })
|
||||
|
||||
+1
-1
@@ -1192,7 +1192,7 @@ function Water.begin(ctx)
|
||||
if ctx.grid then
|
||||
local VoxelGrid = V.require("VoxelGrid")
|
||||
send("gridDark", VoxelGrid.DARK)
|
||||
send("gridWidth", VoxelGrid.WIDTH)
|
||||
send("gridWidth", VoxelGrid.width())
|
||||
end
|
||||
|
||||
Water.sendSky(sh, ctx)
|
||||
|
||||
@@ -82,6 +82,7 @@ local BattleExit = V.require("BattleExit")
|
||||
local DayNight = V.require("DayNight")
|
||||
local DayTint = V.require("DayTint")
|
||||
local Water = V.require("Water")
|
||||
local AntiAlias = V.require("AntiAlias")
|
||||
|
||||
-- Forward declaration: the voxel pipeline's update hook (registered below)
|
||||
-- calls this, and it is defined further down with the settings it drives.
|
||||
@@ -203,20 +204,34 @@ mod.content.render_pipelines:register("voxel", {
|
||||
-- a magnified low-res image, while the FX closures keep drawing in
|
||||
-- world-pixel units.
|
||||
local sw, sh = sceneSize(ctx)
|
||||
local canvas = VoxelScene.render(ctx.state, sw, sh,
|
||||
-- With AA on, the whole pass runs into a canvas BIGGER than the window
|
||||
-- and is folded back down at the end (see AntiAlias). Nothing between
|
||||
-- these two lines knows: every pass in the frame measures itself in the
|
||||
-- canvas it was handed, so the sky's dither, the water's march and the
|
||||
-- camera itself all come out the same picture at a higher sample rate.
|
||||
local rw, rh = AntiAlias.expand(sw, sh)
|
||||
local canvas = VoxelScene.render(ctx.state, rw, rh,
|
||||
ctx.vw, ctx.vh, ctx.paletteFor)
|
||||
if not canvas then return nil end -- fall back to the 2D path
|
||||
if Voxel3D.beginOverlay() then
|
||||
-- the FX closures are ordinary 2D draws sized in DISPLAY pixels, and
|
||||
-- they are drawing into the supersampled canvas alongside everything
|
||||
-- else -- so the scale goes up with it, or the "!" bubble lands the
|
||||
-- right place at half the size. project() already answers in canvas
|
||||
-- pixels, so only the scale needs saying.
|
||||
ctx.drawFx(function(wx, wy) return Voxel3D.project(wx, 0, wy) end,
|
||||
ctx.scale)
|
||||
ctx.scale * AntiAlias.factor())
|
||||
Voxel3D.endOverlay()
|
||||
end
|
||||
return canvas
|
||||
-- and back to the window's own size, which is what the engine composites
|
||||
-- one canvas pixel to one display pixel. A pass-through when AA is off.
|
||||
return AntiAlias.resolve(canvas, sw, sh, "world")
|
||||
end,
|
||||
|
||||
invalidate = function()
|
||||
Voxel3D.invalidate()
|
||||
OverworldBattle.invalidate()
|
||||
AntiAlias.invalidate()
|
||||
ChunkMesher.invalidate() -- no map id = every cached mesh
|
||||
end,
|
||||
})
|
||||
@@ -361,6 +376,21 @@ local SETTINGS = {
|
||||
.. "let CYCLE run it -- ten minutes of sun, ten of moon, with the "
|
||||
.. "shadows, the sky and the light following -- or SYNC it to the "
|
||||
.. "clock on the wall, so Kanto's evening falls when yours does." },
|
||||
-- Marked `full` for the opposite reason the battle rows are: this is not a
|
||||
-- knob on the look at all, it is what the look COSTS. FULL is a preset for
|
||||
-- the diorama, not a licence to spend four times the fill rate on the
|
||||
-- machine it happens to be running on, so it neither sets this nor takes
|
||||
-- the row away -- the player decides what their hardware can carry, from
|
||||
-- inside FULL like anywhere else.
|
||||
{ AntiAlias.setting,
|
||||
"Smooth the stair-stepped edges of the 3D world -- roof ridges, ledge "
|
||||
.. "lips, a tree against the sky -- by rendering the diorama larger than "
|
||||
.. "the window and folding it back down. Every edge in the picture "
|
||||
.. "softens with them, the tileset's own texels included, so the diorama "
|
||||
.. "reads smoother rather than sharper. 2X costs half again as many "
|
||||
.. "pixels in each direction and 4X twice, which makes this the most "
|
||||
.. "expensive row in the mod.",
|
||||
full = true },
|
||||
}
|
||||
|
||||
local schema = {}
|
||||
|
||||
@@ -145,6 +145,9 @@ T.check(not fullIds["DRAMATIC_SHAPE:daytime"], "and DAYTIME")
|
||||
-- back sprite (or no staged fights at all) can still say so from inside FULL
|
||||
T.check(fullIds["DRAMATIC_SHAPE:battles"], "3D-BTL is still on the menu under FULL")
|
||||
T.check(fullIds["DRAMATIC_SHAPE:battleBack"], "and BACK SPRITES with it")
|
||||
-- and AA, for the opposite reason: it is not a knob on the look at all, it is
|
||||
-- what the look COSTS, and only the player knows what their machine can carry
|
||||
T.check(fullIds["DRAMATIC_SHAPE:aa"], "and AA, which FULL neither sets nor owns")
|
||||
|
||||
-- DAYTIME is not only hidden under FULL, it is HELD at SYNC: the row cannot
|
||||
-- be reached while FULL owns it, so a value changed underneath (the mod
|
||||
@@ -379,9 +382,11 @@ end
|
||||
Pipelines.setLevel("voxel", 2)
|
||||
local hookedRows = Runtime.call("ui.options.rows", function(_, r) return r end,
|
||||
{ data = Data }, { { id = "text_speed" } })
|
||||
T.eq(#hookedRows, 7, "the options hook added a row per setting")
|
||||
T.eq(#hookedRows, 8, "the options hook added a row per setting")
|
||||
local grid, curve, water = hookedRows[2], hookedRows[3], hookedRows[4]
|
||||
local battles, backRow, daytime = hookedRows[5], hookedRows[6], hookedRows[7]
|
||||
-- the AA row is hookedRows[8]; it is read in its own block below, because
|
||||
-- this chunk is one main function and has 200 local slots to spend
|
||||
T.eq(water.label, "WATER", "the water row carries its label")
|
||||
T.eq(water.value(), "FULL",
|
||||
"and defaults to FULL -- reflections are the point of having the row")
|
||||
@@ -459,6 +464,57 @@ curve.step(settingGame, 1)
|
||||
curve.step(settingGame, 1)
|
||||
T.eq(curve.value(), "OFF", "the curve is left off for the rows below")
|
||||
|
||||
-- ------- AA renders the pass larger and folds it back down
|
||||
--
|
||||
-- The ladder is SAMPLES per display pixel, so the canvas scale each rung asks
|
||||
-- for is its square root -- and the scale in force has to be readable after
|
||||
-- the fact, because two things are quoted in DISPLAY pixels and have to be
|
||||
-- multiplied up into the canvas the pass actually opened: the wireframe's
|
||||
-- line width and the FX overlay's sprite scale.
|
||||
do
|
||||
local AntiAlias = run.loader.exports.DRAMATIC_SHAPE.lib.require("AntiAlias")
|
||||
local VoxelGrid = run.loader.exports.DRAMATIC_SHAPE.lib.require("VoxelGrid")
|
||||
local aaGame = { save = { options = {} }, mods = { modOptions = {} } }
|
||||
local aa = hookedRows[8]
|
||||
T.eq(aa.label, "AA", "the anti-aliasing row carries its label")
|
||||
T.eq(aa.value(), "OFF",
|
||||
"and starts off -- supersampling is a cost knob, and a mod must not spend "
|
||||
.. "four times the fill rate of the machine it lands on unasked")
|
||||
|
||||
T.eq(AntiAlias.samples(), 0, "AA is off by default")
|
||||
local w, h = AntiAlias.expand(320, 200)
|
||||
T.eq(w, 320, "an OFF row renders at the window's own width")
|
||||
T.eq(h, 200, "and its height")
|
||||
T.eq(AntiAlias.factor(), 1, "with nothing to multiply display pixels by")
|
||||
T.eq(VoxelGrid.width(), VoxelGrid.WIDTH,
|
||||
"so the wireframe is the one-display-pixel line it has always been")
|
||||
|
||||
aa.step(aaGame, 1)
|
||||
T.eq(aa.value(), "2X", "stepping the row climbs to two samples a pixel")
|
||||
T.eq(aaGame.save.options.modOptions.DRAMATIC_SHAPE.aa, 2,
|
||||
"the sample count persists beside the other settings, not over them")
|
||||
w, h = AntiAlias.expand(320, 200)
|
||||
T.eq(w, 453, "two samples a pixel is a canvas root-two wider")
|
||||
T.eq(h, 283, "and root-two taller")
|
||||
T.check(math.abs(AntiAlias.factor() - 453 / 320) < 1e-9,
|
||||
"and the factor is what it MEASURED, not what the row asked for")
|
||||
|
||||
aa.step(aaGame, 1)
|
||||
T.eq(aa.value(), "4X", "and again to four")
|
||||
w, h = AntiAlias.expand(320, 200)
|
||||
T.eq(w, 640, "four samples a pixel is a canvas exactly twice the size")
|
||||
T.eq(h, 400, "in each direction, which is the 2x2 box the fold reads")
|
||||
T.eq(AntiAlias.factor(), 2, "with everything in display pixels doubled")
|
||||
T.eq(VoxelGrid.width(), VoxelGrid.WIDTH * 2,
|
||||
"the wireframe among them -- a seam left at 1.0 would fold down to half a "
|
||||
.. "line, so the smoothing row would appear to fade the grid row out")
|
||||
|
||||
aa.step(aaGame, 1)
|
||||
T.eq(aa.value(), "OFF", "and the ladder wraps back to off")
|
||||
AntiAlias.expand(320, 200)
|
||||
T.eq(AntiAlias.factor(), 1, "leaving nothing behind for the next pass")
|
||||
end
|
||||
|
||||
-- ------- the animated terrain atlas survives an engine without its seams
|
||||
--
|
||||
-- Regression: cycling palette modes with voxel mode on eventually killed
|
||||
|
||||
+104
-11
@@ -547,6 +547,57 @@ TEMPLATES = {
|
||||
roof_rows=17, roof_back=5, roof_front=3, roof_cycle=(5, 9),
|
||||
slab=4, front_eave=4, ledge=None, tileset="forest",
|
||||
),
|
||||
# F01: the starter-ball table in Oak's lab -- the first FURNITURE
|
||||
# through the pipeline, and the first drawing whose plot is smaller
|
||||
# than its grid. Rows 0-15 are the tabletop seen from above; 16-18
|
||||
# are the top slab's own front edge (black/#555/black -- exactly
|
||||
# what the rim treatment paints, so slab=3 and those rows fold into
|
||||
# the roof band instead of extruding); 19-21 are the base: corner
|
||||
# feet and the inset dark panel between them. The legs stand on
|
||||
# open FLOOR, so the measured ground line lands two rows short of
|
||||
# the grid; `depth` keeps the plot to the blocked cell row -- the
|
||||
# legs row of the grid is the walkable cell the player faces the
|
||||
# table from, and D = len(tiles) would stand the model in their
|
||||
# path. 16 top rows onto a 16px plot map 1:1: no cycling, and
|
||||
# roof_cycle is unreachable behind roof_back=16.
|
||||
"lab_table": dict(
|
||||
tiles=[
|
||||
[41, 59, 59, 59, 59, 42],
|
||||
[78, 57, 57, 57, 57, 79],
|
||||
[88, 89, 89, 89, 89, 90],
|
||||
],
|
||||
roof_rows=19, roof_back=16, roof_front=0, roof_cycle=(2, 13),
|
||||
slab=3, front_eave=0, ledge=None, tileset="gym", depth=2,
|
||||
),
|
||||
# F02: the computer bank on Oak's lab's west table -- and the Hall
|
||||
# of Fame's recording machine, the same drawing on the GYM atlas
|
||||
# (one placement each; both registered in voxel_heights). The one
|
||||
# FACADE-ONLY template: rows 0-14 are machines drawn face-on over
|
||||
# the lab-table fascia and base bands, nothing is drawn from above,
|
||||
# so roof_rows=0 and the whole drawing extrudes -- the house facade
|
||||
# treatment with no roof band. The recess pass turns every framed
|
||||
# pane (the monitor, its keys, the right cabinet's inset face) into
|
||||
# relief on its own.
|
||||
"lab_computers": dict(
|
||||
tiles=[
|
||||
[91, 92, 93, 94],
|
||||
[54, 55, 85, 95],
|
||||
[88, 89, 89, 90],
|
||||
],
|
||||
roof_rows=0, roof_back=0, roof_front=0, roof_cycle=(0, 0),
|
||||
slab=0, front_eave=0, ledge=None, tileset="gym", depth=2,
|
||||
),
|
||||
# F03: the empty north table beside it -- the starter table's band
|
||||
# table verbatim on a grid two tiles narrower (one placement).
|
||||
"lab_table_small": dict(
|
||||
tiles=[
|
||||
[41, 59, 59, 42],
|
||||
[78, 57, 57, 79],
|
||||
[88, 89, 89, 90],
|
||||
],
|
||||
roof_rows=19, roof_back=16, roof_front=0, roof_cycle=(2, 13),
|
||||
slab=3, front_eave=0, ledge=None, tileset="gym", depth=2,
|
||||
),
|
||||
# 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(
|
||||
@@ -642,7 +693,14 @@ def profile(sp, t):
|
||||
r = next((y for y in range(H) if inside(x, y)), t["roof_rows"])
|
||||
top.append(min(r, t["roof_rows"]))
|
||||
|
||||
wall_h = H - t["roof_rows"]
|
||||
# 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
|
||||
# legs stop two rows short of its grid -- and extruding against H
|
||||
# would float it that far above its own plot.
|
||||
ground = max((y for y in range(H) for x in range(W) if inside(x, y)),
|
||||
default=H - 1) + 1
|
||||
wall_h = ground - t["roof_rows"]
|
||||
ytop = wall_h - 1 + t["slab"]
|
||||
|
||||
# Recesses: the panes the art seals behind a black frame. Non-black
|
||||
@@ -674,8 +732,12 @@ def profile(sp, t):
|
||||
if x1 - x0 + 1 <= RECESS_MAX and y1 - y0 + 1 <= RECESS_MAX:
|
||||
recess.update(cells)
|
||||
|
||||
# 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,
|
||||
inside=inside, D=H, W=W, H=H)
|
||||
inside=inside, D=t.get("depth", len(t["tiles"])) * 8,
|
||||
ground=ground, W=W, H=H)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- stage 3 --
|
||||
@@ -720,7 +782,9 @@ def build(sp, pr, t):
|
||||
|
||||
# ---- walls: the facade rows extruded straight back, trimmed under the roof
|
||||
for sy in range(t["roof_rows"], H):
|
||||
y = H - 1 - sy
|
||||
y = pr["ground"] - 1 - sy # rows below the ground line are floor
|
||||
if y < 0:
|
||||
continue
|
||||
for sx in range(W):
|
||||
if not inside(sx, sy) or trimmed(sx, y):
|
||||
continue
|
||||
@@ -742,7 +806,7 @@ def build(sp, pr, t):
|
||||
if t["ledge"]:
|
||||
l0, l1 = t["ledge"]
|
||||
for sy in range(l0, l1 + 1):
|
||||
y = H - 1 - sy
|
||||
y = pr["ground"] - 1 - sy
|
||||
for sx in range(W):
|
||||
if inside(sx, sy) and not trimmed(sx, y):
|
||||
for z in (-2, -1, D, D + 1):
|
||||
@@ -750,7 +814,7 @@ def build(sp, pr, t):
|
||||
|
||||
# ---- recesses: the front voxel of every pane sinks, its frame stays proud
|
||||
for sx, sy in pr["recess"]:
|
||||
vox.pop((sx, H - 1 - sy, D - 1), None)
|
||||
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"]
|
||||
@@ -805,6 +869,41 @@ def verify(vox, sp, pr, t):
|
||||
for (x, y, z), _ in vox.items():
|
||||
assert y <= T(x), f"voxel pokes through the roof at {x},{y},{z}"
|
||||
|
||||
# A facade-only template (roof_rows == 0: the lab computer bank) has
|
||||
# no roof band, so there is no surface to hold level or slope and no
|
||||
# coverage to demand of it; the poke-through assert above and the
|
||||
# facade asserts below are what it answers to. Everything with a roof
|
||||
# band answers the full set.
|
||||
if t["roof_rows"] == 0:
|
||||
# its one geometric intent: a straight extrusion, so each column
|
||||
# of the drawing is solid from its lowest voxel to its top. NOT
|
||||
# from the ground -- the drawing's base band is inset like every
|
||||
# lab table's, and the body's outer columns legitimately
|
||||
# overhang it -- and not on the recess layer, which sinks panes.
|
||||
lo, hi = {}, {}
|
||||
for (x, y, z) in vox:
|
||||
lo[(x, z)] = min(lo.get((x, z), y), y)
|
||||
hi[(x, z)] = max(hi.get((x, z), y), y)
|
||||
for (x, z), h in hi.items():
|
||||
if z == D - 1:
|
||||
continue
|
||||
assert all((x, y, z) in vox for y in range(lo[(x, z)], h + 1)), \
|
||||
f"facade column has a hole at {x},{z}"
|
||||
else:
|
||||
verify_roof(vox, pr, t)
|
||||
|
||||
shell = [k for k in vox if not all(
|
||||
(k[0] + d[0], k[1] + d[1], k[2] + d[2]) in vox
|
||||
for d in ((1, 0, 0), (-1, 0, 0), (0, 1, 0),
|
||||
(0, -1, 0), (0, 0, 1), (0, 0, -1)))]
|
||||
return shell
|
||||
|
||||
|
||||
def verify_roof(vox, pr, t):
|
||||
W, H, D = pr["W"], pr["H"], pr["D"]
|
||||
ytop, top, slab = pr["ytop"], pr["top"], t["slab"]
|
||||
T = lambda x: ytop - top[max(0, min(W - 1, x))]
|
||||
|
||||
# The roof surface reads over the columns the drawing actually paints:
|
||||
# a sprite inset from its box says nothing about the rest.
|
||||
roofed = [x for x in range(W) if top[x] < t["roof_rows"]]
|
||||
@@ -845,12 +944,6 @@ def verify(vox, sp, pr, t):
|
||||
elif any((x, y, z) in vox for y in range(0, T(x) - slab + 1)):
|
||||
assert (x, T(x), z) in vox, f"wall uncovered at {x},{z}"
|
||||
|
||||
shell = [k for k in vox if not all(
|
||||
(k[0] + d[0], k[1] + d[1], k[2] + d[2]) in vox
|
||||
for d in ((1, 0, 0), (-1, 0, 0), (0, 1, 0),
|
||||
(0, -1, 0), (0, 0, 1), (0, 0, -1)))]
|
||||
return shell
|
||||
|
||||
|
||||
def preview(shell, vox, sp, name, out, flip, canvas=(1700, 900), pad=20):
|
||||
# Mirroring depth swings the camera round to the other side. Mirror
|
||||
|
||||
Reference in New Issue
Block a user