ドローコール

ドローコールで遊ぼう

draw call triangles

 今回はドローコールで遊びます。種類は7つあります。ざっくり言うと面描画が3つ、線描画が3つ、点描画が1つです。面描画はこれまでに出てきたように、インデックスから3つずついろんなルールで点を取り、三角形を作ります。線描画は2つずつ取って線を引きます。点は、これだけ毛色が違うんですが、描画以外にも使い道があります。それはそのうちやるのでどうでもいいとして...とにかく試してみましょう。

コード全文

作品5のリンク

コードの説明

 コンフィグに使ってるのはlilです。datはバギーな上に更新が終わってしまったので、Threeも使ってるlilにずいぶん前に移行しました。まあコンフィグは便利ですよね。プログラムの生成機構をライブラリ化したので、堂々といろんなプログラムを作成しています。内容的には、POINTS以外のドローコール2種、POINTSで2種です。POINTSは若干特殊なので、2種類用意しました。
 コードを読むと分かりますが、カリングというものをテストしています。

    // バックカリングをするかどうか
    if(config.cullFace){
      gl.enable(gl.CULL_FACE);
      gl.cullFace(gl.BACK);
    }else{
      gl.disable(gl.CULL_FACE);
    }

これについても説明できればと思います。また、プログラムごとの点の配置はこちらです:

position of points

ドローコール各種

 それでは一つ一つ見て行きます。そのまえに参考資料:

draw call explanation

 点の位置に点を打つだけなのでPOINTSは割愛しました。心配しなくてもちゃんと説明します。

TRIANGLES

 まずTRIANGLESについて。これは、0,1,2,3,4,5,6,7,8と数があるとき、たとえば次のようなドローコールですが、

gl.drawArrays(gl.TRIANGLES, 0, 9);

これは「0,1,2」、「3,4,5」、「6,7,8」で三角形を作る命令となります。頭から3つずつ取っていくわけですね。なお端数(7とか8)の場合、三角形が作れない場合は切り捨てです。だから7,8の場合三角形は2つしかできません。切り捨てです。実行すると、次のようになります。

draw call triangles

 ご覧のように、0,1,2と3,4,5で三角形になります。ここで向きが気になるわけです。正規化デバイス座標系はy軸が上を向いているので、正の回転方向は中心、右、上です(反時計回り)。これはWebGLで決められた向きであり絶対です。それで、これを切り分ける処理がカリングです。カリングを有効にすると、正の向きの三角形だけ描画したり、逆に負の向きの三角形だけ描画したりすることができます。enable関数でCULL_FACEを有効にするとこれが機能します。「カリング」とは「摘み取る」という意味で、正の向きはWebGLでは「FRONT(表)」、負の向きはWebGLでは「BACK(裏)」と呼ばれます(3D描画が関係しているんですが今は説明できません...)。デフォルトではcull_Faceはgl.BACKとなっています。つまり単に有効にすると、裏だけカリングされる(三角形の描画がキャンセルされる)わけですね。
 そこでこの描画を見てください。0,1,2は正の向き、3,4,5は負の向きなのがわかるでしょうか。なので、cullFaceを有効にすると、3,4,5の三角形は、消えます:

cull triangle

 これは3D描画の効率化のための機能です。いつか説明できるかもしれません。

TRIANGLE_STRIP

 すでに説明したと思いますが、TRIANGLE_STRIPは帯を描画するための関数です。図のように10個の点が並んでいる場合、「0,1,2」,「2,1,3」,「2,3,4」,「4,3,5」,「4,5,6」,...(以下略)と三角形を作っていきます。ゆえにTRIANGLESと異なり点が3つ以上であればどこで切っても三角形が描画されます。なぜこのような点指定の順になっているかと言えば、ずばり「向きを揃えるため」です。cull_Faceを切り替えてみてください。見た目に変化がないですよね?すべて正の向きだからです。点の配置が上に向かって左、右、左、右、...もしくは右に向かって上、下、上、下、...の場合にすべての三角形が正の向きになるように塩梅が為されているわけです。描画結果はこちら:

draw call triangle_strip

TRIANGLE_FAN

 次にTRIANGLE_FANですが、これは扇状に三角形を作っていきます。STRIPと同様、点が3つ以上であれば必ずすべての三角形が描画されます。指定は単純で、「0,1,2」,「0,2,3」,「0,3,4」,「0,4,5」,...です。つまり0が特別な意味を持ちます。これは点の配置をちょっと変えているのでpg1の方になります。結果はこちら:

draw call triangle_fan

 これもカリングを切り替えても見た目は変化しません。すべて正の向きであることが分かりやすいかと思います。

LINES

 次は毛色を変えて線描画です。LINESは「0,1」,「2,3」,「4,5」,「6,7」,...とペアを作り、線を描いていきます。なお点の個数が奇数の場合、余りは描画されません。切り捨てです。結果はこちら:

draw call lines

 なお線の太さは1で固定です。個人的には便利な仕様だと思います。WebGLは3Dも扱います。3D描画でカメラの位置により線の太さが変わってしまうと、いろいろと不便でしょう。大きいスケールも小さいスケールも扱いたい場合などに、不具合が生じると思います。
 ちなみにカリングの影響範囲はここまでに紹介したTRIANGLE系のみです。これも含め、以降の描画ではカリングは意味を成しません。

LINE_STRIP

 LINE_STRIPはTRIANGLE_STRIPのように点をつないでいきます。「0,1」,「1,2」,「2,3」,「3,4」,...と、おわりまで点をつないで折れ線を作ります。点の間隔が十分近ければ、曲線も表現できます。結果はこちら:

draw call line_strip

 なおLINESでもそうでしたが、色の補間も線上で実行されます。なので自然にグラデーションの付いた線になります。これは2Dにはない強みですね。もっとも2Dでもセグメントごとに色を変えればできなくはないですが......

