Heliodor 2019/03/10 19:58

ピクセルシェーダー

過去作品の話です。

ヴィータ大脱出のステージ3「無限書庫」は巨大な円筒形の部屋にいるという設定で、プレイヤーが移動するにつれ、まるで部屋が回転するように背景が変化するようプログラムしてありました。これは結構インパクトがあったらしく、開発者としてはニヤリといった感じでした。某所では、あの部分だけ3Dでやっているとか、円筒形のポリゴンに画像を張り付けて回転させているとか予想されていましたが、どれもハズレです。

あれは普通の長方形の板ポリにベタッとテクスチャを張っているだけで、円筒形のように見えているのは、そう見えるようにプログラマブルシェーダー(以下ピクセルシェーダー)で歪ませているからです。試しにオプションで描画モードを簡易描画に切り替えると、シェーダーを通さなくなるので歪みが無くなり、無限書庫の背景はただの平面になるのがわかると思います。

このように、ピクセルシェーダー内でテクスチャの取得座標をうまい具合に計算すれば円筒形のように歪んで見せることができますが、ここでややこしいのは、ピクセルシェーダー内で指定するのはテクスチャの座標であって画面の座標ではないということです。もしもピクセルシェーダーの引数で渡されるのがテクスチャ座標で、それを画面のどの位置に表示するべきかを計算せよ、というのであれば話は簡単ですが、そうではありません。引数で渡されるのは画面内の座標であり、その場所にテクスチャのどの部分を表示するのかを指定するのがピクセルシェーダーでの座標指定です。

コツは、まず普通に「長方形を入力すると内側から見た円筒(ぽい)形に歪む」関数を考えて、それから逆関数を導くというものです。まず長方形から円筒(ぽい)形への座標変換ですが、これは割と簡単で、次のように考えれば大まかな式を得られます。

まず話を簡単にするために、テクスチャ座標の原点はテクスチャ中央で、Y軸は上向きであるとします。普通に数学でやったような座標の取り方です。画面座標も同様にします。

さて、円筒形の内側にベタッと貼った画像は、まるで凹の字のように画面中央では原寸に近く、左右の端に近づくにつれて上下に引き延ばされるように歪んで見えます。

もう少し具体的に考えます。元の長方形の画像を、いくつもの細かい縦長の短冊に区切ってみます。シュレッダーにかけるようなイメージです。

これを並べた時、左右の端に近い短冊にほど上下にぐいっと引き伸ばし、中央に近い短冊は少しだけ引き伸ばし、ど真ん中の短冊はなにもいじりません。

こうすると四角形だった画像を、上下対称に凹のような形に歪ませることが出来ます。

これを実際のテクスチャと画面で考えます。前述の例と同じように、テクスチャを幅1ピクセルの短冊に区切って考えます。テクスチャ座標はU, Vとしましょう。Uは、注目している点がどの短冊に含まれているのかを表します。ゼロなら中央の短冊、正の値なら中央右側の短冊、負なら中央左側の短冊です。Vは、注目している点が短冊のどの場所にあるのかを示します。0なら上下の真ん中、正なら上側、負なら下側です。

そして、U, V にあるピクセルを画面上のどこに表示するのか、その場所をX, Y で表します。Xの正と負が画面の右と左に対応し、Yの正と負が画面の上と下に対応します。

たとえばX=U, Y=V とすると、テクスチャと画面の画像は全く同じになります。

さて、先ほどの短冊の話を思い出してください。各短冊は上下には引き伸ばしますが、左右に移動させたりはしません。つまり、短冊の水平方向の位置は、変換前と変換後で変わりません。Xは常にUと同じです。増えも減りもしません。なので、X=Uです。これは簡単です。

次に上下方向の式です。中心にある短冊は原寸のまま変化せず、左右の端になるほど、つまり中心から離れるほど上下に引き伸ばされていきます。つまりら上下方向の拡大です。拡大というのはつまり元の座標に係数をかける事ですから、Y=AVと書くことができます。Aが1.0なら原寸、1.0より大きければ拡大、小さければ縮小です。

しかし、短冊の上下の拡大率は場所によって異なります。中央の短冊は原寸なのでAを1.0にしないといけませんし、中心から離れるに従ってAの値は1.0より大きくなっていかなければなりません。つまり、AはUの値がゼロの時に1.0になり、Uの絶対値が大きくなるにつれ、1.0より大きくなっていって欲しいのです。上記の条件を満たす式をすごく単純に考えると、A=1+|U|と置けば良い事になります。Xがゼロの時、Aは1です。Uの絶対値が大きくなると、Aの値も大きくなります。つまり、

Y = (1+|U|)V

ですね。

ところがこの式をよく見ると、拡大率AはUに関する1次式になっていますから、Aは直線的に変化することになります。中心から外側にむかって、2つの台形が向かい合って置いてあるかのように歪んで見えるのです。これだとまずいので、適当に曲線を描くようにしてみます。一番簡単そうなのは二次式にすることです。A = (1+U^2) とするんです。そうすると、

Y = (1+U^2)V

となります。ただ、そのままUを2乗するだけだとかなりキツイ曲線になって歪みすぎてしまうので、適当な係数をかけて調整します。

Y = (1+kU^2)V です。

kを1.0未満で小さめの値にすると、なんとなくそれっぽく見えるように歪むと思います。

さて、忘れてはいけないのは、ここで考えた式は長方形→円筒(ぽい)形に歪ませる式であるという事です。つまり入力はテクスチャ座標で、出力は画面座標です。しかし実際に必要なのはその逆で、画面上の座標から元のテクスチャ座標を求める式です。なので、この式を逆関数にして使わないといけません。上で求めた式は

X = U
Y = (1+kU^2)V

ですから、これを

U = ???
V = ???

の形にしないといけません。逆関数を求めるのです。まず、X=Uでしたから、当然U=Xです。これは楽勝ですね。次にVの式ですが、

Y = (1+kU^2)V

の左右を入れ替えて

(1+kU^2)V = Y

で、ここから両辺に同じ計算を施していって、左辺にある余計なものを右辺に移動させていきます。今回は簡単で、両辺を (1+kU^2)で割ればよいです。

(1+kU^2)V/(1+kU^2) = Y/(1+kU^2)

すなわち

V = Y/(1+kU^2)

になります。これで、

U = X
V = Y/(1+kU^2)

という式を得ることができました。これをピクセルシェーダーで使ってやれば思った通りに歪んでくれます。具体的には、上式の結果を使って tex2D(U, V) とかやるわけです。と、まあこんな感じで式を決定していくのですが、わりと試行錯誤しながら砂漠ステージなどの背景を作った気がします。

ちなみにさっきからしつこく「円筒形」ではなく「円筒(ぽい)形」と書いていますが、本当に円筒形に見えるように歪ませるなら、二次関数ではダメで、楕円の式を元にしないといけません。ax^2 + by^2 = c みたいなやつです。これをいろいろいじれば、だいぶそれっぽくなるのではないでしょうか。


これは参考のために手前のキャラを消した状態。円筒(ぽい)形。


最後に、あまり関係ない話なんですが、ピクセルシェーダー内で、a % b (a, b は共にint)の結果が期待通りにならない、というバグ(仕様?)に驚愕したことがあります。この時 b は正の奇数だったのですが、計算結果が想定したものと全然違っていたのです。どうやらHLSLの整数同士の剰余演算子 % では、割る数(b)が2^n (nは正の整数) でないと正しく動かないみたいです。なので、a % b ではなく、(int)fmod(a, b) を使ってしのぎました。

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

記事のタグから探す

月別アーカイブ

限定特典から探す

記事を検索