fc2ブログ

9ballsyndrome

Away3D 4.0 カスタムフィルタの作り方(前編)

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オブジェクトが入力として使うテクスチャを取得できる。

つまり、

  1. まず0番目のFilter3DBaseを取得する
  2. 0番目のFilter3DBaseの入力テクスチャの参照を取得する
  3. そのテクスチャに通常通りシーンを描画する
  4. あれば次のFilter3DBaseを取得する。なければnull
  5. 0番目のFilter3DBaseに描画させる。ターゲットは次のFilter3DBaseの入力テクスチャ、次がなければnull(つまりバックバッファ)
  6. 4に戻って繰り返し

といった処理だ。

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つは少し複雑なので次回。

 

Away3D 4.0 カスタムフィルタの作り方(後編)

 

スポンサーサイト



  1. 2011/07/26(火) 14:38:11|
  2. Away3D 4
  3. | トラックバック:0
  4. | コメント:0
<<Away3D 4.0 カスタムフィルタの作り方(後編) | ホーム | Molehill Stage3D API 入門 Stage3DでPerlinノイズ>>

コメント

コメントの投稿


管理者にだけ表示を許可する

トラックバック

トラックバック URL
http://9ballsyndrome.blog.fc2.com/tb.php/28-c78cc937
この記事にトラックバックする(FC2ブログユーザー)