【WebGL】ただの四角を描画しましょう

WebGL

ムズイね…。

はじめに

2021年の1月からThree.jsの勉強をしてきたワケですが、「もっと知りたい」という気持ちが出てきました。難しそうだなぁと思って避けてきた「生のWebGL」に触れてみることで新たな世界が垣間見えるのではなかろうか。そう思うのです。

今回作ったもの

ただの四角形です。。。

手順

あくまでも「四角形を描画するだけの手順」です。

  1. 頂点シェーダーとフラグメントシェーダーの作成
  2. データを保持するためのバッファ(VBOおよびIBO)の作成
  3. VBOを頂点シェーダーのアトリビュートと関連付ける
  4. 描画する

よく分かったところで早速やっていきましょう!

1.頂点シェーダーとフラグメントシェーダーの作成

<!-- 頂点シェーダー -->
<script id="vertex-shader" type="x-shader/x-vertex">
  #version 300 es
  precision mediump float;

  in vec3 aVertexPosition;

  void main(void) {
    gl_Position = vec4(aVertexPosition, 1.0);
  }
</script>

<!-- フラグメントシェーダー -->
<script id="fragment-shader" type="x-shader/x-fragment">
  #version 300 es
  precision mediump float;

  out vec4 fragColor;

  void main(void) {
    fragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
</script>

いきなりすいませんが、何してんのか分かっていません。気にもしませんでした。なぜなら、買ってきた本に「次章で説明するので詳細について気にする必要はありません」って書いてあったから。素直。

分かったことといえば、シェーダーは「GLSL」という、CやC++に似た言語で記述するということ。頂点シェーダーは図形の頂点データをあれこれするということ。フラグメントシェーダーは図形を構成するピクセルの色をあれこれするということ。以上です。

2.データを保持するためのバッファ(VBOおよびIBO)の作成

「バッファ」って言われるといまいちピンと来ませんが、要はアレですよ。シェーダーに渡すデータを一時的に保管してくれてるモノってことですよ。たぶん。

で、今からそれを作っていくんですが、バッファには2種類あるんですよね。それがVBO(Vertex Buffer Object:頂点バッファオブジェクト)とIBO(Index Buffer Object:インデックスバッファオブジェクト)です。

VBOは図形の頂点データを保持します。

一方のIBOは図形の頂点のインデックスを保持します。インデックス=順番みたいな感じですね。各頂点をどういう順番でつなげて図形を作っていくのか、それを決めているようです。

早速VBOから作っていきたいところですが、その前にWebGLコンテキストと呼ばれるものについて説明します。

「コンテキスト」ってなんなん?って感じですけど、「オブジェクト」と同じような意味でしょう。だからアレですわ。WebGLコンテキストというのは、3D描画するためのメソッドとかプロパティとかをたくさん備えたオブジェクトということですよ。というワケで、兎にも角にも、まずはこのWebGLコンテキストを取得しないといけませんね。

// まずはHTMLのcanvas要素を取得
const canvas = document.getElementById('webgl-canvas');

// WebGLコンテキストを取得(glはグローバル変数)
gl = canvas.getContext('webgl2');

はい。これでWebGLコンテキストを手に入れました。なんでcanvas要素なんか必要なのー?ですが、WebGLってcanvas要素に描画するからですね。

ではVBOを作っていきましょう。

// (1)四角形の各頂点の座標を配列で用意する(値3個で1つの座標を表す)
const vertices = [
  -0.5, 0.5, 0,
  -0.5, -0.5, 0,
  0.5, -0.5, 0,
  0.5, 0.5, 0
];

// (2)バッファを作る(squareVertexBufferはグローバル変数)
squareVertexBuffer = gl.createBuffer();

// (3)作ったバッファをVBOとして操作するためにバインド(関連付け)する
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexBuffer);

// (4)バインドしたVBOにデータを入れていく
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

// (5)バインドを解除する
gl.bindBuffer(gl.ARRAY_BUFFER, null);

(1)
まずは四角形の頂点座標を配列で用意します。WebGLは座標を-1から+1の範囲で扱います。これを「クリップ空間座標」と呼ぶそうです。以前の記事で、画面上の座標からクリップ空間座標への変換について書いたことがあったのですんなり受け入れられました。

(2)
次に、gl.createBuffer()でバッファを作ります。

(3)
で、そのバッファをgl.bindBuffer()の第2引数に指定。第1引数にはVBOを表すgl.ARRAY_BUFFERを指定。IBOとして扱いたい時はgl.ELEMENT_ARRAY_BUFFERにしましょう。バインドすることで「今からこのバッファを操作しますよー」となるワケです。

(4)
バインドしたバッファにデータを入れるのがgl.bufferData()です。
第1引数はgl.ARRAY_BUFFERを指定します。VBOを作っているからです。第2引数には配列で用意した頂点座標データを渡すのですが、そのままだと渡せません。型付き配列に変換して渡してあげましょう。パフォーマンスを向上させるために、そういう決まりになっているそうです。第3引数は良く分かりません。gl.STATIC_DRAWgl.DYNAMIC_DRAWgl.STREAM_DRAWの3種類から選べます。

