feat: implement RenderQueue, SpriteBatch for tiles and Low-Res rendering for lights/shadows

This commit is contained in:
PeaAshMeter 2026-04-23 17:34:31 +03:00
parent c2c33cbf1b
commit 1ad38c3103
8 changed files with 206 additions and 130 deletions

View File

@ -32,22 +32,19 @@ function behavior:draw()
local positioned = self.owner:has(Tree.behaviors.positioned) local positioned = self.owner:has(Tree.behaviors.positioned)
if not positioned then return end if not positioned then return end
love.graphics.setBlendMode("add", "premultiplied") Tree.level.render:enqueue(Tree.level.render.LAYERS.LIGHT, positioned.position.y, function()
Tree.level.camera:attach() love.graphics.setBlendMode("add", "premultiplied")
love.graphics.setCanvas(Tree.level.render.textures.lightLayer) local shader = Tree.assets.files.shaders.light
local shader = Tree.assets.files.shaders.light shader:send("color", { self.color.x, self.color.y, self.color.z })
shader:send("color", { self.color.x, self.color.y, self.color.z }) shader:send("time", love.timer:getTime() + self.seed)
shader:send("time", love.timer.getTime() + self.seed) love.graphics.setShader(shader)
love.graphics.setShader(shader) love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2,
love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2, positioned.position.y - self.intensity / 2, 0, self.intensity / 128,
positioned.position.y - self.intensity / 2, 0, self.intensity / 128, self.intensity / 128)
self.intensity / 128)
love.graphics.setBlendMode("alpha") love.graphics.setShader()
love.graphics.setBlendMode("alpha")
love.graphics.setShader() end)
love.graphics.setCanvas()
Tree.level.camera:detach()
end end
return behavior return behavior

View File

@ -10,10 +10,7 @@ function behavior:draw()
local sprite = self.owner:has(Tree.behaviors.sprite) local sprite = self.owner:has(Tree.behaviors.sprite)
local positioned = self.owner:has(Tree.behaviors.positioned) local positioned = self.owner:has(Tree.behaviors.positioned)
if not positioned then return end if not positioned then return end
if not sprite then if not sprite then return end
love.graphics.setCanvas()
return
end
local ppm = Tree.level.camera.pixelsPerMeter local ppm = Tree.level.camera.pixelsPerMeter
local position = positioned.position + Vec3 { 0.5, 0.5 } local position = positioned.position + Vec3 { 0.5, 0.5 }
@ -25,41 +22,40 @@ function behavior:draw()
table.insert(lights, Tree.level.characters[id]) table.insert(lights, Tree.level.characters[id])
end end
-- 1. Эллипс тени
Tree.level.render:enqueue(Tree.level.render.LAYERS.SHADOW, position.y, function()
love.graphics.push()
love.graphics.setColor(0, 0, 0, 1)
love.graphics.translate(position.x, position.y)
love.graphics.ellipse("fill", 0, 0, sprite.manifest.size / 2, sprite.manifest.size / 2 * math.cos(math.pi / 4))
love.graphics.pop()
end)
Tree.level.camera:attach() -- 2. Свет на спрайте
love.graphics.setCanvas(Tree.level.render.textures.shadowLayer) if #lights > 0 then
love.graphics.push() Tree.level.render:enqueue(Tree.level.render.LAYERS.SPRITE_LIGHT, position.y, function()
love.graphics.setColor(0, 0, 0, 1) love.graphics.setBlendMode("add")
love.graphics.translate(position.x, position.y) for _, light in ipairs(lights) do
love.graphics.ellipse("fill", 0, 0, sprite.manifest.size / 2, sprite.manifest.size / 2 * math.cos(math.pi / 4)) local lightPos = light:has(Tree.behaviors.positioned).position
love.graphics.pop() local lightVec = lightPos - position
local lightColor = light:has(Tree.behaviors.light).color
if lightPos.y > position.y then
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
1 - 0.3 * lightVec:length())
elseif position.y - lightPos.y < 3 then
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
(1 - easing.easeInSine((position.y - lightPos.y))) - 0.3 * lightVec:length())
end
sprite.animationTable[sprite.state]:draw(sprite.sheets[sprite.state],
love.graphics.setCanvas(Tree.level.render.textures.spriteLightLayer) position.x,
love.graphics.setBlendMode("add") position.y, nil, 1 / ppm * sprite.side, 1 / ppm, sprite.manifest.base.x, sprite.manifest.base.y)
for _, light in ipairs(lights) do end
local lightPos = light:has(Tree.behaviors.positioned).position love.graphics.setBlendMode("alpha")
local lightVec = lightPos - position love.graphics.setColor(1, 1, 1)
end)
local lightColor = light:has(Tree.behaviors.light).color
if lightPos.y > position.y then
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
1 - 0.3 * lightVec:length())
elseif position.y - lightPos.y < 3 then
love.graphics.setColor(lightColor.x, lightColor.y, lightColor.z,
(1 - easing.easeInSine((position.y - lightPos.y))) - 0.3 * lightVec:length())
end
sprite.animationTable[sprite.state]:draw(sprite.sheets[sprite.state],
position.x,
position.y, nil, 1 / ppm * sprite.side, 1 / ppm, sprite.manifest.base.x, sprite.manifest.base.y)
end end
love.graphics.setBlendMode("alpha")
Tree.level.camera:detach()
love.graphics.setColor(1, 1, 1)
love.graphics.setCanvas()
end end
return behavior return behavior

