improve characters dynamic lighting

This commit is contained in:
PeaAshMeter 2026-04-23 19:25:12 +03:00 committed by neckrat
parent 8e58b8a532
commit 9d11941fe9
3 changed files with 64 additions and 43 deletions

View File

@ -3,6 +3,7 @@
struct Light {
vec2 position;
vec3 color;
float radius;
};
extern Light lights[MAX_LIGHTS];
@ -18,9 +19,14 @@ float easeInSine(float x) {
vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords)
{
// Расчет базового освещения мира: персонаж освещается смесью неба (Sky) и отраженного света (Ambient).
// Это гарантирует, что цвета персонажа всегда соответствуют цветовой гамме текущего времени суток.
vec3 baseLight = ambient + (vec3(1.0) - ambient) * sky;
// Десатурация базового света на 30%. Это необходимо, чтобы яркие цвета неба (например, на закате)
// не перекрывали собственные цвета персонажа слишком сильно, сохраняя его узнаваемость.
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% обесцвечивания, а то глаза выгорают
vec3 characterBaseLight = mix(baseLight, vec3(luma), 0.3);
vec4 texColor = Texel(tex, texture_coords);
if (texColor.a == 0.0) {
@ -31,36 +37,32 @@ vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords)
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 attenuation = 0.0;
// Плавное затухание света по радиусу.
// Свет начинает гаснуть на 20% дистанции до края и полностью исчезает на границе радиуса.
// Это обеспечивает мягкий край света.
float radiusFalloff = smoothstep(radius, radius * 0.2, dist);
// Логика из 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()
// Реализация псевдо-проекции (3D-эффект):
// Чтобы избежать резких скачков яркости при пересечении линии ног персонажа,
// мы используем плавный переход веса освещенности.
// Если источник света ниже персонажа (lightVec.y > 0), он считается "перед" ним.
// Если выше (lightVec.y < 0) — "за спиной".
// Мы плавно переходим от 0% до 100% мощности в диапазоне от -2.0 до 0.5 метров по вертикали.
float frontWeight = smoothstep(-2.0, 0.5, lightVec.y);
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);
float attenuation = radiusFalloff * frontWeight;
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

@ -53,32 +53,51 @@ function sprite:draw()
love.graphics.setColor(1, 1, 1)
-- Собираем источники света для шейдера
local lightIds = Tree.level.lightGrid:query(position, 5)
local queryRadius = 12 -- Увеличенный радиус для плавности
local lightIds = Tree.level.lightGrid:query(position, queryRadius)
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
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)
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
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)
if Tree.level.selector.id == self.owner.id then

View File

@ -78,13 +78,13 @@ 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 }
}
-- --- Это тестовый источник света, привязанный к мышке, и я очень прошу его не трогать
-- 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()