ラスタライズ

ピクセルごとの色の決め方

pixel base triangle

 この節では、ラスタライズの仕組みについて説明します。前回RGB三角形をやったと思いますが、頂点ごとに色を決めると、それらが補間されます。それを可視化しようという企画ですね。計算式とかは出しません。あくまで雰囲気だけです。20x20のピクセル上でWebGLを実行し、それを2Dのキャンバスに落として可視化したいと思います。

コード全文

作品3のリンク

描画の基本的な仕組み

 WebGLは2Dと違う描画方法を採用しているだけで、実質的には2Dです。3Dもできますが、2Dのコンテクストでは3D描画をするのは無理があるので、違う描画方式を採用しているわけです。
 前置きはこのくらいにして、まず今現在WebGLのコードでやっている描画というのは、まず一定の範囲の整数を決めます。このコードだと、

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

にある通り、0から3つ、です。つまり0,1,2ですね。0,1,2で描画しようというわけです。そしてコールがTRIANGLESなので、0,1,2に対応する位置に対して、三角形を一枚描画します。その際、キャンバスのサイズは千差万別ですから、正規化された値を使いたいわけです。なのでバーテックスシェーダでは最終的に正規化された座標値として位置を決めます。それが左下(-1,-1),右上(1,1)の正規化デバイス座標系なんですね。あとは決まった位置に対して描画するだけです。で、その際に頂点ごとに「色」を決めます。いずれ、「値」として抽象化されるんですが、今は色と思って大丈夫です。
 それでアトリビュートというのはこの0,1,2から位置を決めるもう一つの方法です。今のやり方だと整数だけからシェーダ内で位置を決めないといけないので不便です。あらかじめ用意した値を使いたいですよね?いずれ、やります。
 それではラスタライズの説明に移ります。

オフスクリーンキャンバス

 今回は色の補間がどんな感じなのかを見るために20x20のオフスクリーンを使います。作るにはOffscreenCanvasというクラスを使います。

  // 20x20のオフスクリーンを作る。ここにRGBの三角形を用意する。
  const offscreen = new OffscreenCanvas(20, 20);
  const gl = offscreen.getContext('webgl2');

 これはキャンバスエレメントとほぼ同じなんですが、cssの概念もないし、そもそもDOMツリーと独立しています。しかしコンテキストは取得でき、描画も普通に実行できます。つまりキャンバスの描画機構だけを有した簡易版ですね。表示するにはdrawImageやreadPixelsでデータを取り出す必要があります。描画に必要なだけで別に画面に表示する必要が無い場合に便利です。cssの概念が無いので作るときにサイズをダイレクトに指定してしまいます。
 以前、p5の枠組みを借りてやっていた頃はcreateGraphicsという関数でオフキャンバスを作っていたんですが、あれはただのキャンバスエレメントです。非表示にしているだけで、cssをいじると普通に表示されます。やってみてください。楽しいですよ。

RGB三角形

 このオフスクリーンに前回やったようにRGBの三角形を描画します。コンテキストを取得して以降は、サイズが違うだけで、全く同じ処理です。ゆえに割愛します。背景は単純に黒としています。

