今日はtaroです.

FlashPlayer 11でGLSLみたいなことが出来るようになったっぽいってことで、ちょっと触ってみました.

といっても僕は3Dとちゃんと向き合ったことがないし、GLSLも理解していなかったので、FlashPlayerでこれらが使えるようになったことで、結構そういうのを勉強する手間が省けた感じがしました.

遊び方

コンパイルするときにTeaPot-config.xmlを作成し、

<?xml version="1.0" encoding="utf-8" ?>
<flex-config>
	<swf-version>13</swf-version>
</flex-config>

のよにオプションを指定します。

サンプルコードを理解する

ByteArray.orgの記事が大変良いです。このページだけで基本的には十分です。 超手抜きの説明をすると、

  1. シェーダーにはvertexシェーダーとfragmentシェーダーの2種類があります
  2. vertexシェーダーは頂点や法線などの座標変換を主に書きます
  3. fragmentシェーダーはテクスチャーからのサンプリングや色付けなどをします

ってことですが、コードを見て動かしたほうがより理解できるかも知れません。

次にAGALのコマンド一覧を見ます。flash.display3D.Program3Dに一覧がありますが、完全な一覧はcom.adobe.utils.AGALMiniAssemblerの中を読むといいでしょう。

レジスタについての説明

各シェーダー毎に使えるレジスタの個数と意味を簡単に表にまとめてみました

名前Vertexシェーダー用Fragmentシェーダー用意味・使い方
Attribute8n/aContext3D::setVertexBufferAtで設定し、va?で参照できる
Constant12828Context3D::setProgramConstantsで設定し、vc?, fc?で参照できる
Temporary88一時変数用vt?, ft?で参照できる
Output11vertexシェーダーではopで頂点座標を出力し、fragmentシェーダーではocで色を出力する
Varying88v?:vertexシェーダーで書き込み、fragmentシェーダーで読み込む。2つのシェーダーでの値のやり取り用。
Samplern/a8fs?: テクスチャーを読み取る.

1つのレジスタには長さ4のベクトルが入るので、例えば、4x4の行列式はこのレジスタを4つ消費します。

オペコード

OpCode Destination Source1 Source2/Sampler

のようなフォーマット。例えば、

mov vt0 va0

はvertexシェーダーで0番目のAttributeから0番目の一時変数用レジスタに値をコピーします。


以上が基本的な説明です。

PixelBender3Dを試してみましたが・・・

まずはdocsのなかのPixelBender3DReference.pdfを軽く目を通します。まだDraftなので仕様は変わるかも知れませんが、要点だけあげると、

  1. PixelBender3Dを利用する手順
    1. pb3dutilでOpenGL等のシェーダーのような言語をコンパイルし、PB-ASMという形式にコンパイルします
    2. pb3dlib.swcというASのライブラリを使ってPB-ASMをAGALのバイトコードにランタイムでコンパイルします
  2. PixelBender2Dとの比較
    1. arrayタイプが使えるように
    2. functionが定義できるように
    3. for文が使える

僕がpb3dutilを試した理由は、AGALMiniAssembler向けのif文やfor文がオペコードとしては存在しているが、書き方があまり良く分からなかったので、PB3Dでそれが出来るならいいなーなんて思ってやったのですが、

実際はまだdraftの為、僕の手元の環境ではifやforの制御構造のあるシェーダーはpb3dutilsでエラーなしにコンパイル出来ても、pb3dlib.swcで利用する際にランタイムエラーを引き起こしました。arrayは試していません。functionは一応使えました。

また、pb3dutilのコンパイルのチェックが緩いため、pb3dutilでエラーは検出されず、AGALMiniAssemblerでエラーがチェックされて、AGALMiniAssemblerのエラーが出たりしました。つまり実際にコードを書いた時に、AGALMiniAssemblerのオペコードを理解していないとこのエラーの意味が分かりません

なので、現状はAGALMiniAssemblerのオペコードがイミフすぎるというだけで手を出してもあまり意味が無い気がしました。

とは言え、ドラフト。あまりディスっても仕方がありません。for文やfunctionが将来使えるようになるかもしれないっていうのはいいですね。

例えば従来のPixelBenderでは・・・

#define toHSV(rgba,hsv) \
   r = rgba.r; \
   g = rgba.g; \
   b = rgba.b; \
   fmax = max(r,max(g,b)); \
   fmin = min(r,min(g,b)); \
   H = 0.0; \
   S = 0.0; \
   if( fmax != fmin) { \
      f = (r==fmin)?(g-b):((g==fmin)?(b-r):(r-g)); \
      i = (r==fmin)?3.0:((g==fmin)?5.0:1.0); \
      H = (i-f/(fmax-fmin)); \
      if(H>=6.0) {H-=6.0;} \
      H*=60.0; \
      S = (fmax-fmin)/fmax; \
   } \
   V = fmax; \
   hsv = pixel4(H, S, V, 1.0);

