feat: implement sprite_light uber-shader with dynamic lighting and animated outline
This commit is contained in:
parent
9d11941fe9
commit
0934028955
@ -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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user