シェーダー:光のエフェクト
シェーダーが動くかどうかのテスト
全体
ゲームにはレトロな雰囲気を持たせていますが、それでもシェーダーは使いたいと思っています。
下の動画では、光のエフェクトを試しているところが見られます。よく見ると、私が光の中を通り抜けると、プレイヤーにもエフェクトがかかっています。
動くシェーダーも含みます。たとえば、この冒険で必要になるたいまつなどです。
デザイン
図にするとだいたいこんな感じですが、実際に作るのは残念ながらずっと複雑です。

シェーダーの図
経験から言うと、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
なので、ゲームとエディターでも、なんとかこれを再現する必要がありました。

エディターのシェーダー選択
ゲームの開発中、プログラミングに 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 フィルターはいろいろなサイトで見つかるので、すべてを一から考える必要はありません。