Shaders: efeitos de luz

Teste para ver se os shaders funcionam

Geral

Mesmo dando ao jogo um certo visual retrô, quero usar shaders.

No vídeo abaixo você vê que estou experimentando com efeitos de luz. Se olhar com atenção, eu atravesso a luz e o jogador também é afetado pelo efeito.

Incluindo shaders animados, por exemplo para as tochas que vamos precisar nesta aventura.

Design

Se eu tivesse que mostrar isso como um esquema, seria mais ou menos assim, só que infelizmente é bem mais complicado de fazer.

Esquema de shaders

Esquema de shaders

A experiência mostra que no love2d não é possível renderizar vários shaders no mesmo loop de desenho. Isso não é possível, e só descobri quando já tinha um shader funcionando e quis ativar um segundo.

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

Felizmente há uma solução para esse problema: renderização multi-pass. Basicamente, você pega o resultado desenhado no canvas, escreve de novo em outro canvas e depois desenha o shader. Dá para repetir isso para vários shaders.

A função abaixo está funcionando bem por enquanto, mas ainda preciso testá-la melhor.

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

Shader Color Dodge

No design no Photoshop e no Aseprite uso o filtro color dodge.

Color Dodge no Photoshop

Color Dodge no Photoshop

Então no jogo e no editor eu precisava recriar isso de algum jeito.

Seletor de shaders do editor

Seletor de shaders do editor

Quase não uso IA para programar durante o desenvolvimento do jogo. Mas às vezes sei que é uma parte pequena que ainda não conheço e que posso usar como ponto de partida. Então pedi para criar um shader glsl chamado ‘Color Dodge’ que fizesse o mesmo que o Photoshop.

É claro que não funcionou direito, porque as coordenadas que uso são diferentes e também uso escala e outras coisas. O arquivo glsl final, por enquanto, é este.

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;
}

Na classe do shader passo para o shader o Frameset e o efeito escolhido.

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

Para terminar

Consegui trazer o color dodge do Photoshop para o jogo, mas ainda preciso testar outras coisas, como sombras e uma combinação do sistema de partículas para fogo com efeitos de shader.

Mesmo assim acho que é necessário para deixar tudo um pouco mais bonito. Felizmente também dá para encontrar filtros glsl em vários sites, então não preciso inventar tudo do zero.