Pixel Benderというのは、Adobeが開発中の画像・動画処理の仕組みで、出力画像をピクセル毎にプログラマブルに生成することができるようにするものです。
また、GPUのハードウェア支援も得ることができるので、処理によってはかなりの高速化が期待できます。
現段階では、Adobe After Effects CS4とFlash Player 10、Photoshop CS4 に対応しているようです。実際の開発では pbk (Pixel Bender Kernel File) という形式のコードを作成していくのですが、これは基本的にどのアプリケーションでも同じコードで動作するとのことです(実際にはAfter EffectsやFlashは細かい制限がありますので、すべてがそのままで動作するわけではないです)。
今回は実験がてら、法線マップを使ったバンプマッピングをPixel Benderでやってみます。
3D基礎知識
いきなり、「法線マップ」と言われても訳が分からないと思いますので、簡単に3Dの基礎的な部分を書いておきます。
まず、3次元の座標系についてですが、今回は左右をx軸で右が正、上下をy軸で上が正、奥行きをz軸で手前が正というようにします(左図参照)。
次に、法線というのは3D空間において、面あるいは点がどの方向を向いているかを表すベクトルのことです。2次元に置き換えて考えると、常にこちらを向いていることになるので、(0, 0, 1) となります(ベクトルの成分表記で)。
この法線というものは光源に対してその場所がどういう色になるか計算するのに使われます。これをシェーディングといいます。今回はこのシェーディングの計算のうち、ランバートとフォンのモデルを使います。
ランバート反射
あまり反射率が高くない(ぴかぴかしていない)材質の色というのは視点の位置にあまり左右されず同じ色に見えます。これを拡散反射(diffuse reflection)といい、代表的な反射計算のモデルとしてランバート反射というものがあります。
面に対して垂直に光をあてる時がもっとも明るくなることから考えられており、法線ベクトルと光線ベクトルの逆ベクトルの内積(ともに正規化された状態で)から算出できます。
鏡面反射
一方、ぴかぴかした反射は鏡を考えれば明らかなように、視点の位置によって見え方が変わります。これを鏡面反射といいます(specular reflection)。この近似についてもさまざまなシミュレーションモデルが考案されているのですが、有名なものにフォンのモデルがあります。
このフォンのモデルの考え方は、視線の反射ベクトルと光線ベクトルの角度が小さければ小さいほど明るくなり、その角度が大きいほど暗くなるというもので、要は光が反射する先にいればまぶしく感じるけど、そこからずれるほどまぶしさを感じなくなるということから考えられています。
しかし、反射ベクトルを計算するのは大変な場合もあるので、それを簡略化して光線ベクトルと視線ベクトルの中間のハーフベクトルを使う方法が考えられました。これがBlinnによる修正モデルです。
元のフォンのモデルは反射ベクトルと光線ベクトルの内積のべき乗で算出しますが、Blinn-Phong修正モデルではハーフベクトルと法線ベクトルの内積のべき乗を使います。なぜこうなるかは、反射ベクトルと光線ベクトルが一致するとき、ハーフベクトルと法線ベクトルが一致する関係にあることから分かると思います。
法線マップ
法線マップというのは、前述の法線データをピクセルごとに画像に保存したものです。ベクトルというのは正規化すると各座標の成分が-1〜1の範囲におさまるので、それを0〜255の範囲に置き換えてx=>r, y=>g, z=>bにそれぞれ保存できます。
3Dのプログラムでは、このように画像にベクトルデータをマッピングしておくことがよくあります。複雑な照明計算を前もって行っておき、それを保存した画像を使ってレンダリング時の計算を減らすことができます。
Pixel Benderでコーディング
今回のプログラムは点光源(場所によって光線ベクトルが異なる。どの場所でも光線ベクトルが同じ光源は平行光源という)として扱い、光源との距離によって明るさが減衰するようにしてみました。減衰のモデルは A = 1 / (a + D * b + D * D * c) (Dは光源との距離、a,b,cは定数)として、計算結果にAを乗算しています。
実際のコードは以下のようになります。
<languageVersion : 1.0;> kernel BumpFilter < namespace : "jp.flup"; vendor : "Flup Inc."; version : 1; description : "Bump mapping filter"; > { // 500x500限定にしている。parameterにして設定可能にしてもいい const float2 textureSize = float2(500.0, 500.0); // 平面の位置となるZ座標(何でも良いけど光源の位置とかと関係してくるので、相対的に調整が必要) const float textureZ = -1000.0; // 光源の位置 parameter float3 lightPos < minValue: float3(-textureSize.x/2.0,-textureSize.y/2.0,textureZ + 10.0); maxValue: float3(textureSize.x/2.0,textureSize.y/2.0,textureZ + 100.0); defaultValue: float3(0.0, 0.0,textureZ + 20.0); >; // 視点の位置(原点固定) const float3 viewPos = float3(0.0, 0.0, 0.0); // specular計算でのべき乗(大きいほど反射範囲が収束します) parameter float power < minValue: 0.0; maxValue: 1000.0; defaultValue: 30.0; >; // 減衰定数 A = 1 / (x + D * y + D * D * z) parameter float3 attenuate < minValue: float3(0,0,0); maxValue: float3(1.0,1.0,0.04); defaultValue: float3(0.2,0.1,0.001); // z は距離の2乗に乗算するので小さくしています >; // 拡散光の強度(大きいほど素材の色が見えます) parameter float diffuseIntensity < minValue: 1.0; maxValue: 100.0; defaultValue: 20.0; >; // Image1 : diffuse texture input image4 colorTex; // Image2 : normal texture input image4 normalTex; output pixel4 dst; void evaluatePixel() { // 現在計算中の座標 float3 pos = float3(outCoord().x-textureSize.x/2.0,textureSize.y/2.0-outCoord().y,textureZ); // 光線ベクトル(光源から物体へのベクトル) float3 lightVector = pos-lightPos; // 光源と物体の距離(ベクトル長で求まる) float lightDistance = length(lightVector); // 減衰率の計算 float attenuator = attenuate.x + lightDistance * attenuate.y + lightDistance * lightDistance * attenuate.z; if(attenuator == 0.0) attenuator = 1.0; // ゼロ除算防止 float3 light = normalize(lightVector); // 視線ベクトル(視点から物体へのベクトル) float3 viewVector = pos-viewPos; float3 view = normalize(viewVector); // diffuse色の取得 pixel4 inputColor = sampleNearest(colorTex,outCoord()); // 法線ベクトルの取得 float3 normal = normalize(float3(sampleLinear(normalTex, outCoord()).rgb * 2.0 - 1.0)); if(normal.z < 0.0) normal.z = -normal.z; // 座標系の違いでZ値が負になっているときは符号を反転 // ハーフベクトル = 光線の逆ベクトル + 視線の逆ベクトル float3 halfVector = normalize(-light -view); // specular成分の計算 float dp = dot(normal, halfVector); float specular = pow(max(dp, 0.0), power); // diffuseとspecularを加算してclamp dst.rgb = clamp((inputColor.rgb * dot(normal, -light) * diffuseIntensity + float3(1.0,1.0,1.0) * specular)/attenuator, 0.0, 1.0); } }
実行すると、以下のような感じになります。
今回のコードは以下からダウンロードできます。
pixel_bender_bump_mapping.zip
次は、このPixel BenderのコードをFlexで使ってみたいと思います。