着色器:光照效果

测试着色器能否正常工作

概况

虽然我给游戏设定了一种复古风格,但我还是想使用着色器。

在下面的视频里,你可以看到我在试验光照效果。仔细看的话,我走过光线时,玩家也会受到这个效果的影响。

也包括动态着色器,比如这场冒险中会用到的火把。

设计

如果要画成示意图,大概是这样的,只是很遗憾,真正做起来要复杂得多。

着色器示意图

着色器示意图

经验告诉我,在 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)。简单来说,就是把你画到画布上的结果再写到另一个画布上,然后再绘制着色器。这个过程可以对多个着色器重复进行。

下面这个函数目前运行良好,不过我还得再好好测试一下。

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 滤镜。

Photoshop 中的 Color Dodge

Photoshop 中的 Color Dodge

所以在游戏和编辑器里,我得想办法把它还原出来。

编辑器中的着色器选择

编辑器中的着色器选择

开发这款游戏时,我几乎不用 AI 来写代码。不过有时我知道某个小部分是我还不了解的,可以拿来当作起点。于是我请它写了一个名为“Color Dodge”的 glsl 着色器,效果和 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

最后

我成功把 Photoshop 的 color dodge 做进了游戏,但还需要尝试其他东西,比如阴影,以及把用于火焰的粒子系统和着色器效果结合起来。

不过我觉得,要让一切看起来更漂亮一些,这是必要的。幸好很多网站上也能找到 glsl 滤镜,所以我不必全部从零开始想。