ActionScript で 地形生成 を 4k Byte で


4k fly-through | Si+ (wonderfl.net)


本題の前にちょっと後日談。
前回、擬似屈折率について書きますと書いたのですが、あれから色々検証してみて、投稿したコードに大きな間違えが見つかりまいまして。で、修正して無事まともな Cubemap 擬似屈折ができたんですが、正直、投稿したものよりもリアリティが出ない。何でかなと色々考えてく内に、"擬似"の部分が原因であるという結論にいたりました。
ここまでは良いんですが、問題は間違ってたコードがわりと屈折っぽかったという点。明らかに間違ってるんですが、できればこっちに何とか物理的な意味合いを見出して、新しい擬似屈折アルゴリズムにしたいなー、とかグルグル考えているうちに記事が書けなくなりました。
ここら辺、オチが見えてきたら、また別の投稿で書きたいと思います。


さて、今回はちょっと変化球ということでStage3Dで4kプログラムにチャレンジしてみました。普通4kといえばコンパイル後のバイナリファイルのサイズを指すのですが、最近ではバイナリファイルを介さないLL言語が流行っているため、スクリプト文字数の制限を指す事も多いようです。wonderflではバイナリファイルは実質隠蔽されスクリプトが読めますし、何よりバイナリ最適化はめんどくさいので、今回は4096文字しばりでやってみました。
圧縮表記のテクニックはいっぱいあるのですが、そこを解説しても誰得なので、ここでは地形(テクスチャ)生成、参照、描画に絞って概説を書きたいと思います。
残念ながら、一つ解説するだけで1記事になるような様々な技術を組み合わせて使ってるので、それぞれをあまり深くは書けません。興味のある技術はキーワードを拾っていろいろ自分なりに調べていただければ、と思っています。

メッシュ作成:フラクタル地形とPerlinノイズ

wikipedia:フラクタル地形
wikipedia:フラクタル地形(英語)
英語のwikipediaのほうが実際に計算した例が載ってて判りやすいと思いますが、地形生成にはフラクタル地形とよばれる計算方法を用います。要は乱数(ノイズ)で地形っぽい高低を生成する計算方法で、低周波ノイズは大きく高周波ノイズは小さく混ぜる事で自然な凹凸を作ります。ActionScriptには、このノイズを生成するBitmapData.perlinNoise()という便利関数がありますのでこれを用います。もっと本格的に地形生成する場合は、ノイズ合成の際に周波数に対する振幅の減衰率を調整したりするのですが、今回はActionScript標準関数でも十分な結果が得られそうだったので、そのまま用いました。
具体的な実装方法ですが、非常に簡単です。まず、フィールド大きさ分のBitmapDataを作成し(今回は512x512)、BitmapData.perlinNoise()を使ってフラクタルノイズを描画します(line68)。このビットマップに対してgetPixels()で色を取り出して(line157)高さ情報としてメッシュのz値に設定します(line85,94:今回は便宜的にm/8としてpixel色(256階調)の1/8の高さにしています)。

テクスチャ作成:高度による色付け

色付けは高さに対する色をグラデーションで表現して適用しています。具体的には、海(青)→境界(薄い水色)→浜(黄)→森(緑)→土(茶)→雪(白)を適当な間隔で配置したグラデーションを作成し(line81:g()内でbeginGradientFill()を使って1024x1のBitmapDataを描画)テクスチャとして登録(line105:r()内でミップマップを作成しながらテクスチャ登録)、fragmentシェーダでこのグラデーションテクスチャから高さに合わせた色を選択して着色します。ただしこれだけだと、頂点単位でしか色がつかないため、非常に粗い色付けしかできません。そこでこのプログラムでは、テクスチャに高さ情報を色として格納して(line98:b値に格納)1/99に縮小してメッシュ上に貼り付けまていす。着色に用いる高さ情報にこのテクスチャ上の値(を0.05倍した値)を加算してからグラデーションを参照する事で細かい凹凸による色変化がテクスチャ上で再現されます。

ライティング:拡散光計算(陰)と遮蔽マップ(影)、それから簡易バンプマップ

