2018/01/06(土) [n年前の日記]
#1 [love2d] love2dのShader用GLSLコードを最適化
Raspberry Pi Zero W + love2d で、STGサンプルモドキのBGタイルマップサイズを16x16から32x32に戻して動作確認していたのだけど。
どうやら Shader利用についても結構処理がかかってるようだと分かってきた。Shaderを使うと、フレームレートが数フレーム減ってしまう。
ということは、GLSLで書いてる部分も最適化していかないとダメかな、と。てなわけでそのあたり試行錯誤。
今回 Shaderを使ってやっているのは、特定の1色(今回は赤。R=1.0、G=0.0、B=0.0)を別の色で置き換えるという処理。昔の2Dゲームで言うところのパレット書き換えっぽい見た目にしたいなと。
今までは以下のようなコードだったけど…。
その位置の色(R値、G値、B値)が、置換の対象となる色と同じかどうかを判断する際、if文を使っているわけで。巷の解説記事によると、「GLSLでif文を使うと処理が遅くなるから避けるべし」というのが通説らしいので、なんとかしたいところだなと…。
試しに、以下のように書き換えてみた。
ただ、気になるところも。if文を三項演算子で置き換えてみても、はたして効果はあるのだろうか…? 分岐が無くなるだけでも改善されるのか、比較が入るだけで遅くなるのか、さて、どっちなんだろう…。
もし、三項演算子があっても遅くなるのであれば、そこも無くしたいところだよなと。なので、以下のようにしてみたり。
しかし、Raspberry Pi Zero W 上で動作確認してみたら、やっぱり Shader を使うだけで、5〜7フレームぐらいフレームレートが落ちてしまう。うーん。
コレ以上軽くするのは無理かな…。そもそも、下手すると逆に遅くなってたりしないのかな…。調べる方法はないのかな…。
ちなみに今回、640x480のCanvasに描いた内容を、1920x1080の画面解像度で全画面に拡大描画すると60FPSが出ない ―― 50〜56FPSぐらいになるので悩んでる、という状況で。画面解像度が1280x720なら、ずっと60FPSが出ているのだけど…。
どうやら Shader利用についても結構処理がかかってるようだと分かってきた。Shaderを使うと、フレームレートが数フレーム減ってしまう。
ということは、GLSLで書いてる部分も最適化していかないとダメかな、と。てなわけでそのあたり試行錯誤。
今回 Shaderを使ってやっているのは、特定の1色(今回は赤。R=1.0、G=0.0、B=0.0)を別の色で置き換えるという処理。昔の2Dゲームで言うところのパレット書き換えっぽい見た目にしたいなと。
今までは以下のようなコードだったけど…。
function love.load() local shadercode = [[ extern number factor; extern vec3 checkcolor; extern vec3 replacecolor; vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords ){ vec4 pixel = Texel(texture, texture_coords); if (pixel.r == checkcolor.r && pixel.g == checkcolor.g && pixel.b == checkcolor.b) { pixel.r = pixel.r * (1.0 - factor) + replacecolor.r * factor; pixel.g = pixel.g * (1.0 - factor) + replacecolor.g * factor; pixel.b = pixel.b * (1.0 - factor) + replacecolor.b * factor; } return pixel * color; } ]] myshader = love.graphics.newShader(shadercode) myshader:send("checkcolor", {1.0, 0.0, 0.0}) -- R,G,B myshader:send("replacecolor", {0.0, 0.0, 0.0}) -- R,G,B angle = 0 end function love.update(dt) angle = (angle + 90 * dt) % 360.0 local v = 1.0 - math.abs(math.sin(math.rad(angle))) myshader:send("factor", v) -- set 0.0 - 1.0 end
その位置の色(R値、G値、B値)が、置換の対象となる色と同じかどうかを判断する際、if文を使っているわけで。巷の解説記事によると、「GLSLでif文を使うと処理が遅くなるから避けるべし」というのが通説らしいので、なんとかしたいところだなと…。
試しに、以下のように書き換えてみた。
function love.load() local shadercode = [[ extern number factor; extern number checkcolor; extern vec4 replacecolor; vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords ){ vec4 pixel = Texel(texture, texture_coords); float nowcol = (pixel.r * 255.0 * 65536.0) + (pixel.g * 255.0 * 256.0) + (pixel.b * 255.0); return mix(pixel, replacecolor, ((checkcolor == nowcol)? factor : 0.0)) * color; } ]] myshader = love.graphics.newShader(shadercode) -- checkcolor : (R << 16) + (G << 8) + B local chkcol = (1.0 * 255 * 65536) + (0.0 * 255 * 256) + (0.0 * 255) myshader:send("checkcolor", chkcol) myshader:send("replacecolor", {0.0, 0.0, 0.0, 1.0}) -- R,G,B,A angle = 0 end
- R、G、Bを 0xRRGGBB に変換することで、3回比較してたのが1回の比較で済むようになる…はず…。たぶん。
- if文を三項演算子で置き換え。
- mix() 関数を使うと A と B の間の値を得ることができる、と知ったので、置き換え。
ただ、気になるところも。if文を三項演算子で置き換えてみても、はたして効果はあるのだろうか…? 分岐が無くなるだけでも改善されるのか、比較が入るだけで遅くなるのか、さて、どっちなんだろう…。
もし、三項演算子があっても遅くなるのであれば、そこも無くしたいところだよなと。なので、以下のようにしてみたり。
function love.load() local shadercode = [[ extern number factor; extern number checkcolor; extern vec4 replacecolor; vec4 effect( vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords ){ vec4 pixel = Texel(texture, texture_coords); float nowcol = ((pixel.r * 65536.0) + (pixel.g * 256.0) + pixel.b) * 255.0; float fac = (1.0 - sign(abs(nowcol - checkcolor))) * factor; return mix(pixel, replacecolor, fac) * color; } ]] myshader = love.graphics.newShader(shadercode) -- checkcolor : 0xRRGGBB = (R << 16) + (G << 8) + B local checkcolor = ((1.0 * 65536) + (0.0 * 256) + 0.0) * 255.0 myshader:send("checkcolor", checkcolor) myshader:send("replacecolor", {0.0, 0.0, 0.0, 1.0}) -- R,G,B,A angle = 0 end
- その位置の色(0xRRGGBB)から、比較用の色(0XRRGGBB)を引けば、色が同じなら0、色が同じじゃなければ0以外、で考えられる。
- その値を abs() で囲めば、0 or プラス値になる。
- それを sign() で囲めば、0 か 1.0 になる。
- 1.0 からその値を引けば、色が同じなら1.0、色が違うなら0.0、になる。
しかし、Raspberry Pi Zero W 上で動作確認してみたら、やっぱり Shader を使うだけで、5〜7フレームぐらいフレームレートが落ちてしまう。うーん。
コレ以上軽くするのは無理かな…。そもそも、下手すると逆に遅くなってたりしないのかな…。調べる方法はないのかな…。
ちなみに今回、640x480のCanvasに描いた内容を、1920x1080の画面解像度で全画面に拡大描画すると60FPSが出ない ―― 50〜56FPSぐらいになるので悩んでる、という状況で。画面解像度が1280x720なら、ずっと60FPSが出ているのだけど…。
◎ 別のやり方。 :
そもそも、BGの特定の1色だけを変えたいだけなら、別の方法もあるなと思いついたので一応メモ。色を変化させたいところだけ透明色にしておいて、Canvas にアレコレ描画する前の塗りつぶしクリア時に、色を変化させつつ塗りつぶししておく、とか。
ただ、その方法では問題もあって。BGのレイヤーが複数枚あるときは、一番奥のレイヤーしかそれっぽく表示できない…はず。手前のレイヤーの特定部分の色を変える、てのは難しい。まあ、一番奥のレイヤーだけ色が変わればそれでいい、という時は使えるかもだけど。
ただ、その方法では問題もあって。BGのレイヤーが複数枚あるときは、一番奥のレイヤーしかそれっぽく表示できない…はず。手前のレイヤーの特定部分の色を変える、てのは難しい。まあ、一番奥のレイヤーだけ色が変わればそれでいい、という時は使えるかもだけど。
◎ 現状でも凄いといえば凄い。 :
考えてみたら、本体価格が約1,300円のPC上で、640x480のゲーム画面をフルHDの1920x1080に拡大描画して50FPS前後出てる、という状況は結構凄いのではないかと思えてきたりもするわけで。
ガラケー向けのゲームアプリを作ってた頃は、本体価格が5万円ぐらいする機器なのに、QVGA相当のゲーム画面を10〜20FPSで描画するのが関の山…。その状況を思い返せば、Raspberry Pi Zero W は意外とスゴイ。ような気もする。
とは言え、これが Raspberry Pi3 なら60FPS出るのだろうし。ソフトウェア的なアレコレをするなら Raspberry Pi3 を使いましょうよ、Raspberry Pi Zero W なんか買ってしまったらダメですよ、てなところもあるのだけど。
ガラケー向けのゲームアプリを作ってた頃は、本体価格が5万円ぐらいする機器なのに、QVGA相当のゲーム画面を10〜20FPSで描画するのが関の山…。その状況を思い返せば、Raspberry Pi Zero W は意外とスゴイ。ような気もする。
とは言え、これが Raspberry Pi3 なら60FPS出るのだろうし。ソフトウェア的なアレコレをするなら Raspberry Pi3 を使いましょうよ、Raspberry Pi Zero W なんか買ってしまったらダメですよ、てなところもあるのだけど。
[ ツッコむ ]
以上、1 日分です。