画面に表示する(readPixels)

 オフスクリーンへの描画が終わったので、画面に内容を表示します。20x20なので、400個のピクセルが用意されました。これを400x400のキャンバスに1つずつそのままその色で描画します。内容を取り出すには、readPixelsを使います。

  // offscreenからピクセルデータを取り出す
  const pixels = new Uint8Array(20*20*4);
  gl.readPixels(0, 0, 20, 20, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
  // 内容に従って描画
  for(let y=0; y<20; y++){
    for(let x=0; x<20; x++){
      const offset = 4*(x+20*y);
      const r = pixels[offset];
      const g = pixels[offset+1];
      const b = pixels[offset+2];
      const a = pixels[offset+3];
      ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a/255})`;
      ctx.fillRect(x*20, y*20, 20, 20);
    }
  }

 今エスケープしました。不等号はめんどくさいですね...それはさておき、readPixelsという関数を使います。これはWebGLのキャンバスからピクセルデータを取り出すための関数です。drawImageでもいいんでしょうが、readPixelsも便利なので、使い方を紹介する意味でこっちを使います。色データはそれぞれが4バイトで、それが20x20個並んでいます。なのでまず受け皿としてサイズが20x20x4のUint8Arrayを用意します。それを、引数にセットします。
 最初の0,0はオフセットで、次の20,20はそこからx方向に20,y方向に20という意味です。つまりこれで全部ですね。フォーマットはRGBAで、型はUNSIGNED_BYTEです。最後に出力先の配列を置いて、これで取得できます。
 あとはそれを使ってfillStyleを設定して、正方形を1つずつ描いていくだけですね。それで冒頭の画像が...できました。
 おかしいですね。

ビューポート座標とピクセル

 よく考えれば明らかなんですが、WebGLの最もややこしい話題のひとつなので、きちんと説明します。
 正規化デバイス座標系は左下が(-1,-1)で右上が(1,1)です。ですがこの図では赤が左上に来ています。その理由についてですが、まずビューポート座標系について簡単に説明します。
 WebGLの描画先としてビューポートというものが用意されています。そのうちきちんと説明するのでここではざっくりと、左下が(0,0)で右上が(400,400)ということだけ覚えてください。一般には(横幅、縦幅)です。具体的には、まず正規化ビューポート座標を正規化デバイス座標から算出して、それをビューポート変換でピクセル上に出します。正規化ビューポート座標は0~1の値であり、算出するには1を足して2で割ります。そうしてビューポート座標にした場合、書き込まれるピクセルの位置というのがこれで指定された値になります。どこに書き込まれるかというと、「デフォルトのカラーバッファ(色情報の入れ物)」です。そしてreadPixelsはそこからフェッチします。それで(0,0)が左下の色になるわけです。2D描画ではそれを左上に持っていくので上下が逆になります。
 しかし、キャンバスをそのまま描画する場合は普通に左下が赤になります。2Dの描画部分を次のように変更します。

  ctx.clearRect(0,0,400,400);
  ctx.drawImage(offscreen, 0, 0, 20, 20, 0, 0, 400, 400);
triangle canvas

 オフスクリーンへの描画結果をそのまま表示するにはdrawImageを使います。これで左下が赤になります。ボケているのはフェッチの際に補間しているからで、補間をやめればちゃんとピクセルごとになります。そのためには次のようにします。

  ctx.imageSmoothingEnabled = false; // これで滑らかさが消える
  ctx.clearRect(0,0,400,400);
  ctx.drawImage(offscreen, 0, 0, 20, 20, 0, 0, 400, 400);

 これは画像を滑らかに表示するのをやめるものです。結果はこちら:

triangle canvas

 つまり、キャンバスにはきちんと左下が赤で描画されています。readPixelsはカラーバッファから直接取得するので、上下が逆になるんですね。テクスチャやフレームバッファのところでこの話題がまた登場すると思うので、気に留めておいてください。
 以降は補足です。

コンテキストアトリビュート

 コンテキストの取得の際にいろいろオプションを付けられます。設定は基本的に後から変更することはしません。取得するにはgetContextAttributesを使います。

  const attributes = gl.getContextAttributes();
  console.dir(attributes);
alpha: true
antialias: true
depth: true
desynchronized: false
failIfMajorPerformanceCaveat: false
powerPreference: "default"
premultipliedAlpha: true
preserveDrawingBuffer: false
stencil: false
xrCompatible: false

 たとえばalphaは透明度バッファを使うかどうかを決めるもので、falseにすると常に不透明で描画されます。antialiasは、今回注目するのはここですが、境界を滑らかにするのをやめるものです(境界でなければ普通に補間する)。これを作る際にtrueにしてみます。

  const offscreen = new OffscreenCanvas(20, 20);
  // こうするとantialiasがfalseになる
  const gl = offscreen.getContext('webgl2', {antialias:false});
triangle canvas

 境界が真っ黒になりました。そういうことですね。

補間の実際

 この辺はどっちかというと高校や中学の数学の話ですが、補間がどのように行われるのかについて、です。3つの頂点座標があると三角形ができます(2次元で考える)。一つの点があるとき、その点がその三角形に含まれてるかどうか、また含まれている場合に3つの点の割合というか、寄与を計算する方法があるわけです。それは数学の領域です。

triangle canvas

 つまり、面積比で出ます。また面積というよりは外積により計算される符号付きの面積です。それらがすべて正であれば、内部というわけです。なんとなくイメージできると思いますが、内部の点が一つの頂点に向かうとき、その点と反対側の辺と、内部の点で出来る三角形は全体に近づきますよね?そして他の内部三角形はつぶれていきます。それで何となくわかるかと思います。この比率をウェイトとして重み付き平均を取っているというわけです。
 符号付きの面積は外積で出せます。加減乗除で完結するので高速です。そういうわけですね。

 長くなりましたが、補間の様子がなんとなくわかったと思います。今回はここまでです。次回は、プログラム生成機構を作ろうかと思います。さすがに今のスタイルだと複数のプログラムを扱えなかったりして不便なので...その次はドローコールで遊んでみようと思います。あとそれから深度値などに触れる記事も書ければと思います。gl_Positionの第3成分と第4成分の話です。それで一応入門は終わりです。

追記:キャンバスの上下

 WebGLのレンダリングコンテキストにより、キャンバスに描画が実行されます。左下は赤です。それは正規化デバイス座標で(-1,-1)に赤が割り当てられているからで、これがビューポート座標の(0,0)に対応するからで、見た目上は左下です。これはキャンバスで言うところのcss上のleft-bottomに対応します。

 css上のtop,bottomの概念はy軸の上下とは無関係です。同様にleft-rightもx軸の左右とは無関係です。というか座標の概念が不要です。見た目上の上下と、座標の大小は本来全く別の概念なので、上下いずれの方向に向かうと座標値が大きくなるかには任意性があるわけです。それで、キャンバスの内容をダイレクトに2Dの関数などで表示すると、キャンバスに描画された通りに表示されるわけです。それで左下が赤になるんですね。

 しかし、readPixelsの場合は違います。あれは明確に(0,0)がどこかを指定したうえで1ピクセルずつ読み取る処理です。その位置というのがWebGLの場合左下なので、どうしても逆になってしまうわけですね。

 他方、2Dの場合は、getImageDataという関数を使うんですが、これによりサーチする場合は左上が(0,0)になります。ImageDataオブジェクトのdataプロパティにUint8ClampedArrayが入ってて、これを使います。

 その都度、落ち着いて考えましょう。補足は以上です。

今回登場した関数

getContextAttributes
readPixels