View File

@ -49,25 +49,22 @@ function sprite:draw()
local ppm = Tree.level.camera.pixelsPerMeter local ppm = Tree.level.camera.pixelsPerMeter
local position = pos.position + Vec3 { 0.5, 0.5 } local position = pos.position + Vec3 { 0.5, 0.5 }
love.graphics.setCanvas(Tree.level.render.textures.spriteLayer) Tree.level.render:enqueue(Tree.level.render.LAYERS.SPRITE, position.y, function()
Tree.level.camera:attach() love.graphics.setColor(1, 1, 1)
if Tree.level.selector.id == self.owner.id then
local texW, texH = self.sheets[self.state]:getWidth(),
self.sheets[self.state]:getHeight()
local shader = Tree.assets.files.shaders.outline
shader:send("texSize", { texW, texH })
shader:send("time", love.timer:getTime())
love.graphics.setShader(shader)
end
self.animationTable[self.state]:draw(self.sheets[self.state],
position.x,
position.y, nil, 1 / ppm * self.side, 1 / ppm, self.manifest.base.x, self.manifest.base.y)
love.graphics.setColor(1, 1, 1) love.graphics.setShader()
if Tree.level.selector.id == self.owner.id then end)
local texW, texH = self.sheets[self.state]:getWidth(),
self.sheets[self.state]:getHeight()
local shader = Tree.assets.files.shaders.outline
shader:send("texSize", { texW, texH })
shader:send("time", love.timer:getTime())
love.graphics.setShader(shader)
end
self.animationTable[self.state]:draw(self.sheets[self.state],
position.x,
position.y, nil, 1 / ppm * self.side, 1 / ppm, self.manifest.base.x, self.manifest.base.y)
love.graphics.setShader()
Tree.level.camera:detach()
love.graphics.setCanvas()
end end
) )
end end

View File

@ -9,17 +9,32 @@ map.__index = map
--- @param size? Vec3 --- @param size? Vec3
local function new(type, template, size) local function new(type, template, size)
local tMap = require('lib.level.' .. type).new(template, size) local tMap = require('lib.level.' .. type).new(template, size)
return setmetatable({ __grid = tMap }, map) local grid = setmetatable({ __grid = tMap }, map)
grid:refreshBatch()
return grid
end
function map:refreshBatch()
-- Находим атлас первого попавшегося тайла (предполагаем, что он один для всех)
local _, firstTile = next(self.__grid)
if not firstTile then return end
local atlas = firstTile.atlasData.atlas
local count = 0
for _ in pairs(self.__grid) do count = count + 1 end
self.batch = love.graphics.newSpriteBatch(atlas, count)
for _, tile in pairs(self.__grid) do
-- 1/32 это масштаб, так как размер тайла в мире 1x1 метр, а в атласе 32x32 пикселя
self.batch:add(tile.atlasData.quad, tile.position.x, tile.position.y, 0, 1 / 32, 1 / 32)
end
end end
function map:draw() function map:draw()
love.graphics.setCanvas(Tree.level.render.textures.floorLayer) if not self.batch then return end
Tree.level.camera:attach() Tree.level.render:enqueue(Tree.level.render.LAYERS.FLOOR, 0, function()
utils.each(self.__grid, function(el) love.graphics.draw(self.batch)
el:draw()
end) end)
Tree.level.camera:detach()
love.graphics.setCanvas()
end end
return { new = new } return { new = new }

View File

