feat: implement sprite_light uber-shader with dynamic lighting and animated outline
This commit is contained in:
parent
254e94fc29
commit
cb53fa8d88
@ -12,6 +12,11 @@ extern vec2 sprite_pos; // Мировая позиция спрайта (в ме
|
|||||||
extern vec3 ambient; // Эмбиентное освещение
|
extern vec3 ambient; // Эмбиентное освещение
|
||||||
extern vec3 sky; // Цвет неба
|
extern vec3 sky; // Цвет неба
|
||||||
|
|
||||||
|
// Параметры выделения
|
||||||
|
extern bool is_selected;
|
||||||
|
extern vec2 tex_size;
|
||||||
|
extern float time;
|
||||||
|
|
||||||
// Функция для имитации easing.easeInSine
|
// Функция для имитации easing.easeInSine
|
||||||
float easeInSine(float x) {
|
float easeInSine(float x) {
|
||||||
return 1.0 - cos((x * 3.14159) / 2.0);
|
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)
|
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);
|
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) {
|
if (texColor.a == 0.0) {
|
||||||
return vec4(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);
|
vec3 pointLight = vec3(0.0);
|
||||||
|
|
||||||
for (int i = 0; i < num_lights; i++) {
|
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);
|
float dist = length(lightVec);
|
||||||
|
|
||||||
// Плавное затухание света по радиусу.
|
// Плавное затухание света по радиусу.
|
||||||
// Свет начинает гаснуть на 20% дистанции до края и полностью исчезает на границе радиуса.
|
|
||||||
// Это обеспечивает мягкий край света.
|
|
||||||
float radiusFalloff = smoothstep(radius, radius * 0.2, dist);
|
float radiusFalloff = smoothstep(radius, radius * 0.2, dist);
|
||||||
|
|
||||||
// Реализация псевдо-проекции (3D-эффект):
|
// Реализация псевдо-проекции (3D-эффект):
|
||||||
// Чтобы избежать резких скачков яркости при пересечении линии ног персонажа,
|
|
||||||
// мы используем плавный переход веса освещенности.
|
|
||||||
// Если источник света ниже персонажа (lightVec.y > 0), он считается "перед" ним.
|
|
||||||
// Если выше (lightVec.y < 0) — "за спиной".
|
|
||||||
// Мы плавно переходим от 0% до 100% мощности в диапазоне от -2.0 до 0.5 метров по вертикали.
|
|
||||||
float frontWeight = smoothstep(-2.0, 0.5, lightVec.y);
|
float frontWeight = smoothstep(-2.0, 0.5, lightVec.y);
|
||||||
|
|
||||||
float attenuation = radiusFalloff * frontWeight;
|
float attenuation = radiusFalloff * frontWeight;
|
||||||
pointLight += lights[i].color * attenuation;
|
pointLight += lights[i].color * attenuation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ограничиваем суммарную яркость точечных источников, чтобы избежать пересветов в белое
|
|
||||||
pointLight = clamp(pointLight, 0.0, 1.0);
|
pointLight = clamp(pointLight, 0.0, 1.0);
|
||||||
|
|
||||||
// Финальный расчет цвета:
|
// Финальный расчет цвета:
|
||||||
// Мы берем текстуру персонажа и умножаем ее на сумму базового света мира и всех точечных источников.
|
|
||||||
// Это создает эффект, где персонаж одновременно "вписан" в атмосферу уровня и реагирует на динамические огни.
|
|
||||||
return vec4(texColor.rgb * (characterBaseLight + pointLight), texColor.a);
|
return vec4(texColor.rgb * (characterBaseLight + pointLight), texColor.a);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,12 +83,20 @@ function sprite:draw()
|
|||||||
local lightShader = Tree.assets.files.shaders.sprite_light
|
local lightShader = Tree.assets.files.shaders.sprite_light
|
||||||
local weather = Tree.level.weather
|
local weather = Tree.level.weather
|
||||||
local numLights = math.min(#lightsData, 8)
|
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("sprite_pos", { position.x, position.y })
|
||||||
lightShader:send("sky", { weather.skyLight.x, weather.skyLight.y, weather.skyLight.z })
|
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("ambient", { weather.ambientLight.x, weather.ambientLight.y, weather.ambientLight.z })
|
||||||
lightShader:send("num_lights", numLights)
|
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
|
for i = 1, numLights do
|
||||||
local l = lightsData[i]
|
local l = lightsData[i]
|
||||||
local idx = i - 1
|
local idx = i - 1
|
||||||
@ -100,12 +108,6 @@ function sprite:draw()
|
|||||||
|
|
||||||
love.graphics.setShader(lightShader)
|
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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user