飴屋

Flash3D/AGAL実践編

実際にシェーダーを作ってみる

AGALの仕様の基本的なことをおよそ理解したので、実際に自分でシェーダーを作ってみようと思います。習作なのであんまり複雑なものは遠慮しておきたいところですので、ワイヤーフレームでポリゴンを描画するシェーダーにしてみたいと思います。ワイヤーフレームというのはポリゴンの三角形の辺に当たる部分に直線をひくだけで、面の部分には何も描画しません。その結果モデルの骨組みのような構造物が画面に描画されます。GPUなんてものがなかった時代、ソフトウェアでのリアルタイムな描画が困難でしたので、陰面処理や面の塗りつぶしなどの計算コストが省けるワイヤーフレームはとても重宝しましたが、最近だとどうなんでしょうね。ボーンとか埋め込むときにモデルを透視するときとかに使うのかな。

ワイヤーフレームシェーダー

さて、ワイヤーフレームを描画するにしても、AGALで書けるのはバーテックスシェーダーとフラグメントシェーダーですので、この二つで三角形の辺だけ書く方法を考えてみます。バーテックスシェーダーでは三角形の頂点の三次元座標を画面上の二次元座標に変換する計算をしてくれますが、その二次元座標をつなぐように線を引くOpCodeなんてありません。次のフラグメントシェーダーではポリゴンに含まれるピクセルの色を一つずつ決定していくことになりますので、

  • 辺を構成するピクセルのときは色をつける
  • 他のピクセルのときは描画しない

という内容のフラグメントシェーダーを作ることになります。問題は辺を構成するピクセルかどうかをどうやって判断するのか、すなわち処理中のピクセルが三角形のどのくらいの位置にあるのかという情報が必要になります。

バッファ

Vector.<Number>([
x1, y1, z1, 1, 0, 0, 0.0005,
x2, y2, z2, 0, 1, 0, 0.0005,
x3, y3, z3, 0, 0, 1, 0.0005
])

VertexBuffer3D.uploadFromVectorにこんな感じの数列を渡します。数列の構成は「頂点座標(3),ベクトル値(4) 」×3となっています。x1-3,y1-3,z1-3は何か適当に座標の位置を入れてもらうとして、三頂点のそれぞれに重複のないように(1,0,0)、(0,1,0)、(0,0,1)をくっつけます。辺で接する三角形が増えたときは、その辺の対角の数値を使えば重複しないで済みますね。

context3D.setVertexBufferAt(0, vertexbuf, 0, Context3DVertexBufferFormat.FLOAT_3);
context3D.setVertexBufferAt(0, vertexbuf, 0, Context3DVertexBufferFormat.FLOAT_4);

描画命令の前にバーテックスバッファの各頂点の前半三つが座標で後半4つがそれ以外であることを明示すると、バーテックスシェーダー内で「va0、va1」のレジスタが使えるようになります。

インデックスバッファの方は三点をつなぐだけなので、省略します。

バーテックスシェーダー

m44 op, va0, vc0
mov v0, va1

設定したバッファを使って頂点座標を計算するのが一行目です。vc0にはモデルをどのように画面に投影するかを決定するマトリックスが予め設定されてるということにしましょう。

二行目ではバッファ内の二つ目のベクトル値をフラグメントシェーダーに申し送りしています。これでフラグメントシェーダー内ではva1ではなく、v0を使ってこのベクトル値にアクセスすることができます。さて、バーテックスバッファでは三頂点分の三つのベクトル値を用意しましたが、フラグメントシェーダーでは頂点以外の各ピクセルについてもベクトル値が必要になります。しかし、心配しなくても、頂点以外の各中間ピクセルで必要になるベクトル値について、フラグメントシェーダーではv0に三頂点の情報から線形に補完された情報が勝手に入ってくれるので安心なのでした。

この線形に補完された情報を利用すると、フラグメントシェーダーの中で各ピクセルが三角形のどの辺に位置するのかわかります。(1,0,0)の頂点と(0,1,0)の頂点を結ぶ辺の中点ではベクトル値が(0.5, 0.5, 0)になりますし、ポリゴンの三角形の重心のあたりだと(0.333, 0.333, 0.333)となります、多分。(x,y,z)の三項のうち各頂点ごとに重複しないようにいずれかの項が1になっていて、他の二項が0になっているので、例えば、頂点(1,0,0)から遠いピクセルになるほどx項が減り、その分yz項が増えるわけです。

