From cb53fa8d88ad01d40907e6abd0c0a98da2659082 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Thu, 23 Apr 2026 19:34:35 +0300 Subject: [PATCH] feat: implement sprite_light uber-shader with dynamic lighting and animated outline --- assets/shaders/sprite_light.glsl | 55 +++++++++++++++++++----------- lib/character/behaviors/sprite.lua | 14 ++++---- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/assets/shaders/sprite_light.glsl b/assets/shaders/sprite_light.glsl index 1bc49d9..f55b5ae 100644 --- a/assets/shaders/sprite_light.glsl +++ b/assets/shaders/sprite_light.glsl @@ -12,6 +12,11 @@ extern vec2 sprite_pos; // Мировая позиция спрайта (в ме extern vec3 ambient; // Эмбиентное освещение extern vec3 sky; // Цвет неба +// Параметры выделения +extern bool is_selected; +extern vec2 tex_size; +extern float time; + // Функция для имитации easing.easeInSine float easeInSine(float x) { return 1.0 - cos((x * 3.14159) / 2.0); @@ -19,20 +24,42 @@ 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); - vec4 texColor = Texel(tex, texture_coords); + + // ЛОГИКА ОБВОДКИ (Outline) + // Мы выполняем ее до расчетов освещения. Если пиксель прозрачный и объект выбран, + // проверяем соседей, чтобы понять, не граница ли это. + // Обводка рисуется "самосветящейся", чтобы выделение было видно даже в полной темноте. + if (is_selected && texColor.a <= 0.0) { + float maxAlpha = 0.0; + + // Проверяем соседние пиксели (квадратом 3x3) + for (float x = -1.0; x <= 1.0; x++) { + for (float y = -1.0; y <= 1.0; y++) { + if (x == 0.0 && y == 0.0) continue; + vec2 offset = vec2(x, y) / tex_size; + maxAlpha = max(maxAlpha, Texel(tex, texture_coords + offset).a); + } + } + + if (maxAlpha > 0.0) { + // Эффект пульсации и "бегущей волны" из оригинального шейдера outline.glsl + float modY = (0.75 + sin(time) * 0.25) * (0.5 + cos(texture_coords.y * 10.0 + time * 2.0) * 0.5); + return vec4(vec3(modY, 0.2 * sin(time) + 0.5, 0.5), 1.0); + } + } + if (texColor.a == 0.0) { return vec4(0.0); } + // Расчет базового освещения мира: персонаж освещается смесью неба (Sky) и отраженного света (Ambient). + vec3 baseLight = ambient + (vec3(1.0) - ambient) * sky; + + // Десатурация базового света на 30%. + float luma = dot(baseLight, vec3(0.2126, 0.7152, 0.0722)); + vec3 characterBaseLight = mix(baseLight, vec3(luma), 0.3); + vec3 pointLight = vec3(0.0); for (int i = 0; i < num_lights; i++) { @@ -42,27 +69,17 @@ vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords) float dist = length(lightVec); // Плавное затухание света по радиусу. - // Свет начинает гаснуть на 20% дистанции до края и полностью исчезает на границе радиуса. - // Это обеспечивает мягкий край света. float radiusFalloff = smoothstep(radius, radius * 0.2, dist); // Реализация псевдо-проекции (3D-эффект): - // Чтобы избежать резких скачков яркости при пересечении линии ног персонажа, - // мы используем плавный переход веса освещенности. - // Если источник света ниже персонажа (lightVec.y > 0), он считается "перед" ним. - // Если выше (lightVec.y < 0) — "за спиной". - // Мы плавно переходим от 0% до 100% мощности в диапазоне от -2.0 до 0.5 метров по вертикали. float frontWeight = smoothstep(-2.0, 0.5, lightVec.y); float attenuation = radiusFalloff * frontWeight; pointLight += lights[i].color * attenuation; } - // Ограничиваем суммарную яркость точечных источников, чтобы избежать пересветов в белое pointLight = clamp(pointLight, 0.0, 1.0); // Финальный расчет цвета: - // Мы берем текстуру персонажа и умножаем ее на сумму базового света мира и всех точечных источников. - // Это создает эффект, где персонаж одновременно "вписан" в атмосферу уровня и реагирует на динамические огни. return vec4(texColor.rgb * (characterBaseLight + pointLight), texColor.a); } diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 3dda50f..318f2ed 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -83,12 +83,20 @@ function sprite:draw() local lightShader = Tree.assets.files.shaders.sprite_light local weather = Tree.level.weather local numLights = math.min(#lightsData, 8) + local isSelected = (Tree.level.selector.id == self.owner.id) 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) + lightShader:send("is_selected", isSelected) + if isSelected then + local sheet = self.sheets[self.state] + lightShader:send("tex_size", { sheet:getWidth(), sheet:getHeight() }) + lightShader:send("time", love.timer:getTime()) + end + for i = 1, numLights do local l = lightsData[i] local idx = i - 1 @@ -100,12 +108,6 @@ function sprite:draw() 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)