mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 21:16:28 +02:00
fix(render): replace per-entity grass overdraw with full-screen cell pass
The previous drawCellBottom calls fired only for cells containing a tracked entity. While walking this worked acceptably because the sprite's sub-pixel tween kept the visual overlap plausible, but while standing still the sprite is pixel-aligned with the cell and the opaque leaf-edge pixels in the grass bottom row paint over the player's feet. This change removes the per-entity isGrassCell checks and replaces them with a single post-sprite pass that overdraws every visible grass cell. TileRenderer: - ensureWindow now builds grassCells (all paths) and grassBatch (DMG/SGB shader path) alongside winBatch during the existing tile scan loop. A grassSeen table deduplicates cells so each cx/cy pair is only recorded once despite having two bottom-row tiles. - drawGrassOverdraw: DMG/SGB draws the grassBatch under color0KeyShader in one call; GBC iterates grassCells and calls drawCellBottomRaw per cell (pre-keyed images can't share a SpriteBatch). - markGrassOverdrawRedraw: iterates grassCells and calls markCellBottomRedraw for the post-zone OBP-replay pass (GBC only). - releaseBatches cleans up grassBatch and grassCells. OverworldController (flat path): - Entity loop draws sprites only; grass overdraw fires once after the loop via drawGrassOverdraw + markGrassOverdrawRedraw. OverworldController (tilt path): - Grass cells are injected into the billboard sort queue keyed on the world-pixel foot of each cell's bottom tile row (cy*16+16), so they depth-sort correctly against entities at different y positions. Each grass cell billboards via drawCellBottomRaw inside the upright pass. Fixes: standing-in-tall-grass feet overdraw (Gen 2 confirmed, Gen 1 improved); NPCs and Pikachu follower in grass benefit automatically. Parity test: tests/parity_grass_seam.lua 10/10, engine 228/228.
This commit is contained in:
@@ -760,6 +760,36 @@ function TileRenderer:markCellBottomRedraw(cx, cy, camX, camY, colors)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Draw the bottom tile row of every visible grass cell after sprites.
|
||||||
|
-- DMG/SGB: one SpriteBatch draw under the color-0-key shader.
|
||||||
|
-- GBC: per-cell draws using pre-keyed images (can't share one batch).
|
||||||
|
function TileRenderer:drawGrassOverdraw(camX, camY)
|
||||||
|
local ox, oy = -math.floor(camX), -math.floor(camY)
|
||||||
|
if self.gbcCtx then
|
||||||
|
if self.grassCells then
|
||||||
|
for _, c in ipairs(self.grassCells) do
|
||||||
|
self:drawCellBottomRaw(c[1], c[2], camX, camY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local shader = getColor0KeyShader()
|
||||||
|
if shader then love.graphics.setShader(shader) end
|
||||||
|
if self.grassBatch then
|
||||||
|
love.graphics.draw(self.grassBatch, ox, oy)
|
||||||
|
end
|
||||||
|
if shader then love.graphics.setShader() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Queue every visible grass cell's bottom row for the post-zone
|
||||||
|
-- sprite-redraw pass (GBC OBP replay; mirrors markCellBottomRedraw).
|
||||||
|
function TileRenderer:markGrassOverdrawRedraw(camX, camY, colors)
|
||||||
|
if not self.grassCells then return end
|
||||||
|
for _, c in ipairs(self.grassCells) do
|
||||||
|
self:markCellBottomRedraw(c[1], c[2], camX, camY, colors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local WINDOW_MARGIN = 8 -- tiles of slack kept around the view between refills
|
local WINDOW_MARGIN = 8 -- tiles of slack kept around the view between refills
|
||||||
|
|
||||||
function TileRenderer:ensureWindow(camX, camY, vw, vh)
|
function TileRenderer:ensureWindow(camX, camY, vw, vh)
|
||||||
@@ -785,6 +815,16 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
|
|||||||
self.winBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic")
|
self.winBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic")
|
||||||
end
|
end
|
||||||
self.winBatch:clear()
|
self.winBatch:clear()
|
||||||
|
-- grass-overdraw pass structures: a SpriteBatch for the DMG/SGB shader
|
||||||
|
-- path (one atlas, one shader draw call) and a plain list for the GBC
|
||||||
|
-- path (per-cell pre-keyed images can't share a single SpriteBatch).
|
||||||
|
if not self.gbcCtx then
|
||||||
|
if not self.grassBatch then
|
||||||
|
self.grassBatch = love.graphics.newSpriteBatch(self.image, 1024, "dynamic")
|
||||||
|
end
|
||||||
|
self.grassBatch:clear()
|
||||||
|
end
|
||||||
|
self.grassCells = {}
|
||||||
local anims = self.anims
|
local anims = self.anims
|
||||||
for _, anim in ipairs(anims) do
|
for _, anim in ipairs(anims) do
|
||||||
if not anim.batch then
|
if not anim.batch then
|
||||||
@@ -794,6 +834,9 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
|
|||||||
end
|
end
|
||||||
local map, quads = self.map, self.quads
|
local map, quads = self.map, self.quads
|
||||||
local claimedBy, aliasMap = self.claimedBy, self.aliasMap
|
local claimedBy, aliasMap = self.claimedBy, self.aliasMap
|
||||||
|
-- track which grass cells we've already added so each cx/cy pair is only
|
||||||
|
-- recorded once even though its two bottom-row tiles share the same cell.
|
||||||
|
local grassSeen = {}
|
||||||
for ty = ty0, ty1 - 1 do
|
for ty = ty0, ty1 - 1 do
|
||||||
local by = math.floor(ty / 4)
|
local by = math.floor(ty / 4)
|
||||||
local ty4 = ty % 4
|
local ty4 = ty % 4
|
||||||
@@ -816,6 +859,25 @@ function TileRenderer:ensureWindow(camX, camY, vw, vh)
|
|||||||
anim.batch:add(wx, wy)
|
anim.batch:add(wx, wy)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- bottom tile row of a 2×2 cell (ty is odd) that is a grass cell:
|
||||||
|
-- record it for the post-sprite grass overdraw pass.
|
||||||
|
if ty % 2 == 1 then
|
||||||
|
local cx, cy = math.floor(tx / 2), math.floor(ty / 2)
|
||||||
|
local key = cy * 65536 + cx
|
||||||
|
if not grassSeen[key] and map:isGrassCell(cx, cy) then
|
||||||
|
grassSeen[key] = true
|
||||||
|
self.grassCells[#self.grassCells + 1] = { cx, cy }
|
||||||
|
-- DMG/SGB path: bake both tiles of the bottom row into the batch
|
||||||
|
if self.grassBatch then
|
||||||
|
-- left tile (tx = cx*2)
|
||||||
|
local lq = quads[map:tileAt(cx * 2, ty)]
|
||||||
|
if lq then self.grassBatch:add(lq, cx * 16, ty * 8) end
|
||||||
|
-- right tile (tx = cx*2+1)
|
||||||
|
local rq = quads[map:tileAt(cx * 2 + 1, ty)]
|
||||||
|
if rq then self.grassBatch:add(rq, cx * 16 + 8, ty * 8) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -891,6 +953,8 @@ end
|
|||||||
-- :release on eviction.
|
-- :release on eviction.
|
||||||
function TileRenderer:releaseBatches()
|
function TileRenderer:releaseBatches()
|
||||||
safeRelease(self.winBatch); self.winBatch = nil
|
safeRelease(self.winBatch); self.winBatch = nil
|
||||||
|
safeRelease(self.grassBatch); self.grassBatch = nil
|
||||||
|
self.grassCells = nil
|
||||||
safeRelease(self.borderFill); self.borderFill = nil
|
safeRelease(self.borderFill); self.borderFill = nil
|
||||||
safeRelease(self.borderQuad); self.borderQuad = nil
|
safeRelease(self.borderQuad); self.borderQuad = nil
|
||||||
-- shared shift-variant cache; only drop the reference
|
-- shared shift-variant cache; only drop the reference
|
||||||
|
|||||||
@@ -5303,24 +5303,15 @@ function OverworldState:drawWorld()
|
|||||||
if not ((self.flyAnim or self.flyArrive or self.playerHidden)
|
if not ((self.flyAnim or self.flyArrive or self.playerHidden)
|
||||||
and e == self.player) then
|
and e == self.player) then
|
||||||
e:draw(cam.x, cam.y)
|
e:draw(cam.x, cam.y)
|
||||||
-- tall grass overdraws the sprite's feet (GB sprite priority);
|
end
|
||||||
-- the overdraw is BG tiles, so it rides the shake offset too
|
end
|
||||||
|
-- tall grass overdraws every visible grass cell's feet row after all
|
||||||
|
-- sprites (GB sprite-priority parity). One pass regardless of how many
|
||||||
|
-- entities are on screen -- see TileRenderer:drawGrassOverdraw.
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
if self.map:isGrassCell(e.cellX, e.cellY) then
|
self.map.renderer:drawGrassOverdraw(cam.x, bgY)
|
||||||
self.map.renderer:drawCellBottom(e.cellX, e.cellY, cam.x, bgY)
|
|
||||||
if grassColors then
|
if grassColors then
|
||||||
self.map.renderer:markCellBottomRedraw(e.cellX, e.cellY,
|
self.map.renderer:markGrassOverdrawRedraw(cam.x, bgY, grassColors)
|
||||||
cam.x, bgY, grassColors)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if e.targetX and self.map:isGrassCell(e.targetX, e.targetY) then
|
|
||||||
self.map.renderer:drawCellBottom(e.targetX, e.targetY, cam.x, bgY)
|
|
||||||
if grassColors then
|
|
||||||
self.map.renderer:markCellBottomRedraw(e.targetX, e.targetY,
|
|
||||||
cam.x, bgY, grassColors)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
fxHeal()
|
fxHeal()
|
||||||
fxDust()
|
fxDust()
|
||||||
@@ -5357,6 +5348,18 @@ function OverworldState:drawWorld()
|
|||||||
items[#items + 1] = { y = e.py + 16, kind = "entity", e = e }
|
items[#items + 1] = { y = e.py + 16, kind = "entity", e = e }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- Inject grass-cell overdraw items into the same depth-sorted queue so
|
||||||
|
-- they occlude entities at lower y correctly (back-to-front by cell foot).
|
||||||
|
-- Each cell's foot y = cy*16 + 16 in world pixels (bottom of its two rows).
|
||||||
|
local grassCells = self.map.renderer.grassCells
|
||||||
|
if grassCells then
|
||||||
|
for _, c in ipairs(grassCells) do
|
||||||
|
local cx, cy = c[1], c[2]
|
||||||
|
-- world-pixel foot of the grass cell's bottom tile row
|
||||||
|
local cellFootY = (cy * 2 + 2) * 8 -- == cy*16+16
|
||||||
|
items[#items + 1] = { y = cellFootY, kind = "grass", cx = cx, cy = cy }
|
||||||
|
end
|
||||||
|
end
|
||||||
table.sort(items, function(a, b) return a.y < b.y end)
|
table.sort(items, function(a, b) return a.y < b.y end)
|
||||||
|
|
||||||
for _, it in ipairs(items) do
|
for _, it in ipairs(items) do
|
||||||
@@ -5368,6 +5371,20 @@ function OverworldState:drawWorld()
|
|||||||
local fy = g.npc.py - cam.y + g.oy + 16
|
local fy = g.npc.py - cam.y + g.oy + 16
|
||||||
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false,
|
self:billboard(fx, fy, vw, vh, zoneColorsAt(zones, fx, fy), false,
|
||||||
function() g.npc:draw(cam.x - g.ox, cam.y - g.oy) end)
|
function() g.npc:draw(cam.x - g.ox, cam.y - g.oy) end)
|
||||||
|
elseif it.kind == "grass" then
|
||||||
|
-- tall-grass bottom-row overdraw: billboarded at the cell's foot so
|
||||||
|
-- it depth-sorts correctly against any entity in the same y column.
|
||||||
|
-- bgY keeps the elevator-shake offset; drawCellBottomRaw lets the
|
||||||
|
-- billboard own the shader (color-0 keying baked into the keyed image
|
||||||
|
-- on GBC, or applied by drawCellBottom's shader on DMG/SGB).
|
||||||
|
local cx, cy = it.cx, it.cy
|
||||||
|
local fx = cx * 16 - cam.x + 8 -- horizontal centre of the cell
|
||||||
|
local fy = (cy * 2 + 2) * 8 - cam.y -- foot of the bottom tile row
|
||||||
|
local colors = zoneColorsAt(zones, fx, fy)
|
||||||
|
self:billboard(fx, fy, vw, vh, colors, true, function()
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
self.map.renderer:drawCellBottomRaw(cx, cy, cam.x, bgY)
|
||||||
|
end)
|
||||||
else
|
else
|
||||||
local e = it.e
|
local e = it.e
|
||||||
local fx = e.px - cam.x + 8
|
local fx = e.px - cam.x + 8
|
||||||
@@ -5375,22 +5392,6 @@ function OverworldState:drawWorld()
|
|||||||
local colors = zoneColorsAt(zones, fx, fy)
|
local colors = zoneColorsAt(zones, fx, fy)
|
||||||
self:billboard(fx, fy, vw, vh, colors, false,
|
self:billboard(fx, fy, vw, vh, colors, false,
|
||||||
function() e:draw(cam.x, cam.y) end)
|
function() e:draw(cam.x, cam.y) end)
|
||||||
-- tall-grass feet overdraw glued to the sprite: same anchor + depth
|
|
||||||
-- so it keeps hiding the feet, color-0-keyed palette so its white
|
|
||||||
-- gaps still show the sprite through (drawCellBottomRaw lets the
|
|
||||||
-- billboard own the shader; bgY keeps the elevator-shake offset).
|
|
||||||
if self.map:isGrassCell(e.cellX, e.cellY) then
|
|
||||||
self:billboard(fx, fy, vw, vh, colors, true, function()
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
self.map.renderer:drawCellBottomRaw(e.cellX, e.cellY, cam.x, bgY)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
if e.targetX and self.map:isGrassCell(e.targetX, e.targetY) then
|
|
||||||
self:billboard(fx, fy, vw, vh, colors, true, function()
|
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
|
||||||
self.map.renderer:drawCellBottomRaw(e.targetX, e.targetY, cam.x, bgY)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user