Away3D 4.0のFlash Player 11 β対応バージョンから実装されたSrage3Dのフィルタ機能。
View3Dのfilters3dプロパティにFilter3DBaseクラスの配列を
view3D.filters3d = [new BlurFilter3D(4, 4)];
のように代入すれば使えれる非常にお手軽な機能だ。
今のところ使えるフィルタは4種類。
DepthOfFieldFilter3Dフィルタがちょっとよくわからないが、このプリセットのフィルタを簡易的に確認するデモがこちら。
demo1 ( 要 Flash Player 11 )
今回はこのフィルタを自作してみよう。
適当に5つのカスタムフィルタを加えたデモがこちら
demo2 ( 要 Flash Player 11 )
まずはAway3D内でフィルタとはどのように処理されているのかを見てみよう。
View3Dクラスのrenderメソッド内に以下のような記述がある。
if (numFilters > 0 && context) { var nextFilter : Filter3DBase; var filter : Filter3DBase = Filter3DBase(_filters3d[0]); targetTexture = filter.getInputTexture(context, this); _renderer.render(_entityCollector, targetTexture); for (var i : uint = 1; i <= numFilters; ++i) { nextFilter = i < numFilters? Filter3DBase(_filters3d[i]) : null; filter.render(stage3DProxy, nextFilter? nextFilter.getInputTexture(context, this) : null, _camera, _depthRender); filter = nextFilter; } context.present(); } else _renderer.render(_entityCollector);
簡単に言うと、numFiltersが1以上の場合は特別な処理をして、それ以外は普通に処理しますよ、といった感じだ。
_filters3dはfilters3dプロパティのprivate版で、numFiltersはこの_filters3dのlengthをとったものだ。
_renderer.render()はシーンをレンダリングする命令で、第一引数にレンダリング対象を、第二引数にレンダリングターゲットを指定するようになっているようだ。
レンダリングターゲットにはテクスチャを渡し、そのテクスチャにレンダリング結果が描画される。デフォルトかnullを渡した場合は通常のバックバッファに描画されるようになっている。
filter.getInputTexture()でFilter3DBaseオブジェクトが入力として使うテクスチャを取得できる。
つまり、
といった処理だ。
Filter3DBaseの配列に描画したテクスチャを渡し、そのテクスチャをもとにフィルタ処理を行っていく。フィルタの描画ターゲットは次のフィルタの入力テクスチャで、最後のフィルタはバックバッファに描画する。
このFilter3DBaseを継承してカスタムフィルタを作ればプリセットのフィルタと同じ使い方で任意のフィルタ機能を実現できるだろう。
Filter3DBaseのフィルタ機能は簡単に言うと、4つの頂点に2つのポリゴンで平面を画面等倍に作り、入力テクスチャを貼り付けて、fragmentシェーダで各ピクセルに対して処理を行う、というものだ。
Filter3DBaseのinitBuffers()メソッドを見るとまさにそんなvertex bufferとindex bufferになっていることがわかるだろう。
vertexシェーダは頂点をそのまま投影するだけなので主人公はfragmentシェーダということになる。
BlurFilter3Dクラスのrender()メソッドを見て大体の作り方を学ぼう。実際の処理の部分を抜き出すと、以下のものだろう。
if (target) context.setRenderToTexture(target, false, 0, 0); else context.setRenderToBackBuffer(); stage3DProxy.setProgram(_program3d); 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.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, _data, 2); context.drawTriangles(_indexBuffer, 0, 2); stage3DProxy.setTextureAt(0, null); stage3DProxy.setSimpleVertexBuffer(0, null); stage3DProxy.setSimpleVertexBuffer(1, null);
targetはrender()メソッドの引数で次のフィルタの入力テクスチャかnullが与えられている。
これがnullの場合はバックバッファに、テクスチャの場合はそのテクスチャにContext3D.setRenderToをしている。
現在のフィルタの入力テクスチャをtexture samplerにセットして4頂点のvertex bufferをattributeレジスタに、fragmentシェーダで使う定数をconstantレジスタにセットして描写するというおなじみの処理だ。
stage3DProxyとcontextの使い分けは良くわからないが、まあコピペすれば大丈夫。
とりあえずBlurFilter3Dクラスを下敷きに、入力テクスチャをグレースケールにするGrayFilter3Dクラスを作ってみよう。
GrayFilter3D
package { import away3d.cameras.Camera3D; 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.Context3DVertexBufferFormat; import flash.display3D.Program3D; import flash.display3D.textures.Texture; /** * ... * @author */ public class GrayFilter3D extends Filter3DBase { private var _program3D:Program3D; private var _data:Vector.; public function GrayFilter3D(type:uint = 0){ super(false); if (type == 0){ //simple _data = Vector. ([1 / 3, 1 / 3, 1 / 3, 0]); } else { //NTSC _data = Vector. ([0.298912, 0.586611, 0.114478, 0]); } } 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 (!_program3D) initProgram(context); if (target) context.setRenderToTexture(target, false, 0, 0); else context.setRenderToBackBuffer(); stage3DProxy.setProgram(_program3D); 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); context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, _data, 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 { _program3D = context.createProgram(); _program3D.upload(new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.VERTEX, getVertexCode()), new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.FRAGMENT, getFragmentCode())); } protected function getVertexCode():String { return "mov op, va0\n" + "mov v0, va1"; } protected function getFragmentCode():String { var code:String; code = ""; code += "tex ft0 v0 fs0<2d,linear,clamp>\n"; code += "dp3 ft0.xyz, ft0, fc0\n"; code += "mov oc, ft0\n"; return code; } } }
RGBに分解されて与えられたピクセルカラーをグレースケールにするだけなので非常に簡単だ。
一番単純なのはRGB全ての値を足して3で割るという単純平均法。
これはconstantレジスタfc0のxyzにそれぞれ1/3をセットし、ピクセルカラーが分解されたxyzに3次元の内積(dp3命令)をし、そのピクセルのRGB全てをその値にすることで実装できる。
これだけじゃアレなのでNTSC係数による加重平均法も実装し、コンストラクタで選べるようにしてみる。
[1/3, 1/3, 1/3]だったものを[0.298912, 0.586611, 0.114478]に変えただけだ。
ピクセルに対して処理する専門のシェーダだけあって、こういった処理は簡単にできる。
次は入力テクスチャをセピア調に変えるSepiaFilter3Dだ。
SepiaFilter3D
package { import away3d.cameras.Camera3D; 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.Context3DVertexBufferFormat; import flash.display3D.Program3D; import flash.display3D.textures.Texture; /** * ... * @author */ public class SepiaFilter3D extends Filter3DBase { private var _program3D:Program3D; private var _data:Vector.; public function SepiaFilter3D(type:uint = 0){ super(false); if (type == 0){ //simple _data = Vector. ([1 / 3, 1 / 3, 1 / 3, 0]); } else { //NTSC _data = Vector. ([0.298912, 0.586611, 0.114478, 0]); } } 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 (!_program3D) initProgram(context); if (target) context.setRenderToTexture(target, false, 0, 0); else context.setRenderToBackBuffer(); stage3DProxy.setProgram(_program3D); 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); context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, _data, 1); context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 1, Vector. ([0.9, 0.7, 0.4, 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 { _program3D = context.createProgram(); _program3D.upload(new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.VERTEX, getVertexCode()), new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.FRAGMENT, getFragmentCode())); } protected function getVertexCode():String { return "mov op, va0\n" + "mov v0, va1"; } protected function getFragmentCode():String { var code:String; code = ""; code += "tex ft0 v0 fs0<2d,linear,clamp>\n"; code += "dp3 ft0.xyz, ft0, fc0\n"; code += "mul oc, ft0.xyz, fc1.xyz\n"; return code; } } }
セピア調にするのは、グレースケールに変換したものに、R...0.9、G...0.7、B...0.4くらいの数値をかけるだけで実装できる。先ほどのGrayFilter3Dを下敷きに少し付け足しただけだ。
最後に入力テクスチャをネガのように反転するNegFilter3D。
NegFilter3D
package { import away3d.cameras.Camera3D; 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.Context3DVertexBufferFormat; import flash.display3D.Program3D; import flash.display3D.textures.Texture; /** * ... * @author */ public class NegFilter3D extends Filter3DBase { private var _program3D:Program3D; public function NegFilter3D(){ 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 (!_program3D) initProgram(context); if (target) context.setRenderToTexture(target, false, 0, 0); else context.setRenderToBackBuffer(); stage3DProxy.setProgram(_program3D); 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); context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, Vector.([1, 0, 0, 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 { _program3D = context.createProgram(); _program3D.upload(new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.VERTEX, getVertexCode()), new AGALMiniAssembler(Debug.active).assemble(Context3DProgramType.FRAGMENT, getFragmentCode())); } protected function getVertexCode():String { return "mov op, va0\n" + "mov v0, va1"; } protected function getFragmentCode():String { var code:String; code = ""; code += "tex ft0 v0 fs0<2d,linear,clamp>\n"; code += "sub ft0, fc0.x, ft0\n"; code += "mov oc, ft0\n"; return code; } } }
ネガ調にするのも非常に単純で、255-R、255-G、255-Bするだけだ。
fragmentシェーダ内ではRGBは最高値1にスケーリングされているので実際には1から引くことになる。
以上の3つはアルゴリズムが簡単で似たような実装方法なのでまとめて紹介した。
これで各ピクセルを加算したり平均化したりする簡単なカスタムフィルタは書けるようになったはずだ。
残りの2つは少し複雑なので次回。
Author:9ballsyn
ActionScriptについて
最近はMolehill