というようなマクロを書いたりするわけなのですが、それも必要なくなるということです。

また、ループが使えるようになると、フラクタル図形を書いたり、レイトレーシングが出来るわけでこれもまた夢が広がります。

サンプルコード

まずはzendenmushiさんが素晴らしいサンプル・コードをwonderfl上に公開していますので勝手にそれを解説します。

fork元のコード自体結構最適化をしていてあまりストレートなロジックではなく、その説明だけでブログ記事が1本書けてしまいますので、省略します。今回やはり気になるところはどのようなシェーダーを書いたか?ということです。まずはvertexシェーダー

m44 op va0 vc0
mov v0 va2
mul vt0.x va1.x vc4.x
add v0.x va2.x vt0.x

vertexシェーダーの基本的なお仕事はopレジスターに頂点を出力することです。

そういう意味では既に1行目でそのお仕事を終えています。va0レジスタに格納されているベクトルとvc0レジスタに格納されている4x4行列の積がアウトプットです。

vc0はなにかというと、296行目でVertexシェーダーの0番目以降の4レジスタに格納されているMatrix3D orthoのデータとなります。

context.setProgramConstantsFromMatrix(
    Context3DProgramType.VERTEX, 0, ortho, true);

va0はなにかというと、299行目でセットされているインデックス0のAttributeレジスタで

context.setVertexBufferAt(0, vBuffer, 0, Context3DVertexBufferFormat.FLOAT_2);

これはさらにいうと272行目以降で設定されている

vb[index++] = (_posX - 465.0/2)-8;
vb[index++] = (_posY - 465.0/2)-8;  

のように設定されている、矢印の画像の右上、右下、左下、左上の4隅の座標だと思ってください

ということでこのコードはorthoのMatrix3Dを使ってこれらの4隅の座標をGPUを使って座標変換するコードです。

では2行目のmov v0 va2について説明します。これはAttributeレジスタの2番からVarryingレジスタの0番目に値をコピーしています

Varryingレジスタはvertexシェーダーとfragmentシェーダーで値を共有できるレジスタでした。

もっと正確にはvertexシェーダーで値がセットされ、fragmenシェーダーでその値を利用します。

これは一般的にはvertexシェーダーには座標変換用のMatrixをConstantレジスタに書き込んであるからそれを利用して、法線や光源を座標変換してfragmentシェーダーに値を引き渡すというような用途に用いられます。

ここでva2は

context.setVertexBufferAt(
    2, uvBuffer, 0, Context3DVertexBufferFormat.FLOAT_2);

のようにContext3d::setVertexBufferAt(?, ....)がva?に対応し、その値はuvBufferの各頂点ごとの0番目からの値となります。uvBufferには198行目以降で

uvb.push( 0,          0);
uvb.push( 1/ROT_STEPS,0);
uvb.push( 1/ROT_STEPS,1);
uvb.push( 0,          1);  

のように値がセットされていてこれは矢印の各4隅のテクスチャーのuv座標になります。このコードを見て一瞬戸惑うかもしれませんが、

それは、描画の最適化のために矢印を回転させた画像をキャッシュさせてテクスチャーを作っているからで、実はこんな感じのとても長い画像です。

texture.png

なので、u座標がテクスチャーの回転の状態と直結します。ということを踏まえて次の行を見ます。

mul vt0.x va1.x vc4.x

これはつまり、vt0.x = va1.x * vc4.xということです。va1.xには300行目で

context.setVertexBufferAt(1, vBuffer, 2, Context3DVertexBufferFormat.FLOAT_2);

vBufferの頂点データの2番目以降に2つ格納されているfloat型のデータです。それはつまり、

 vb[index++] = (_posX - 465.0/2)-8; // 0番目
 vb[index++] = (_posY - 465.0/2)-8; // 1番目
 vb[index++] = angle >> 0;          // 2番目
 index++;                           // 3番目

ということですので、angleが入っています。angleといってもこれは結局先ほどの長い矢印のテクスチャーの左から何番目かという整数のインデックスです。

さてvc4.xですが、これはConstantレジスタの4番目のレジスタの最初のデータということですが、297行目で

context.setProgramConstantsFromVector(
    Context3DProgramType.VERTEX, 4, r_rot_steps);  

とあり、さらにこのr_rot_stepsは189行目で

r_rot_steps[0] = 1/ROT_STEPS;

