move character lighting to shader

This commit is contained in:
PeaAshMeter 2026-04-23 18:58:08 +03:00
parent 1ad38c3103
commit 606c1158e3
5 changed files with 115 additions and 50 deletions

View File

@ -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);
}

View File

@ -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.ellipse("fill", 0, 0, sprite.manifest.size / 2, sprite.manifest.size / 2 * math.cos(math.pi / 4))
love.graphics.pop() love.graphics.pop()
end) 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 end
return behavior return behavior

View File

@ -51,14 +51,42 @@ function sprite:draw()
Tree.level.render:enqueue(Tree.level.render.LAYERS.SPRITE, position.y, function() 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 lightIds = Tree.level.lightGrid:query(position, 5)
local shader = Tree.assets.files.shaders.outline local lightsData = {}
shader:send("texSize", { texW, texH }) local numLights = 0
shader:send("time", love.timer:getTime()) for _, id in ipairs(lightIds) do
love.graphics.setShader(shader) 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 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], self.animationTable[self.state]:draw(self.sheets[self.state],
position.x, position.x,
position.y, nil, 1 / ppm * self.side, 1 / ppm, self.manifest.base.x, self.manifest.base.y) position.y, nil, 1 / ppm * self.side, 1 / ppm, self.manifest.base.x, self.manifest.base.y)

View File

@ -9,8 +9,7 @@ local render = {
SHADOW = 2, SHADOW = 2,
LIGHT = 3, LIGHT = 3,
SPRITE = 4, SPRITE = 4,
SPRITE_LIGHT = 5, OVERLAY = 5
OVERLAY = 6
} }
} }
@ -23,8 +22,6 @@ function render:clear()
love.graphics.clear() love.graphics.clear()
love.graphics.setCanvas(txs.spriteLayer) love.graphics.setCanvas(txs.spriteLayer)
love.graphics.clear() 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.setCanvas(txs.floorLayer)
love.graphics.clear() love.graphics.clear()
love.graphics.setCanvas(txs.lightLayer) love.graphics.setCanvas(txs.lightLayer)
@ -86,7 +83,6 @@ local function new(params)
textures = { textures = {
shadowLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale), shadowLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
spriteLayer = love.graphics.newCanvas(w, h), spriteLayer = love.graphics.newCanvas(w, h),
spriteLightLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
floorLayer = love.graphics.newCanvas(w, h), floorLayer = love.graphics.newCanvas(w, h),
overlayLayer = love.graphics.newCanvas(w, h), overlayLayer = love.graphics.newCanvas(w, h),
lightLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale), lightLayer = love.graphics.newCanvas(w * lowResScale, h * lowResScale),
@ -114,15 +110,13 @@ function render:draw()
if entry.layer ~= currentLayer then if entry.layer ~= currentLayer then
if currentLayer then if currentLayer then
Tree.level.camera:detach() Tree.level.camera:detach()
local wasLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.SPRITE_LIGHT or local wasLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.LIGHT
currentLayer == self.LAYERS.LIGHT
if wasLowRes then if wasLowRes then
love.graphics.pop() love.graphics.pop()
end end
end end
currentLayer = entry.layer currentLayer = entry.layer
local isLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.SPRITE_LIGHT or local isLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.LIGHT
currentLayer == self.LAYERS.LIGHT
if currentLayer == self.LAYERS.FLOOR then if currentLayer == self.LAYERS.FLOOR then
love.graphics.setCanvas(txs.floorLayer) love.graphics.setCanvas(txs.floorLayer)
@ -132,8 +126,6 @@ function render:draw()
love.graphics.setCanvas(txs.lightLayer) love.graphics.setCanvas(txs.lightLayer)
elseif currentLayer == self.LAYERS.SPRITE then elseif currentLayer == self.LAYERS.SPRITE then
love.graphics.setCanvas(txs.spriteLayer) love.graphics.setCanvas(txs.spriteLayer)
elseif currentLayer == self.LAYERS.SPRITE_LIGHT then
love.graphics.setCanvas(txs.spriteLightLayer)
elseif currentLayer == self.LAYERS.OVERLAY then elseif currentLayer == self.LAYERS.OVERLAY then
love.graphics.setCanvas(txs.overlayLayer) love.graphics.setCanvas(txs.overlayLayer)
end end
@ -148,8 +140,7 @@ function render:draw()
end end
if currentLayer then if currentLayer then
Tree.level.camera:detach() Tree.level.camera:detach()
local wasLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.SPRITE_LIGHT or local wasLowRes = currentLayer == self.LAYERS.SHADOW or currentLayer == self.LAYERS.LIGHT
currentLayer == self.LAYERS.LIGHT
if wasLowRes then if wasLowRes then
love.graphics.pop() love.graphics.pop()
end end
@ -171,12 +162,10 @@ function render:draw()
love.graphics.setShader() love.graphics.setShader()
love.graphics.draw(txs.overlayLayer) love.graphics.draw(txs.overlayLayer)
love.graphics.setShader(lightShader)
lightShader:send("scene", txs.spriteLayer) -- Спрайты уже полностью освещены в SpriteBehavior (с учетом ambient и point lights)
lightShader:send("light", txs.spriteLightLayer) -- Поэтому рисуем их "как есть"
love.graphics.draw(txs.spriteLayer) love.graphics.draw(txs.spriteLayer)
love.graphics.setShader()
end end
return { new = new } return { new = new }

View File

@ -62,6 +62,14 @@ function love.load()
Tree.level.turnOrder:add(id) Tree.level.turnOrder:add(id)
end 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() Tree.level.turnOrder:endRound()
print("Now playing:", Tree.level.turnOrder.current) print("Now playing:", Tree.level.turnOrder.current)