@ -56,9 +56,9 @@ end
function level:draw() function level:draw()
self.render:clear() self.render:clear()
self.tileGrid:draw() self.tileGrid:draw()
while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n utils.each(self.characters, function(char)
self.characterGrid.yOrderQueue:pop():draw() char:draw()
end end)
self.render:draw() self.render:draw()
end end

View File

@ -1,12 +1,24 @@
--- @class Render --- @class Render
--- @field textures table<string, love.Canvas> --- @field textures table<string, love.Canvas>
--- @field queue table[]
local render = { local render = {
textures = {} textures = {},
queue = {},
LAYERS = {
FLOOR = 1,
SHADOW = 2,
LIGHT = 3,
SPRITE = 4,
SPRITE_LIGHT = 5,
OVERLAY = 6
}
} }
function render:clear() function render:clear()
local weather = Tree.level.weather local weather = Tree.level.weather
local txs = self.textures local txs = self.textures
self.queue = {}
love.graphics.setCanvas(txs.shadowLayer) love.graphics.setCanvas(txs.shadowLayer)
love.graphics.clear() love.graphics.clear()
love.graphics.setCanvas(txs.spriteLayer) love.graphics.setCanvas(txs.spriteLayer)
@ -21,6 +33,10 @@ function render:clear()
love.graphics.clear() love.graphics.clear()
end end
function render:enqueue(layer, z, func)
table.insert(self.queue, { layer = layer, z = z, func = func })
end
function render:free() function render:free()
for _, tx in pairs(self.textures) do for _, tx in pairs(self.textures) do
tx:release() tx:release()
@ -57,19 +73,98 @@ function render:applyBlur(input, radius)
return self.textures.tmp2 return self.textures.tmp2
end end
---@param params {w: number?, h: number?}
---@return table|Render
local function new(params)
local w = params.w or love.graphics.getWidth()
local h = params.h or love.graphics.getHeight()
local lowResScale = 0.5
return setmetatable({
lowResScale = lowResScale,
queue = {},
textures = {
shadowLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
spriteLayer = love.graphics.newCanvas(w, h),
spriteLightLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
floorLayer = love.graphics.newCanvas(w, h),
overlayLayer = love.graphics.newCanvas(w, h),
lightLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
tmp1 = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
tmp2 = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
}
}, { __index = render })
end
function render:draw() function render:draw()
local weather = Tree.level.weather local weather = Tree.level.weather
local txs = self.textures local txs = self.textures
love.graphics.setCanvas(txs.lightLayer)
love.graphics.draw(self:applyBlur(txs.shadowLayer, 4 * Tree.level.camera.scale)) -- 1. Сортировка очереди
table.sort(self.queue, function(a, b)
if a.layer ~= b.layer then
return a.layer < b.layer
end
return a.z < b.z
end)
-- 2. Рендеринг очереди в соответствующие Canvas
local currentLayer = nil
for _, entry in ipairs(self.queue) do
if entry.layer ~= currentLayer then
if currentLayer then
Tree.level.camera:detach()
local wasLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.SPRITE_LIGHT or
currentLayer == self.LAYERS.LIGHT
if wasLowRes then
love.graphics.pop()
end
end
currentLayer = entry.layer
local isLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.SPRITE_LIGHT or
currentLayer == self.LAYERS.LIGHT
if currentLayer == self.LAYERS.FLOOR then
love.graphics.setCanvas(txs.floorLayer)
elseif currentLayer == self.LAYERS.SHADOW then
love.graphics.setCanvas(txs.shadowLayer)
elseif currentLayer == self.LAYERS.LIGHT then
love.graphics.setCanvas(txs.lightLayer)
elseif currentLayer == self.LAYERS.SPRITE then
love.graphics.setCanvas(txs.spriteLayer)
elseif currentLayer == self.LAYERS.SPRITE_LIGHT then
love.graphics.setCanvas(txs.spriteLightLayer)
elseif currentLayer == self.LAYERS.OVERLAY then
love.graphics.setCanvas(txs.overlayLayer)
end
if isLowRes then
love.graphics.push()
love.graphics.scale(self.lowResScale)
end
Tree.level.camera:attach()
end
entry.func()
end
if currentLayer then
Tree.level.camera:detach()
local wasLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.SPRITE_LIGHT or
currentLayer == self.LAYERS.LIGHT
if wasLowRes then
love.graphics.pop()
end
end
love.graphics.setCanvas() love.graphics.setCanvas()
-- self.lightLayer:newImageData():encode("png", "lightLayer.png") -- 3. Пост-процессинг и композиция
-- os.exit(0) love.graphics.setCanvas(txs.lightLayer)
-- Радиус блюра тоже масштабируем, так как текстура меньше
love.graphics.draw(self:applyBlur(txs.shadowLayer, 4 * Tree.level.camera.scale * self.lowResScale))
love.graphics.setCanvas()
local lightShader = Tree.assets.files.shaders.light_postprocess local lightShader = Tree.assets.files.shaders.light_postprocess
lightShader:send("scene", txs.floorLayer) lightShader:send("scene", txs.floorLayer)
lightShader:send("light", self:applyBlur(txs.lightLayer, 2)) lightShader:send("light", self:applyBlur(txs.lightLayer, 2 * self.lowResScale))
lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z }) lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z })
love.graphics.setShader(lightShader) love.graphics.setShader(lightShader)
love.graphics.draw(txs.floorLayer) love.graphics.draw(txs.floorLayer)
@ -84,23 +179,4 @@ function render:draw()
love.graphics.setShader() love.graphics.setShader()
end end
---@param params {w: number?, h: number?}
---@return table|Render
local function new(params)
local w = params.w or love.graphics.getWidth()
local h = params.h or love.graphics.getHeight()
return setmetatable({
textures = {
shadowLayer = love.graphics.newCanvas(w, h),
spriteLayer = love.graphics.newCanvas(w, h),
spriteLightLayer = love.graphics.newCanvas(w, h),
floorLayer = love.graphics.newCanvas(w, h),
overlayLayer = love.graphics.newCanvas(w, h),
lightLayer = love.graphics.newCanvas(w, h),
tmp1 = love.graphics.newCanvas(w, h),
tmp2 = love.graphics.newCanvas(w, h),
}
}, { __index = render })
end
return { new = new } return { new = new }

