Flex Builderでのアプリケーション構築に関する一般的な経験。この記事のサンプルは、Flex BuilderおよびFlex 3.2 SDKで作成されています。
中級
メモ:この記事のサンプルに含まれるプロジェクトは、ChannelScrambler、CheckerFill、GaussianBlur、GrainBlend、HardLightBlend、MultiplyBlend、ScreenBlendです。この記事には、これらのサンプルのSWFコンテンツが掲載されています。サンプルを詳しく検証したい場合は、付属のサンプルファイルをダウンロードし、Pixel Bender ToolkitおよびFlex Builderでこれらのファイルを開くようにしてください。
Pixel Benderは、Adobe Flash Player 10、Adobe After EffectおよびAdobe Photoshop(近々)がサポートするグラフィックス処理エンジンです。Pixel Benderの言語は、3Dレンダリングのピクセル描画処理の最適化に用いられる、フラグメントシェーダ言語(GLSL:OpenGL Shading Languageなど)に基づいて開発されています。Flexでは、Pixel Benderのプログラムを利用して、フィルタやブレンド、面または線の塗りを作成することができます。
Pixel Benderのエフェクトは、画像、ベクトルグラフィックスからデジタルビデオに至るまで、あらゆる表示オブジェクトに適用できます。実行処理速度も特筆に値します。ActionScriptではフレームあたり数秒かかっていたようなエフェクトでさえ、これからはリアルタイムで処理できるようになります(図1を参照)。
図1. あらゆる表示オブジェクトに適用可能なPixel Benderのエフェクト
Pixel Benderのプログラムはカーネルと呼ばれ、Adobe Pixel Bender Toolkitを利用して記述・コンパイルします。このツールキットによって生成されるコンパイル済みのバイトコードは、Shaderオブジェクトに読み込むことで、SWFコンテンツ上で利用することができます。なお、Pixel Bender ToolkitはFlash CS4 Professionalのインストール時に自動的にインストールされます。
Adobe Flex BuilderでのPixel Benderの使用法についてのドキュメンテーションは、『ActionScript 3.0のプログラミング』の「Pixel Benderシェーダの操作」の章、および『ActionScriptコンポーネント・言語リファレンス』の「Shaderクラス」の節に用意されています。このドキュメンテーションには、FlexおよびFlashでPixel Benderを使用する際に利用可能なオブジェクトの詳しい解説が含まれています。また、Pixel Bender言語のドキュメンテーションには、Pixel Bender Toolkitのヘルプメニューからアクセスできます。
Pixel Benderによって何が可能になるかを知りたい、あるいは新しい技を習得したい、と考えるユーザには、Pixel Benderのプログラムサンプルをチェックすることをお勧めします。この記事で紹介するサンプル以外にも、アドビがホスティングするカーネルのパブリックリポジトリである Pixel Bender Exchangeには、様々なサンプルが掲載されています。これらのカーネルプログラムの作成者は、Flashデベロッパーコミュニティに自らの作品を披露することに同意した親切な方々ばかりです。是非皆さんも、作成したカーネルをExchangeに投稿してみてください。また、Pixel Benderプログラミングについてのディスカッションフォーラムも存在します。
カーネルのコーディングを始める前に、まずPixel Bender言語について簡単に説明します。この記事を読むだけでPixel Bender言語のエキスパートになれるわけではありませんが、以下を読み進むことで、この言語で記述されたプログラムが理解しやすくなるはずです。より詳しい情報については、Pixel Bender ToolkitアプリケーションのHelpメニューからアクセスできる、「Adobe Pixel Bender言語1.0チュートリアルおよびリファレンスガイド」を参照してください。
Pixel BenderではC、Java、ActionScriptなどの開発言語に似た、手続き型のシンタックスを使用します。Pixel Benderには、画像処理に特化した既製のデータ型および関数が含まれています。既にActionScriptに慣れていれば、Pixel Benderカーネルの記述法を習得するのは難しくありません。以下に、ActionScriptとPixel Bender言語のシンタックスの主な相違点を示します。
var foo:int;
代わりに、次の構文を使用します。
int foo;
newキーワードはサポート(必要と)されません。float4 rgbaPixel = float4( 1.0, 0.3, 0.2, 0.8 );
rgbaPixelという名前の新しいベクトル変数が宣言され、これに色の値が代入されています。float4( 1.0, 0.3, 0.2, 0.8 )の部分の表現では、rgbaPixel変数に代入されるfloat4ベクトルの定数リテラルが定義されています。ベクトルの構成メンバーには、r,g,b,a、x,y,z,wまたはs,t,p,qの3つの要素名のセットのいずれかと、ドット表記を用いることでアクセスできます。スウィズリングがサポートされていることにより、これらの要素の並び順は単に要素名を入れ替えるだけで変更できます。例えば、次に示すステートメントでは、ピクセルの値を他の変数に代入する際に、ピクセルベクトルの赤と緑のカラーチャンネルが置き替えられます。
pixel4 mixedUp = rgbaPixel.grba;
次のように、チャンネルを繰り返すことも可能です。
pixel4 allRed = rgbaPixel.rrra;
また、次に示すように、チャンネルを削除することも可能です。
pixel2 redAndBlue = rgbaPixel.rb;
メモ:ベクトル変数で使用する変数名のセットは任意に選択できます。例えば、myVector.rはmyVector.xと同じものを指します。ベストプラクティスとしてはrgbaのセットを色指定、xyzwまたはstpqのセットを位置指定にそれぞれ使用することが推奨されます。一度の参照において、異なるセットの要素名を混同させることはできません。
evaluatePixel()以外)および配列はサポートされません。メモ:Flash PlayerまたはAIR向けのPixel Benderカーネルを開発する際には、(ツールキットウィンドウのBuildメニューにある)「Turn on Flash Player Warnings and Errors」オプションがオンになっていることを確認してください。このオプションがオンになっていれば、サポート対象外のPixel Bender言語機能が用いられた際に、コンパイラが即座にその旨を通知します。(オプションがオフになっている場合、カーネルをFlash Playerに書き出すまでツールキットはエラーを報告しません。)
Pixel Benderの典型的なカーネルは以下の処理を行います。
次に示す簡潔なPixel Benderカーネルは、これらの処理をすべて行います。このプログラムは「ChannelScrambler」という名のカーネルを定義するものです。
<languageVersion : 1.0;>
kernel ChannelScramblerFilter
< namespace : "com.adobe.example";
vendor : "Adobe Systems Inc.";
version : 1;
description : "Changes the color channel order from rgba to gbra.";
>
{
input image4 inputImage;
output pixel4 outPixel;
void evaluatePixel()
{
pixel4 samplePixel = sampleNearest( inputImage, outCoord() );
outPixel = samplePixel.gbra;
}
}
このカーネルでは、inputImageと名付けられた入力画像、およびoutPixelと名付けられた出力画像が宣言されています。evaluatePixel()関数では、現在処理されている出力座標ピクセルにアクセスするために、内蔵関数のsampleNearest()とoutCoord()が用いられています。そして、サンプリング済みのピクセルは、スウィズリングを利用してカラーチャンネルの順序が並べ替えられた後、outPixel変数に割り当てられています。
このカーネルは、図2に示す結果を生成します。
図2. Channel Samplerの結果
Pixel Benderプログラムには、必須要素であるlanguageVersionタグを含める必要があります。
<languageVersion: 1.0;>
また、カーネル名の宣言も必須です。
kernel ChannelScrambler{...}
カーネル内には、必ず単一の出力宣言(output pixel4 outPixelなど)とevaluatePixel()関数を配置する必要があります。利用できる入力数に制限はありません。入力を一切使用しないことも可能です。ただし、カーネルをFlexでどのように使用するかによっては、入力数に対して他の要件が発生することがあります。ブレンドとして用いられるシェーダには2つの入力、ファイルとして用いられるものには1つの入力が必要とされます。塗りとして用いられるシェーダに関しては、入力は一切不要です。
Pixel Benderカーネルは、出力画像の各ピクセルに対して一度実行されます。なお、実行間にステート情報は一切保存されません。したがって、各実行時のサンプリング済みピクセルを蓄積し、平均のピクセル値を求めるようなことはできません。また、必要な情報はすべて、カーネルの各実行時に演算される必要があります。ただし、あらかじめActionScriptで演算処理を行い、この結果を入力またはパラメータとして渡すことは可能です。
入力画像のピクセル値には、必ずサンプリング関数を用いてアクセスします。サンプリング処理は次の内蔵関数を用いて行います。
sampleNearest():所定の座標に最も近いピクセルのチャンネル値が含まれたベクトルを返します。(動作がやや異なるsampleLinear()関数もあります。)outCoord():現在の出力ピクセルの座標を返します。 Pixel Benderが画像を処理する際、カーネルは出力画像のすべてのピクセルに対して実行されます。現在のピクセルの座標は、outCoord()関数によって返されます。Pixel Benderの座標系はFlash PlayerおよびAIRの座標系に似ています。原点は左上の隅にあり、正の値は右側および下側への移動を示します。ピクセルは常に正方形です。
サンプリングの対象ピクセルは、outCoord()のポジションに限定されるわけではありません。例えば、次のサンプリングステートメントを利用すれば、現在のピクセルから10ピクセル右、かつ5ピクセル下のピクセルをサンプリングできます。
pixel4 inputPixel = sampleNearest( inputImage, outCoord() + float2( 10.0, 5.0 ) );
表現outCoord() + float2( 10.0, 5.0 )は、outCoord()関数によって生成される座標ベクトルと2要素からなるベクトルの和を求めます。このコードは、float2(outCoord().x + 10.0, outCoord().y + 5.0)と記述するのに相当します。
サンプリング対象の座標が入力画像の範囲外になるような場合は、すべて0が含まれたカラーベクトルが返されます。例えば、入力のデータ型がimage4の場合に範囲外のピクセルをサンプリングすると、完全に透明な黒のピクセルが返されます。入力のデータ型がimage3であれば、(アルファチャンネルなしの)黒のピクセルが返されます。Pixel Benderの座標空間の大きさは、理論的には無限です。ただし、表現できる座標の範囲については実用的な制限があります。また、現存しないサンプリングデータの有用性についても制約が存在します。
一旦ピクセルを表現するベクトルが取得できたら、これらのカラー値を様々な方法で操作できます。仮に、pixの名のpixel4変数があった場合、個々のカラーチャンネルまたはチャンネルの組み合わせは以下の様々な方法で参照できます。
pix.rまたはpix[0] pix.gまたはpix[1] pix.bまたはpix[2] pix.aまたはpix[3]pix.ra pix.bgr個々のカラーチャンネルは32-bitの浮動小数点数で表され、通常、0.0(黒)から1.0(白)の間の値になります。出力側のカラー値はこの範囲外でも指定できますが、この場合はレンダリング結果には表現されません。つまり、pixel3(-1.0, -1.0, -1.0)とpixel3(0.0, 0.0, 0.0)は、ビットマップとしてレンダリングされる際、いずれも同じ黒になります。(ただし、同一画像に対して複数のフィルタを実行するような場合は、当該ピクセルをサンプリングするにあたり2番目のフィルタが(0.0, 0.0, 0.0)ではなく、(-1.0, -1.0, -1.0)を検出するので、実行結果に大きな差が現れることがあります。)
ピクセルベクトルの演算処理には、スカラー値とベクトル値のどちらも利用できます。スカラー値を使用する場合は、演算処理が各チャンネルに適用されます。例えば、次に示す演算処理では、各チャンネル(アルファチャンネルを含む)の値が半減されます。
pixel4 pix = sampleNearest( inputImage, outCoord());
pix = pix / 2.0;
ベクトルを用いてこれと同じことを記述すると、次のようになります。
pix = pix * pixel4( 0.5, 0.5, 0.5, 0.5 );
Pixel Bender上の画像にはチャンネルあたり 32-bitが用意されていますが、Flash PlayerおよびAIRのグラフィックスにはチャンネルあたり 8-bitしか用意されていません。カーネルを実行する際、入力画像のデータは一旦チャンネルあたり32-bitに変換され、その後カーネルの実行完了時に、チャンネルあたり8-bitへと再変換されます。
入力の宣言には、次に示すように、inputキーワードを用います。
input image4 sourceImage;
宣言時に使用できるデータ型は、image1、image2、image3またはimage4です。カーネル内の個々の入力に、異なる数のチャンネルが含まれていても問題ありません。シェーダによって生成される出力画像のチャンネル数は、入力側のデータ型ではなく、出力ピクセルのデータ型によって決定されます。
宣言する入力の数は、カーネルがFlexによって使用される際に必要な数より多くても問題ありません。ただし、Flash PlayerまたはAIRランタイムが自動的に画像データを割り当てるのは、必要とされる入力に対してのみです。なお、この余分な入力に対しては、カーネルがブレンド、フィルタ、塗りのいずれであるかを指定する前に、画像を割り当てる必要があります。例えば、テクスチャ付きのエフェクトを作成するためにフィルタカーネルで追加の画像を使用した場合、当該カーネルが含まれたShaderFilterを表示オブジェクトのフィルタ配列に割り振る前に、必ず当該テクスチャをカーネルの入力として割り当てる必要があります(この方法については後ほど詳しく解説します)。
カーネルには、入力画像以外の値もパラメータとして供給できます。パラメータはparameterキーワードを用いて宣言し、データ型としてはimage(およびregion、ただしこれはFlash PlayerまたはAIR向けに記述したカーネルではそもそも使用不可)以外のすべてのものが利用できます。パラメータのメタデータを宣言して、デフォルト値、最小値および最大値を指定することも可能です。また、説明を供給することも可能です。メタデータは山括弧(<>)で挟む形で宣言します。メタデータにはActionScriptコードでもアクセスできます。パラメータを使用する際には、あらかじめ合理的なデフォルト値を定義しておくことが推奨されます。
次のパラメータステートメントでは、メタデータが備わったfloat3パラメータが宣言されています。
parameter float3 weights
<
defaultValue : float3( 0.5, 0.5, 0.5 );
minValue : float3( 0.1, 0.1, 0.1 );
maxValue : float3( 0.9, 0.9, 0.9 );
description : "A three element vector of weight values."
>;
ActionScriptコードでパラメータの値にアクセスするには、当該カーネルを含むActionScript Shaderオブジェクトのdataプロパティを用います。前述のパラメータの現在値、最小値、最大値にActionScriptからアクセスしたい場合は、次のステートメントが利用できます。(ここでは、これらのパラメータが備わったカーネルを含むShaderオブジェクトの名がmyShaderであるとします。)
var currentWeights:Array = myShader.data.weights.value;
var minWeights:Array = myShader.data.weights.minimumValue;
var maxWeights:Array = myShader.data.weights.maximumValue;
この例のパラメータはfloat3のベクトル型であるため、返されるActionScript配列には3つの要素が含まれます。パラメータがスカラー型であった場合は(floatなど)、返される配列には単一の要素が含まれます。
Flash PlayerおよびAIRランタイムで使用するカーネルのコンパイルと書き出しは、Pixel Bender ToolkitのFileメニューにある「Use the Export Kernel Filter for Flash Player」コマンドを用いて行います。書き出したカーネルには、.pdjというファイル拡張子が付きます(図3を参照)。
Pixel BenderカーネルをFlexアプリケーションに読み込むには、コンパイル済みカーネルを埋め込むか、または読み込む必要があります。
Embedタグは、ActionScriptコンパイラに対して、SWFファイルの作成時にPixel Benderカーネルを埋め込むことを指示します。次の例に示すように、MIMEタイプ宣言を含める必要があります。
[Embed(source="channelscrambler.pbj", mimeType="application/octet-stream")]
var ChannelScramblerKernel:Class;
カーネルを使用するには、当該クラスのインスタンスを作成します(ここではChannelScramblerFilter)。次のコードでは、埋め込まれたカーネルを利用して、Imageインスタンスに適用される新規のシェーダとShaderFilterオブジェクトが作成されます。
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" applicationComplete="applyFilter()"
width="476" height="281" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #FFFFFF]">
<mx:Script>
<![CDATA[
//Embed the PixelBender kernel in the output SWF
[Embed(source="kernels/channelscrambler.pbj", mimeType="application/octet-stream")]
private var ChannelScramblerKernel:Class;
private function applyFilter():void
{
var shader:Shader = new Shader(new ChannelScramblerKernel() );
var shaderFilter:ShaderFilter = new ShaderFilter( shader );
camellia.filters = [ shaderFilter ];
}
]]>
</mx:Script>
<mx:HBox width="410" height="100%">
<mx:VBox height="100%">
<mx:Image source="images/camellia.jpg" scaleContent="false" autoLoad="true"/>
<mx:Label text="Original" width="200" textAlign="center"/>
</mx:VBox>
<mx:VBox height="100%">
<mx:Image source="images/camellia.jpg" id="camellia" scaleContent="false" autoLoad="true"/>
<mx:Label text="Channel Scrambler" width="199" textAlign="center"/>
</mx:VBox>
</mx:HBox>
</mx:Application>
大半のケースでは、Embedタグを使う方法がPixel Benderカーネルを読み込むための最も簡単な手段です。しかし、必要であればカーネルをランタイム時に読み込むことも可能です。次の例は、URLLoaderクラスを使用してカーネルを読み込む方法を示します。
function loadFilter()
{
var urlRequest:URLRequest = new URLRequest( "channelscrambler.pbj" );
var urlLoader:URLLoader = new URLLoader();
urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
urlLoader.addEventListener( Event.COMPLETE, applyFilter );
urlLoader.load( urlRequest );
}
function applyFilter( event:Event ):void
{
trace("apply");
urlLoader.removeEventListener( Event.COMPLETE, applyFilter );
var shader:Shader = new Shader( event.target.data );
var shaderFilter:ShaderFilter = new ShaderFilter( shader );
camellia.filters = [ shaderFilter ];
}
Pixel BenderカーネルをAIRアプリケーションで使用する方法は、ブラウザをターゲットとするアプリケーションの場合と全く同じです。カーネルを動的に読み込む場合、アプリケーションパッケージにカーネルを含める必要があります。Flex Builderでは、ソースディレクトリにあるカーネルファイルは通常、AIRファイルを書き出すときに自動的に含められます。(カーネルを埋め込んだ場合は、アプリケーションのSWFファイルにカーネルが既に含まれているので、AIRパッケージに追加する必要はありません。)
この記事のサンプルは、すべてブラウザをターゲットとしているので、ライブで見ることができます。サンプルをAIRアプリケーションに変換するには、アプリケーションディスクリプタを作成します。例として、ChannelScramblerサンプル用の最小限のアプリケーションディスクリプタを以下に示します。
<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://ns.adobe.com/air/application/1.5">
<id>com.adobe.examples.flex.ChannelScrambler</id>
<version>1.5</version>
<filename>Channel Scrambler</filename>
<name>Channel Scrambler -- Flex-based AIR Example</name>
<description>Illustrates how to use Pixel Bender Kernels in Flex and AIR.</description>
<copyright>© 2007 Adobe Systems Inc.</copyright>
<initialWindow>
<title>Channel Scrambler</title>
<content>ChannelScrambler.swf</content>
<systemChrome>standard</systemChrome>
<transparent>false</transparent>
<visible>true</visible>
</initialWindow>
<installFolder>Adobe/AIR Examples</installFolder>
<programMenuFolder>Adobe/AIR Examples</programMenuFolder>
</application>
xmlns属性に指定された名前空間は、AIR 1.5をターゲットとする必要があることに注意してください。1.0または1.1をターゲットとした場合、アプリケーションから使用できるのはFlash Player 9のAPIだけになります。
ブレンドは、ブレンド適用対象の表示オブジェクトのカラーと、ステージ上の当該オブジェクトの背後のカラーを組み合わせます。Flash Player APIは数種類の既製ブレンドをサポートしており、これらはBlendModeクラスで定義されています。ここでは練習課題として、既製ブレンドのいくつかを再現してみることにします。また、既製のブレンドを使用するだけでは簡単に達成しえないような、ブレンドの作成にも挑戦します。
表示オブジェクトにブレンドを適用するには、読み込み済みのカーネルバイトコードでShaderオブジェクトを作成し、これを当該表示オブジェクトのblendShaderプロパティに割り当てます。ブレンドカーネルには必ず2つの入力が必要となります。1つ目の入力は(blendShaderプロパティが設定される)前景の表示オブジェクト、そして2つ目の入力は前景オブジェクトの下にあるなんらかのものです。他の入力も使用したい場合は(マスクやテクスチャを作成する場合など)、ブレンドを適用する前に、これらの入力に明示的にBitmapDataオブジェクトとして画像を割り当てる必要があります。
乗算ブレンドでは、前景オブジェクトの各カラーと背景オブジェクトのカラーの積が求められます。片方の画像が純白である場合を除いて、このブレンドは結果を暗くする効果があります(図4を参照)。
図4. あらゆる表示オブジェクトに適用可能なPixel Benderのエフェクト
次に示すカーネルには「foreground」と「background」の名が付いた2つの入力と、出力、および名前付きの結果が宣言されています。evaluatePixel()関数では、各画像の現在の座標のピクセルがsampleNearest()関数によってサンプリングされ、最後にピクセル同士の積が求められています。
<languageVersion : 1.0;>
kernel MultiplyBlend
< namespace : "com.adobe.example";
vendor : "Adobe Systems Inc.";
version : 1;
description : "A simple multiply blend";
>
{
input image4 foreground;
input image4 background;
output pixel4 result;
void evaluatePixel()
{
pixel4 a = sampleNearest( foreground, outCoord() );
pixel4 b = sampleNearest( background, outCoord() );
result = a * b;
}
}
メモ:Pixel Bender言語上で2つのベクトルの乗算を行うと、対応する要素の値同士の積が求められます。つまり、仮にaとbがいずれもfloat2のベクトルで、次に示すステートメントが記述されていたとします。
a * b
この場合、上記ステートメントは、次の2つのステートメントと同じことになります。
a.x * b.x
a.y * b.y
ブレンドの読み込みと適用には、次のActionScriptコードを使用します。
<mx:Script>
<![CDATA[
//Embed the Pixel Bender kernel in the output SWF
[Embed(source="kernels/multiplyblend.pbj", mimeType="application/octet-stream")]
private var MultiplyBlendKernel:Class;
private function applyBlend():void
{
var shader:Shader = new Shader( new MultiplyBlendKernel() );
camellia.blendShader = shader;
}
]]>
</mx:Script>
スクリーンブレンドでは、カラーの積が求められた後、その値が反転されます。つまり、このブレンドの効果は、乗算ブレンドを適用したときと正反対になります。片方の画像が黒である場合を除いて、このブレンドは結果を明るくする効果があります(図5を参照)。
図5. 結果を明るくするスクリーンブレンド
<languageVersion : 1.0;>
kernel ScreenBlend
< namespace : "com.adobe.example";
vendor : "Adobe Systems Inc.";
version : 1;
description : "Screen blend";
>
{
input image4 foreground;
input image4 background;
output pixel4 result;
void evaluatePixel()
{
pixel4 a = sampleNearest( background, outCoord() );
pixel4 b = sampleNearest( foreground, outCoord() );
result = 1.0 - (1.0 - a) * (1.0 - b);
}
}
このカーネルは、先ほどの乗算カーネルとほぼ同じであることが確認できます。resultに対する数値演算処理の内容が、唯一の変更点です。このサンプルには、読み込み・適用対象のシェーダが異なるという例外を除けば、同じActionScriptコードを使用できます。
ハードライトブレンドは、乗算ブレンドとスクリーンブレンドを組み合わせたものです。前景のピクセルが50%グレーより明るい場合はスクリーンブレンドが適用され、それ以外の場合は、乗算ブレンドが適用されます(図6を参照)。
図6.乗算ブレンドとスクリーンブレンドを組み合わせハードライトブレンド
<languageVersion : 1.0;>
kernel HardLightBlend
< namespace : "com.adobe.example";
vendor : "Adobe Systems Inc.";
version : 1;
description : "Hard light blend";
>
{
input image4 foreground;
input image4 background;
output pixel4 result;
void evaluatePixel()
{
pixel4 a = sampleNearest( background, outCoord() );
pixel4 b = sampleNearest( foreground, outCoord() );
float gray = (b.r + b.g + b.b)/3.0;
if( gray < 0.5 )
{
result = a * b;
}
else
{
result = 1.0 - (1.0 - a) * (1.0 - b);
}
}
}
ハードライトブレンドは、これまでのものよりやや複雑です。このブレンドでは、まず前景画像のピクセルのカラーチャンネルの平均値を用いて、当該ピクセルのグレーレベルが算出されます。そして、ifステートメントを用いることで、乗算とスクリーンのいずれかのブレンド処理が行われます。
このサンプルに対しても、読み込み・適用対象のシェーダが異なるという例外を除けば、これまでと同じActionScriptコードを使用できます。
次に、既製のブレンドだけでは達成しがたいブレンドにチャレンジすることにします。ここで紹介するフィルタは、ノイズテクスチャとsin()関数を用いて、木目または大理石模様のエフェクトを作成します。このエフェクトの出来映えは、ノイズテクスチャの特性に左右されやすいといえますが、たいがいの場合、パーリンタイプのノイズが良好な結果をもたらします(図7を参照)。
図7.乗算ブレンドとスクリーンブレンドを組み合わせハードライトブレンド
このシェーダは、ノイズ画像のピクセル値をサンプリングすることで機能します。また、このシェーダは画像で直接ノイズピクセルを使用する代わりに、ノイズの値を一連のsin()関数にフィードします。そして、その結果と背景の積が求められます。結果エフェクトの曲線具合は、turbulenceパラメータを用いて制御されます。
<languageVersion: 1.0;>
kernel GrainBlend
< namespace : "com.adobe.example";
vendor : "Adobe Systems Inc.";
version : 1;
description : "Creates a wood grain or marbling effect"; >
{
input image4 background;
input image4 noise;
output pixel4 dst;
parameter float turbulence
<
maxValue : 500.0;
minValue : 0.0;
defaultValue : 150.0;
>;
void evaluatePixel()
{
pixel4 a = sampleNearest(background, outCoord());
pixel4 b = sampleNearest(noise, outCoord());
float alpha = a.a; //save the original alpha
if( (b.a > 0.0) && (a.a > 0.0)){
float seed = outCoord().x + (((b.r + b.g + b.b)/3.0) * turbulence);
float grain = (0.7 * sin(seed) + 0.3 * sin(2.0 * seed + 0.3) + 0.2 * sin(3.0 * seed + 0.2));
dst = sampleNearest(background, outCoord()) * (grain + 0.5);
dst.a = alpha; //restore the original alpha
}
else {
//Just copy the background pixel when outside the area of the noise image
dst = sampleNearest(background, outCoord());
}
}
}
このサンプルに対して使用するActionScriptでは、これまでのサンプルと同じ方法でシェーダの読み込みと適用が処理されます。ただし、このコードには、turbulenceパラメータを制御するためのスライダを生成するActionScriptも含まれています。
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" height="297" width="662"
applicationComplete="init()" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #FFFFFF]">
<mx:Script>
<![CDATA[
import mx.events.SliderEvent;
//Embed the Pixel Bender kernel in the output SWF
[Embed(source="kernels/grainblend.pbj", mimeType="application/octet-stream")]
private var GrainBlendKernel:Class;
//Create the Shader object
private var shader:Shader = new Shader( new GrainBlendKernel() );
private function init():void
{
//Set the slider values based on the parameter metadata
turbulence.minimum = shader.data.turbulence.minValue;
turbulence.maximum = shader.data.turbulence.maxValue;
turbulence.value = shader.data.turbulence.defaultValue;
turbulence.liveDragging = true;
turbulence.addEventListener( SliderEvent.CHANGE, updateFilter );
//Apply the blend
noise.blendShader = shader;
}
private function updateFilter( event:SliderEvent ):void
{
shader.data.turbulence.value = [turbulence.value];
noise.blendMode = BlendMode.NORMAL;
noise.blendShader = shader;
}
]]>
</mx:Script>
<mx:VBox height="100%" width="100%">
<mx:HBox width="100%" height="100%" horizontalAlign="center" verticalAlign="top">
<mx:VBox>
<mx:Canvas width="195" height="194" backgroundColor="#663300"/>
<mx:Label text="Background" textAlign="center" width="196"/>
</mx:VBox>
<mx:VBox>
<mx:Image source="images/noise.jpg" width="195" scaleContent="false" autoLoad="true" height="194"/>
<mx:Label text="Perlin noise" width="196" textAlign="center"/>
</mx:VBox>
<mx:VBox>
<mx:Canvas width="195" height="194" backgroundColor="#663300">
<mx:Image source="images/noise.jpg" id="noise" scaleContent="false" autoLoad="true" width="195" height="194"/>
</mx:Canvas>
<mx:Label text="Grain blend" width="196" textAlign="center"/>
</mx:VBox>
</mx:HBox>
<mx:HBox width="100%" horizontalAlign="center" verticalAlign="middle">
<mx:Label text="Turbulence"/>
<mx:HSlider id="turbulence" width="100%"/>
</mx:HBox>
</mx:VBox>
</mx:Application>
このサンプルでは、シェーダのturbulenceパラメータを制御するためにスライダが用いられています。スライダの最小値、最大値およびデフォルト値は、パラメータのメタデータに基づいて設定されます。そして、Sliderオブジェクトによって変更イベントがディスパッチされた際には、パラメータの値を変更するためにupdateFilter()メソッドが用いられます。なお、表示オブジェクトのblendShaderプロパティを設定する際には、Shaderオブジェクトがクローンされます。したがって、単に元のShaderオブジェクトのパラメータ値を変更すれば良いというわけではなく、必ず、新たに更新されたShaderオブジェクトをblendShaderプロパティに割り当て直す必要があります。
このサンプルでは、説明を簡素化するためにビットマップをノイズのテクスチャとして使用しましたが、BitmapDataクラスのperlinNoise()関数を利用して他のテクスチャを作成することも可能です。
フィルタとして使用するシェーダは単一の画像に適用されます。ブレンドでの作業同様に、ここでもShaderオブジェクトを作成しますが、他にも、カーネルが含まれたShaderを渡すShaderFilterオブジェクトを作成する必要があります。
var shader:Shader = new Shader( loadedBytes );
var shaderFilter:ShaderFilter = new ShaderFilter( shader );
ShaderFilterオブジェクトはシェーダを「包む」役割を担い、単に、表示オブジェクトのfilters配列に追加するだけの操作で、シェーダを既製フィルタ同様に扱うことを可能にします。
displayObject.filters = [ shaderFilter ];
シェーダが適用されるオブジェクトは、当該カーネルの最初の入力に自動的に設定されます。フィルタカーネルが入力として他の画像も受け付ける場合は、フィルタが表示オブジェクトに割り当てられる前にこれらの画像を設定しておく必要があります。
簡単なフィルタについてはChannelScramblerの説明で既に触れているので、ここではより複雑な「ぼかし(ガウス)」を例に解説を進めることにします。
ぼかし(ガウス)は、畳み込みフィルタの1つです。(「畳み込み」フィルタとは、周辺のピクセルの加重平均を計算するフィルタのことです。)Flash Player APIには任意のサイズの畳み込みフィルタを作成するための内蔵クラスが用意されていますが、Pixel Benderでぼかし(ガウス)をプログラミングしてみることは、Pixel Benderのいくつかの重要ポイントを紹介する上でとても有効です(図8を参照)。
図8. ぼかし(ガウス)は畳み込みフィルタの1つ
一般的な用途の畳み込みフィルタをPixel Benderで実現することは簡単ではありません。これは、Flash PlayerおよびAIRランタイムがカーネルコード内でのループ処理をサポートしていないからです。ここでは、forループを使用する代わりに、周囲の各ピクセルをサンプリングするための、個々のプログラムステートメントを記述する必要があります。ここで紹介するサンプルカーネルは、サンプリング半径が1から6(畳み込み行列のサイズ、3×3から13×13に相当)のガウスぼかしを作成することができます。
このフィルタでは、ガウスぼかしが分割処理可能であるという特性を生かして、処理が2段階に分けて行われます。最初の段階では画像に水平方向のぼかし処理が行われ、次の段階では垂直方向のぼかしが処理されます。これにより、出力1ピクセルにつき、「2x半径^2」ではなく「4x半径」の数のピクセルに対してのみサンプリングと加重平均を求める処理を行えば済むようになるため、ピクセルあたりの総演算量を減らすことができます。例えば、このフィルタがサポートする最大半径を使用する場合、最終的な出力1ピクセルあたり、水平、垂直の処理を合わせて26個の入力ピクセルがサンプリングされます。仮に、フィルタがこのぼかしを1段階で処理するのであれば、出力ピクセルにつき169もの入力ピクセルをサンプリングする必要があります。視覚的な結果と数学的な結果は一致します。
このカーネルでは、forループが使えないことを克服するために、整数の半径値が個別に扱われます。有効なそれぞれの半径値に対しては、現在のピクセルから所定の半径分両側に離れた、2つのピクセルがサンプリングされます。この2つのピクセルに対するガウス加重は同じであることから、これらは単に加算されます。必要なピクセルのサンプリングがすべて完了したら、ウェイトとスケールの係数が適用されます。
水平方向の処理には、次のカーネルコードが用いられます(垂直方向の処理でも、同様のカーネルを使用)。
<languageVersion: 1.0;>
kernel HorizontalGaussianBlur
< namespace : "com.adobe.example";
vendor : "Adobe Systems Inc.";
version : 1;
description : "The horizontal convolution of a Gaussian blur"; >
{
input image4 src;
output float4 result;
parameter int radius
<
minValue : 1;
maxValue : 6;
defaultValue : 6;
>;
void evaluatePixel()
{
pixel4 center, band1, band2, band3, band4, band5, band6;
float2 pos = outCoord();
//Sample image in bands
if( radius > 5 )
{
band6 = sampleNearest(src, float2(pos.x - 6.0, pos.y))
+ sampleNearest(src, float2(pos.x + 6.0, pos.y));
}
if( radius > 4 )
{
band5 = sampleNearest(src, float2(pos.x - 5.0, pos.y))
+ sampleNearest(src, float2(pos.x + 5.0, pos.y));
}
if( radius > 3 )
{
band4 = sampleNearest(src, float2(pos.x - 4.0, pos.y))
+ sampleNearest(src, float2(pos.x + 4.0, pos.y));
}
if( radius > 2 )
{
band3 = sampleNearest(src, float2(pos.x - 3.0, pos.y))
+ sampleNearest(src, float2(pos.x + 3.0, pos.y));
}
if( radius > 1 )
{
band2 = sampleNearest(src, float2(pos.x - 2.0, pos.y))
+ sampleNearest(src, float2(pos.x + 2.0, pos.y));
}
band1 = sampleNearest(src, float2(pos.x - 1.0, pos.y))
+ sampleNearest(src, float2(pos.x + 1.0, pos.y));
center = sampleNearest(src, pos);
//Apply weights and compute resulting pixel
if( radius == 6 )
{
result = (band6 + (band5 * 12.0) + (band4 * 66.0) + (band3 * 220.0) + (band2 * 495.0) + (band1 * 792.0) + (center * 924.0))/4096.0;
}
if( radius == 5 )
{
result = (band5 + (band4 * 10.0) + (band3 * 45.0) + (band2 * 120.0) + (band1 * 210.0) + (center * 252.0))/1024.0;
}
if( radius == 4 )
{
result = (band4 + (band3 * 8.0) + (band2 * 28.0) + (band1 * 56.0) + (center * 70.0))/256.0;
}
if( radius == 3 )
{
result = (band3 + (band2 * 6.0) + (band1 * 15.0) + (center * 20.0))/64.0;
}
if( radius == 2 )
{
result = (band2 + (band1 * 4.0) + (center * 6.0))/16.0;
}
if( radius == 1 )
{
result = (band1 + (center * 2.0))/4.0;
}
}
}
エフェクトを完成させるには、両方のカーネルをフィルタとして適用する必要があります。なお、フィルタを適用する順序に決まりはありません。次に示すActionScriptコードでは、半径パラメータを制御するためにスライダが用いられています。
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" width="334" height="287" applicationComplete="init()"
backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #FFFFFF]" viewSourceURL="srcview/index.html">
<mx:Script>
<![CDATA[
import mx.events.SliderEvent;
//Embed the Pixel Bender kernel in the output SWF
[Embed(source="kernels/verticalgaussianblur.pbj", mimeType="application/octet-stream")]
private var VerticalBlurKernel:Class;
[Embed(source="kernels/horizontalgaussianblur.pbj", mimeType="application/octet-stream")]
private var HorizontalBlurKernel:Class;
//Create the shaders
private var vBlurShader:Shader = new Shader( new VerticalBlurKernel() );
private var hBlurShader:Shader = new Shader( new HorizontalBlurKernel() );
//Create the filters
private var hBlurFilter:ShaderFilter = new ShaderFilter( hBlurShader );
private var vBlurFilter:ShaderFilter = new ShaderFilter( vBlurShader );
private function init():void
{
//Initialize the slider using the radius parameter metadata
radiusSlider.value = hBlurShader.data.radius.value;
radiusSlider.minimum = hBlurShader.data.radius.minValue;
radiusSlider.maximum = hBlurShader.data.radius.maxValue;
radiusSlider.addEventListener( SliderEvent.CHANGE, updateFilters );
//Apply the filters
camellia.filters = [ hBlurFilter, vBlurFilter ];
}
//Reapply the filters when the slider is changed
private function updateFilters( event:SliderEvent ):void
{
hBlurShader.data.radius.value = vBlurShader.data.radius.value = [radiusSlider.value];
camellia.filters = [ hBlurFilter, vBlurFilter ];
}
]]>
</mx:Script>
<mx:VBox height="100%" width="285" horizontalAlign="center">
<mx:Image id="camellia" source="images/camellia.jpg" scaleContent="false" autoLoad="true"/>
<mx:HBox width="100%" verticalAlign="middle" horizontalAlign="center">
<mx:Label text="Gaussian blur"/>
<mx:HSlider id="radiusSlider" liveDragging="true" snapInterval="1"/>
</mx:HBox>
</mx:VBox>
</mx:Application>
このサンプルの場合、各フィルタの半径には同じ値が設定されます。前出のブレンドと同じように、パラメータの値が変更された際には、シェーダフィルタを表示オブジェクトに再度割り当てる必要があります。
カーネルを面の塗りとして使用するには、カーネルバイトコードが含まれたShaderオブジェクトを作成し、これをオブジェクトの描画時に、表示オブジェクトのgraphicsプロパティのbeginShaderFill()関数に渡します。ビットマップの塗り同様に、シェーダの塗りも表示オブジェクトのオリジンに登録されます。この登録情報は、変換行列を用いることで調整できます。
面または線の塗りとして使用されるシェーダにおいて、入力画像は自動的に割り当てられません。(塗りアルゴリズムの一環として画像が必要になる場合は、beginShaderFill()メソッドを呼び出す前に、その画像を入力に明示的に割り当てる必要があります。)
次に取り上げる塗りのサンプルでは、市松模様を作成することにします。模様の正方形の大きさおよび色は、カーネルのパラメータによって制御されます(図9を参照)。
図9. 模様の正方形の大きさと色はカーネルのパラメータで制御
この模様のアルゴリズムでは、モジュロ除算を用いて現在のピクセル位置の値と模様サイズを倍にした値の演算が行われます。
float vertical = mod(position.x, checkerSize * 2.0);
このモジュロ関数は、x座標をcheckerSize x2で割った時の余りの値を返します。例えば、仮にcheckerSizeが10であるとした場合、画像のx値の増加に伴って、パターンは0-19, 1-19, 2-19,...になります。演算結果が模様のサイズ(checkerSize)より小さな場合は、カーネルがAの色を描画し、それ以外の場面ではBの色を描画します。これで縞模様が作成されます。この縞模様を市松状にするには、次に示すように、このテクニックを縦と横の両方に適用する必要があります。
float vertical = mod(position.x, checkerSize * 2.0);
float horizontal = mod(position.y, checkerSize * 2.0);
後は、これらの結果をどのように組み合わせるかが問題です。ここは、片方の入力がtrueの場合に限りtrueを返す、論理演算子XOR(^^)の出番です。
( vertical < checkerSize ) ^^ ( horizontal < checkerSize )
以下に、完全なカーネルコードを示します。
<languageVersion : 1.0;>
kernel CheckerFill
< namespace : "com.adobe.example";
vendor : "Adobe Systems Inc.";
version : 1;
description : "A checkered field generator";
>
{
output pixel4 dst;
parameter float checkerSize
<
defaultValue : 10.0;
minValue : 1.0;
maxValue : 75.0;
>;
parameter pixel4 colorA
<
defaultValue : pixel4(0.0, 1.0, 1.0, 1.0);
>;
parameter pixel4 colorB
<
defaultValue : pixel4( 0.0, 0.0, 0.0, 1.0 );
>;
void evaluatePixel()
{
float2 position = outCoord();
float vertical = mod(position.x, checkerSize * 2.0);
float horizontal = mod(position.y, checkerSize * 2.0);
dst = (( vertical < checkerSize ) ^^ ( horizontal < checkerSize )) ? colorA : colorB;
}
}
このサンプルに対して使用するActionScriptコードは、これまでのものよりやや複雑です。これは、カーネルに一段と多くのパラメータが用いられていることと、パラメータ自体がより複雑であることに起因しています。
このサンプルでは、applicationCompleteイベントの時点でinit()関数が呼び出されます。この関数はShaderオブジェクトを作成し、カーネルパラメータのメタデータを使用してコントロール群の初期値を設定します。そして、シェーダの塗りを用いて描画を行うdrawShape()関数を呼び出します。
塗りの更新は、いずれかのコントロールによってchangeイベントが発せられた際に、再びdrawShape()関数が呼び出されることで処理されます。この関数は、現在のコントロール値に基づいてカーネルのパラメータを規定し、現在のグラフィックスを消去してから当該シェイプを再描画します。
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" height="273" width="336"
applicationComplete="init()" backgroundGradientAlphas="[1.0, 1.0]" backgroundGradientColors="[#FFFFFF, #FFFFFF]">
<mx:Script>
<![CDATA[
import mx.core.UIComponent;
import mx.events.ColorPickerEvent;
import mx.events.SliderEvent;
private var filledShape:UIComponent = new UIComponent();
private var areaShader:Shader;
//Embed the Pixel Bender kernel in the output SWF
[Embed(source="kernels/checkerfill.pbj", mimeType="application/octet-stream")]
private var CheckerFillKernel:Class;
private function init():void
{
//Create the Shader object using the embedded kernel
areaShader = new Shader( new CheckerFillKernel() );
//Set controls using shader parameter metadata
checkerSize.minimum = areaShader.data.checkerSize.minValue;
checkerSize.maximum = areaShader.data.checkerSize.maxValue;
checkerSize.value = areaShader.data.checkerSize.value;
checkerSize.liveDragging = true;
checkerSize.addEventListener( SliderEvent.CHANGE, updateFilters );
colorA.selectedColor = vectorToColor( areaShader.data.colorA.value );
colorA.addEventListener( ColorPickerEvent.CHANGE, updateFilters );
colorB.selectedColor = vectorToColor( areaShader.data.colorB.value );
colorB.addEventListener( ColorPickerEvent.CHANGE, updateFilters );
drawShape();
shapeHolder.addChild( filledShape );
}
private function updateFilters( event:Event ):void
{
areaShader.data.checkerSize.value = [ checkerSize.value ];
areaShader.data.colorA.value = colorToVector( colorA.selectedColor | 0xff000000 );
areaShader.data.colorB.value = colorToVector( colorB.selectedColor | 0xff000000 );
drawShape();
}
private function drawShape():void
{
with( filledShape.graphics )
{
clear();
beginShaderFill( areaShader );
drawCircle( 125, 75, 75 );
endFill();
}
}
private function vectorToColor( pixelChannels:Array ):uint
{
return (pixelChannels[3] * 0xff << 24) |
(pixelChannels[0] * 0xff << 16) |
(pixelChannels[1] * 0xff << 8) |
pixelChannels[2] * 0xff;
}
private function colorToVector( color:uint ):Array
{
var result:Array = new Array(4);
result[3] = ((color >> 24) & 0×000000ff)/0xff;
result[0] = ((color >> 16) & 0×000000ff)/0xff;
result[1] = ((color >> 8) & 0×000000ff)/0xff;
result[2] = (color & 0×000000ff)/0xff;
return result;
}
]]>
</mx:Script>
<mx:VBox height="100%">
<mx:HBox id="shapeHolder" width="100%" height="160">
</mx:HBox>
<mx:HBox width="100%">
<mx:Label text="Square size"/>
<mx:HSlider id="checkerSize"/>
</mx:HBox>
<mx:HBox width="100%">
<mx:Label text="Color A" width="33%"/>
<mx:ColorPicker id="colorA"/>
<mx:Spacer width="33%"/>
<mx:Label text="Color B" width="33%"/>
<mx:ColorPicker id="colorB"/>
</mx:HBox>
</mx:VBox>
</mx:Application>
前出のサンプルでは、カーネルパラメータを規定するためのスライダが組み込まれていました。ここではカラーピッカーコントロールも使用することにしますが、この実装はスライダよりやや複雑になります。ActionScriptでは、パラメータ値は配列としてアクセスされます。checkerSizeのようなスカラー値のパラメータであれば、配列には単一の値が含まれますが、色のようなベクトル型の場合は、ベクトルの各要素に対して配列に1つずつ配列要素が含まれます。colorAとcolorBのパラメータはいずれもpixel4型であるため、配列には、ピクセルのチャンネルあたり1つ、合計4つの値が配置されます。一方のActionScript上では、カラーは、すべてのチャンネル情報を含む単一の32-bitユニット値で表現されます。また、アルファチャンネルは、このユニットカラーでは最初のチャンネルであるものの、Shaderオブジェクトのパラメータ配列では最後のチャンネルとして扱われています。サンプル関数のvectorToColor()とcolorToVector()は、選択したカラーをColorPickerオブジェクトとシェーダパラメータの間でやり取りできるようにするために、この2種類のカラーを変換することができます。
vectorToColor()関数では、Pixel Benderカーネルから返された各カラーチャンネルと256(16進法の値:0xff)の積が求められます。この演算結果は、ビットワイズ左シフト演算子(<<)を用いて32-bitユニット値の適切なargb位置にシフトされます。そして最後に、ビットワイズOR演算子(|)を用いて4つのチャンネルが1つのユニットに組み合わされた後、返されます。
colorToVector()関数は、上記手順の逆を処理するためのものです。この関数では、各チャンネルに対してビットワイズ右シフト演算(>>)が行われ、ビットワイズAND演算子(&)によって他のチャンネルに属するビットがマスクアウトされます。そして、その結果を256(0xff)で割ることによって、値が0から1の間に補正されます。この結果は、Pixel Benderで必要とされる順序で配列の適切な要素へと割り当てられた後、返されます。