diff --git a/assets/shaders/sprite_light.glsl b/assets/shaders/sprite_light.glsl new file mode 100644 index 0000000..f55b5ae --- /dev/null +++ b/assets/shaders/sprite_light.glsl @@ -0,0 +1,85 @@ +#define MAX_LIGHTS 8 + +struct Light { + vec2 position; + vec3 color; + float radius; +}; + +extern Light lights[MAX_LIGHTS]; +extern int num_lights; +extern vec2 sprite_pos; // Мировая позиция спрайта (в метрах) +extern vec3 ambient; // Эмбиентное освещение +extern vec3 sky; // Цвет неба + +// Параметры выделения +extern bool is_selected; +extern vec2 tex_size; +extern float time; + +// Функция для имитации easing.easeInSine +float easeInSine(float x) { + return 1.0 - cos((x * 3.14159) / 2.0); +} + +vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords) +{ + vec4 texColor = Texel(tex, texture_coords); + + // ЛОГИКА ОБВОДКИ (Outline) + // Мы выполняем ее до расчетов освещения. Если пиксель прозрачный и объект выбран, + // проверяем соседей, чтобы понять, не граница ли это. + // Обводка рисуется "самосветящейся", чтобы выделение было видно даже в полной темноте. + if (is_selected && texColor.a <= 0.0) { + float maxAlpha = 0.0; + + // Проверяем соседние пиксели (квадратом 3x3) + for (float x = -1.0; x <= 1.0; x++) { + for (float y = -1.0; y <= 1.0; y++) { + if (x == 0.0 && y == 0.0) continue; + vec2 offset = vec2(x, y) / tex_size; + maxAlpha = max(maxAlpha, Texel(tex, texture_coords + offset).a); + } + } + + if (maxAlpha > 0.0) { + // Эффект пульсации и "бегущей волны" из оригинального шейдера outline.glsl + float modY = (0.75 + sin(time) * 0.25) * (0.5 + cos(texture_coords.y * 10.0 + time * 2.0) * 0.5); + return vec4(vec3(modY, 0.2 * sin(time) + 0.5, 0.5), 1.0); + } + } + + if (texColor.a == 0.0) { + return vec4(0.0); + } + + // Расчет базового освещения мира: персонаж освещается смесью неба (Sky) и отраженного света (Ambient). + vec3 baseLight = ambient + (vec3(1.0) - ambient) * sky; + + // Десатурация базового света на 30%. + float luma = dot(baseLight, vec3(0.2126, 0.7152, 0.0722)); + vec3 characterBaseLight = mix(baseLight, vec3(luma), 0.3); + + vec3 pointLight = vec3(0.0); + + for (int i = 0; i < num_lights; i++) { + vec2 lightPos = lights[i].position; + float radius = lights[i].radius; + vec2 lightVec = lightPos - sprite_pos; + float dist = length(lightVec); + + // Плавное затухание света по радиусу. + float radiusFalloff = smoothstep(radius, radius * 0.2, dist); + + // Реализация псевдо-проекции (3D-эффект): + float frontWeight = smoothstep(-2.0, 0.5, lightVec.y); + + float attenuation = radiusFalloff * frontWeight; + pointLight += lights[i].color * attenuation; + } + + pointLight = clamp(pointLight, 0.0, 1.0); + + // Финальный расчет цвета: + return vec4(texColor.rgb * (characterBaseLight + pointLight), texColor.a); +} diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua index 1f8b9d8..dd26adc 100644 --- a/lib/character/behaviors/light.lua +++ b/lib/character/behaviors/light.lua @@ -32,22 +32,19 @@ function behavior:draw() local positioned = self.owner:has(Tree.behaviors.positioned) if not positioned then return end - love.graphics.setBlendMode("add", "premultiplied") - Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.textures.lightLayer) - local shader = Tree.assets.files.shaders.light - shader:send("color", { self.color.x, self.color.y, self.color.z }) - shader:send("time", love.timer.getTime() + self.seed) - love.graphics.setShader(shader) - love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2, - positioned.position.y - self.intensity / 2, 0, self.intensity / 128, - self.intensity / 128) + Tree.level.render:enqueue(Tree.level.render.LAYERS.LIGHT, positioned.position.y, function() + love.graphics.setBlendMode("add", "premultiplied") + local shader = Tree.assets.files.shaders.light + shader:send("color", { self.color.x, self.color.y, self.color.z }) + shader:send("time", love.timer:getTime() + self.seed) + love.graphics.setShader(shader) + love.graphics.draw(Tree.assets.files.masks.circle128, positioned.position.x - self.intensity / 2, + positioned.position.y - self.intensity / 2, 0, self.intensity / 128, + self.intensity / 128) - love.graphics.setBlendMode("alpha") - - love.graphics.setShader() - love.graphics.setCanvas() - Tree.level.camera:detach() + love.graphics.setShader() + love.graphics.setBlendMode("alpha") + end) end return behavior diff --git a/lib/character/behaviors/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua index c343cf8..0b2daeb 100644 --- a/lib/character/behaviors/shadowcaster.lua +++ b/lib/character/behaviors/shadowcaster.lua @@ -10,10 +10,7 @@ function behavior:draw() local sprite = self.owner:has(Tree.behaviors.sprite) local positioned = self.owner:has(Tree.behaviors.positioned) if not positioned then return end - if not sprite then - love.graphics.setCanvas() - return - end + if not sprite then return end local ppm = Tree.level.camera.pixelsPerMeter local position = positioned.position + Vec3 { 0.5, 0.5 } @@ -25,41 +22,14 @@ function behavior:draw() table.insert(lights, Tree.level.characters[id]) end - - Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.textures.shadowLayer) - 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() - - - - love.graphics.setCanvas(Tree.level.render.textures.spriteLightLayer) - love.graphics.setBlendMode("add") - for _, light in ipairs(lights) do - local lightPos = light:has(Tree.behaviors.positioned).position - 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], - position.x, - position.y, nil, 1 / ppm * sprite.side, 1 / ppm, sprite.manifest.base.x, sprite.manifest.base.y) - end - love.graphics.setBlendMode("alpha") - - Tree.level.camera:detach() - love.graphics.setColor(1, 1, 1) - love.graphics.setCanvas() + -- 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) end return behavior diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 0325a4b..318f2ed 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -49,25 +49,71 @@ function sprite:draw() local ppm = Tree.level.camera.pixelsPerMeter local position = pos.position + Vec3 { 0.5, 0.5 } - love.graphics.setCanvas(Tree.level.render.textures.spriteLayer) - Tree.level.camera:attach() + Tree.level.render:enqueue(Tree.level.render.LAYERS.SPRITE, position.y, function() + love.graphics.setColor(1, 1, 1) - 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) + -- Собираем источники света для шейдера + local queryRadius = 12 -- Увеличенный радиус для плавности + local lightIds = Tree.level.lightGrid:query(position, queryRadius) + local lightsData = {} - love.graphics.setShader() - Tree.level.camera:detach() - love.graphics.setCanvas() + for _, id in ipairs(lightIds) do + local lightChar = Tree.level.characters[id] + local b = lightChar:has(Tree.behaviors.light) --[[@as LightBehavior]] + local lPos = lightChar:has(Tree.behaviors.positioned).position + local dist = (lPos - position):length() + + -- Берем только те, что могут дотянуться до нас своим радиусом + if dist < b.intensity + 2 then + table.insert(lightsData, { + x = lPos.x, + y = lPos.y, + r = b.color.x, + g = b.color.y, + b = b.color.z, + radius = b.intensity, + dist = dist + }) + end + end + + -- Сортируем по дистанции, чтобы выбрать 8 самых влиятельных + table.sort(lightsData, function(a, b) return a.dist < b.dist end) + + local lightShader = Tree.assets.files.shaders.sprite_light + local weather = Tree.level.weather + local numLights = math.min(#lightsData, 8) + local isSelected = (Tree.level.selector.id == self.owner.id) + + lightShader:send("sprite_pos", { position.x, position.y }) + lightShader:send("sky", { weather.skyLight.x, weather.skyLight.y, weather.skyLight.z }) + lightShader:send("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z }) + lightShader:send("num_lights", numLights) + + lightShader:send("is_selected", isSelected) + if isSelected then + local sheet = self.sheets[self.state] + lightShader:send("tex_size", { sheet:getWidth(), sheet:getHeight() }) + lightShader:send("time", love.timer:getTime()) + end + + for i = 1, numLights do + local l = lightsData[i] + local idx = i - 1 + lightShader:send(string.format("lights[%d].position", idx), { l.x, l.y }) + lightShader:send(string.format("lights[%d].color", idx), { l.r, l.g, l.b }) + lightShader:send(string.format("lights[%d].radius", idx), l.radius) + end + + + love.graphics.setShader(lightShader) + + 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() + end) end ) end diff --git a/lib/level/grid/tile_grid.lua b/lib/level/grid/tile_grid.lua index 1a89a10..8982abb 100644 --- a/lib/level/grid/tile_grid.lua +++ b/lib/level/grid/tile_grid.lua @@ -9,17 +9,32 @@ map.__index = map --- @param size? Vec3 local function new(type, 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 function map:draw() - love.graphics.setCanvas(Tree.level.render.textures.floorLayer) - Tree.level.camera:attach() - utils.each(self.__grid, function(el) - el:draw() + if not self.batch then return end + Tree.level.render:enqueue(Tree.level.render.LAYERS.FLOOR, 0, function() + love.graphics.draw(self.batch) end) - Tree.level.camera:detach() - love.graphics.setCanvas() end return { new = new } diff --git a/lib/level/level.lua b/lib/level/level.lua index 67945eb..16238e7 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -56,9 +56,9 @@ end function level:draw() self.render:clear() self.tileGrid:draw() - while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n - self.characterGrid.yOrderQueue:pop():draw() - end + utils.each(self.characters, function(char) + char:draw() + end) self.render:draw() end diff --git a/lib/level/render.lua b/lib/level/render.lua index b0901d8..8cf636b 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -1,18 +1,30 @@ --- @class Render --- @field textures table +--- @field queue table[] +--- @field lowResScale number +--- @field LAYERS table local render = { - textures = {} + textures = {}, + queue = {}, + lowResScale = 1.0, + LAYERS = { + FLOOR = 1, + SHADOW = 2, + LIGHT = 3, + SPRITE = 4, + OVERLAY = 5 + } } function render:clear() local weather = Tree.level.weather local txs = self.textures + self.queue = {} + love.graphics.setCanvas(txs.shadowLayer) love.graphics.clear() love.graphics.setCanvas(txs.spriteLayer) love.graphics.clear() - love.graphics.setCanvas(txs.spriteLightLayer) - love.graphics.clear(weather.skyLight.x, weather.skyLight.y, weather.skyLight.z) love.graphics.setCanvas(txs.floorLayer) love.graphics.clear() love.graphics.setCanvas(txs.lightLayer) @@ -21,6 +33,10 @@ function render:clear() love.graphics.clear() end +function render:enqueue(layer, z, func) + table.insert(self.queue, { layer = layer, z = z, func = func }) +end + function render:free() for _, tx in pairs(self.textures) do tx:release() @@ -57,50 +73,102 @@ function render:applyBlur(input, radius) return self.textures.tmp2 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), + 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() local weather = Tree.level.weather 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.LIGHT + if wasLowRes then + love.graphics.pop() + end + end + currentLayer = entry.layer + local isLowRes = currentLayer == self.LAYERS.SHADOW 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.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.LIGHT + if wasLowRes then + love.graphics.pop() + end + end love.graphics.setCanvas() - -- self.lightLayer:newImageData():encode("png", "lightLayer.png") - -- os.exit(0) + -- 3. Пост-процессинг и композиция + 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 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 }) love.graphics.setShader(lightShader) love.graphics.draw(txs.floorLayer) love.graphics.setShader() love.graphics.draw(txs.overlayLayer) - love.graphics.setShader(lightShader) - lightShader:send("scene", txs.spriteLayer) - lightShader:send("light", txs.spriteLightLayer) + -- Спрайты уже полностью освещены в SpriteBehavior (с учетом ambient и point lights) + -- Поэтому рисуем их "как есть" love.graphics.draw(txs.spriteLayer) - love.graphics.setShader() -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 } diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index 5ef80f0..e57ffba 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -54,33 +54,28 @@ function spell:draw() local path = self.path --[[@as Deque?]] if not path then return end --- Это отрисовка пути персонажа к мышке - Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) - local i = 0 - path:pop_front() - for p in path:values() do - i = i + 1 - local s = 1 / Tree.level.camera.pixelsPerMeter - 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) - end - love.graphics.setCanvas() - Tree.level.camera:detach() - love.graphics.setColor(1, 1, 1) + Tree.level.render:enqueue(Tree.level.render.LAYERS.OVERLAY, 0, function() + local i = 0 + path:pop_front() + for p in path:values() do + i = i + 1 + local s = 1 / Tree.level.camera.pixelsPerMeter + 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) + end + love.graphics.setColor(1, 1, 1) + end) else - Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) - love.graphics.setColor(1, 1, 1, 0.5) - for _, p in pairs(self.targets) do - local s = self.tSize / Tree.level.camera.pixelsPerMeter - 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) - end - love.graphics.setShader() - - love.graphics.setCanvas() - Tree.level.camera:detach() - love.graphics.setColor(1, 1, 1) + Tree.level.render:enqueue(Tree.level.render.LAYERS.OVERLAY, 0, function() + love.graphics.setColor(1, 1, 1, 0.5) + for _, p in pairs(self.targets) do + local s = self.tSize / Tree.level.camera.pixelsPerMeter + 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) + end + love.graphics.setShader() + love.graphics.setColor(1, 1, 1) + end) end end diff --git a/main.lua b/main.lua index f2e56ad..112fc59 100644 --- a/main.lua +++ b/main.lua @@ -62,6 +62,14 @@ function love.load() Tree.level.turnOrder:add(id) end + -- --- Это тестовый источник света, привязанный к мышке, и я очень прошу его не трогать + -- character.spawn("light") + -- :addBehavior { + -- Tree.behaviors.positioned.new(), + -- Tree.behaviors.cursor.new(), + -- Tree.behaviors.light.new { color = Vec3 { 0.5, 0.5, 0.5 }, intensity = 5 } + -- } + Tree.level.turnOrder:endRound() print("Now playing:", Tree.level.turnOrder.current) @@ -130,5 +138,5 @@ function love.resize(w, h) local render = Tree.level.render if not render then return end 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