LINE_LOOP

 LINE系の3つ目はLOOPです。これはSTRIPとほぼ同じですが、LINE_LOOPは最後の点を0とつなぎます。文字通り、ループです。STRIPでは閉曲線を表現する場合、重複が必要になるんですが、LOOPは自然にクローズドカーブになってくれます。結果はこちら(FANと同じ点配置です):

draw call line_loop

 最後に、若干変わり種であるPOINTSについて解説します。

POINTS

 これだけバーテックスシェーダを変えています。なぜかというと重要な変更点があるからです。

#version 300 es
const vec2[6] points = vec2[](
  vec2(-0.5, -0.75), vec2(0.5, -0.75), vec2(-0.5, 0.0), vec2(0.5, 0.0), vec2(-0.5, 0.75), vec2(0.5, 0.75)
);
const vec3[6] colors = vec3[](
  vec3(1.0, 1.0, 1.0), vec3(1.0, 0.5, 1.0), vec3(1.0, 1.0, 0.5),
  vec3(1.0, 0.5, 0.5), vec3(1.0, 0.5, 0.0), vec3(1.0, 0.0, 0.5)
);
out vec3 vColor;
void main(){
  vColor = colors[gl_VertexID];
  vec2 p = points[gl_VertexID];
  gl_Position = vec4(p, 0.0, 1.0);
  gl_PointSize = 40.0; // ※ここを省略した状態でPOINTSを実行すると何も起こらない!
}

 POINTS命令は名前の通り、指定された位置に点を描画します。点と言っても見た目の通り、正方形です。指定した点を中心に描画されます。結果はこちら:

draw call points

 色もそのままです。0,6の場合、0,1,2,3,4,5の位置に点が置かれます。なおデフォルトです。プログラムは2種類あるんですがpg2の方ですね。
 重要なポイントがあります。バーテックスシェーダ内でgl_PointSizeを指定することです。POINTSのドローコールは例外なくすべて、これを記述しないと、何も実行されません:

gl_PointSize = 40.0;

 これを書き忘れたことによるバグを何度しでかしたことか...何にも指摘してくれないので本当に気づかないんです。気を付けましょう。
 gl_PointCoordについてもちょっとだけ触れておきます。コンフィグにusePointCoordというのがあると思いますが、これをチェックするとPOINTSの命令がpg3になります。その場合の結果はこちら:

draw call points use_pointcoord

 これはどうやっているかというと、上記の正方形で簡単な板ポリ芸のようなことをしています。

#version 300 es
precision highp float;
out vec4 fragColor;
in vec3 vColor;
void main(){
  vec2 p = gl_PointCoord;
  p -= vec2(0.5, 0.5);
  p *= 2.0;
  float alpha = 1.0 - length(p);
  fragColor = vec4(vColor, 1.0) * alpha;
}

 実は、フラグメントシェーダにおいてgl_PointCoordという変数を呼び出すと、この点領域にアクセスできます。座標の内容は(0,0)~(1,1)で、実は左上が(0,0)で右下が(1,1)です。メイン関数を次のように書き換えます:

void main(){
  vec2 p = gl_PointCoord;
  if(p.y>0.5){fragColor = vec4(1.0);}else{fragColor=vec4(0.5);}
}

こうなります:

pointcoord test

 なぜ正規化デバイスと違ってy軸が下を向いてるかというと、これは2Dの座標系が関係しています。いわゆるポイントスプライトと呼ばれる技術で、PIXIなどで高速描画のために多用されています。
 該当コードの内容は、中心である(0.5,0.5)が原点になるように正規化したうえで、lengthを使って減衰させています。そして全体にalphaを掛けています。第4成分もいじっていますが...実はblendというものを使っています。2Dでも出てくるアルファブレンドです。がっつりやらないんですが、さわりだけ...

  gl.useProgram(pg3);
  // やってないけど通常のアルファブレンド
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
  gl.drawArrays(gl.POINTS, 0, 6);
  gl.disable(gl.BLEND);

 アルファブレンドは一般に、次のように計算されます。

alpha blending

 RGB値はソースとデストをソースのアルファで線形補間します。アルファが1のときソースべったりになるように、アルファが0のときデストオンリーになるように、です。A値は、いわゆるスクリーン乗算をします。なぜ加算ではないかというと、両者のアルファが共に1より小さいときに結果が1になると不自然だからです。もっとも絵作りなので、どう構成するかは自由ですが、少なくとも2Dではそうしているので、合わせるのが無難でしょう。
 ここでの計算の内容を簡単に説明すると、まずblendを有効にするにはenableを使います。デフォルトでは機能していません。これは地のキャンバスに新しく描画する際に、色の合成をどうするかを決めるものです。デフォルトでは完全に上書きというわけです。この記事の場合、ONE,ONE_MINUS_SRC_ALPHAを指定しています。blendFuncはRGB,Aの順に指定するんですが、これは係数です。やってることの内容は、シェーダーで出力されるRGBとAに、これらのRGB係数とA係数をそれぞれ掛けて、足している、だけ、です。とても単純!
 その単純な計算の結果、ただしいRGBが計算されます。それでシェーダーを見てください。内部で構成したRGBとアルファ(この場合は1)に、外に向かって減衰するアルファを掛けていると思います。こうしてsRGB*sAとsAができるので、あとは係数を掛けてそれぞれ足し算するだけです。これをしないと、背景の暗い青にうまく溶け込まないので、あった方がいいですね。
 blendは使い道次第です。テクスチャ描画では必須となります。いずれまた扱います。

 今回は盛り沢山でした。それではまた次回。

今回登場した関数

drawArrays
enable/disable
cullFace