前回でStage3Dで頂点を設定し、テクスチャが貼れるようになった。
今回は練習にperlinノイズを作ってみよう。
perlinノイズとはいわゆる雲模様フィルタの元となるようなやつで、規則性のあるランダムさを表現するのにとても便利だ。
ActionScriptのBitmapDataクラスにも実装されているのだが、Octaveを増やし、特定のOctaveをガンガン動かすとなるととても重くて使えたもんじゃない。
そこで今回GPUを使ってperlinノイズを描画してみよう。
htmlのwmodeを変えただけだが、Stage3DをGPUを使ったものと、ソフトウェアエミュレートと2種類のデモを用意した。
FPSやCPUの使用率を見るとGPUを使ったStage3Dの優位性が見えてくると思う。
demo1 Hardware ( 要 Flash Player 11 )
demo2 Software ( 要 Flash Player 11 )
perlinノイズの作成方法は
仮眠プログラマーのつぶやき "パーリンノイズアルゴリズム 前編"
を参考にした。
どうやら、グレースケールのランダムノイズ画像を複数枚加算合成すればいいようだ。
この際、ランダムノイズの画像を小さくしていき、引き延ばして使う。引き延ばしの際は補間する。
また、元画像が小さければ小さいほど(引き延ばし率が大きいほど)ブレンド係数を大きくする。
具体的には画像の幅と高さを半分にしていき、ブレンド係数を倍にしていけば良さそうだ。
これだけだ。
Stage3Dではテクスチャは指定したuv座標に合わせて拡大され、間は補間することができる。perlinノイズにピッタリじゃん。
AGALのfragmentシェーダに慣れるのに最適なかんじがする。
package { import com.adobe.utils.AGALMiniAssembler; import flash.display.BitmapData; 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.Context3DTextureFormat; import flash.display3D.Context3DVertexBufferFormat; import flash.display3D.IndexBuffer3D; import flash.display3D.Program3D; import flash.display3D.textures.Texture; import flash.display3D.VertexBuffer3D; import flash.events.Event; import flash.geom.Matrix3D; import flash.text.TextField; import flash.utils.ByteArray; import net.hires.debug.Stats; /** * ... * @author */ [SWF(width="512",height="512")] public class Main extends Sprite { private const RAD_T:Number = Math.PI / 180 / 10; // private var stage3D:Stage3D; private var context3D:Context3D; // private var program:Program3D; private var indexBuffer:IndexBuffer3D; // private var textures:Vector.<Texture>; private var dxy:Vector.<Number>; private var rgb:Vector.<Number>; private var count:uint = 800; public function Main():void { addChild(new Stats()); stage.frameRate = 60; stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; // stage3D = stage.stage3Ds[0]; stage3D.x = 0; stage3D.y = 0; stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreate); stage3D.requestContext3D(Context3DRenderMode.AUTO); } private function onContextCreate(e:Event):void { context3D = stage3D.context3D; context3D.enableErrorChecking = true; context3D.configureBackBuffer(512, 512, 0, false); //text var tf:TextField = new TextField(); tf.wordWrap = true; tf.width = 400; tf.text = context3D.driverInfo; tf.y = 460; addChild(tf); //create createShaders(); setConstant(); setBuffer(); textures = new Vector.<Texture>(6); for (var i:int = 0; i < 6; i++){ textures[i] = createRandomTexture(256 >> i) } //set context3D.setProgram(program); context3D.setTextureAt(0, textures[0]); context3D.setTextureAt(1, textures[1]); context3D.setTextureAt(2, textures[2]); context3D.setTextureAt(3, textures[3]); context3D.setTextureAt(4, textures[4]); context3D.setTextureAt(5, textures[5]); context3D.setRenderToBackBuffer(); //run addEventListener(Event.ENTER_FRAME, onEnter); } private function onEnter(e:Event):void { context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 2, rgb, 1); context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 3, dxy, 1); dxy[0] += 0.004; dxy[1] += 0.0008; var sin:Number = Math.sin(count * RAD_T); rgb[0] = 2 - sin; rgb[2] = 2 + sin; count++; // context3D.clear(0, 0, 0, 1); context3D.drawTriangles(indexBuffer); context3D.present(); } // private function createRandomTexture(size:uint):Texture { var bd:BitmapData = new BitmapData(size, size, false); bd.noise(Math.random() * 0xFFFFFFFF >> 0, 0, 255, 7, true); var texture:Texture = context3D.createTexture(size, size, Context3DTextureFormat.BGRA, false); texture.uploadFromBitmapData(bd); return texture; } private function createShaders():void { //create shaders var agalAssembler:AGALMiniAssembler = new AGALMiniAssembler(); // //vertex var vertexShader:ByteArray = agalAssembler.assemble(Context3DProgramType.VERTEX, "m44 op, va0, vc0 \n" + "mov v0, va1\n"); // //fragment var code:String = ""; //load and blend texture code += "mov ft0 v0\n"; code += "add ft0.xy, ft0.xy fc3.xy\n"; code += "tex ft1, ft0, fs0<2d,repeat,linear>\n"; code += "mul ft1, ft1, fc1.y\n"; code += "tex ft2, ft0, fs1<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc1.x\n"; code += "add ft1, ft1, ft2\n"; code += "tex ft2, ft0, fs2<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc0.w\n"; code += "add ft1, ft1, ft2\n"; code += "add ft0.xy, ft0.xy fc3.xy\n"; code += "tex ft2, ft0, fs3<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc0.z\n"; code += "add ft1, ft1, ft2\n"; code += "tex ft2, ft0, fs4<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc0.y\n"; code += "add ft1, ft1, ft2\n"; code += "tex ft2, ft0, fs5<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc0.x\n"; code += "add ft1, ft1, ft2\n"; code += "mov ft0, ft1\n"; //set color code += "mul ft0, ft0, fc2\n"; code += "mov oc, ft0\n"; var fragmentShader:ByteArray = agalAssembler.assemble(Context3DProgramType.FRAGMENT, code); // //set shaders to program program = context3D.createProgram(); program.upload(vertexShader, fragmentShader); } private function setConstant():void { //vc var mtx:Matrix3D = new Matrix3D(); context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, mtx, false); //ft var blend:Vector.<Number> = new Vector.<Number>(6); var max:Number = 0.6; for (var i:int = 0; i < 6; i++){ blend[i] = max; max /= 2; } context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([blend[0], blend[1], blend[2], blend[3]]), 1); context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 1, Vector.<Number>([blend[4], blend[5], 0, 0]), 1); rgb = Vector.<Number>([1, 1, 1, 0]); dxy = Vector.<Number>([0, 0, 0, 0]); } private function setBuffer():void { //vertex buffer var vertexBuffer:VertexBuffer3D = context3D.createVertexBuffer(4, 4); context3D.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); context3D.setVertexBufferAt(1, vertexBuffer, 2, Context3DVertexBufferFormat.FLOAT_2); vertexBuffer.uploadFromVector(Vector.<Number>([-1, -1, 0, 1, -1, 1, 0, 0, 1, -1, 1, 1, 1, 1, 1, 0]), 0, 4); //index buffer indexBuffer = context3D.createIndexBuffer(6); indexBuffer.uploadFromVector(Vector.<uint>([0, 1, 2, 1, 2, 3]), 0, 6); } } }
まずはコンストラクタから。
stage3D = stage.stage3Ds[0]; stage3D.x = 0; stage3D.y = 0; stage3D.addEventListener(Event.CONTEXT3D_CREATE, onContextCreate); stage3D.requestContext3D(Context3DRenderMode.AUTO);
いつものようにstageからStage3Dオブジェクトを取得し、CONTEXT3D_CREATEイベントを設定したうえでContext3Dを要求する。もう説明するのがめんどくさいくらい毎回出てくる処理。
Context3DRenderMode.AUTOにすることで、できればGPU使ってね、って言っとく。
context3D = stage3D.context3D; context3D.enableErrorChecking = true; context3D.configureBackBuffer(512, 512, 0, false);
CONTEXT3D_CREATEイベントを受け取ったら頻繁に使うのでContext3Dオブジェクトを変数にいれておく。
エラーチェックを有効にして(好み?)、バックバッファのサイズを決める。
//text var tf:TextField = new TextField(); tf.wordWrap = true; tf.width = 400; tf.text = context3D.driverInfo; tf.y = 460; addChild(tf);
Context3D.driverInfoプロパティには現在使用中のGPUの情報が入っているのでこれを表示させてみる。それだけ。
private function setBuffer():void { //vertex buffer var vertexBuffer:VertexBuffer3D = context3D.createVertexBuffer(4, 4); context3D.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); context3D.setVertexBufferAt(1, vertexBuffer, 2, Context3DVertexBufferFormat.FLOAT_2); vertexBuffer.uploadFromVector(Vector.<Number>([-1, -1, 0, 1, -1, 1, 0, 0, 1, -1, 1, 1, 1, 1, 1, 0]), 0, 4); //index buffer indexBuffer = context3D.createIndexBuffer(6); indexBuffer.uploadFromVector(Vector.<uint>([0, 1, 2, 1, 2, 3]), 0, 6); }
順番が前後してしまうが、先にsetBufferの方を説明する。
このメソッドでvertex bufferとindex bufferを作り、値を設定し、セットまでする。
まずvertex bufferだが、正方形を表示させるだけので4頂点だ。また、z座標はいらないので(x,y)と(u,v)の4種類の情報で十分だ。FLOAT_2を2つ使いva0とva1に設定しよう。
(-1, -1),(0, 1)
(-1, 1),(0, 0)
(1, -1),(1, 1)
(1, 1),(1, 0)
となる。四角形にuv座標をそのまま貼り付けた形だ。
index bufferは(0→1→2)と(1→2→3)の2つのポリゴンで四角形にする。
詳しい設定方法は以前の記事のどっかにあるはず。
private function setConstant():void { //vc var mtx:Matrix3D = new Matrix3D(); context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, mtx, false); //ft var blend:Vector.<Number> = new Vector.<Number>(6); var max:Number = 0.6; for (var i:int = 0; i < 6; i++){ blend[i] = max; max /= 2; } context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([blend[0], blend[1], blend[2], blend[3]]), 1); context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 1, Vector.<Number>([blend[4], blend[5], 0, 0]), 1); rgb = Vector.<Number>([1, 1, 1, 0]); dxy = Vector.<Number>([0, 0, 0, 0]);
}
次にシェーダで使う定数を作る。
頂点シェーダでは変換行列を使うのでMatrinx3Dオブジェクトを作る。そのままの投影なので、行列はデフォルトのまま(4*4単位行列)でよい。
この定数はレンダリングごとに変えるものではないのでセットしてしまおう。vc0(~vc3)に入れる。
ピクセルシェーダではどんな定数を使うか。
テクスチャをそのまま出力させるだけなら定数はいらないが、今回は複数のテクスチャを加算合成する時のブレンド率が必要だ。
適当に0.6を最大値として、半分になっていくようにしよう。これをfcにセットするが、テクスチャは6枚使う予定なのでブレンド率は6つ必要だ。
1つのconstantレジスタには4つの32ビット値までしか入れられないので、2つ使うことになる。
fc0.x、fc0.y、fc0.z、fc0.w、fc1.x、fc1.yでそれぞれにアクセスできる。
fc1.zとfc1.wは余るが、特にレジスタ数に困っていないのであけておこう。
これもレンダリングごとに変わらないのでセットをする。
ついでにperlinノイズの色と座標をを時間変化させてみよう。
時間ごとにrgbの率と座標を変える。このためのパラメータはconstntレジスタに入れて使うが、レンダリングごとにパラメータは変わるのでそのたびにセットする。ここでは基となる配列だけ作っておこう。
private function createShaders():void { //create shaders var agalAssembler:AGALMiniAssembler = new AGALMiniAssembler(); // //vertex var vertexShader:ByteArray = agalAssembler.assemble(Context3DProgramType.VERTEX, "m44 op, va0, vc0 \n" + "mov v0, va1\n");
次にシェーダを作ろう。
まず頂点シェーダは、4頂点をそのままスクリーンに投影してくれればいい。あとは頂点に対応したuv座標を補間してピクセルシェーダに渡してもらう。
vc0には先ほど変換行列を入れた。va0には頂点座標が入っている。m44命令でopに変換を出力できる。
また、va1にあるuv座標をvaryingレジスタv0に移しておく。
//fragment var code:String = ""; //load and blend texture code += "mov ft0 v0\n"; code += "add ft0.xy, ft0.xy fc3.xy\n"; code += "tex ft1, ft0, fs0<2d,repeat,linear>\n"; code += "mul ft1, ft1, fc1.y\n"; code += "tex ft2, ft0, fs1<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc1.x\n"; code += "add ft1, ft1, ft2\n"; code += "tex ft2, ft0, fs2<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc0.w\n"; code += "add ft1, ft1, ft2\n"; code += "add ft0.xy, ft0.xy fc3.xy\n"; code += "tex ft2, ft0, fs3<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc0.z\n"; code += "add ft1, ft1, ft2\n"; code += "tex ft2, ft0, fs4<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc0.y\n"; code += "add ft1, ft1, ft2\n"; code += "tex ft2, ft0, fs5<2d,repeat,linear>\n"; code += "mul ft2, ft2, fc0.x\n"; code += "add ft1, ft1, ft2\n"; code += "mov ft0, ft1\n"; //set color code += "mul ft0, ft0, fc2\n"; code += "mov oc, ft0\n"; var fragmentShader:ByteArray = agalAssembler.assemble(Context3DProgramType.FRAGMENT, code); // //set shaders to program program = context3D.createProgram(); program.upload(vertexShader, fragmentShader); }
次はピクセルシェーダだが、長くなるのでString型変数に入れながら作ろう。
まずは6枚のテクスチャを読み込み、加算合成する。
code += "mov ft0 v0\n";
code += "add ft0.xy, ft0.xy fc3.xy\n";
v0レジスタの補間されたuv座標をtemporaryレジスタft0にコピーする。
そしてuv座標の移動距離が入ったfc3のxyを加算する。これで各ピクセルuv座標がずれることになる。
ずれて動いているように見えるのはいいが0~1の範囲をはみ出したらどうするんだ、となるが、テクスチャの属性をrepeatにすることによってずれたuv座標にも繰り返しテクスチャを貼ることができる。
たとえば(u, v)=(1.3, -0.7)なら(0.3, 0.3)と同じテクスチャが貼られるわけだ。
code += "tex ft1, ft0, fs0<2d,repeat,linear>\n";
code += "mul ft1, ft1, fc1.y\n";
code += "tex ft2, ft0, fs1<2d,repeat,linear>\n";
code += "mul ft2, ft2, fc1.x\n";
code += "add ft1, ft1, ft2\n";
code += "tex ft2, ft0, fs2<2d,repeat,linear>\n";
code += "mul ft2, ft2, fc0.w\n";
code += "add ft1, ft1, ft2\n";
tex命令でft1にft0のuv座標をもとにfs0のテクスチャカラーをコピーする。線形補間linearとテクスチャの繰り返しrepeatのオプションを忘れずに。
このテクスチャの色にブレンド率をかける。fs0には一番大きなテクスチャが入っているので(後述)ブレンド率は一番小さなものをかける。fc1.yに先ほどいれたものだ。
同様にfs1のテクスチャをft2に読み込み、二番目に小さなブレンド率fc1.xをかける。そしてそのカラーをft1(ブレンド率をかけた最初のテクスチャをいれてある)に加算(ブレンド)する。
また同様にfs2のテクスチャを読み込み、ブレンド率fc0.wをかけてft1に加算する。ft2の情報はもういらなっているのでft2に読み込む。これで3枚のテクスチャがブレンド率に応じて加算された。
code += "add ft0.xy, ft0.xy fc3.xy\n";
code += "tex ft2, ft0, fs3<2d,repeat,linear>\n";
code += "mul ft2, ft2, fc0.z\n";
code += "add ft1, ft1, ft2\n";
code += "tex ft2, ft0, fs4<2d,repeat,linear>\n";
code += "mul ft2, ft2, fc0.y\n";
code += "add ft1, ft1, ft2\n";
code += "tex ft2, ft0, fs5<2d,repeat,linear>\n";
code += "mul ft2, ft2, fc0.x\n";
code += "add ft1, ft1, ft2\n";
code += "mov ft0, ft1\n";
3枚合成したところで、uv座標ft0にもう一度、移動量fc3.xyを足してみよう。この新しいft0を使うことで先ほどの3枚の2倍の速さでテクスチャが進んでいるように見える。
perlinノイズのテクスチャを動かすだけなら毎フレーム6枚合成する必要はなく、いったん合成したテクスチャ1枚を使えばいいだけなのでGPUを使う意味はあんまりない。
リアルタイム合成こそがGPUの本領発揮なので6つのOctaveぜんぶ別々に操作できますよ、というアピールのために3枚づつ動かしてみた。
新しいuv座標ft0を使ってfs3、fs4、fs5を読み込み、ブレンド率をかけてft1に合成する。最後に6枚を合成したft1のカラーをft0コピーした(特に意味のある行為ではない)。
これで移動量に従ったperlinノイズがft0に合成された。
//set color
code += "mul ft0, ft0, fc2\n";
code += "mov oc, ft0\n";
var fragmentShader:ByteArray = agalAssembler.assemble(Context3DProgramType.FRAGMENT, code);
//
//set shaders to program
program = context3D.createProgram();
program.upload(vertexShader, fragmentShader);
ところで、元テクスチャはグレースケールなので(後述)色をつけよう。
fc2にrgb各色の倍率がいれてあるので(後述)、ft0にかける。
最後にocに出力して完成だ。
AGALMiniAssemblerオブジェクトでコンパイルし、Program3Dを作ってシェーダをセットする。
//create createShaders(); setConstant(); setBuffer(); textures = new Vector.<Texture>(6); for (var i:int = 0; i < 6; i++){ textures[i] = createRandomTexture(256 >> i) }
最後にテクスチャを作る。
createRandomTexture()は引数のサイズのBitmapDataを作り、グレースケールのランダムノイズを発生させたものを同じサイズのテクスチャを作って設定し、返すメソッドだ。
256*256から8*8まで6枚のテクスチャを作った。
//set context3D.setProgram(program); context3D.setTextureAt(0, textures[0]); context3D.setTextureAt(1, textures[1]); context3D.setTextureAt(2, textures[2]); context3D.setTextureAt(3, textures[3]); context3D.setTextureAt(4, textures[4]); context3D.setTextureAt(5, textures[5]); context3D.setRenderToBackBuffer(); //run addEventListener(Event.ENTER_FRAME, onEnter);
今回のシェーダプログラムはレンダリングごとに変わるわけではないので、先ほどのプログラムをセットしてしまう。
また、テクスチャも同様なので大きい順にfs0~fs5にセットしよう。テクスチャにではなくバックバッファに描画することを明示して準備は完了だ。
ENTER_FRAMEイベントを開始する。
private function onEnter(e:Event):void { context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 2, rgb, 1); context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 3, dxy, 1); dxy[0] += 0.004; dxy[1] += 0.0008; var sin:Number = Math.sin(count * RAD_T); rgb[0] = 2 - sin; rgb[2] = 2 + sin; count++; // context3D.clear(0, 0, 0, 1); context3D.drawTriangles(indexBuffer); context3D.present(); }
このフレームで使うdxyとrgbをフラグメントシェーダのconstantレジスタfc2とfc3にセットする。この命令がなければrgbを変えても反映されないので注意。
ついでに次のフレームで使うdxyとrgbに更新しておく。rとbがグレースケールから互い違いに1~3倍になるかんじにしてある。
最後にバッファをクリアし、drawTriangles()でバックバッファに描画し、present()で実際の画面と同期してperlinノイズは完成だ。
このようにフラグメントシェーダを使うと画像の任意ブレンド率での合成、引き伸ばしの線形補間が簡単に、高速で実現できる。
今回の考え方を応用すればいろいろな表現が可能になるだろう。
Stage3Dの画面は従来のようにフィルタをかけることができないらしいが、フラグメントシェーダを使えば工夫次第ではこれも可能になるんじゃないだろうか。
Author:9ballsyn
ActionScriptについて
最近はMolehill