のようにセットされています。これはつまりUV座標のU座標の矢印1個分の幅ということです。

なので、この式は総合すると、va1レジスタに格納されている回転の情報を元にテクスチャーのU座標を決定しているということになります。

ここで一つだけ注意して頂きたいのは、このConstantレジスタのインデックスを1でなく4とし、vc4で参照していることです。

それは何故かというと、0番目から始まる4つのレジスタにはMatrix3Dの4x4の行列式が格納されているからです。

つまり、レジスタは4次のベクトルが入り、4x4の行列式を格納するにはレジスタが4つ消費されるということです。

add v0.x va2.x vt0.x  

この行により、v0.xの値を横にスライドさせ回転されたテクスチャーのU座標へと移動します。

ではfragmentシェーダーの方を見ていきましょう。

mov ft0, v0
tex ft1, ft0.xy, fs1 <2d,repeat,nearest>
mov oc, ft1

v0はVarryingレジスタで先ほどのvertexシェーダーから渡される値です。

mov ft0 v0により、Temporaryレジスタ0番ft0へv0の値をコピーします

tex ft1, ft0.xy, fs1 <2d,repeat,nearest>により、uv座標ft0.xyの色情報をTemporaryレジスタft1へコピーします。

ここで参照されているfs1はtextureとしてcontext3dに渡しているもので、218行目

context.setTextureAt( 1, texture );

のように1番にセットされたテクスチャーです。そしてこれは先ほどの横に長いテクスチャーです。

最後に出力用のレジスタocにこの色をコピーしてこのシェーダーはお仕事を終えます。

ちなみにこの辺のレジスターのコピーはちょっと無駄な気がする人もいらっしゃるでしょう。

  tex oc, v0.xy, fs1 <2d,repeat,nearest>

と書いても良いでしょう。以上ちょっと長めに書いてみましたが、AGALMiniAssembler用のアセンブラコードを書きたい人の参考になればと思います。

上のコードをforkしてみた

上のコードを紹介するのにかなりの文字数を使ってしまいましたので、こちらについては紹介程度に留めますが、

先ほどのvertexシェーダーではビューポートに座標変換をする2次元のものだったので、これをトーラスにマッピングしてみました。

fragmentシェーダーは上と同じで、vertexシェーダーだけ変えます

mul vt0.x va0.x vc5.z
mul vt0.y va0.y vc5.w
cos vt1.x vt0.x
cos vt1.y vt0.y
sin vt1.z vt0.x
sin vt1.w vt0.y
mul vt0.x vt1.y vc5.y
mul vt2.z vt1.w vc5.y
add vt0.y vt0.x vc5.x
mul vt2.x vt0.y vt1.x
mul vt2.y vt0.y vt1.z
mov vt2.w va0.w
m44 op vt2 vc0
mov v0 va2
mul vt0.x va1.x vc4.x
add v0.x, va2.x, vt0.x  

式で見るとなんだか凄いことになっていますが、特に大したことはしていません。

正方形をトーラスにマッピングするには、

torus.png

という変換を施します。また、今度は3Dの座標になるので、透視変換用の行列式をかけます。

FlashPlayer11をインストールされている方はオンライン・デモをどうぞ。

コードはwonderfl上で配布しています。

まとめ

  1. MolehillのAPIを使うと3Dライブラリ無しでもGPUを使って素早いレンダリングが可能です。3Dライブラリをほとんどやったことが無い僕でも3Dの計算とかの基礎知識を勉強する必要はありましたが、すぐに馴染めました。
    • ただし、アクセサビリティーの話が付きまといますが、ゲームがしたいがためにGPU使えるマシンを買う時代になってもいいんじゃないかなんて思っています。(半分本気ですが、冗談ですw)
  2. PixelBender3Dはまだまだ、結構ドラフトでした。まだレジスタ等低レベルのことが分からないとデバッグ厳しいかもしれません。
    • きっとこの辺はコンパイラが良くなるはず。また、GUIのツールも出来るんじゃないかと思っています。
  3. Molehill APIで出来ること・将来出来るようになること
    • 環境マッピング。環境マッピング用のテクスチャーのAPIが提供されています
    • 法線や光源を元にシェイディング
    • 2D, 3D空間での大量の座標変換
    • GPUを使ったフラクタル図形のレンダリング
    • レイトレーシング
    • GPUを使った音声合成とかは出来ないのかな・・・?これからのAPI次第ですね。
    等々。夢広がりますね。

個人的にはGLSLでも使われるシェーダーの基礎概念を理解したのが収穫でした。iPhoneやAndroidでGPUレンダリングに挑戦したいです。

HTML5飯