では、辺に位置するピクセルかどうかを判断する方法をフラグメントシェーダーにまかせましょう。

フラグメントシェーダー

mul ft0.x, v0.x, v0.y
mul ft0.x, ft0.x, v0.z
sub ft0.x, v0.w, ft0.x
kil ft0.x
mov oc, ft0.x

フラグメントシェーダーの一行目にft0というレジスタが登場しました。これはフラグメントシェーダー内で一時的に値を保持できるレジスタです。バーテックスシェーダーの方にも同様のレジスタvt0というのがありますね。このレジスタにはスカラ値でもベクトル値でもマトリックス値でも入るようですが今回はベクトル値として使おうと思います。ft0.xとマスクを指定することで、ft0の中のx項が演算の対象になります。

1,2行目ではバーテックスシェーダーから渡ってきた「ピクセルが三角形のどの辺にあるか」という情報のx,y,z項を掛け算して、ft0.xに入れています。一行でかけないのはAGALが「OpCode 出力1, 入力1, 入力2」という構造上二回に分けて掛け算をしているからです。x,y,zの各項のうち一つでも0があれば、それは三角形の頂点か辺に位置していることを意味します。ですから三項を掛け合わせて0になるかどうかが描画するかしないかの分岐点となるわけです。

しかし、そんな理想的な演算ではうまくいきませんでした。理由はフラグメントシェーダーが各ピクセル毎に演算するという特性にあります。例えば、ラスタ画像とベクタ画像を比較した場合、ベクタ画像が小数点を含む座標の点を描画できるのに対して、ラスタ画像では近似する整数値の座標に点を打たざるをえないのと似て、フラグメントシェーダーは各ピクセルを整数値の座標としてしかとらえられません。すなわち、辺を構成する点の座標が小数点以下の端数を持っているとその点はフラグメントシェーダーの計算対象にならないということになり、辺の描画ができなくなります。

そこで、辺の近くにある点ほど3項の掛け算結果が0に近づくことに着目して、ある程度の遊びをもたせてみることにしました。「3項の積=0」ではなく、「三項の積<0.0005」という条件であれば辺の近くの点が描画されて見た目に辺っぽいものが現れそうです。厳密には辺ではないですし、三角形の頂点近くだと閾値の0.0005をクリアするピクセルが増えてワイヤーがちょっぴり太めになりますので、旧来のワイヤーフレーム通りに表現したい場合は別の方法を考えないといけませんが、とりあえず気にしない方向で進めます。

0.0005という数字はこれまで説明を省いてきたバーテックスバッファの二つ目のデータのw項に該当します。この数字を大きくするとワイヤーが三角形の内側に向かって太くなりますし、小さくしすぎると見た目に現れないくらい細くなってしまいます。バーテックスバッファに値を設定しているので、各頂点ごとにワイヤの太さを調節することができますが、どの頂点も0.0005でよければ、Context3D.setProgramConstantsFromMatrixやContext3D.setProgramConstantsFromVectorとかで定数fc0としてフラグメントシェーダーに閾値を渡してやるのでもいいと思います。

三行目では「0.0005-三項の積」を演算しています。三項の積が0.0005より大きくなったら計算結果が負の値になるのがポイントです。四行目のkilで「0.0005-三項の積」が負の値になったらピクセルに色を塗る前にシェーダーの処理を中断することができました。ワイヤー部分以外は後ろが透けて見えるという実装になります。

五行目では「0.0005-三項の積」の計算結果をそのまま色情報として出力レジスタのocに設定しています。この場合、色は0.0005以下、0以上の値になるので、まぁ黒くワイヤーが塗られます。別途ワイヤーの色をバーテックスバッファやシェーダーの定数値として持たせてあげることもできるでしょうね。

以上で、習作ワイヤーフレームシェーダーが完成です。作りながら思ったことは、閾値をもうちょっと大きくして、ワイヤーとワイヤー以外の部分を色で塗りわけたら、穴のあいたブロックチーズシェーダーができそうだなということでした。

Date: 2011/11/23

Flash3D

Last-Modified