Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ menu.
| 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 |
| the **SHELF TOPS** options row | TILE / SOLID — how bookcase and shelf tops are painted. **TILE** wears the drawn top or cap trim across the plateau; **SOLID** paints a flat of the face's majority colour, the same rule interior walls use |

## VR

Expand Down
13 changes: 12 additions & 1 deletion data/voxel_heights.lua
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@
-- so the books stand in the shelf instead of
-- being painted on it. A tileset that
-- borrows the collapse for something that is
-- not a shelf says `bookcase_relief = false`
-- not a shelf says `bookcase_relief = false`.
-- A tileset that borrows it for a MACHINE
-- whose "lid" still reads as face art on the
-- plateau (Bill's transporter drums) says
-- `bookcase_solid_top = true` and the top
-- wears a solid of the face's majority fill,
-- the same rule interior walls use
-- cylinder / canopy round scenery (tree canopies): a voxel hull
-- / stump cut from the art's own darkest-pixel outline,
-- round in depth -- one 16px cell, a 2x2-cell
Expand Down Expand Up @@ -1901,6 +1907,11 @@ return {
-- light regions are the barrel's own lit face and not panes
-- behind a frame. Sinking them would dent the drum.
bookcase_relief = false,
-- Same reason the top is a solid of the face colour rather than
-- the lid tile laid flat: 7/8/9/10 is the drum's drawn lid, but
-- laid across the plateau it still reads as the barrel's circles
-- smeared on top. Interior walls already take this path.
bookcase_solid_top = true,
-- The big OCTAGONAL boardroom table -- the Fan Club's and Silph
-- 11F's, the same drawing in both -- half a cell high, and half a
-- cell on purpose. A `counter` is ONE band: exactly the drawing's
Expand Down
62 changes: 48 additions & 14 deletions lib/ChunkMesher.lua
Original file line number Diff line number Diff line change
Expand Up @@ -370,12 +370,22 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)

-- `to` routes the quad somewhere other than the main sink -- the water
-- surface is the only caller that ever does (see runGeometry's header).
local function topQuad(x0, z0, h, tile, shade, to)
local u0, u1, v0, v1 = uvRect(tile, 0, 8)
-- `solid` paints the plateau as one texel: the majority body colour of
-- `tile` (see Structures.faceCapUV). Interior walls have no top-view
-- art, and stretching any strip of the face still reads as a smeared
-- facade -- a flat of the face's own fill does not.
local function topQuad(x0, z0, h, tile, shade, to, solid)
local uvs
if solid then
local u, v = Structures.faceCapUV(tileset, tile, atlasW, atlasH)
uvs = { { u, v }, { u, v }, { u, v }, { u, v } }
else
local u0, u1, v0, v1 = uvRect(tile, 0, 8)
uvs = { { u0, v0 }, { u1, v0 }, { u1, v1 }, { u0, v1 } }
end
;(to or push)({ { x0, h, z0 }, { x0 + 8, h, z0 },
{ x0 + 8, h, z0 + 8 }, { x0, h, z0 + 8 } },
{ { u0, v0 }, { u1, v0 }, { u1, v1 }, { u0, v1 } },
aoShades(x0 / 8, z0 / 8, h, shade))
uvs, aoShades(x0 / 8, z0 / 8, h, shade))
end

-- vertical quad for face direction `d` of the tile column at (x0, z0),
Expand Down Expand Up @@ -528,19 +538,38 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
{ x0 + 8, neY, z0 }, { x0, nwY, z0 } },
{ { u0, v1 }, { u1, v1 }, { u1, v0 }, { u0, v0 } }, 0.95)
elseif run then
local m = math.min(2, run.extent)
local topTile = map:tileAt(tx, run.north + ((ty - run.north) % m))
topQuad(x0, z0, h, topTile, VOLUME_TOP_SHADE)
-- Indoors a volume has no roof rows: its north tiles are the
-- top of a face-on drawing (Oak's Lab wall band, unpinned
-- partitions). A solid of that face's majority body colour
-- -- sampled once from the run's north row, so every cell of
-- a deep wall shares one flat -- keeps panels on the south
-- face; outdoors the north tiles really are roof art and the
-- top rows tile across the footprint as before.
local solid = not S.outdoor and run.rise == 0
local topTile
if solid then
topTile = map:tileAt(tx, run.north)
else
local m = math.min(2, run.extent)
topTile = map:tileAt(tx, run.north + ((ty - run.north) % m))
end
topQuad(x0, z0, h, topTile, VOLUME_TOP_SHADE, nil, solid)
else
local topTile = tile
-- When a fully-folded upright has no trim row above it, only its
-- face-on north tile is left for the plateau. Stretching that
-- tile (or even its top edge) lays windows and posters across
-- the top; a solid of the face's majority fill does not.
local solidTop = false
if s.art == "upright" and s.authored then
-- Top art for a pinned box. A furniture drawing is top-view
-- rows over floor(h/8) face-on rows the fold stands upright;
-- a face row's top would repeat its front art lying flat, so
-- it wears the nearest row above the face block instead --
-- the drawn tabletop (and whatever sits on it) stays on top,
-- and a fully-folded structure (wall, desk) tops with its
-- northmost row.
-- and a fully-folded structure (wall, desk) tops with a
-- solid of its northmost face when nothing above supplies a
-- trim.
local north, front = ty, ty
while ty - north < 6 do
local bs = S.shapeAt[keyOf(tx, north - 1)]
Expand All @@ -562,11 +591,15 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
if row < north then
-- the whole run folded onto the face: top with the drawn
-- row just above it when that row is furniture too (a
-- bookcase wearing its shelf-top trim), else with the
-- run's own top row
-- bookcase wearing its shelf-top trim), else with a solid
-- of the run's own face colour
local above = S.shapeAt[keyOf(tx, north - 1)]
row = (above and above.authored and above.art == "upright")
and (north - 1) or north
if above and above.authored and above.art == "upright" then
row = north - 1
else
row = north
solidTop = true
end
end
topTile = S.tileAt[keyOf(tx, row)]
end
Expand All @@ -577,7 +610,8 @@ local function runGeometry(map, bodyOnly, masks, sink, waterSink)
-- on the pond.
topQuad(x0, z0, h, topTile,
s.art == "upright" and VOLUME_TOP_SHADE or 1,
(s.class == "water") and waterPush or nil)
(s.class == "water") and waterPush or nil,
solidTop)
end

