improve characters dynamic lighting
This commit is contained in:
parent
606c1158e3
commit
254e94fc29
@ -3,6 +3,7 @@
|
|||||||
struct Light {
|
struct Light {
|
||||||
vec2 position;
|
vec2 position;
|
||||||
vec3 color;
|
vec3 color;
|
||||||
|
float radius;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Light lights[MAX_LIGHTS];
|
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)
|
vec4 effect(vec4 vcolor, Image tex, vec2 texture_coords, vec2 screen_coords)
|
||||||
{
|
{
|
||||||
|
// Расчет базового освещения мира: персонаж освещается смесью неба (Sky) и отраженного света (Ambient).
|
||||||
|
// Это гарантирует, что цвета персонажа всегда соответствуют цветовой гамме текущего времени суток.
|
||||||
vec3 baseLight = ambient + (vec3(1.0) - ambient) * sky;
|
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
|
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);
|
vec4 texColor = Texel(tex, texture_coords);
|
||||||
if (texColor.a == 0.0) {
|
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++) {
|
for (int i = 0; i < num_lights; i++) {
|
||||||
vec2 lightPos = lights[i].position;
|
vec2 lightPos = lights[i].position;
|
||||||
|
float radius = lights[i].radius;
|
||||||
vec2 lightVec = lightPos - sprite_pos;
|
vec2 lightVec = lightPos - sprite_pos;
|
||||||
float dist = length(lightVec);
|
float dist = length(lightVec);
|
||||||
|
|
||||||
float attenuation = 0.0;
|
// Плавное затухание света по радиусу.
|
||||||
|
// Свет начинает гаснуть на 20% дистанции до края и полностью исчезает на границе радиуса.
|
||||||
|
// Это обеспечивает мягкий край света.
|
||||||
|
float radiusFalloff = smoothstep(radius, radius * 0.2, dist);
|
||||||
|
|
||||||
// Логика из shadowcaster.lua:
|
// Реализация псевдо-проекции (3D-эффект):
|
||||||
// if lightPos.y > position.y then
|
// Чтобы избежать резких скачков яркости при пересечении линии ног персонажа,
|
||||||
// 1 - 0.3 * lightVec:length()
|
// мы используем плавный переход веса освещенности.
|
||||||
// elseif position.y - lightPos.y < 3 then
|
// Если источник света ниже персонажа (lightVec.y > 0), он считается "перед" ним.
|
||||||
// (1 - easing.easeInSine((position.y - lightPos.y))) - 0.3 * lightVec:length()
|
// Если выше (lightVec.y < 0) — "за спиной".
|
||||||
|
// Мы плавно переходим от 0% до 100% мощности в диапазоне от -2.0 до 0.5 метров по вертикали.
|
||||||
|
float frontWeight = smoothstep(-2.0, 0.5, lightVec.y);
|
||||||
|
|
||||||
if (lightPos.y > sprite_pos.y) {
|
float attenuation = radiusFalloff * frontWeight;
|
||||||
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);
|
|
||||||
pointLight += lights[i].color * attenuation;
|
pointLight += lights[i].color * attenuation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ограничиваем суммарную яркость точечных источников, чтобы избежать пересветов в белое
|
||||||
pointLight = clamp(pointLight, 0.0, 1.0);
|
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);
|
return vec4(texColor.rgb * (characterBaseLight + pointLight), texColor.a);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,32 +53,51 @@ function sprite:draw()
|
|||||||
love.graphics.setColor(1, 1, 1)
|
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 lightsData = {}
|
||||||
local numLights = 0
|
|
||||||
for _, id in ipairs(lightIds) do
|
for _, id in ipairs(lightIds) do
|
||||||
local light = Tree.level.characters[id]
|
local lightChar = Tree.level.characters[id]
|
||||||
local lPos = light:has(Tree.behaviors.positioned).position
|
local b = lightChar:has(Tree.behaviors.light) --[[@as LightBehavior]]
|
||||||
local lColor = light:has(Tree.behaviors.light).color
|
local lPos = lightChar:has(Tree.behaviors.positioned).position
|
||||||
table.insert(lightsData, { lPos.x, lPos.y, lColor.x, lColor.y, lColor.z })
|
local dist = (lPos - position):length()
|
||||||
numLights = numLights + 1
|
|
||||||
if numLights >= 8 then break end
|
-- Берем только те, что могут дотянуться до нас своим радиусом
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Сортируем по дистанции, чтобы выбрать 8 самых влиятельных
|
||||||
|
table.sort(lightsData, function(a, b) return a.dist < b.dist end)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
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)
|
||||||
if numLights > 0 then
|
|
||||||
-- Формируем массив для шейдера: lights[i].position и lights[i].color
|
for i = 1, numLights do
|
||||||
for i, data in ipairs(lightsData) do
|
local l = lightsData[i]
|
||||||
lightShader:send(string.format("lights[%d].position", i - 1), { data[1], data[2] })
|
local idx = i - 1
|
||||||
lightShader:send(string.format("lights[%d].color", i - 1), { data[3], data[4], data[5] })
|
lightShader:send(string.format("lights[%d].position", idx), { l.x, l.y })
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
|
|
||||||
love.graphics.setShader(lightShader)
|
love.graphics.setShader(lightShader)
|
||||||
|
|
||||||
if Tree.level.selector.id == self.owner.id then
|
if Tree.level.selector.id == self.owner.id then
|
||||||
|
|||||||
14
main.lua
14
main.lua
@ -62,13 +62,13 @@ function love.load()
|
|||||||
Tree.level.turnOrder:add(id)
|
Tree.level.turnOrder:add(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Это тестовый источник света, привязанный к мышке, и я очень прошу его не трогать
|
-- --- Это тестовый источник света, привязанный к мышке, и я очень прошу его не трогать
|
||||||
character.spawn("light")
|
-- character.spawn("light")
|
||||||
:addBehavior {
|
-- :addBehavior {
|
||||||
Tree.behaviors.positioned.new(),
|
-- Tree.behaviors.positioned.new(),
|
||||||
Tree.behaviors.cursor.new(),
|
-- Tree.behaviors.cursor.new(),
|
||||||
Tree.behaviors.light.new { color = Vec3 { 0.5, 0.5, 0.5 }, intensity = 5 }
|
-- Tree.behaviors.light.new { color = Vec3 { 0.5, 0.5, 0.5 }, intensity = 5 }
|
||||||
}
|
-- }
|
||||||
|
|
||||||
|
|
||||||
Tree.level.turnOrder:endRound()
|
Tree.level.turnOrder:endRound()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user