Alternativa3Dはちょっと休憩して。
Molehill Stage3D APIでのGPUを利用した最小限のコードを読み解いてみる。
コードはcellfusion "Molehill のローレベル API で遊ぼう"のMolehill の最小コード — Gistを参考にさせてもらった。
まずはあたごんとFlash "[news] molehill 詳細について part2"、_level0 Kayac "MolehillとPB3Dで遊んでみた"でお勉強。
package { import com.adobe.utils.AGALMiniAssembler; import flash.display.Sprite; import flash.display.Stage3D; import flash.display.StageAlign; import flash.display.StageScaleMode; import flash.display3D.Context3D; import flash.display3D.Context3DProgramType; import flash.display3D.Context3DRenderMode; import flash.display3D.Context3DVertexBufferFormat; import flash.display3D.IndexBuffer3D; import flash.display3D.Program3D; import flash.display3D.VertexBuffer3D; import flash.events.Event; import flash.geom.Matrix3D; import flash.geom.Rectangle; /** * ... * @author */ [SWF(width="550",height="400")] public class Main extends Sprite { private var stage3D:Stage3D; private var context3D:Context3D; public function Main():void { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; // stage3D = stage.stage3Ds[0]; //stage3D.viewPort = new Rectangle(0, 0, 550, 400); stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreate); stage3D.requestContext3D(Context3DRenderMode.AUTO); } private function onContextCreate(e:Event):void { context3D = stage3D.context3D; trace(context3D.driverInfo); context3D.enableErrorChecking = true; context3D.configureBackBuffer(550, 400, 0, true); // var vertexShader:AGALMiniAssembler = new AGALMiniAssembler(); vertexShader.assemble(Context3DProgramType.VERTEX, "m44 op, va0, vc0 \n" + "mov v0, va1\n"); // var mtx:Matrix3D = new Matrix3D(); mtx.appendScale(1, 0.5, 1); context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, mtx, false); // var fragmentShader:AGALMiniAssembler = new AGALMiniAssembler(); fragmentShader.assemble(Context3DProgramType.FRAGMENT, "mov oc v0"); // var program:Program3D = context3D.createProgram(); program.upload(vertexShader.agalcode, fragmentShader.agalcode); // var vertexBuffer:VertexBuffer3D = context3D.createVertexBuffer(3, 6); context3D.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3); context3D.setVertexBufferAt(1, vertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_3); vertexBuffer.uploadFromVector(Vector.<Number>([-1, -1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, -1, 0, 0, 0, 1]), 0, 3); // var indexBuffer:IndexBuffer3D = context3D.createIndexBuffer(3); indexBuffer.uploadFromVector(Vector.<uint>([0, 1, 2]), 0, 3); // context3D.clear(0, 0, 0, 1); context3D.setProgram(program); context3D.drawTriangles(indexBuffer); context3D.present(); } } }
stage3D = stage.stage3Ds[0];
//stage3D.viewPort = new Rectangle(0, 0, 550, 400);
stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreate);
stage3D.requestContext3D(Context3DRenderMode.AUTO);
おなじみのContext3Dの取得。stagestage3Dsは4つのStage3Dを持っている。
つまり取得できるContext3Dは4つってことかな。
viewportプロパティをRectangleで指定する。
このviewportの範囲に3Dのレンダリングの結果がピクセルごとに書き出される。
このプロパティはいつでも(Context3Dをrequestする前でも)設定できる。
Context3Dを破棄せずにStage3Dを隠すにはこのプロパティをnullにしたりRectangleの大きさを小さくしたりすることで可能。
viewportプロパティはβ2から削除されました。現在は使えません。
requestContext3D()はレンダーモードを引数にとる。
Context3DRenderMode.AUTOはGPUが使えるなら優先的にハードウェアレンダリングが、使えなければソフトウェアレンダリングが自動で選ばれる。
Context3DRenderMode.SOFTAWAREはソフトウェアレンダリングが強制的に選ばれる。
requestContext3D()のデフォルトはAUTO。
今まで気にしてなかったけどAUTOになってたんだね。
実際のレンダーモードはhtml側のwmodeのほうも加味して決まる。
AUTO、winodow...ソフトウェアレンダリング
AUTO、direct...(あれば)ハードウェアレンダリング
SOFTWARE、winodow...ソフトウェアレンダリング
SOFTWARE、direct...ソフトウェアレンダリング
FP11正式リリースからwmode="window"でContext3Dを作るとエラーがでるようになりました。なのでレンダリングの種類は以下のようになります。
AUTO、direct...(あれば)ハードウェアレンダリング
SOFTWARE、direct...ソフトウェアレンダリング
context3D = stage3D.context3D;
trace(context3D.driverInfo);
context3D.enableErrorChecking = true;
context3D.configureBackBuffer(550, 400, 0, true);
CONTEXT3D_CREATEイベントが送出されたらContext3Dが取得できるようになる。
Context3DのdriverInfoプロパティで現在のグラッフィックスライブラリドライバが得られるので、ソフトウェアだった場合に分岐処理をするなどの時に参照する。
enableErrorCheckingプロパティをtrueにするとなんかある種の変なことしたときにエラーを吐いてくれる。
デフォルトではfalseで、変なことしてもエラーを吐かない場合がある。
パフォーマンスが多少落ちるとか。
最初はtrueにしておいたほうがよさそう。
configureBackBuffer()はバックバッファのサイズを指定する。
レンダリング結果はバックバッファに順次描写され、実際に画面に描写するときはバックバッファから画像をコピーする。
引数ではwidthとheight、アンチエイリアスのレベルと深度、ステンシルバッファの有無を指定する。
今回は550*400、アンチエイリアスなし深度あり。
var vertexShader:AGALMiniAssembler = new AGALMiniAssembler();
vertexShader.assemble(Context3DProgramType.VERTEX, "m44 op, va0, vc0 \n" + "mov v0, va1\n");
いよいよシェーダを作っていく。
まずはvertexシェーダだ。
AGALMiniAssemblerオブジェクトを作り、assemble()メソッドでシェーダをバイトコードにアッセンブルする。
assemble()の引数はプログラムの種類とプログラムのストリングだ(もうひとつあるけどとりあず無視)。
プログラムの種類はその実2種類しかない。vertexシェーダ用のContext3DProgramType.VERTEXとfragmentシェーダ用のContext3DProgramType.FRAGMENTだ。
AGALはアセンブラみたいなもののようだ。
オペコード 出力先 入力1 入力2
のように書く。
以下にレジスタの種類と使い方をあげる。
type | name | vertex | fragment | 使い方 |
0 | Attribute | 8 | なし | Context3D.setVertexBufferAt()メソッドで設定する。 va0 |
1 | Constant | 128 | 28 | Context3D.setProgramConstantsFromVector()メソッドなどで設定する。vc0、fc0 |
2 | Temporary | 8 | 8 | 一時保存用のレジスタ。vt0、ft0 |
3 | Output | 1 | 1 | vertexシェーダではopに頂点座標を、fragmentシェーダではocに色を出力すrのが最終目標。 |
4 | Varying | 8 | 8 | vertexシェーダからfragmentシェーダに値を送るときに使う。v0 |
5 | Sampler | なし | 8 | テクスチャを読み込む。fs0 |
1つのレジスタには4次元の32bit変数が与えられる。
va0でContext3D.setVertexBufferAt()メソッドの第一引数に0を入れたvertex bufferにアクセスできる。
つまりvaは各頂点の情報だ。入力と言っても差支えないかと。これをいじり倒すのがvertexシェーダの基本的な操作になる。
最高数が8なのでattributeは最高8種類ということに注意。
vc0でContext3D.setProgramConstantsFromVector()メソッドの第一引数にContext3DProgramType.VERTEXを、第二引数に0を入れた定数にアクセスできる。
たとえば各頂点にかける行列は全部の頂点で同じなのでこの定数に入れておく。
vertexシェーダでは128本の32bit4次元ベクトルが定数として使えので、たとえば4*4行列をコンスタントに保存する際は4本消費する。
vt0は一時保存用のレジスタだ。計算結果をちょっと退避させたいときや数回使うときに使うといいのだろう。
opやocに計算結果を保存するのが各シェーダの役割になる。データの最終目的地。
v0はvertexシェーダからコピーし、fragmentシェーダから読み込む。
fragmentシェーダからva0などのレジスタにはアクセスできないのでvertex bufferの値をfragmentシェーダで使いたいときはvertexシェーダでv0にコピーしておく。
例えば色情報やuv座標など。
fs0はContext3D.setTextureAt()メソッドの第一引数に0を入れたテクスチャにアクセスできる。
実際にvertexシェーダに設定したプログラムを見ていこう。
m44 op, va0, vc0
m44命令は4*4のマトリックス変換をやってくれる。実際にやることは4次元ベクトルの内積の命令であるdp4を用いて
dp4 op.x, va0, vc0
dp4 op.y, va0, vc1
dp4 op.z, va0, vc2
dp4 op.w, va0, vc3
と同じだが、m44を使うことでコードの省略になるそうだ。
vc?を指定するとvc?~vc?+3を行列として扱ってくれる。
このマトリックス変換はよく使うからこのように専用の命令にまとめたのだろう。
つまりm44 op, va0, vc0はopに"attribute0の頂点情報の4次元ベクトル"に"vc0~vc3を行列としたもの"をかけた結果を格納せよという命令だ。va0、vc0~vc3にはあらかじめ値を設定しておくが、説明はのちほど。
mov v0, va1
さて、opに出力したところでvertexシェーダの役割は基本的に完了だが、今回塗りつぶす色は頂点によって変わる、つまり頂点情報を用いるので、vertex bufferの値を使う。
色を塗るのはfragmentシェーダの役割だが、fragmentシェーダはvertex bufferの値であるAttributeレジスタを読めないのでVaryingレジスタに送っておこう。
mov命令は値をコピーする。va1には各頂点の色情報を入れておいた(説明はのちほど)のでVaryingレジスタの1つ、v0にコピーしておこう。
これでこのvertexシェーダの役割はおしまいだ。
ところでAGALプログラムはStringで渡さなくてはならない。各オペレーションとオペレーションの間は\nでつなごう。
再度このコードをみると、
var vertexShader:AGALMiniAssembler = new AGALMiniAssembler();
vertexShader.assemble(Context3DProgramType.VERTEX, "m44 op, va0, vc0 \n" + "mov v0, va1\n");
vertexShaderというAGALMiniAssemblerオブジェクトを作り、プログラムのタイプをVERTEXと指定し、"頂点情報va0にvc0~vc3行列をかけ、va1頂点情報をv0にコピーしておくプログラム"をアセンブルする、といった意味だ。
var mtx:Matrix3D = new Matrix3D();
mtx.appendScale(1, 0.5, 1);
context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, mtx, false);
では次に移ろう。
先ほどのvertexシェーダで使う予定のvc0~vc3をセットする。
context3D.setProgramConstantsFromMatrix()メソッドは第一引数で指定したタイプの第二引数番目の定数に第三引数の4*4行列をセットする。第四引数は行列を転置するかどうかだ。
行列はMatrix3Dクラスで指定する。Matrix3Dクラスはnewするとデフォルトではベクトルにかけてもそのまま(拡大縮小、回転、移動をしない)な行列[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]を勝手に作る。
この行列mtxをappendScale()メソッドを使いスケーリングをしてみた。
このメソッド実行した結果、mtxは"かけると回転、移動はしないがy方向にだけ0.5倍される"行列になる。
この行列をsetProgramConstantsFromMatrix()に入れる。
vertexシェーダ用の定数なので第一引数にはContext3DProgramType.VERTEXを、va0~va3にこの行列を入れたいので第二引数firstRegisterには0を入れる。転置はしないので第五引数はfalseだ。
これでv0~v3にmtxが入った。setProgramConstantsFromMatrix()メソッドはレジスタを4つ消費するので、次以降にContext3DProgramType.VERTEXに定数を入れたいときはfirstRegisterは4以降になるので注意。
var fragmentShader:AGALMiniAssembler = new AGALMiniAssembler();
fragmentShader.assemble(Context3DProgramType.FRAGMENT, "mov oc v0");
次はfragmentシェーダを作る。
vertexシェーダと同じようにAGALMiniAssemblerオブジェクトを作り、AGALプログラムをアセンブルする。
プログラムの種類はfragmentシェーダなのでContext3DProgramType.FRAGMENTだ。
さきほどv0にva1の値を送っておいたので、va1の値はv0としてfragmentシェーダ側から読むことができる。これをocにコピーするだけのプログラムだ。
2つのシェーダを合わせると、va1ベクトルをvc0~vc3行列にかけて、va2ベクトルの値を色として各頂点に出力、といった処理内容だ。
var program:Program3D = context3D.createProgram();
program.upload(vertexShader.agalcode, fragmentShader.agalcode);
シェーダをプログラムとしてGPUに実行させるにはProgram3Dオブジェクトを渡さなくてはならない。
このオブジェクトはコンストラクタから作るのではなく、Context3D.createProgram()メソッドから作らなくてはいけないらしい。
AGALMiniAssembler.assemble()メソッドで作ったシェーダのバイトコートはagalcodeプロパティで取得できる。
Program3D.upload()メソッドでvertexシェーダとfragmentシェーダのバイトコードをセットする。
var vertexBuffer:VertexBuffer3D = context3D.createVertexBuffer(3, 6);
context3D.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
context3D.setVertexBufferAt(1, vertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_3);
vertexBuffer.uploadFromVector(Vector.<Number>([-1, -1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, -1, 0, 0, 0, 1]), 0, 3);
プログラムは書き終わったのでここで実際に操作する頂点を作ろう。
頂点の各情報はVertexBuffer3Dオブジェクトに格納する。
このオブジェクトもコンストラクタでは作らず、Context3D.createVertexBuffer()メソッドからの作成できる。
第一引数は頂点の数、第二引数は1つの頂点がもつ32bit情報の数だ。x,y,zなら3つなので3を指定する。
最高64種類の情報を持たせることができる(ただし、使えるAttributeレジスタは8本しかないので注意。1本のレジスタは4次元の32bitデータなので同時に使える頂点の情報は32種類までと思われる)。
今回は頂点を3つ、頂点の情報をx,y,zとr,g,bの6種類使用する。
Context3D.setVertexBufferAt()メソッドでVeretexBufferオブジェクトに実際の情報をセットできる。
第一引数は関連付けるAttributeレジスタの番号を、第二引数はセットするVertexBufferオブジェクトを、第三引数は頂点情報の種類が何番目からかを、第四引数にはデータのフォーマットを入れる。
このフォーマットは1本のAttributeレジスタをどう使うかということだ。具体的には1本に含まれる4次元のうち何次元使うのかと、floatかunsigned byteどちらとして使うのかだ。
フォーマットはFLOAT_1~FLOAT_4とBYTES_4の5種類しかない。
注意しなくてはならないのが、FLOAT_1として1次元しか使わなくてもAttributeレジスタを1本消費することだろう。残りの3次元は無視される。
頂点情報の種類が何番目からかは、このフォーマットがかかわってくる。
0番目にFLOAT_2をいれてあれば、次は2番目を選ぼう。この2番目にBYTES_4を入れれば次は6番目だ。
プログラムを書くときに使ったva0とva1を思い出してほしい。
今回はattribute0にxyz座標を入れる。3次元なのでFLOAT_3だ。
また、attribute1には色の情報を入れる。こちらもrgbの3次元を、各成分255を1としたFLOAT_3で入れよう。
VertexBufferにattributeを指定したら、次は実際の頂点情報を入力する。
VertexBuffer.uploadFromVector()メソッドはVertexBufferにVector.<Number>から入力する。
第二引数は何番目の頂点からセットするか、第三引数は頂点いくつぶんかだ。
先ほど指定したattributeの順番通りに、(x,y,z,r,g,b)の順番で入れよう。
(-1, -1, 0)にr=255, g=0, b=0
(0, 1, 0)にr=0, g=255, b=0
(1, -1, 0)にr=0, g=0, b=255
という意味だ。頂点は0番目から3つぶんなので第二、第三引数に0,3をセットする。
var indexBuffer:IndexBuffer3D = context3D.createIndexBuffer(3);
indexBuffer.uploadFromVector(Vector.<uint>([0, 1, 2]), 0, 3);
最後にIndexBufferオブジェクトにポリゴンを成す頂点のインデックスを入れる。
IndexBufferオブジェクトもコンストラクタからではなく、Context3D.createIndexBuffer()メソッドから作ろう。
引数はポリゴンの数ではなく、ポリゴンを成す頂点の数(入れるインデックスの数)だ。
今回はポリゴン1つなので3つの頂点を指定する。よって3だ。
IndexBufferへはindexBuffer.uploadFromVector()メソッドを使ってイデックス指定を入れる。
第一引数は順番をインデックスで指定したVector.<uint>を、第二引数には何番目に入れるか、第三引数は入れる数だ。
今回は[0, 1, 2]を1つのポリゴンとするので0番目から3つぶん入れよう。次のポリゴンを入れるときは3番目からになる。
context3D.clear(0, 0, 0, 1);
context3D.setProgram(program);
context3D.drawTriangles(indexBuffer);
context3D.present();
全ての準備が終わったので実際にレンダリングしよう。
まず、描画の前にContext3D.clear()メソッドで全てのバッファをクリアしなくてはならない。
次にcontext3D.setProgram()メソッドでContext3DにProgram3Dオブジェクトにしたシェーダプログラムをセットしよう。
Context3D.drawTriangles()メソッドで実際にバックバッファにレンダリング結果のポリゴンを描画する。
引数はIndexBufferオブジェクトだ。第二、第三引数でポリゴンの数を指定することもできる。
そしてContext3D.present()メソッドでバックバッファの内容をStage3Dのviewport領域に描画する。
するとこのようになるだろう。
画面の中心を(0, 0, 0)として潰れた感じのカラフルなポリゴンが表示される。
各頂点の色を三原色として、その間は線形補間されているようだ。
以上がMolehill Stage3D APIを使ったGPUレンダリングのおおまかな流れだ。
Author:9ballsyn
ActionScriptについて
最近はMolehill