-- sides: 8px bands wherever the neighbour is lower. Band k spans
Expand Down
71 changes: 70 additions & 1 deletion lib/Structures.lua
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ local cache = {}
-- ---------------------------------------------------------------- pixels --

local atlasData = {}
-- tile id -> { u, v } majority-body atlas UV, keyed under atlasData's
-- path so an atlas reload drops both together
local faceCapUV = {}

local function pixels(tileset)
local path = tileset.image
Expand All @@ -99,6 +102,60 @@ local function pixels(tileset)
return atlasData[path] or nil
end

-- Atlas UV of the majority non-outline texel of an 8x8 tile. Interior
-- walls have no top-view art -- their plateau used to wear the face
-- drawing laid flat -- so the mesher points every corner of the top
-- quad at this one texel and the top reads as a flat of the face's
-- body colour (white plaster, blue panel, yellow cabinet).
function Structures.faceCapUV(tileset, tile, atlasW, atlasH)
local path = tileset.image or ""
local cache = faceCapUV[path]
if not cache then
cache = {}
faceCapUV[path] = cache
end
local hit = cache[tile]
if hit then return hit[1], hit[2] end

local perRow = tileset.tilesPerRow or 16
local ax = (tile % perRow) * 8
local ay = math.floor(tile / perRow) * 8
local aw = atlasW or tileset.imageWidth or (perRow * 8)
local ah = atlasH or tileset.imageHeight or 48
local data = pixels(tileset)
local bestN, bestPx, bestPy = 0, 4, 4
if data then
local counts = {}
for py = 0, 7 do
for px = 0, 7 do
local r, g, b, a = data:getPixel(ax + px, ay + py)
-- skip clear and black outline: the body fill is what a wall
-- top should wear, not the stroke around the panels
if a > 0 and Structures.shadeClass(math.min(r, g, b)) ~= "black" then
local key = math.floor(r * 15 + 0.5) * 256
+ math.floor(g * 15 + 0.5) * 16
+ math.floor(b * 15 + 0.5)
local e = counts[key]
if e then
e.n = e.n + 1
else
counts[key] = { n = 1, px = px, py = py }
end
end
end
end
for _, e in pairs(counts) do
if e.n > bestN then
bestN, bestPx, bestPy = e.n, e.px, e.py
end
end
end
local u = (ax + bestPx + 0.5) / aw
local v = (ay + bestPy + 0.5) / ah
cache[tile] = { u, v }
return u, v
end

