diff --git a/assets/shaders/sprite_light.glsl b/assets/shaders/sprite_light.glsl index 116e669..1bc49d9 100644 --- a/assets/shaders/sprite_light.glsl +++ b/assets/shaders/sprite_light.glsl @@ -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); } diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index a3aa0ac..3dda50f 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -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 diff --git a/main.lua b/main.lua index 304959b..112fc59 100644 --- a/main.lua +++ b/main.lua @@ -62,13 +62,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()