HSP | 透過 PNG で gmode 7 する

※大幅に修正したものを別記事にしました

あまりにも修正したので別記事にしましたが、この記事はこの記事で何かの役に立つと思うので残しておきます (^^;

HSP | 透過 PNG で gmode 7 する

gmode 7 (ピクセルアルファブレンドコピー) は、アルファ値がない画像と、アルファ値だけの画像を横に並べると、そのアルファ値通りに画像合成ができるという代物です。
HSP はいつからか PNG 読み込みができるようになりました!
…が、読み込んだ後に透過を上手く扱う手段が (標準機能では) 実装されていない…。
そこで、まずはググります!
とりあえず検索の上位に出てきたのは以下のようなもの。

1. HSP gmode 7用 アルファブレンド画像作成モジュール - AkicanBlog
COM オブジェクトと GDI の API を用いて実装されている。
おそらく HSP が PNG 未対応時代の代物。
PNG ファイルフォーマットを解析して描画 (デコードは COM オブジェクトまかせ)。

2. HSP用 画像関連モジュール - 略して仮。
DLL 色々呼んでます。めっちゃ高機能!

3. なんだか雲行きの怪しい雑記帖 HSPのPNG読み込みからgmode7で使える形式に
標準機能だけを用いてアルファ値と色を算出。
※今回のプログラムの参考とさせていただきました。

4. PNG2Gmode7 - SoupSeed
何か良く分からないけど、作成者曰く、色が微妙に変わってしまうバグがあるそうなのでボツ。

1 や 2 でも手ごろで良かったのですが、他のプラットフォームに移植することなどを考えると、なるべく標準命令だけで実装したほうがいいかな~とか考えてしまって、3 を使うことにしました (^^

ただ、 3 はモジュール化されていなかったので、モジュール化したのと、速度が遅かったので、なるべく速くしようとプログラムを書き替えていたら、大幅にプログラムが書き変わったので、ここに載せようと思います (^^;

ソースコード

※簡単のため、インクルードガートがないのと、モジュール名がテキトーです。

/**
 * モジュール
 */
#module Pixela

// 
// フィールド
// 
#define global picload_pixela(%1) picload_pixela@Pixela %1

/**
 * 画像ファイルをピクセルアルファブレンドコピーに対応させて読み込み
 * 
 * gmode 7
 */
#deffunc local picload_pixela str fname

load@Pixela fname
draw@Pixela

return

/**
 * 画像読み込み
 */
#deffunc local load str fname

// 出力ウィンドウ
BUFFER_ID_PIXELA = ginfo_sel

// 背景黒バッファ
BUFFER_ID_BG_BLACK = ginfo_newid

buffer BUFFER_ID_BG_BLACK

picload fname, 2

/* 画像サイズ取得 */
img_height = ginfo_winy
img_width  = ginfo_winx

// 背景白バッファ
BUFFER_ID_BG_WHITE = ginfo_newid

buffer BUFFER_ID_BG_WHITE, img_width, img_height

pos 0, 0 : picload fname, 1

// 出力ウィンドウ 初期化
type = ginfo_type(BUFFER_ID_PIXELA)
if ( type == 1 ) {
buffer BUFFER_ID_PIXELA, img_width * 2, img_height
} else : if ( type == 2 ) {
screen BUFFER_ID_PIXELA, img_width * 2, img_height
} else : if ( type == 3 ) {
bgscr  BUFFER_ID_PIXELA, img_width * 2, img_height
}

return

/**
 * ウィンドウの種類取得
 * 
 * 1 = buffer, 2 = screen, 3 = bgscr
 */
#defcfunc local ginfo_type int id
mref bmscr, 96 + id // mref bmscr, 67
return bmscr.17

/**
 * RGBA を計算し出力
 */
#deffunc local draw

gsel BUFFER_ID_BG_BLACK
mref img_bgblack, 66
gsel BUFFER_ID_BG_WHITE
mref img_bgwhite, 66
gsel BUFFER_ID_PIXELA
mref img_rgba, 66

repeat img_height : y = cnt
index_height = img_width * (img_height - 1 - y)
repeat img_width : x = cnt
// VRAM インデックス
index  = (index_height     + x            ) * 3 // 背景黒・白
index2 = (index_height * 2 + x            ) * 3 // RGB
index3 = (index_height * 2 + x + img_width) * 3 // alpha
// 色取得 (配列にすると 15% ほど遅くなる HSP ver.3.4)
black_r = peek(img_bgblack, index + 2)
black_g = peek(img_bgblack, index + 1)
black_b = peek(img_bgblack, index + 0)
white_r = peek(img_bgwhite, index + 2)
white_g = peek(img_bgwhite, index + 1)
white_b = peek(img_bgwhite, index + 0)
// RGB 出力
poke img_rgba, index2 + 2, 255 * black_r / (255 - white_r + black_r)
poke img_rgba, index2 + 1, 255 * black_g / (255 - white_g + black_g)
poke img_rgba, index2 + 0, 255 * black_b / (255 - white_b + black_b)
// alpha 出力
alpha = 255 - white_r + black_r
poke img_rgba, index3 + 2, alpha
poke img_rgba, index3 + 1, alpha
poke img_rgba, index3 + 0, alpha
loop
loop

return

#global

サンプル

// ファイル選択
dialog "png", 16, "png file!"
if ( stat == 0 ) : end
fname = refstr

// ファイル読み込み
buffer 1
picload_pixela fname

img_width  = ginfo_winx / 2
img_height = ginfo_winy

// 描画
gsel 0
gmode 7

redraw 0

/* 左上: 背景黒 */
color 0, 0, 0 : boxf 0, 0, img_width - 1, img_height - 1
pos 0, 0 : gcopy 1, 0, 0, img_width, img_height

/* 右上: 背景白 */
color 255, 255, 255 : boxf img_width, 0, img_width * 2 - 1, img_height - 1
pos img_width, 0 : gcopy 1, 0, 0, img_width, img_height

/* 左下: 背景赤 */
color 255, 0, 0 : boxf 0, img_height, img_width - 1, img_height * 2 - 1
pos 0, img_height : gcopy 1, 0, 0, img_width, img_height

/* 右下: 背景青 */
color 0, 0, 255 : boxf img_width, img_height, img_width * 2 - 1, img_height * 2 - 1
pos img_width, img_height : gcopy 1, 0, 0, img_width, img_height

redraw

主な変更点

・未使用ウィンドウを処理中のバッファに使うように変更
モジュールなので、既に使用中のバッファを上書きしてしまったら問題ですもんね…。
本当は使用後に開放したいですが、現時点の HSP だとムリなようです… (^^;
・配列変数に代入せず、直接 VRAM に読み書きするよう変更
これが今回大きな効果がありました!!
後に詳しく書きます…。
・アルファ値を正規化しないよう変更
結局は 0 ~ 255 で出力するのですから、0.0 ~ 1.0 にする必要はないでしょう (おそらく) 。
・色 (RGB) の算出式を変更
なぜか元のプログラムでは、正規化したアルファ値を利用して RGB を求めているのですが、連立二次方程式で求めるのですから、アルファ値を用いなくても RGB を算出できます。
というわけで変更!
・その他、不要な関数を削除
abs や limit を用いて数を調整しているところがあったのですが、正しい値が入力されていれば、負になったり値が範囲外になったりしないはずなので削除 (カンタンに証明できます)。
・(サンプルで) redraw の追加
redraw を使うと、pset, pget を使う場合でもかなり速くなります!
・その他、数式を一回変数に代入
HSP はもともと速度が遅い言語なので、これをするだけでも目に見えるぐらい速くなりました! (^^;
もっと弄れば速くできるかも??
あと、本質的ではないですが、サンプルの boxf の範囲がズレていたので直しました。

「配列変数に代入せず、直接 VRAM に読み書きするよう変更」

これをする前に、色々な状況を想定して実際にコードを書き、実行時間を測定しました!
(ただし、かなりテキトーな計測だったので、数値は割愛します (^^;
以下のパターンを想定しました。
  1. 配列変数を用いて一括 pset, pget
  2. 地道に pset, pget
  3. VRAM 直接アクセス
結論から言いますと、上の方が遅く、下の方が速いです。

自分のプログラムでは、元のプログラムより 5 倍ぐらい速くなりました! (数式の変更なども含めて)

元のソースコードのコメントを見ると、「// 処理用ばっふぁ、いちいちpgetが面倒なので」とあるのですが、「いちいちpget」の方が速いです (^^;
gsel は第二引数を指定しなければ、内部的に操作先のウィンドウ ID を変更するだけなので、地道にウィンドウ (バッファ) を切り替えて処理してもあまり処理時間は変わらなかったです。
問題は、「配列変数」を用いていることでした!
HSP の配列変数ってスゴく遅いのですよ…あまりにも遅いので、軽くですが検証までしてしまいました…。
特に読み込みがとんでもなく遅いようです。
なので、「いちいちpget」の方が速いのですね。
…が、最終的には、VRAM に直接アクセスするのが一番速かったです (^^;
HSP のことなので、poke や peek すら遅かったり、どっちかが極端に遅かったりするんじゃないかと思いましたが、配列変数を用いて代入するより速かったです…。配列変数の代入より命令 (関数) の方が速いとはなんのこっちゃ (^^;
本当はシフト演算を用いて一括代入した場合なども検証してみたかったのですが、時間なかったのでこの状態でブログに投下します…。

誤差

自分のプログラムでは正規化したアルファ値を用いず、すべて整数で処理しているので、誤差が気になります… (^^;
主に割り算がある場所ですね…。
自分のプログラムで割り算を行っているのは、「RGB」を算出しているところだけです。

poke img_rgba, index2 + 2, 255 * black_r / (255 - white_r + black_r)

足し算引き算をした後、一回割り算を行い、それを色の値として出力しています。
割り切れなかった場合切り下げが起きます。
PNG ファイルフォーマットをちゃんと調べていないのですが、画像ファイルだしおそらくアルファ値も 0 ~ 255 で格納してますよね??
と、仮定すると、この値は絶対割り切れるはずなのです!
…が、そう上手くいくわけないんですよね (^^;
PNG 読み込み時の、「白背景」でも、内部的に割る処理がなされていて、誤差が出ます。これはどうしようもないですね…。
せめて、このプログラムで、四捨五入すべきかもしれません…。
正規化はせずに (0 ~ 255 のまま) double にキャスト、0.5 プラス、int にキャスト…。
今回はとりあえず省略します。

雑談

透過 PNG をいじるツールが作りたいなーと思ってこれを作ったのですが、最近何かと忙しいので、これだけブログに投下して終わるかも (^^;
…いや、なるべく作るように頑張ります!
ではまたいつか~ ノシ