-- tiles whose art is entirely black or transparent (interior darkness):
-- these never extrude, whatever class they resolved to
local function voidTiles(tileset)
Expand Down Expand Up @@ -1843,8 +1900,19 @@ local function bookcaseRank(S, map, perRow, run, i, j, k, pane, srcU, srcV,
end
end

-- Cap trim when the rank adopted one, otherwise the northmost drawn
-- row. Real shelves wear that tile whole on the plateau; a tileset
-- that borrowed the collapse for a machine (Bill's drums) opts into
-- a solid of the face's majority fill instead -- same rule as an
-- interior wall, see TileShape.bookcaseSolidTop.
local topTile = capTile or map:tileAt(tx, northTy)
local u0, u1, v0, v1 = uvRect(topTile)
local u0, u1, v0, v1
if TileShape.bookcaseSolidTop(map.tileset.id) then
local u, v = Structures.faceCapUV(map.tileset, topTile, atlasW, atlasH)
u0, u1, v0, v1 = u, u, v, v
else
u0, u1, v0, v1 = uvRect(topTile)
end
for seg = 0, depth / 8 - 1 do
local sz0 = z0 + seg * 8
quads[#quads + 1] = { { x0, h, sz0 }, { x1, h, sz0 },
Expand Down Expand Up @@ -3612,6 +3680,7 @@ function Structures.invalidate(mapId)
else
cache = {}
atlasData = {}
faceCapUV = {}
roundCache = {}
Buildings.invalidate()
end
Expand Down
36 changes: 36 additions & 0 deletions lib/TileShape.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
-- the mod namespace (see main.lua): V.data loads a shipped data file
local V = ...

local ModSetting = V.require("ModSetting")

local TileShape = {}

-- class -> height fallbacks, used when data/voxel_heights.lua is missing
Expand Down Expand Up @@ -700,6 +702,40 @@ function TileShape.bookcaseRelief(tilesetId)
return not (entry and entry.bookcase_relief == false)
end

--- Does this tileset's `bookcase` top wear a SOLID of the face's majority
--- body colour? Yes when the SHELF TOPS row is SOLID (every shelf, like
--- an interior wall), or when the tileset opts in with
--- `bookcase_solid_top` (Bill's transporter drums -- machines that
--- borrow the collapse and whose lid still smears as face art).
--- Default / TILE: real shelves wear their northmost or cap tile whole.
TileShape.SHELF_TOP_KEY = "shelf_top"
TileShape.shelfTopSetting = ModSetting.new(
TileShape.SHELF_TOP_KEY, "SHELF TOPS",
{ "tile", "solid" }, { "TILE", "SOLID" })

-- Tops are baked into the mesh at build time, so flipping the row has to
-- drop every cached analysis / mesh or the old plateau stays on screen.
do
local baseRow = TileShape.shelfTopSetting.row
function TileShape.shelfTopSetting:row()
local r = baseRow(self)
local step = r.step
r.step = function(game, dir)
local ok = step(game, dir)
V.require("ChunkMesher").invalidate()
return ok
end
return r
end
end

function TileShape.bookcaseSolidTop(tilesetId)
if TileShape.shelfTopSetting:get() == "solid" then return true end
local s = load()
local entry = s and s.tilesets and s.tilesets[tilesetId]
return entry and entry.bookcase_solid_top == true or false
end

-- Drop the cache: a mod that shadows data/voxel_heights.lua or a tileset
-- record needs the next lookup to re-resolve (hot reload, mod toggle).
function TileShape.invalidate()
Expand Down
14 changes: 14 additions & 0 deletions main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ local AntiAlias = V.require("AntiAlias")
local FirstPerson = V.require("FirstPerson")
local FreeMove = V.require("FreeMove")
local VR = V.require("VR")
local TileShape = V.require("TileShape")

-- Forward declaration: the voxel pipeline's update hook (registered below)
-- calls this, and it is defined further down with the settings it drives.
Expand Down Expand Up @@ -445,6 +446,14 @@ local SETTINGS = {
-- says why); off Windows -- mobile above all -- there is no VR to have
-- and the row does not exist
when = function() return VR.supported() end, full = true },
-- Last among the look knobs: a preference about furniture tops, not a
-- mode of the diorama itself, so it sits at the end of the block.
{ TileShape.shelfTopSetting,
"How shelf tops are painted. TILE wears the drawn top or cap trim "
.. "across the plateau -- the usual look for bookcases. SOLID paints "
.. "a flat of the face's majority colour, the same rule interior "
.. "walls use, which cleans up machines that borrow the shelf "
.. "collapse (and anything else whose lid still reads as face art)." },
}

local schema = {}
Expand Down Expand Up @@ -706,6 +715,11 @@ mod.events:on("mod.options_changed", function(payload)
for _, entry in ipairs(SETTINGS) do
if payload.key == entry[1].key then entry[1]:sync(payload.value) end
end
-- Tops are baked into the mesh: flipping SHELF TOPS from the manager
-- has to drop every cached analysis the same way the OPTIONS row does.
if payload.key == TileShape.SHELF_TOP_KEY then
ChunkMesher.invalidate()
end
-- 3D-BTL switched on from the manager's page pins BATTLE LAYOUT exactly as
-- the OPTIONS row does. The manager persists its own value; this is the one
-- that has to follow it.
Expand Down
9 changes: 8 additions & 1 deletion tests/dramatic_shape_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ T.check(not fullIds["pipeline:tiltshift"],
"FULL takes T-SHIFT off the menu -- it owns the blur")
T.check(not fullIds["DRAMATIC_SHAPE:grid"], "and V-GRID")
T.check(not fullIds["DRAMATIC_SHAPE:curve"], "and V-CURVE")
T.check(not fullIds["DRAMATIC_SHAPE:shelf_top"], "and SHELF TOPS")
T.check(not fullIds["DRAMATIC_SHAPE:daytime"], "and DAYTIME")

-- but the battle rows survive it: they are not knobs on the look, and FULL
Expand Down Expand Up @@ -310,6 +311,8 @@ T.eq(order["DRAMATIC_SHAPE:battles"] - order["pipeline:tiltshift"], 4,
"and sit in one unbroken block, not scattered to the end of the list")
T.check(order["void_fill"] > order["DRAMATIC_SHAPE:battles"],
"with the engine's own later rows still after them")
T.check(order["DRAMATIC_SHAPE:shelf_top"] > order["DRAMATIC_SHAPE:aa"],
"SHELF TOPS sits at the end of the mode's settings block")

-- ------- the open menu notices when FULL is stepped onto or off
--
Expand Down Expand Up @@ -387,7 +390,7 @@ 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, 9, "the options hook added a row per setting")
T.eq(#hookedRows, 10, "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
Expand Down Expand Up @@ -415,6 +418,10 @@ T.eq(backRow.value(), "OFF",
.. "map, so the classic slot is opt-in")
T.check(backRow.id ~= battles.id and backRow.id:find("battleBack", 1, true),
"on its own key, so it persists beside 3D-BTL rather than over it")
T.eq(hookedRows[#hookedRows].label, "SHELF TOPS",
"SHELF TOPS is the last of the mode's settings")
T.eq(hookedRows[#hookedRows].value(), "TILE",
"and defaults to TILE -- shelves wear their drawn top or cap trim")

-- stepping writes through to the one place both rows read
local settingGame = { save = { options = {} }, mods = { modOptions = {} } }
Expand Down