Шейдеры: световые эффекты
Проверка, работают ли шейдеры
Общее
Хотя я придаю игре определённый ретро-вид, я хочу использовать шейдеры.
В видео ниже видно, что я экспериментирую со световыми эффектами. Если присмотреться, я прохожу сквозь свет, и эффект влияет и на игрока.
Включая анимированные шейдеры, например для факелов, которые понадобятся нам в этом приключении.
Дизайн
Если изобразить это схемой, то выглядит примерно так, только, к сожалению, сделать это гораздо сложнее.

Схема шейдеров
Опыт показывает, что в love2d нельзя рендерить несколько шейдеров в одном цикле отрисовки. Это невозможно, и я понял это только тогда, когда один шейдер уже работал, а я захотел включить второй.
shader1 = love.graphics.newShader('color-dodge.glsl')
shader2 = love.graphics.newShader('color-dodge.glsl')
function love.draw()
love.graphics.push("all")
love.graphics.setCanvas(src)
love.graphics.clear()
love.graphics.setShader()
love.graphics.draw(scene, 0, 0)
love.graphics.draw(player, playerPos.x, playerPos.y)
love.graphics.setShader(shader1)
love.graphics.setShader(shader2)
love.graphics.setCanvas()
love.graphics.setShader()
love.graphics.draw(src, 0, 0)
end
К счастью, у этой проблемы есть решение: многопроходный рендеринг (multi-pass). Суть в том, что результат, нарисованный на canvas, заново записывается на другой canvas, а потом рисуется шейдер. Это можно повторять для нескольких шейдеров.
Функция ниже пока работает хорошо, хотя её ещё нужно как следует протестировать.
function Engine3:applyIntermediateShader(shader)
if not shader then return end
local src = self.canvas:getCanvas()
local front = self.canvas:getCanvasFront()
-- Store current graphics state
local currentCanvas = love.graphics.getCanvas()
local currentShader = love.graphics.getShader()
-- Apply shader: src -> front
love.graphics.push("all")
love.graphics.origin()
love.graphics.setCanvas(front)
love.graphics.clear()
love.graphics.setShader(shader:getShader())
love.graphics.draw(src, 0, 0)
love.graphics.setShader()
love.graphics.pop()
-- Copy result back to main canvas: front -> src
love.graphics.push("all")
love.graphics.origin()
love.graphics.setCanvas(src)
love.graphics.clear()
love.graphics.draw(front, 0, 0)
love.graphics.pop()
-- Restore graphics state for continued rendering
love.graphics.setCanvas(currentCanvas)
love.graphics.setShader(currentShader)
end
Шейдер Color Dodge
В дизайне в Photoshop и Aseprite я использую фильтр color dodge.

Color Dodge в Photoshop
Значит, в игре и в редакторе мне нужно было как-то его воссоздать.

Выбор шейдера в редакторе
При разработке игры я почти не использую ИИ для программирования. Но иногда я понимаю, что речь идёт о небольшой части, в которой я пока не разбираюсь, и её можно взять как отправную точку. Поэтому я попросил сделать glsl шейдер под названием ‘Color Dodge’, который работает так же, как в Photoshop.
Конечно, это сработало не очень хорошо, потому что координаты у меня другие, а ещё я использую масштабирование и другие вещи. Итоговый glsl файл сейчас выглядит так.
extern vec2 position;
extern Image image;
extern float scale;
extern float overlayIntensity;
extern vec4 quadRect;
vec3 colorDodge(vec3 base, vec3 blend) {
return vec3(
blend.r >= 0.999 ? 1.0 : min(1.0, base.r / max(0.001, 1.0 - blend.r)),
blend.g >= 0.999 ? 1.0 : min(1.0, base.g / max(0.001, 1.0 - blend.g)),
blend.b >= 0.999 ? 1.0 : min(1.0, base.b / max(0.001, 1.0 - blend.b))
);
}
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc)
{
vec4 base = texture(tex, tc);
vec2 texSize = vec2(textureSize(image, 0));
vec2 qpos = quadRect.xy;
vec2 qsize = quadRect.zw;
vec2 uv = (sc - position) / (qsize * scale);
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
return base;
}
uv = (uv * qsize + qpos) / texSize;
vec4 overlay = texture(image, uv);
if (overlay.a > 0.0) {
vec3 enhancedOverlay = min(vec3(1.0), overlay.rgb * overlayIntensity);
vec3 dodged = colorDodge(base.rgb, enhancedOverlay);
float enhancedAlpha = min(1.0, overlay.a * overlayIntensity);
vec3 result = mix(base.rgb, dodged, enhancedAlpha);
return vec4(result, base.a);
}
return base;
}
В классе шейдера я передаю шейдеру Frameset и выбранный эффект.
if frameset then
self:setImage(frameset.resource)
self:setScale(nil, true)
self:setPosition(frameset:getFrameX(), frameset:getFrameY(), true)
if frameset:getQuad() then
self:setQuad(frameset:getQuad():getViewport())
end
end
Напоследок
Мне удалось перенести color dodge из Photoshop в игру, но нужно попробовать и другое, например тени и сочетание системы частиц для огня с шейдерными эффектами.
И всё же я думаю, что это нужно, чтобы всё выглядело немного красивее. К счастью, glsl фильтры можно найти на разных сайтах, так что придумывать их с нуля не придётся.