From 82fdd0d609b1141569439fc49dd7a2274b287668 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Thu, 8 Jan 2026 12:45:59 +0300 Subject: [PATCH] light source test --- assets/masks/circle128.png | Bin 0 -> 572 bytes assets/masks/gradientCircle256.png | Bin 0 -> 11445 bytes assets/shaders/light.glsl | 30 +++++++ assets/shaders/light_postprocess.glsl | 20 +++++ lib/annotations.lua | 1 + lib/character/behaviors/light.lua | 45 +++++++++++ lib/character/behaviors/shadowcaster.lua | 99 +++++++---------------- lib/character/character.lua | 15 +--- lib/level/grid/character_grid.lua | 8 +- lib/level/grid/light_grid.lua | 64 +++++++++++++++ lib/level/level.lua | 5 +- lib/level/render.lua | 34 +++++--- main.lua | 56 +++++++++++-- 13 files changed, 275 insertions(+), 102 deletions(-) create mode 100644 assets/masks/circle128.png create mode 100644 assets/masks/gradientCircle256.png create mode 100644 assets/shaders/light.glsl create mode 100644 assets/shaders/light_postprocess.glsl create mode 100644 lib/character/behaviors/light.lua create mode 100644 lib/level/grid/light_grid.lua diff --git a/assets/masks/circle128.png b/assets/masks/circle128.png new file mode 100644 index 0000000000000000000000000000000000000000..007a29dc7d8c9b866700ab51aa3127e9fcb312d8 GIT binary patch literal 572 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%0A(RP0i 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 BP;CGJ literal 0 HcmV?d00001 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 })