Shaders : effets de lumière

Test pour vérifier que les shaders fonctionnent

En général

Même si je donne au jeu un certain look rétro, je veux utiliser des shaders.

Dans la vidéo ci-dessous, vous voyez que je fais des essais avec des effets de lumière. Si vous regardez bien, je traverse la lumière et le joueur est lui aussi touché par l’effet.

Y compris des shaders animés, par exemple pour les torches dont nous aurons besoin dans cette aventure.

Conception

Si je devais le représenter sous forme de schéma, ce serait à peu près comme ça, sauf que c’est malheureusement bien plus compliqué à réaliser.

Schéma des shaders

Schéma des shaders

L’expérience montre qu’avec love2d, il n’est pas possible de rendre plusieurs shaders dans la même boucle de dessin. Ce n’est pas possible, et je ne l’ai découvert qu’une fois un premier shader fonctionnel, en voulant en activer un second.

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

Heureusement, il existe une solution à ce problème : le rendu multi-pass. En gros, on prend le résultat dessiné sur le canvas, on le réécrit sur un autre canvas, puis on dessine le shader. On peut ensuite répéter l’opération pour plusieurs shaders.

La fonction ci-dessous fonctionne bien pour l’instant, même si je dois encore mieux la tester.

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

Dans le design sous Photoshop et Aseprite, j’utilise le filtre color dodge.

Color Dodge dans Photoshop

Color Dodge dans Photoshop

Dans le jeu et dans l’éditeur, je devais donc le reproduire d’une manière ou d’une autre.

Sélecteur de shaders de l’éditeur

Sélecteur de shaders de l’éditeur

Je n’utilise presque pas d’IA pour programmer pendant le développement du jeu. Mais parfois, je sais qu’il s’agit d’une petite partie que je ne maîtrise pas encore et que je peux utiliser comme point de départ. J’ai donc demandé de créer un shader glsl appelé ‘Color Dodge’ qui fasse la même chose que Photoshop.

Évidemment, ça n’a pas bien fonctionné, car les coordonnées que j’utilise sont différentes et j’utilise aussi le scaling et d’autres choses. Voici le fichier glsl final pour l’instant.

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

Dans la classe du shader, je transmets au shader le Frameset et l’effet choisi.

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

Pour finir

J’ai réussi à reproduire le color dodge de Photoshop dans le jeu, mais je dois encore essayer d’autres choses, comme les ombres et une combinaison du système de particules pour le feu avec des effets de shader.

Je pense quand même que c’est nécessaire pour rendre le tout un peu plus beau. Heureusement, on trouve aussi des filtres glsl sur différents sites, donc je n’ai pas à tout réinventer.