拡散光と遮蔽マップの2つのライティングを採用しています。今回は光源位置が固定なので予め計算した結果をテクスチャに色として格納して(line90-98)fragmentシェーダで参照しています。
拡散光とは、光源に対して垂直な平面で最も明るく角度がつくにつれて徐々に暗くなる光成分の事です。具体的には、各頂点における法線を計算して(line91,92)光源方向との内積値(line97)をテクスチャのg値に保存(line98)します。
遮蔽マップとは、山によって光が隠れてしまうエリアのマップです。要は山の影です。具体的には、計算したい点から光源方向に向かって高さを調べていき角度(tan()値)が一番大きい点の値を取り出します(line93-96)。この値が一定値(光源の角度のtan()値)より小さければ、光源が山にさえぎられている事になります。遮蔽マップはこの値を格納したマップですが、今回は影になってる部分が分かれば良いだけなので、影の内か外かまで判定してr値に保存(line98)しています。
あとは、fragmentシェーダでこれらの値を掛け算すれば(大まかな)陰影が描画できます。
これだけでもそれなりのイメージになるのですが、影が大まかなので若干つやつやテカテカして見えます。実はここで計算した陰影データを縮小してテクスチャとして貼り付けるだけで簡易バンプマップになります。本来のバンプマップは、各平面からみた光源方向を計算して陰影を計算する必要があるのですが、今回の場合は地形なので、平面は全て上向きで最大傾斜でも45度程度までしかありません。そこでザックリすべての平面が上向き、と仮定すれば、ここで計算した陰影はそのままバンプマップで計算した結果となります。というわけで、さきほどの色付けと同様1/99に縮小して拡散光計算結果をテクスチャとしてレンダリングしました。本当は、ここで遮蔽マップ結果も貼り付けると凹凸が非常にはっきりした画像になるのですが、海や雪の部分で凹凸がはっきりしすぎてリアルでなくなってしまったため、今回は描画していません。

地形データの参照:伝統的なタイルマップ

さて、無事地形のメッシュデータが作成できましたので、今度はどうやって描画するかについてです。
ここで作成したのは512x512の比較的広大なフィールド全体のメッシュデータですが、実際に描画するのはそのごく一部ですので、そのままdrawTrianglesしてしまうと膨大な無駄が発生してしまいます。そこで今回は、非常に伝統的なタイルマップを採用しました。タイルマップとは、ドラクエみたいなファミコンRPGを思い浮かべるとわかりやすいです。地形を適当な大きさのタイルを貼り付けるように表現します。具体的には、512x512の地形メッシュデータを32x32の小さなメッシュ256個に分割してそれぞれをVertexBufferとして登録(line85-103)、描画時に必要なメッシュのVertexBufferだけ適切な位置に移動してdrawTriangle()します(line52-64:圧縮表記のため非常に読みづらくなってます。すいません)。描画するメッシュの選択は現在位置の周囲15x15のメッシュに対して自分の前方向(向きベクトルと相対位置ベクトルの内積が正)のメッシュだけ描画しています。本来は視錐台カリングによってもっと絞り込むことでパフォーマンスが上がるのですが、文字数の関係で割愛しました。

空の作成と描画:フラクタルノイズ再び

というわけで地上は描画できましたが、これだけだとかなりさびしいかったので空も作成しました。
空テクスチャはこれまで散々用いたフラクタルノイズを青と白のグラデーションで変調(BitmapData.palletMap)してしまえばおしまいです(line106-112)。雲にボリュームを出したいなら先ほどの拡散光計算を重ね合わせることで立体感がでますが、今回は文字数の都合で入れてません。
メッシュの形状は色々と考えられるのですが、今回はパースをかける(遠くの雲を小さく描画する)必要があるため、極端に平べったい四角錐にしました(line118-120:高さ200/辺2000の四角錐)。この四角錐を常にカメラの真上に設置することで、遠くまでの空を描画しています。ただしこの方法は、地形は200程度の距離まで描画すれば良いのに対して、空は1000程度まで描画する必要があるため効率的ではありません。通常は、予めパースをかけたテクスチャを準備して半球の内面上に描画します。
あとこういう地形を描画する場合、地形と空の境界の扱いが悩みの種となります。今回は、Fogの要領でカメラからのz方向距離に応じてメッシュのアルファ値を下げて、遠方で地形が透過するようにしました。これはゲームなどでよく使わる手法ですが、もう少しちゃんと誤魔化すために、空と地形の間に遠方地形のモデルを描画する事も多いようです。

てか、何で4kなん?

BreakPoint2009 elevated by Rgba
当時かなり話題になったので「地形で4k」と聞いてピンッときた人もいるかと思います。ドイツのチーム RGBA が作った 4k demoscene です。最初はこんなのをやろうと思ってました。が、実際やってみると音楽すら入れそうになかったので、途中で路線変更して今回の形になりました。やりたい事の半分もできなかった感じです。

改善点:4096文字って意外と厳しいね...

4kByte Script でどの程度できるのか見当もつかなかったので、見切り発車でつくってみたんですが、思ったより色々できたなーと思う反面、やりたいことはあんまり出来なかったなーという感想です。作り方としては、まず4200文字位を目処に基盤を作ってからショートコーディングして、余った文字数で機能を追加してく感じになります。最後の方になると「35文字の余裕ができたから、あの機能とあの機能が入れられるな」みたいな事を考えながらコーディングできたので、なかなか新鮮で面白い経験でした。ただ、そういうのを考えていくと、あれもやりたいこれもやりたいと色々出てきてしまうので、結局やりたいことの半分は我慢しなくてはならない、という感じになりました。
遮蔽影付きのバンプマップ、水面での鏡面反射や雪面でのスペキュラ、Level of Detail(遠方のメッシュを粗くする手法)、視錐台カリング、太陽/グレア/レンズフレアの描画、モーションブラー、ソフトシンセ。。。今度は文字数制限ナシでちゃんとやってみたいところです。