(5)
もろもろ終わったらgl.bindBuffer()の第2引数にnullを指定して、バインドを解除しておきましょう。使い終わったら片づける。それがお作法なのだとか。

次はIBOの作成です。

// インデックスを配列で用意する(indicesはグローバル変数)
indices = [0, 1, 2, 0, 2, 3];

// バッファを作る(squareIndexBufferはグローバル変数)
squareIndexBuffer = gl.createBuffer();

// 作ったバッファをIBOとして操作するためにバインド(関連付け)する
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareIndexBuffer);

// バインドしたIBOにデータを入れていく
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

// バインドを解除する
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

VBOとほぼ同じです。配列で用意したインデックスについてだけ説明しておきます。絵で。

分かりましたでしょうか。僕はボンヤリ分かりました。とにかくですね、インデックスは頂点をつなぐ順番です。で、今回は三角形を反時計回りにつないでいますけど、統一されていればどっち回りでも良いみたいです。ちなみに、逆向きにつないだ三角形は「裏側を向いている」と判断されるそうです。面白いですね。

これでようやくバッファの作成まで終わりました。

3.VBOを頂点シェーダーのアトリビュートと関連付ける

先程作ったVBOですが、これは必ずしも一つだけとは限りません。頂点情報には色々な種類があります。座標や色、法線、テクスチャ座標などなど。これらの頂点情報の種類一つにつき、一つのVBOが必要になります。

そして、頂点シェーダーには、頂点情報の種類ごとに専用の「アトリビュート」と呼ばれる変数があります。

  • aVertexPosition(座標)
  • aVertextColor(色)
  • aVertexNormal(法線)
  • aVertexScalar(スカラ値)
  • aVertexOtherProperty(その他)

あるVBOに入っているデータが座標データであるなら、aVertexPositionと関連付けてあげることで、頂点シェーダーが「あ。これは座標なんですね。じゃあそういうつもりで処理しますよ。」となって意図したものが描画できるというワケです。

// (1)VBOをバインドする
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexBuffer);

// (2)バインドされたVBOとアトリビュートを関連付ける
gl.vertexAttribPointer(program.aVertexPosition, 3, gl.FLOAT, false, 0, 0);

// (3)アトリビュートを有効にする
gl.enableVertexAttribArray(program.aVertexPosition);

// (4)バインドを解除する
gl.bindBuffer(gl.ARRAY_BUFFER, null);

(1)
まずは関連付けしたいVBOをバインドします。VBOを作成した時と同じくgl.bindBuffer()を使います。

(2)
次に、バインドしたVBOと頂点シェーダーのアトリビュートとの関連付けです。gl.vertextAttribPointer()を使います。
第1引数には関連付けしたいアトリビュートを指定。
第2引数にはVBOに保存されている頂点ごとの値。
第3引数はVBOに保存されているデータの型を指定します。
第4引数は「入門書の範疇を超えるため説明は省略する」と書いてあるので素直に従いfalse。
第5引数に0を指定すると頂点データがVBO内に順番に保存されていることを示すそうです。トリッキーな事をするのでなければ0を入れておけばいいのでしょう。
第6引数はアトリビュートにデータを渡し始めるVBO内のデータの位置。0にすれば先頭からになる。

(3)
頂点シェーダーのアトリビュートを有効化します。gl.enableVertexArrayAttrib()の引数に有効化したいアトリビュートを指定します。

(4)
使い終わったバッファはバインドを解除しておきましょう。お作法です。

はい。これでVBOとアトリビュートの関連付けが終わりました。もうひとふんばりです。

4.描画する

描画にはgl.drawArrays()もしくはgl.drawElements()を使用します。

ただ、gl.drawArrays()はインデックス(IBO)を使用しません。そのせいでパフォーマンスが落ちるため推奨されていないとか。そんなワケでgl.drawElements()を使って描画します。

// (1)IBOをバインドする
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, squareIndexBuffer);

// (2)描画する
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

// (3)バインドを解除する
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

(1)
まずはIBOをバインドします。

(2)
gl.drawElements()で描画します。
第1引数には描画するプリミティブの種類を指定します。プリミティブとは「単純な形」という意味合いです。点、線、三角形を指定できるようです。今回は三角形を二つ組み合わせて四角形を描画するのでgl.TRIANGLEです。
第2引数は描画される要素数。インデックスの要素数ですね。
第3引数はインデックスのデータ型。インデックスは整数値なのでgl.UNSIGNED_BYTEgl.UNSIGNED_SHORTを指定します。
第4引数は描画開始点となるIBO内のデータの位置。0にすれば先頭からになる。

(3)
バインドを解除しておきます。

これで描画ができました。

まとめ

ここで紹介したコードだけでは動きません。この他にもWebGLのコードをあれこれ書く必要がありますが、そこまではまだ理解できていません。

が、シェーダーへデータを渡すためにはバッファが必要であるということ、そのバッファの作り方、アトリビュートとは何ぞや、VBOとアトリビュートの紐づけなどの、おそらく描画をするための基礎の基礎ぐらいは分かったような気がします。

「初めてのWebGL2」。40ページしか進んでいません。まだまだ先は長いですが、コツコツやっていこうと思います。今回は以上です。ありがとうがございました。

おしゃれ度

★☆☆☆☆

Posted by ナカタ