簡単そうで簡単ではなかった話
今回書く話は周知の事実なんだろうか。
どうでもいいけど、この文章書くのも簡単そうで時間がかかりまくってしまった…。
なんでだろ。
ノベルゲーム・ポーズチェンジ
それは今から2年程前、私がとある事情でノベルゲームを製作していたときの話である。
ノベルゲームの作成などアルゴリズム的に難しい部分なんて何もない。
…もとい、ほとんどないのだが、その当時なんでもないと思われた部分に
あっさりと詰まったのである。
それはポーズチェンジの部分である。奇しくも今月号のCマガジンの
某箇所にこの話がさらりと流されていたりするわけなのだが、
別にそこを見てネタにしようとか思ったわけではないので誤解なきよう。
ちなみに、Cマガに載っている通りだと"おかしい"です。
ポーズチェンジ、というのはいかにも言葉のその通り、
ノベルゲームは一般的に背景があってその上に何らかのキャラクターを表示するなどして
状況を表現していたりするのだが、そのキャラクターの部分を違うポーズなり表情なりに
変更することを言っている。その際になるべく見栄えを良くするために、
これまた一般的にはクロスフェードを行うはずだが、
このクロスフェードが実は難しいんじゃなかろうかと思うのである。
そんなものどこが難しいのか?
直感的には画像Aから画像Bに変更したいような場合、
画像Aの半透明度を徐々に下げつつ、画像Bの半透明度を徐々に上げていけばよさそうに思う。
(今月号のCマガでもそんな風になっていたりする)
だが、それでは画像が不自然な感じになってしまうのである。
例えばちょうどクロスフェードの中間あたりの状況を考えてみる。
このフレームで行うことは、
・背景の転送
・その上に画像Aを半透明度0.5で描画
・その上に画像Bを半透明度0.5で描画
になるはずなのだが、このような処理を行うと、任意の地点にて
最低でも元の背景が0.25程度は透けて見えることになる。
(背景の画素をx,画像Aの画素をa,画像Bの画素をbとすると、演算結果は(x+a+2b)/4になるので)
結果としてこのクロスフェードはその途中で背景がだんだん透けて見えるようになり、
それがどうにも不自然なのである。
背景の上のキャラクタをだんだん変えていくような効果を期待するわけなので、
画像A、画像Bともに透明じゃないドットがある箇所は背景が透けるべきではないのである。
では、どうすればいいのか?
本来行いたい処理から考えると、
画像Aと画像Bを適当な比率(クロスフェードの進み具合によって)で混ぜ合わせた後、
それを100%の濃度で背景に転送すればよい。
とまぁ、書くのは簡単なのではあるが…。
このようなことを行おうとするとテンポラリな画像はα値付き画像であることが必要になる。
まぁ、昨今のゲームなんかだと境界を目立たせなくするために元の画像も
α値付き画像であることが多い(?)ので、ここで画像はすべてα値付き画像として扱うことにする。
すると、α値つき画像間での透過度指定転送なるものを定義する必要があるのであるが、
これが若干ややこしい。
α値付き画像Aにα値付き画像Bを半透明度sで転送するとき、
(以降簡便のため、α値は0〜1の範囲で正規化されているものとする)
Aの画素をα値aa、色ac、Bの画素をα値ba、色bc、とすると、
結果の画素は
α値 aa*(1-s)+ba*s 色 (aa*(1-s)*ac+ba*s*bc)/(aa*(1-s)+ba*s)
となる。
このようなα値付き画像間の透過度指定転送を実装して
テンポラリなバッファを用いて計算を行っても良いのだが、
これはどうみても高速では無さそうだし、テンポラリなバッファが必要になるのも良くない。
そういうわけで、当時の私が実際に取った方法は
背景に対して画像Aと画像Bを混ぜ合わせて転送するという
かなり特殊な転送ルーチンを作成するというものだった。
これはかなりカスタムで汎用性のかけらも無いのだが、
現実的な速度を出すためにはやむを得まい。
これを一気に行う計算式は、
背景色をd、画像Aのα値をaa、色をac、画像Bのα値をba、色をbc、Bの半透明度をsとすると、
d*(1-(aa*(1-s)+ba*s))+ac*aa*(1-s)+bc*ba*s
となる。
これで余分なメモリアクセスも生じないし、
計算も二回半透明転送するよりも軽そうなので嬉々として実装したのだが、
最近になってふと、本当にこんな計算が必要だったのだろうかと思ったりした。
なんというか、もっと普通にハードのアクセラレーションで何とかなる範囲の転送を
組み合わせてうまいことできるような気もする。
(或いはうまく見える近似とか…)