diff --git a/assets/masks/circle128.png b/assets/masks/circle128.png new file mode 100644 index 0000000..007a29d Binary files /dev/null and b/assets/masks/circle128.png differ diff --git a/assets/masks/gradientCircle256.png b/assets/masks/gradientCircle256.png new file mode 100644 index 0000000..d93611c Binary files /dev/null and b/assets/masks/gradientCircle256.png differ diff --git a/assets/shaders/light.glsl b/assets/shaders/light.glsl new file mode 100644 index 0000000..3b28fb3 --- /dev/null +++ b/assets/shaders/light.glsl @@ -0,0 +1,30 @@ +extern vec3 color; +extern number time; + +vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords) +{ + vec4 texColor = Texel(tex, texture_coords); + + float mask = texColor.r; + + vec2 uv = texture_coords - 0.5; + float dist = length(uv * 2.0); + + float t = time; + + float wave = sin((uv.x + uv.y) * 6.0 + t * 1.5) * 0.03; + float ripple = sin(length(uv) * 20.0 - t * 2.0) * 0.02; + float flicker = sin(t * 2.5) * 0.02; + + dist += wave + ripple + flicker; + + float intensity = 1.0 - smoothstep(0.0, 1.0, dist); + intensity = pow(intensity, 2.0); + + float colorShift = sin(t * 3.0) * 0.1; + vec3 flickerColor = color + vec3(colorShift, colorShift * 0.5, -colorShift * 0.3); + + vec3 finalColor = flickerColor * intensity * mask; + + return vec4(finalColor, mask * intensity); +} diff --git a/assets/shaders/light_postprocess.glsl b/assets/shaders/light_postprocess.glsl new file mode 100644 index 0000000..7e046f7 --- /dev/null +++ b/assets/shaders/light_postprocess.glsl @@ -0,0 +1,20 @@ +extern Image scene; +extern Image light; + +extern vec3 ambient; + +vec4 effect(vec4 vcolor, Image unused, vec2 uv, vec2 px) +{ + vec4 s = Texel(scene, uv); + vec3 l = Texel(light, uv).rgb; + + l = clamp(l, 0.0, 1.0); + vec3 a = clamp(ambient, 0.0, 1.0); + + // Канальный множитель: от ambient до 1 в зависимости от света + vec3 m = a + (vec3(1.0) - a) * l; + + vec3 rgb = s.rgb * m; + + return vec4(rgb, s.a); +} diff --git a/lib/annotations.lua b/lib/annotations.lua index 944093b..63bfea7 100644 --- a/lib/annotations.lua +++ b/lib/annotations.lua @@ -5,3 +5,4 @@ Tree.behaviors.sprite = require "lib.character.behaviors.sprite" Tree.behaviors.stats = require "lib.character.behaviors.stats" Tree.behaviors.residentsleeper = require "lib.character.behaviors.residentsleeper" Tree.behaviors.shadowcaster = require "lib.character.behaviors.shadowcaster" +Tree.behaviors.light = require "character.behaviors.light" diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua new file mode 100644 index 0000000..6f40dd4 --- /dev/null +++ b/lib/character/behaviors/light.lua @@ -0,0 +1,45 @@ +--- @class LightBehavior : Behavior +--- @field intensity number +--- @field color Vec3 +--- @field position Vec3 +--- @field seed integer +local behavior = {} +behavior.__index = behavior +behavior.id = "light" + +---@param values {intensity: number?, color: Vec3?, position: Vec3?, seed: integer?} +---@return LightBehavior +function behavior.new(values) + return setmetatable({ + intensity = values.intensity or 1, + color = values.color or Vec3 { 1, 1, 1 }, + position = values.position or Vec3 {}, + seed = values.seed or math.random(math.pow(2, 16)) + }, behavior) +end + +function behavior:update() + local mx, my = love.mouse.getX(), love.mouse.getY() + self.position = Tree.level.camera:toWorldPosition(Vec3 { mx, my }) +end + +function behavior:draw() + Tree.level.camera:attach() + love.graphics.setCanvas(Tree.level.render.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.setBlendMode("add") + love.graphics.draw(Tree.assets.files.masks.circle128, self.position.x - self.intensity / 2, + self.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() +end + +return behavior diff --git a/lib/character/behaviors/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua index d1c833a..dbbf915 100644 --- a/lib/character/behaviors/shadowcaster.lua +++ b/lib/character/behaviors/shadowcaster.lua @@ -1,3 +1,4 @@ +local easing = require "lib.utils.easing" --- @class ShadowcasterBehavior : Behavior local behavior = {} behavior.id = "shadowcaster" @@ -5,50 +6,6 @@ behavior.__index = behavior function behavior.new() return setmetatable({}, behavior) end -local function makeGradientMesh(w, h, topColor, bottomColor) - local vertices = { - { 0, 0, 0, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- левый верх - { w, 0, 1, 0, topColor[1], topColor[2], topColor[3], topColor[4] }, -- правый верх - { w + w * 0.1, h, 1, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- правый низ - { 0 - w * 0.1, h, 0, 1, bottomColor[1], bottomColor[2], bottomColor[3], bottomColor[4] }, -- левый низ - } - local mesh = love.graphics.newMesh(vertices, "fan", "static") - return mesh -end - ---- @param phi number угол, под которым падает свет ---- @return boolean, number: рисуем ли тень * её прозрачность -local function getFakeShadow(phi) - local pi = math.pi - - if phi <= pi / 4 then - -- 1 - return false, 1 - end - if phi <= pi / 2 then - -- 2 - return true, 1 - (phi - pi / 4) / (pi / 4) - end - if phi <= 3 * pi / 4 then - -- 3 - return true, (phi - pi / 2) / (pi / 4) - end - if phi <= 5 * pi / 4 then - -- 4 - return false, 1 - end - if phi <= 3 * pi / 2 then - -- 5 - return true, 1 - (phi - 5 * pi / 4) / (pi / 4) - end - if phi <= 7 * pi / 4 then - -- 6 - return true, (phi - 3 * pi / 2) / (pi / 4) - end - -- 1 - return false, 1 -end - function behavior:draw() local sprite = self.owner:has(Tree.behaviors.sprite) local map = self.owner:has(Tree.behaviors.map) @@ -57,42 +14,48 @@ function behavior:draw() local ppm = Tree.level.camera.pixelsPerMeter local position = map.displayedPosition + Vec3 { 0.5, 0.5 } + local lightIds = Tree.level.lightGrid:query(position, 5) + --- @type Character[] + local lights = {} + for _, id in ipairs(lightIds) do + table.insert(lights, Tree.level.characters[id]) + end + + Tree.level.camera:attach() love.graphics.setCanvas(Tree.level.render.shadowLayer) love.graphics.push() - love.graphics.scale(ppm) - love.graphics.setColor(0, 0, 0, 0.5) + love.graphics.setColor(0, 0, 0, 1) love.graphics.translate(position.x, position.y) love.graphics.ellipse("fill", 0, 0, 0.2, 0.2 * math.cos(math.pi / 4)) + love.graphics.pop() if not sprite then - love.graphics.pop() + love.graphics.setCanvas() return end - local phi = love.timer.getTime() % (2 * math.pi) + love.graphics.setCanvas(Tree.level.render.spriteLightLayer) + love.graphics.setBlendMode("add") + for _, light in ipairs(lights) do + local lightPos = light:has(Tree.behaviors.light).position + local lightVec = lightPos - position - local drawFakeShadow, opacity = getFakeShadow(phi) - local nangle = (math.pi + phi) % (2 * math.pi) - love.graphics.rotate(nangle) + 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 - love.graphics.setColor(0, 0, 0, math.min(opacity * opacity, 0.5)) - sprite.animationTable[sprite.state]:draw(Tree.assets.files.sprites.character[sprite.state], - 0, - 0, nil, ((nangle >= math.pi / 2 and nangle < (3 * math.pi / 2)) and -1 or 1) / ppm * sprite.side, - 1.2 / ppm, - 38, 47) - - if drawFakeShadow then - love.graphics.setColor(0, 0, 0, 1) - local mesh = makeGradientMesh(0.4, 1, { 0, 0, 0, 0.15 }, - { 0, 0, 0, 0.0 }) - love.graphics.push() - love.graphics.rotate(math.pi) - love.graphics.translate(-0.2, 0) - love.graphics.draw(mesh) - love.graphics.pop() + sprite.animationTable[sprite.state]:draw(Tree.assets.files.sprites.character[sprite.state], + position.x, + position.y, nil, 1 / ppm * sprite.side, 1 / ppm, 38, 47) end - love.graphics.pop() + love.graphics.setBlendMode("alpha") + + Tree.level.camera:detach() love.graphics.setColor(1, 1, 1) love.graphics.setCanvas() end diff --git a/lib/character/character.lua b/lib/character/character.lua index b5344ba..d64f9d0 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -13,11 +13,7 @@ character.__index = character --- Создаёт персонажа, которым будет управлять или игрок или компьютер --- @param name string ---- @param spriteDir table ---- @param position? Vec3 ---- @param size? Vec3 ---- @param initiative? integer -local function spawn(name, spriteDir, position, size, initiative) +local function spawn(name) local char = {} char = setmetatable(char, character) @@ -26,15 +22,6 @@ local function spawn(name, spriteDir, position, size, initiative) char.behaviors = {} char._behaviorsIdx = {} - char:addBehavior { - Tree.behaviors.residentsleeper.new(), - Tree.behaviors.stats.new(nil, nil, initiative), - Tree.behaviors.map.new(position, size), - Tree.behaviors.sprite.new(spriteDir), - Tree.behaviors.shadowcaster.new(), - Tree.behaviors.spellcaster.new() - } - Tree.level.characters[char.id] = char return char end diff --git a/lib/level/grid/character_grid.lua b/lib/level/grid/character_grid.lua index b21772b..e04c357 100644 --- a/lib/level/grid/character_grid.lua +++ b/lib/level/grid/character_grid.lua @@ -29,10 +29,12 @@ end --- @param a Character --- @param b Character local function drawCmp(a, b) - -- здесь персонажи гарантированно имеют нужное поведение - return a:has(Tree.behaviors.map).displayedPosition.y + --- @TODO: это захардкожено, надо разделить поведения + return (a:has(Tree.behaviors.map) and a:has(Tree.behaviors.map).displayedPosition.y or + a:has(Tree.behaviors.light).position.y) < - b:has(Tree.behaviors.map).displayedPosition.y + (b:has(Tree.behaviors.map) and b:has(Tree.behaviors.map).displayedPosition.y or + b:has(Tree.behaviors.light).position.y) end --- fills the grid with the actual data diff --git a/lib/level/grid/light_grid.lua b/lib/level/grid/light_grid.lua new file mode 100644 index 0000000..ace03d7 --- /dev/null +++ b/lib/level/grid/light_grid.lua @@ -0,0 +1,64 @@ +local utils = require "lib.utils.utils" +--- Пометровая сетка источников света, чтобы быстро искать ближайшие для некоторого объекта +--- @class LightGrid : Grid +--- @field __grid {string: [Id]} +local grid = setmetatable({}, require "lib.level.grid.base") +grid.__index = grid + +--- Adds a character id to the grid +--- @private +--- @param id Id +function grid:add(id) + local character = Tree.level.characters[id] + if not character then return end + + local lightB = character:has(Tree.behaviors.light) + if not lightB then return end + + + + local key = tostring(Vec3 { lightB.position.x, lightB.position.y }:floor()) + if not self.__grid[key] then self.__grid[key] = {} end + table.insert(self.__grid[key], character.id) +end + +--- fills the grid with the actual data +--- +--- should be called as early as possible during every tick +function grid:reload() + self:reset() + utils.each(Tree.level.characters, function(c) + self:add(c.id) + end) +end + +--- Возвращает все источники света, которые находятся в пределах круга с диаметром [distance] в [метрике Чебышёва](https://ru.wikipedia.org/wiki/Расстояние_Чебышёва) +--- @param position Vec3 +--- @param distance integer +function grid:query(position, distance) + --- @type Id[] + local res = {} + local topLeft = position:subtract(Vec3 { distance / 2, distance / 2 }):floor() + for i = 0, distance, 1 do + for j = 0, distance, 1 do + --- @type Id[]? + local lights = self:get(topLeft:add(Vec3 { i, j })) + if lights then + for _, lightChar in ipairs(lights) do + table.insert(res, lightChar) + end + end + end + end + return res +end + +--- Generates an empty grid +--- @return LightGrid +local function new() + return setmetatable({ + __grid = {} + }, grid) +end + +return { new = new } diff --git a/lib/level/level.lua b/lib/level/level.lua index 1d8cf96..00c425c 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -4,6 +4,7 @@ local utils = require "lib.utils.utils" --- @field size Vec3 --- @field characters Character[] --- @field characterGrid CharacterGrid +--- @field lightGrid LightGrid --- @field selector Selector --- @field camera Camera --- @field tileGrid TileGrid @@ -12,8 +13,6 @@ local utils = require "lib.utils.utils" local level = {} level.__index = level -local path = nil - --- @param type "procedural"|"handmaded" --- @param template Procedural|Handmaded local function new(type, template) @@ -23,6 +22,7 @@ local function new(type, template) size = size, characters = {}, characterGrid = (require "lib.level.grid.character_grid").new(), + lightGrid = (require "lib.level.grid.light_grid").new(), tileGrid = (require "lib.level.grid.tile_grid").new(type, template, size), selector = (require "lib.level.selector").new(), camera = (require "lib.level.camera").new(), @@ -33,6 +33,7 @@ end function level:update(dt) self.characterGrid:reload() + self.lightGrid:reload() utils.each(self.characters, function(el) el:update(dt) end) diff --git a/lib/level/render.lua b/lib/level/render.lua index dd4425f..27d77b3 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -1,7 +1,9 @@ --- @class Render --- @field shadowLayer love.Canvas --- @field spriteLayer love.Canvas +--- @field spriteLightLayer love.Canvas --- @field floorLayer love.Canvas +--- @field lightLayer love.Canvas --- @field overlayLayer love.Canvas local render = {} render.__index = render @@ -11,8 +13,12 @@ function render:clear() love.graphics.clear() love.graphics.setCanvas(self.spriteLayer) love.graphics.clear() + love.graphics.setCanvas(self.spriteLightLayer) + love.graphics.clear() love.graphics.setCanvas(self.floorLayer) love.graphics.clear() + love.graphics.setCanvas(self.lightLayer) + love.graphics.clear() love.graphics.setCanvas(self.overlayLayer) love.graphics.clear() end @@ -48,20 +54,26 @@ local function applyBlur(input, radius) end function render:draw() - -- пол -> тени -> спрайты -> оверлей + -- пол -> тени -> спрайты -> свет -> оверлей + love.graphics.setCanvas(self.lightLayer) + love.graphics.draw(applyBlur(self.shadowLayer, 4 * Tree.level.camera.scale)) + love.graphics.setCanvas() + + -- self.lightLayer:newImageData():encode("png", "lightLayer.png") + -- os.exit(0) + + local lightShader = Tree.assets.files.shaders.light_postprocess + lightShader:send("scene", self.floorLayer) + lightShader:send("light", self.lightLayer) + lightShader:send("ambient", { 0.24, 0.28, 0.40 }) + love.graphics.setShader(lightShader) love.graphics.draw(self.floorLayer) - love.graphics.push() - local blurred = applyBlur(self.shadowLayer, 2) - local wc, hc = love.graphics.getWidth() / 2, love.graphics.getHeight() / 2 - love.graphics.translate(wc, hc) - love.graphics.scale(Tree.level.camera.scale, Tree.level.camera.scale) - love.graphics.translate(-Tree.level.camera.position.x * Tree.level.camera.pixelsPerMeter, - -Tree.level.camera.position.y * Tree.level.camera.pixelsPerMeter) - love.graphics.draw(blurred) - love.graphics.pop() + lightShader:send("scene", self.spriteLayer) + lightShader:send("light", self.spriteLightLayer) love.graphics.draw(self.spriteLayer) + love.graphics.setShader() love.graphics.draw(self.overlayLayer) end @@ -70,8 +82,10 @@ local function new() return setmetatable({ shadowLayer = love.graphics.newCanvas(1280, 720), spriteLayer = love.graphics.newCanvas(1280, 720), + spriteLightLayer = love.graphics.newCanvas(1280, 720), floorLayer = love.graphics.newCanvas(1280, 720), overlayLayer = love.graphics.newCanvas(1280, 720), + lightLayer = love.graphics.newCanvas(1280, 720) }, render) end diff --git a/main.lua b/main.lua index e77cdba..506dbb1 100644 --- a/main.lua +++ b/main.lua @@ -9,13 +9,59 @@ function love.conf(t) end function love.load() - character.spawn("Foodor", Tree.assets.files.sprites.character, nil, nil, 1) - character.spawn("Baris", Tree.assets.files.sprites.character, Vec3 { 3, 3 }, nil, 2) - character.spawn("Foodor Jr", Tree.assets.files.sprites.character, Vec3 { 0, 3 }, nil, 3) - character.spawn("Baris Jr", Tree.assets.files.sprites.character, Vec3 { 0, 6 }, nil, 4) - for id, _ in pairs(Tree.level.characters) do + local chars = { + character.spawn("Foodor") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 1), + Tree.behaviors.map.new(), + Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), + Tree.behaviors.shadowcaster.new(), + Tree.behaviors.spellcaster.new() + }, + character.spawn("Baris") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 2), + Tree.behaviors.map.new(Vec3 { 3, 3 }), + Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), + Tree.behaviors.shadowcaster.new(), + Tree.behaviors.spellcaster.new() + }, + character.spawn("Foodor Jr") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 3), + Tree.behaviors.map.new(Vec3 { 0, 3 }), + Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), + Tree.behaviors.shadowcaster.new(), + Tree.behaviors.spellcaster.new() + }, + character.spawn("Baris Jr") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 4), + Tree.behaviors.map.new(Vec3 { 0, 6 }), + Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), + Tree.behaviors.shadowcaster.new(), + Tree.behaviors.spellcaster.new() + }, + } + + for id, _ in pairs(chars) do Tree.level.turnOrder:add(id) end + + for i = 1, 1, 1 do + for j = 1, 1, 1 do + character.spawn("My Light") + :addBehavior { + Tree.behaviors.light.new { color = Vec3 { 88, 34, 13 }, position = Vec3 { i, j } * 3, intensity = 10 } + } + end + end + + Tree.level.turnOrder:endRound() print("Now playing:", Tree.level.turnOrder.current) love.window.setMode(1280, 720, { resizable = true, msaa = 4, vsync = true })