diff --git a/assets/shaders/sprite_light.glsl b/assets/shaders/sprite_light.glsl new file mode 100644 index 0000000..116e669 --- /dev/null +++ b/assets/shaders/sprite_light.glsl @@ -0,0 +1,66 @@ +#define MAX_LIGHTS 8 + +struct Light { + vec2 position; + vec3 color; +}; + +extern Light lights[MAX_LIGHTS]; +extern int num_lights; +extern vec2 sprite_pos; // Мировая позиция спрайта (в метрах) +extern vec3 ambient; // Эмбиентное освещение +extern vec3 sky; // Цвет неба + +// Функция для имитации 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) +{ + vec3 baseLight = ambient + (vec3(1.0) - ambient) * sky; + float luma = dot(baseLight, vec3(0.2126, 0.7152, 0.0722)); // https://en.wikipedia.org/wiki/Relative_luminance + vec3 characterBaseLight = mix(baseLight, vec3(luma), 0.3); // 30% обесцвечивания, а то глаза выгорают + + vec4 texColor = Texel(tex, texture_coords); + if (texColor.a == 0.0) { + return vec4(0.0); + } + + vec3 pointLight = vec3(0.0); + + for (int i = 0; i < num_lights; i++) { + vec2 lightPos = lights[i].position; + vec2 lightVec = lightPos - sprite_pos; + float dist = length(lightVec); + + float attenuation = 0.0; + + // Логика из shadowcaster.lua: + // if lightPos.y > position.y then + // 1 - 0.3 * lightVec:length() + // elseif position.y - lightPos.y < 3 then + // (1 - easing.easeInSine((position.y - lightPos.y))) - 0.3 * lightVec:length() + + if (lightPos.y > sprite_pos.y) { + attenuation = 1.0 - 0.3 * dist; + } else { + float yDiff = sprite_pos.y - lightPos.y; + if (yDiff < 3.0) { + attenuation = (1.0 - easeInSine(yDiff)) - 0.3 * dist; + } + } + + attenuation = max(attenuation, 0.0); + pointLight += lights[i].color * attenuation; + } + + pointLight = clamp(pointLight, 0.0, 1.0); + vec3 a = clamp(ambient, 0.0, 1.0); + + // Канальный множитель: от ambient до 1 в зависимости от точечного света + // Это гарантирует, что спрайт не будет черным в отсутствие источников света + vec3 lightMultiplier = a + (vec3(1.0) - a) * pointLight; + + return vec4(texColor.rgb * (characterBaseLight + pointLight), texColor.a); +} diff --git a/lib/character/behaviors/shadowcaster.lua b/lib/character/behaviors/shadowcaster.lua index 904322e..0b2daeb 100644 --- a/lib/character/behaviors/shadowcaster.lua +++ b/lib/character/behaviors/shadowcaster.lua @@ -30,32 +30,6 @@ function behavior:draw() love.graphics.ellipse("fill", 0, 0, sprite.manifest.size / 2, sprite.manifest.size / 2 * math.cos(math.pi / 4)) love.graphics.pop() end) - - -- 2. Свет на спрайте - if #lights > 0 then - Tree.level.render:enqueue(Tree.level.render.LAYERS.SPRITE_LIGHT, position.y, function() - 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") - love.graphics.setColor(1, 1, 1) - end) - end end return behavior diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 6625b01..a3aa0ac 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -51,14 +51,42 @@ function sprite:draw() Tree.level.render:enqueue(Tree.level.render.LAYERS.SPRITE, position.y, function() 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) + + -- Собираем источники света для шейдера + local lightIds = Tree.level.lightGrid:query(position, 5) + local lightsData = {} + local numLights = 0 + for _, id in ipairs(lightIds) do + local light = Tree.level.characters[id] + local lPos = light:has(Tree.behaviors.positioned).position + local lColor = light:has(Tree.behaviors.light).color + table.insert(lightsData, { lPos.x, lPos.y, lColor.x, lColor.y, lColor.z }) + numLights = numLights + 1 + if numLights >= 8 then break end end + + local lightShader = Tree.assets.files.shaders.sprite_light + local weather = Tree.level.weather + 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) + if numLights > 0 then + -- Формируем массив для шейдера: lights[i].position и lights[i].color + for i, data in ipairs(lightsData) do + lightShader:send(string.format("lights[%d].position", i - 1), { data[1], data[2] }) + lightShader:send(string.format("lights[%d].color", i - 1), { data[3], data[4], data[5] }) + end + end + + love.graphics.setShader(lightShader) + + if Tree.level.selector.id == self.owner.id then + -- Если выбран, то рисуем еще и обводку? + -- В LÖVE нельзя поставить два шейдера сразу через setShader. + -- Для простоты пока оставим только свет, либо нужно комбинировать шейдеры. + 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) diff --git a/lib/level/render.lua b/lib/level/render.lua index ae23047..94a12ad 100644 --- a/lib/level/render.lua +++ b/lib/level/render.lua @@ -9,8 +9,7 @@ local render = { SHADOW = 2, LIGHT = 3, SPRITE = 4, - SPRITE_LIGHT = 5, - OVERLAY = 6 + OVERLAY = 5 } } @@ -23,8 +22,6 @@ function render:clear() 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) @@ -86,7 +83,6 @@ local function new(params) 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), @@ -114,15 +110,13 @@ function render:draw() 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 + 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.SPRITE_LIGHT or - currentLayer == self.LAYERS.LIGHT + local isLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.LIGHT if currentLayer == self.LAYERS.FLOOR then love.graphics.setCanvas(txs.floorLayer) @@ -132,8 +126,6 @@ function render:draw() 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 @@ -148,8 +140,7 @@ function render:draw() 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 + local wasLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.LIGHT if wasLowRes then love.graphics.pop() end @@ -171,12 +162,10 @@ function render:draw() 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 return { new = new } diff --git a/main.lua b/main.lua index 4369671..304959b 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)