続:WebGLでベジェ曲線を描いてみた
12月になり、各所でアドベントカレンダーが始まっていますね。ソニックムーブのスタッフブログも25日まで毎日更新となります。
ソニックムーブ Advent Calendar 2014 開催です!
初日を担当します、フロントエンジニアのらくさんです。元々3日目を担当する予定だったので初日らしい内容というわけでもなく、WebGLでベジェ曲線を描いてみた の続きになります。前回、2次ベジェ曲線の断片ひとつをシェーダで描画することはできました。今回は、複数の2次ベジェ曲線で構成されるパスの内部を塗りつぶします。
ステンシルバッファを使って凹型ポリゴンを塗りつぶす
本題に入る前に、ステンシルバッファを使って凹型ポリゴンを塗りつぶす方法を紹介します。次節では、これを2次ベジェ曲線のパスを塗りつぶすのに応用します。
上図左の凹型ポリゴンABCDEを塗りつぶす場合を考えます。まず、このポリゴン上の頂点のうちどれか一つを選びます。ここでは点Aを選ぶものとします。次に、凹型ポリゴン上の隣り合った2つの頂点と点Aが作る三角形を全て作ります。この場合、ABC, ACD, ADE の3つが作られます。これらの三角形を全て塗ったとき、奇数回塗られた領域は凹型ポリゴンの内側、偶数回塗られた(または一度も塗られなかった)領域は外側となります。
この方法はステンシルバッファを使うと簡単に実装できます。はじめにステンシルバッファを0でクリアし、ステンシル関数(stencilFunc)を ALWAYS、ステンシル操作(stencilOp)を INVERT にして三角形 ABC, ACD, ADE を描画します。すると「奇数回塗られた領域」だけがステンシルバッファ上で0でない状態になります。次に、ステンシル関数 NOTEQUAL で凹型ポリゴン全体を覆う形状(バウンディングボックスや凸包等)を描画すれば、ステンシルバッファでマスクされた結果がカラーバッファに描画されます。
ステンシルバッファにABC, ACD, ADEを描画:
1 2 3 4 5 6 7 |
[sourcecode lang="javascript" firstline="149"]gl.clear(gl.STENCIL_BUFFER_BIT | gl.COLOR_BUFFER_BIT); gl.stencilFunc(gl.ALWAYS, 0, ~0); gl.stencilOp(gl.KEEP, gl.INVERT, gl.INVERT); gl.colorMask(false, false, false, false); gl.drawArrays(gl.TRIANGLES, ...); // ABC, ACD, ADEを描画 [/sourcecode] |
ステンシルバッファでマスクしてカラーバッファに描画:
1 2 3 4 5 |
[sourcecode lang="javascript" firstline="159"]gl.stencilFunc(gl.NOTEQUAL, 0, ~0); gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); gl.colorMask(true, true, true, true); gl.drawArrays(gl.TRIANGLE_FAN, ...); // 凸包を描画 [/sourcecode] |
ここで紹介した方法は OpenGLプログラミングガイド 14章「様々なテクニック」の「ステンシルバッファを用いた、塗りつぶされた凹型ポリゴンの描画」に記載されています。詳しくはそちらをご覧ください。
2次ベジェ曲線への応用
上図左の2次ベジェ曲線(赤い線)の内側を塗りつぶすことを考えます。まず、この2次ベジェ曲線のアンカーポイント(黒い点)だけを直線で繋いでポリゴンを作ります。そして、その内側(上図左でグレーの領域)を前述の方法でステンシルバッファに書き込みます。次に、2次ベジェ曲線によって作られる領域をステンシルバッファに加えたり削ったりします。上図右の青いところが加える領域、緑のところが削る領域です。
このデモのソースを見ていただくとわかると思いますが、ポリゴン部分の描画と2次ベジェ曲線によって作られる領域の加えたり削ったりは、一回の描画処理にまとめてしまうことができます。ポリゴン部分の頂点全てに、2次ベジェ曲線の塗りつぶされる側となる頂点属性、例えば (0.5,0.5) を渡せば、ポリゴン部分の処理も2次ベジェ曲線のシェーダでできてしまいます。
アンチエイリアス
前節のデモでは、塗りつぶした領域の境界部分にジャギーが発生しています。これを滑らかにするには、境界近傍のフラグメントには境界線までの距離によって0〜1のアルファ値が書き込まれるようにします。
アンチエイリアス対応フラグメントシェーダ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[sourcecode lang="C" firstline="23"]#extension GL_OES_standard_derivatives : enable precision mediump float; varying vec2 p; void main(void) { vec2 px = dFdx(p); vec2 py = dFdy(p); float fx = (2.0*p.x)*px.x - px.y; float fy = (2.0*p.x)*py.x - py.y; float sd = (p.x*p.x - p.y)/sqrt(fx*fx + fy*fy); float alpha = 0.5 - sd; if (alpha > 1.0) { // inside gl_FragColor = vec4(1.0); } else if (alpha < 0.0) { // outside discard; } else { // near boundary gl_FragColor = vec4(alpha); } }[/sourcecode] |
これは、前回の参考文献に挙げた GPU Gems 3 : Chapter 25. Rendering Vector Art on the GPU にあるHLSLのソースコードをGLSLに書き換えたものになります。詳細はそちらをご覧ください。
このシェーダでは dFdx, dFdy という関数を使用していますが、これは OES_standard_derivatives という拡張機能になります。これを使用するために、次のコードを実行して拡張機能を有効にします。
1 2 3 |
[sourcecode lang="C" firstline="67"]if (!gl.getExtension("OES_standard_derivatives")) { alert("no extension: OES_standard_derivatives"); [/sourcecode] |
また、フラグメントに書き込んだアルファ値をMSAAの遮蔽率として解釈させるために SAMPLE_ALPHA_TO_COVERAGE を有効にします。
1 |
[sourcecode lang="C" firstline="207"]gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);[/sourcecode] |
(しかし、iOS8のWebGLはアンチエイリアスが効かないようです…)
WebGLでフォントを描画してみる
最後に、WebGLで2次ベジェ曲線を描画する応用例としてWebGLでフォントを描画してみます。
このデモは、freetypeをEmscriptenでJavaScriptに変換して使用しています。この記事では2次ベジェ曲線しか扱っていないため、このデモはTrueTypeベースのフォント(NotoSerif-Bold.ttf)を使用しています。PostScriptベースのフォントを描画するには3次ベジェ曲線に対応する必要があります。
(このデモはかなりやっつけで書いたので、ソースが汚い&たぶん色々おかしいところがあると思います…)