View File

@ -54,33 +54,28 @@ function spell:draw()
local path = self.path --[[@as Deque?]] local path = self.path --[[@as Deque?]]
if not path then return end if not path then return end
--- Это отрисовка пути персонажа к мышке --- Это отрисовка пути персонажа к мышке
Tree.level.camera:attach() Tree.level.render:enqueue(Tree.level.render.LAYERS.OVERLAY, 0, function()
love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) local i = 0
local i = 0 path:pop_front()
path:pop_front() for p in path:values() do
for p in path:values() do i = i + 1
i = i + 1 local s = 1 / Tree.level.camera.pixelsPerMeter
local s = 1 / Tree.level.camera.pixelsPerMeter local quad = i > self.distance and icons:pickQuad('dev_path_closed') or icons:pickQuad('dev_path')
local quad = i > self.distance and icons:pickQuad('dev_path_closed') or icons:pickQuad('dev_path') love.graphics.draw(icons.atlas, quad, p.x, p.y, 0, s, s)
love.graphics.draw(icons.atlas, quad, p.x, p.y, 0, s, s) end
end love.graphics.setColor(1, 1, 1)
love.graphics.setCanvas() end)
Tree.level.camera:detach()
love.graphics.setColor(1, 1, 1)
else else
Tree.level.camera:attach() Tree.level.render:enqueue(Tree.level.render.LAYERS.OVERLAY, 0, function()
love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) love.graphics.setColor(1, 1, 1, 0.5)
love.graphics.setColor(1, 1, 1, 0.5) for _, p in pairs(self.targets) do
for _, p in pairs(self.targets) do local s = self.tSize / Tree.level.camera.pixelsPerMeter
local s = self.tSize / Tree.level.camera.pixelsPerMeter local quad = icons:pickQuad('dev_target')
local quad = icons:pickQuad('dev_target') love.graphics.draw(icons.atlas, quad, p.x + 0.5 - self.tSize / 2, p.y + 0.5 - self.tSize / 2, 0, s, s)
love.graphics.draw(icons.atlas, quad, p.x + 0.5 - self.tSize / 2, p.y + 0.5 - self.tSize / 2, 0, s, s) end
end love.graphics.setShader()
love.graphics.setShader() love.graphics.setColor(1, 1, 1)
end)
love.graphics.setCanvas()
Tree.level.camera:detach()
love.graphics.setColor(1, 1, 1)
end end
end end

View File

@ -130,5 +130,5 @@ function love.resize(w, h)
local render = Tree.level.render local render = Tree.level.render
if not render then return end if not render then return end
render:free() render:free()
Tree.level.render = (require "lib.level.render").new { w, h } Tree.level.render = (require "lib.level.render").new { w = w, h = h }
end end