シェーダー:光のエフェクト

シェーダーが動くかどうかのテスト

全体

ゲームにはレトロな雰囲気を持たせていますが、それでもシェーダーは使いたいと思っています。

下の動画では、光のエフェクトを試しているところが見られます。よく見ると、私が光の中を通り抜けると、プレイヤーにもエフェクトがかかっています。

動くシェーダーも含みます。たとえば、この冒険で必要になるたいまつなどです。

デザイン

図にするとだいたいこんな感じですが、実際に作るのは残念ながらずっと複雑です。

シェーダーの図

シェーダーの図

経験から言うと、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

幸い、この問題には解決策があります。マルチパスレンダリングです。キャンバスに描いた結果を別のキャンバスに書き出し、そのあとでシェーダーを描画する、というものです。これを複数のシェーダーで繰り返せます。

下の関数は今のところうまく動いていますが、もっとしっかりテストする必要があります。

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 はほとんど使っていません。ただ、まだ知識のない小さな部分で、たたき台として使えるとわかっているときもあります。そこで、Photoshop と同じ処理をする「Color Dodge」という名前の glsl シェーダーを作ってもらいました。

もちろん、そのままではうまく動きませんでした。私が使っている座標系は違いますし、スケーリングなども使っているからです。現時点での最終的な 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 フィルターはいろいろなサイトで見つかるので、すべてを一から考える必要はありません。