引き続きカスタムフィルタを作る。
次にキラキラするTwinkleFilter3Dだが、そもそものキラキラの元祖?はおそらくこちら。
Saqooshaさんのキラキラする雪が文字に積もるというもの。
キラキラのロジックは画面を1/4の大きさにしながらBitmapData.draw()でキャプチャして、4倍に引き延ばしたものを加算合成するという単純な操作。
なぜこれだけでキラキラしたりしなかったりするのかというと、おそらく
といった仕組みかと思われる。
簡単な処理で絶大な視覚的効果を発揮する驚嘆すべき発想で、以来様々な人が利用している。まさにwonderflの理想的な使われ方の例だろう。
最初はStage3Dでは、通常のstageと違うレイヤー、BitmapDataにdrawできない(drawToBitmapDataでできることはできるが遅い)のでこの方法が使えないのではと思っていた。
しかし今回のようなフィルタの観点から見ると、この縮小(抜け落ち)→拡大(補間)→加算合成の手順はfragmentシェーダにうってつけの処理だ。
縮小したテクスチャを作っておき、そこにsetRenderToTextureして拡大したものを加算すればいいだけだからだ。
TwinkleFilter3D
package { import away3d.cameras.Camera3D; import away3d.containers.View3D; import away3d.core.managers.Stage3DProxy; import away3d.debug.Debug; import away3d.filters.Filter3DBase; import com.adobe.utils.AGALMiniAssembler; import flash.display3D.Context3D; import flash.display3D.Context3DProgramType; import flash.display3D.Context3DTextureFormat; import flash.display3D.Context3DVertexBufferFormat; import flash.display3D.Program3D; import flash.display3D.textures.Texture; /** * ... * @author */ public class TwinkleFilter3D extends Filter3DBase { private var _shrinkProgram3D:Program3D; private var _compositeProgram3D:Program3D; private var _shrinkTexture:Texture; private var _strength:uint; public function TwinkleFilter3D(strength:uint = 4){ super(false); _strength = strength < 2 ? 2 : strength; } override public function render(stage3DProxy:Stage3DProxy, target:Texture, camera:Camera3D, depthRender:Texture = null):void { var context:Context3D = stage3DProxy.context3D; super.render(stage3DProxy, target, camera); if (!_shrinkProgram3D) initProgram(context); stage3DProxy.setProgram(_shrinkProgram3D); context.setRenderToTexture(_shrinkTexture, false, 0, 0); context.clear(0.0, 0.0, 0.0, 1.0); context.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); context.setVertexBufferAt(1, _vertexBuffer, 2, Context3DVertexBufferFormat.FLOAT_2); stage3DProxy.setTextureAt(0, _inputTexture); context.drawTriangles(_indexBuffer, 0, 2); if (target) context.setRenderToTexture(target, false, 0, 0); else context.setRenderToBackBuffer(); stage3DProxy.setProgram(_compositeProgram3D); context.clear(0.0, 0.0, 0.0, 1.0); stage3DProxy.setTextureAt(0, _inputTexture); stage3DProxy.setTextureAt(1, _shrinkTexture); context.drawTriangles(_indexBuffer, 0, 2); stage3DProxy.setTextureAt(0, null); stage3DProxy.setTextureAt(1, null); stage3DProxy.setSimpleVertexBuffer(0, null); stage3DProxy.setSimpleVertexBuffer(1, null); } override protected function initTextures(context:Context3D, view:View3D):void { var w:int; var h:int; super.initTextures(context, view); w = _textureWidth >> _strength; h = _textureHeight >> _strength; if (w < 1) w = 1; if (h < 1) h = 1; _shrinkTexture = context.createTexture(w, h, Context3DTextureFormat.BGRA, true); } private function initProgram(context:Context3D):void { _shrinkProgram3D = context.createProgram(); _shrinkProgram3D.upload(new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.VERTEX, getVertexCode()), new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.FRAGMENT, getShrinkFragmentCode())); // _compositeProgram3D = context.createProgram(); _compositeProgram3D.upload(new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.VERTEX, getVertexCode()), new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.FRAGMENT, getCompositeFragmentCode())); } protected function getVertexCode():String { return "mov op, va0\n" + "mov v0, va1"; } protected function getShrinkFragmentCode():String { var code:String; code = "mov ft0, v0 \n"; code += "tex ft0 ft0 fs0<2d,linear,clamp>\n"; code += "mov oc, ft0\n"; return code; } protected function getCompositeFragmentCode():String { var code:String; code = "tex ft0, v0, fs0 <2d,linear,clamp>\n" code += "tex ft1, v0, fs1 <2d,linear,clamp>\n" code += "add oc, ft0, ft1\n" return code; } } }
まず大事なことはfragmentシェーダの出力先は1つしか設定できないことだ。
テクスチャに描画する場合、当然バックバッファに描画+テクスチャにも描画ということはできないし、テクスチャ2枚に描画することもできない。
そしてテクスチャに出力できるのがfragmantシェーダの最後の処理である以上、"テクスチャに縮小したスクリーンを描画し、それを拡大して加算"の処理は1つのシェーダでは実現できない。
よって今回は
の2つのシェーダプログラムが必要になる。
override protected function initTextures(context:Context3D, view:View3D):void { var w:int; var h:int; super.initTextures(context, view); w = _textureWidth >> _strength; h = _textureHeight >> _strength; if (w < 1) w = 1; if (h < 1) h = 1; _shrinkTexture = context.createTexture(w, h, Context3DTextureFormat.BGRA, true); }
まずは一時保存用のテクスチャを作る。スーパークラスでinitTexturesが呼ばれ、そこで画面サイズも得られるのでoverrideして縮小テクスチャもそこで作っておく。
strengthはコンストラクタで決める2以上の整数で、縮小、拡大の際の1辺の長さの倍率だ。大きければ大きいほどキラキラも拡大されるが抜け落ちる情報も多くなる。
両辺1/strengthにしたテクスチャを作る。この際、第四引数をtrueにしておく。この第四引数は作成したテクスチャに描画することがあるかどうかで、trueにされると最適化されるようだ。
この_shrinkTextureに描画し、拡大する。
protected function getShrinkFragmentCode():String { var code:String; code = "mov ft0, v0 \n"; code += "tex ft0 ft0 fs0<2d,linear,clamp>\n"; code += "mov oc, ft0\n"; return code; } protected function getCompositeFragmentCode():String { var code:String; code = "tex ft0, v0, fs0 <2d,linear,clamp>\n" code += "tex ft1, v0, fs1 <2d,linear,clamp>\n" code += "add oc, ft0, ft1\n" return code; }
縮小するシェーダの_shrinkProgram3Dと合成するシェーダの_compositeProgram3Dを作るが、vertexシェーダは共通のものを使う。
getShrinkFragmentCodeではテクスチャを読み込み、そのまま出力するだけのコードだが、出力対象を縮小したテクスチャにsetRenderToTextureすることで勝手に縮小してくれる。
getCompositeFragmentCodeでは1枚のテクスチャを読み込み、それぞれRGB成分足して出力するコードだが、_shrinkTextureは拡大する際に補間される。
stage3DProxy.setProgram(_shrinkProgram3D); context.setRenderToTexture(_shrinkTexture, false, 0, 0); context.clear(0.0, 0.0, 0.0, 1.0); context.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); context.setVertexBufferAt(1, _vertexBuffer, 2, Context3DVertexBufferFormat.FLOAT_2); stage3DProxy.setTextureAt(0, _inputTexture); context.drawTriangles(_indexBuffer, 0, 2); if (target) context.setRenderToTexture(target, false, 0, 0); else context.setRenderToBackBuffer(); stage3DProxy.setProgram(_compositeProgram3D); context.clear(0.0, 0.0, 0.0, 1.0); stage3DProxy.setTextureAt(0, _inputTexture); stage3DProxy.setTextureAt(1, _shrinkTexture); context.drawTriangles(_indexBuffer, 0, 2); stage3DProxy.setTextureAt(0, null); stage3DProxy.setTextureAt(1, null); stage3DProxy.setSimpleVertexBuffer(0, null); stage3DProxy.setSimpleVertexBuffer(1, null);
実際のレンダリングの部分だが、まず_shrinkProgram3Dをセットし、出力対象を_shrinkTextureにする。
元の画像は_inputTextureに入っているのでこれを0版のsamplerにセットして描写する。
次に、レンダリング対象を次のフィルタがあればそのフィルタの_inputTextureに、なければバックバッファにし、_compositeProgram3Dをセットする。
加算する元の_inputTextureと縮小された_shrinkTextureをsamplerにセットし、描画する。
最後に後かたづけだ。Away3Dに限らず、setTextureAtでsamplerにセットしてあるテクスチャを次のシェーダプログラムで使わないと、実行時にエラーが投げらので、使ったテクスチャのsampler番号にsetTextureAtでnullを入れる。
これハマりやすく、エラーの原因を探すのに無駄に悩むことになるので注意。
あーちなみにBloomFilter3Dとの違いがわからない。効果は似たような感じだけどあっちはどんな処理やってるのか読んでない
これで複数枚のテクスチャの扱い、テクスチャに一時的に出力して複数のシェーダプログラムを使うことに慣れたはずだ。
最後にモザイク効果をかけるMosaicFilter3Dだ。
モザイク画像は、画像をあるピクセル単位で分割し、その単位内のピクセルカラーの平均を取り、単位内すべてのピクセルを平均カラーで埋めることでできる。
ところで、fragmentシェーダで処理中のピクセルから他のピクセルにアクセスするにはどうしたらいいだろう。
普通のstageならforループでBitmapDataをgetPixelすればいいのだが、Stage3Dではこれができない。
fragmentシェーダの各ピクセルでの処理はGPUが持つ複数のコアで並列に行われるからだ。むしろそのためのハードウェアがグラフィックボードと言ってもいいくらい。
並列に実行される以上、処理に依存関係があってはならない。なのでfragmentシェーダ実行中に他のピクセルにアクセスすることはできないというわけだ。
fragmentシェーダがアクセスできるのは、そのピクセルに割り当てられたvaryingレジスタの値と、それをuv座標として読み込んだテクスチャの該当ピクセル、constantレジスタの定数のみだ。
ならばuv座標をずらして同じテクスチャをサンプリングすることで他のピクセルにアクセスできるのではないか。
具体的にはuv座標はテクスチャのサイズに対応した0~1までの値なので、1/テクスチャの幅 だけu座標に足してからテクスチャを読み込めば、1ピクセル右のテクスチャカラーを読み込むことができる。
同じテクスチャを何度も読み込むことはできるので、uv座標をずらして複数回読み込みなおすことでモザイクのピクセルカラーの平均化処理をすることができる。
MosaicFilter3D
package { import away3d.cameras.Camera3D; import away3d.containers.View3D; import away3d.core.managers.Stage3DProxy; import away3d.debug.Debug; import away3d.filters.Filter3DBase; import com.adobe.utils.AGALMiniAssembler; import flash.display3D.Context3D; import flash.display3D.Context3DProgramType; import flash.display3D.Context3DTextureFormat; import flash.display3D.Context3DVertexBufferFormat; import flash.display3D.Program3D; import flash.display3D.textures.Texture; /** * ... * @author */ public class MosaicFilter3D extends Filter3DBase { private var _numPixel:uint; private var _simpleMosaic:Boolean; private var _mosaicTexture:Texture; private var _averageProgram3D:Program3D; private var _mosaicProgram3D:Program3D; public function MosaicFilter3D(numPixel:uint = 4, simpleMosaic:Boolean = false){ if (numPixel < 1){ _numPixel = 1; } else { _numPixel = numPixel; } _simpleMosaic = simpleMosaic; super(false); } override public function render(stage3DProxy:Stage3DProxy, target:Texture, camera:Camera3D, depthRender:Texture = null):void { var context:Context3D = stage3DProxy.context3D; super.render(stage3DProxy, target, camera); if (!_averageProgram3D) initProgram(context); stage3DProxy.setProgram(_averageProgram3D); context.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); context.setVertexBufferAt(1, _vertexBuffer, 2, Context3DVertexBufferFormat.FLOAT_2); if (!_simpleMosaic){ var tmp:Texture; var wh:uint; for (var i:int = 0; i < _numPixel; i++){ wh = 1 << i; context.setRenderToTexture(_mosaicTexture); context.clear(0.0, 0.0, 0.0, 1.0); context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([wh / _textureWidth, wh / _textureHeight, 4, 0]), 1); stage3DProxy.setTextureAt(0, _inputTexture); context.drawTriangles(_indexBuffer, 0, 2); tmp = _inputTexture; _inputTexture = _mosaicTexture; _mosaicTexture = tmp; } } if (target) context.setRenderToTexture(target, false, 0, 0); else context.setRenderToBackBuffer(); stage3DProxy.setProgram(_mosaicProgram3D); context.clear(0.0, 0.0, 0.0, 1.0); context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([_textureWidth, _textureHeight, 1 << _numPixel, 0]), 1); stage3DProxy.setTextureAt(0, _inputTexture); context.drawTriangles(_indexBuffer, 0, 2); stage3DProxy.setTextureAt(0, null); stage3DProxy.setSimpleVertexBuffer(0, null); stage3DProxy.setSimpleVertexBuffer(1, null); } private function initProgram(context:Context3D):void { _averageProgram3D = context.createProgram(); _averageProgram3D.upload(new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.VERTEX, getVertexCode()), new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.FRAGMENT, getAverageFragmentCode())); _mosaicProgram3D = context.createProgram(); _mosaicProgram3D.upload(new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.VERTEX, getVertexCode()), new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.FRAGMENT, getMosaicFragmentCode())); } override protected function initTextures(context:Context3D, view:View3D):void { super.initTextures(context, view); _mosaicTexture = context.createTexture(_textureWidth, _textureHeight, Context3DTextureFormat.BGRA, true); } protected function getVertexCode():String { return "mov op, va0\n" + "mov v0, va1"; } protected function getAverageFragmentCode():String { var code:String; code = ""; code += "tex ft0 v0 fs0<2d,linear,clamp>\n"; code += "mov ft1 v0\n"; code += "add ft1.x ft1.x fc0.x\n"; code += "tex ft1 ft1 fs0<2d,linear,clamp>\n"; code += "add ft0, ft0, ft1\n"; code += "mov ft1 v0\n"; code += "add ft1.y ft1.y fc0.y\n"; code += "tex ft1 ft1 fs0<2d,linear,clamp>\n"; code += "add ft0, ft0, ft1\n"; code += "mov ft1 v0\n"; code += "add ft1.xy ft1.xy fc0.xy\n"; code += "tex ft1 ft1 fs0<2d,linear,clamp>\n"; code += "add ft0, ft0, ft1\n"; code += "div ft0, ft0, fc0.z\n"; code += "mov oc, ft0\n"; return code; } protected function getMosaicFragmentCode():String { var code:String; code = ""; code += "mul ft0 v0, fc0\n"; code += "div ft1.xy ft0.xy, fc0.z\n"; code += "frc ft1.xy ft1.xy\n"; code += "mul ft1.xy ft1.xy, fc0.z\n"; code += "sub ft0.xy ft0.xy, ft1.xy\n"; code += "div ft0.xy ft0.xy, fc0.xy\n"; code += "tex ft0 ft0 fs0<2d,linear,clamp>\n"; code += "mov oc, ft0\n"; return code; } } }
先ほどと同じようにinitTexturesで、一時的に平均化した画像を保存する_mosaicTextureを今度は同じサイズで作る。
protected function getAverageFragmentCode():String { var code:String; code = ""; code += "tex ft0 v0 fs0<2d,linear,clamp>\n"; code += "mov ft1 v0\n"; code += "add ft1.x ft1.x fc0.x\n"; code += "tex ft1 ft1 fs0<2d,linear,clamp>\n"; code += "add ft0, ft0, ft1\n"; code += "mov ft1 v0\n"; code += "add ft1.y ft1.y fc0.y\n"; code += "tex ft1 ft1 fs0<2d,linear,clamp>\n"; code += "add ft0, ft0, ft1\n"; code += "mov ft1 v0\n"; code += "add ft1.xy ft1.xy fc0.xy\n"; code += "tex ft1 ft1 fs0<2d,linear,clamp>\n"; code += "add ft0, ft0, ft1\n"; code += "div ft0, ft0, fc0.z\n"; code += "mov oc, ft0\n"; return code; }
getAverageFragmentCodeで平均化の部分のfragmentシェーダを作る。
v0にはuv座標、fc0には[1 / _textureWidth, 1 / _textureHeight, 4, 0]が入っていると考えると、
という処理になる。
ここで、右端と下端のピクセルは4で割ってはいけなかったりするのだが今回は無視。使わないしね。
これで各ピクセルは1ピクセル右、1ピクセル下、1ピクセルずつ右下、のピクセルカラーの平均が出力テクスチャに描画された。
このままバックバッファに出力しないのは、これがモザイク画像ではないから。
本来なら4ピクセルの正方形が同じ平均色でなくてはならないのだが、そうなっていない。
(0,0)のピクセルには(0,0)(1,0)(0,1)(1,1)の平均色が
(1,0)のピクセルには(1,0)(2,0)(1,1)(2,1)の平均色が
(0,1)のピクセルには(0,1)(1,1)(0,2)(1,2)の平均色が
(1,1)のピクセルには(1,1)(2,1)(1,2)(2,2)の平均色が
それぞれ入っているが、これら分割された単位としての4つのピクセルは全て(0,0)の色を入れるべきだ。なので_mosaicProgram3Dでこの処理を行う。
protected function getMosaicFragmentCode():String { var code:String; code = ""; code += "mul ft0 v0, fc0\n"; code += "div ft1.xy ft0.xy, fc0.z\n"; code += "frc ft1.xy ft1.xy\n"; code += "mul ft1.xy ft1.xy, fc0.z\n"; code += "sub ft0.xy ft0.xy, ft1.xy\n"; code += "div ft0.xy ft0.xy, fc0.xy\n"; code += "tex ft0 ft0 fs0<2d,linear,clamp>\n"; code += "mov oc, ft0\n"; return code; }
今度はv0にはuv座標、fs0には先ほどの平均化されたテクスチャ、fc0には[_textureWidth, _textureHeight, 2 , 0]が入っていると考えると、
この処理でfc0.zを一辺とする正方形の範囲がすべて正方形の一番左上のピクセルカラーになる。
実は平均をとらなくてもこの処理だけで多少モザイクっぽく見えるので、高速化のためにこの処理だけを行うモザイクフィルタもコンストラクタで選べるようにしてみた。
実際のレンダリング処理では、まず平均をとる。
stage3DProxy.setProgram(_averageProgram3D); context.setVertexBufferAt(0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_2); context.setVertexBufferAt(1, _vertexBuffer, 2, Context3DVertexBufferFormat.FLOAT_2); if (!_simpleMosaic){ var tmp:Texture; var wh:uint; for (var i:int = 0; i < _numPixel; i++){ wh = 1 << i; context.setRenderToTexture(_mosaicTexture); context.clear(0.0, 0.0, 0.0, 1.0); context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([wh / _textureWidth, wh / _textureHeight, 4, 0]), 1); stage3DProxy.setTextureAt(0, _inputTexture); context.drawTriangles(_indexBuffer, 0, 2); tmp = _inputTexture; _inputTexture = _mosaicTexture; _mosaicTexture = tmp; } }
_averageProgram3Dと4頂点をセットし、_simpleMosaicでなければ_numPixelぶん平均化レンダリングを繰り返す。
_numPixelはコンストラクタで指定し、一辺が2の_numPixel乗ピクセルの正方形のモザイクになる。
処理が面倒なので今回はモザイクのサイズを2の乗数ピクセル限定にしてしまった。
whは1*2^iで、何ピクセル隣のピクセルカラーを平均化に使うかだ。
最初はwh=1なので先ほど説明したように平均化される。
次はwh=2なので1ピクセルとばして2ピクセル隣のピクセルカラーを使う。この2ピクセル隣もすでに周囲4ピクセルの平均なので、これらを使うことで周囲16ピクセルの平均をとったことになる。
_mosaicTextureに出力するように設定し、_inputTextureをfs0に設定しているので、ループの最後にこれらのテクスチャの参照を入れ替えている。
これで常にループ内では使うテクスチャが前回出力されたテクスチャに設定される。参照が変わっているので毎回セットしなおすことを忘れずに。
if (target) context.setRenderToTexture(target, false, 0, 0); else context.setRenderToBackBuffer(); stage3DProxy.setProgram(_mosaicProgram3D); context.clear(0.0, 0.0, 0.0, 1.0); context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.<Number>([_textureWidth, _textureHeight, 1 << _numPixel, 0]), 1); stage3DProxy.setTextureAt(0, _inputTexture); context.drawTriangles(_indexBuffer, 0, 2); stage3DProxy.setTextureAt(0, null); stage3DProxy.setSimpleVertexBuffer(0, null); stage3DProxy.setSimpleVertexBuffer(1, null);
次のフィルタの有無で出力先をバックバッファかテクスチャか選び、_mosaicProgram3Dをセットする。
先ほどのループでは最終的に出力されたテクスチャは_inputTextureに入っているのでこれをfs0として使う。
fc0.zに1 << _numPixelを入れることで、何ピクセル四方にわたって同じピクセルカラーを使うかを指定する。
で最後に後かたづけ。
uv座標にn/wを加えることでnピクセルずらしてアクセスすることができるようになった。
これと今までやってきた、複数シェーダを使う、複数テクスチャを使う、カラーのRGBにそれぞれ違う処理をする、を合わせればアイディア次第でかなり幅広い種類のフィルタが作れるようになると思う。
また、Filter3DBaseの仕組みを真似すればAway3Dに限らずStage3Dのフィルタ効果が作れる。
RGBをHSVに変換する方法を考え中だが、元の式をAGALオペコードだけで再現するのはかなり大変そうだ…。
Author:9ballsyn
ActionScriptについて
最近はMolehill