Subterranean Flower

DartでWebGL入門-基礎編

Author
古都こと
ふよんとえんよぷよぐやま!!!

Dartについて書かれている記事は少ない。WebGLについて書かれている記事も少ない。両方となると……?

2DグラフィックスにおけるWebGLのパフォーマンスの記事ではDartを使ってプログラムを記述していたものの、何の説明もしないというのは少しマズいのではないかと感じたので、DartからWebGLを利用する方法について簡単に解説することにした。

ただ、あくまでWebGLの利用方法に焦点を当てたいので、3Dグラフィックスの仕組みや線形代数については取り扱わない。

DartでWebGL入門

WebGLとは

WebGLはブラウザ上で3DCGを扱うための標準規格である。OpenGL ES 2.0がベースとなっており、全体としての仕様もそれに準拠する。GPUの恩恵を受けた高速な処理が可能で、レンダリングはcanvas要素に対して行われる。

現状で正式サポートしているのはChromeとFirefoxのみだが、IE12やSafari8でもサポート予定となっている。

DartからWebGLを使う

WebGLは複雑なAPIなので、まずは単純なところから始めよう。とりあえずは下の図のような四角形を描画することを目標にする。

動作サンプルを開く

webgl_colored_quad

前準備

WebGLの使用を始める前にいくつかの準備がある。

import 'dart:html';
import 'dart:web_gl';
import 'dart:typed_data';

void main() {
  // 今回使うcanvas要素。
  var display = new CanvasElement(width: 500, height: 500);
  document.body.append(display);
  
  // WebGL用のコンテキストを取得する。
  RenderingContext context = display.getContext3d();
  
  // ビューポートの設定。
  // 通常はcanvasと同じサイズに設定する。
  context.viewport(0, 0, display.width, display.height);
  
  /* ここから続きを書く。 */
}

WebGLを扱うためにはdart:web_gldart:typed_dataライブラリが必要になるので、それぞれimportしておく。レンダリング対象としてのcanvasも必要なので作成しておこう。

canvasが準備できたら、getContext3d()でRenderingContextオブジェクトを取得する。そのコンテキストを操作することでWebGLが利用できる。WebGLに未対応のブラウザならおそらくnullが返ってくるが、今は未対応ブラウザについては考えないこととする。

コンテキストが取得できたら、まずはビューポートの設定を行っておきたい。特別の事情がないかぎり、canvas要素と同じ大きさでかまわないだろう。

シェーダの準備

コンテキストを取得できたのでさっそく描画と行きたいところだが、WebGLには描画のための固定機能パイプラインが用意されておらず、プログラマブルシェーダで記述する必要がある。とは言え、用意しなければならないシェーダは2つだけなので、凝ったことをしなければ大した手間にはならないだろう。webgl_pipeline ひとつはバーテックスシェーダ(頂点シェーダ)と呼ばれているもので、頂点情報の処理を担当する。3D座標を画面上の座標へと変換するのが主な仕事になる。

もうひとつはフラグメントシェーダ(ピクセルシェーダ)と呼ばれていて、ピクセルレベルの処理を担当する。ポリゴンに色を付けたり、テクスチャを貼り付けるのが主な仕事だ。

2つのシェーダはGLSL(OpenGL Shading Language)という言語で記述する。C言語に似た構文なので、Dart利用者ならばすぐに飲み込めるだろう。細かな文法については見ればわかると思うので説明しない。

シェーダプログラムは外部ファイルとして保存しておくのが普通ではあるが、読み込み処理などを省きたいので、ここではDartプログラムに直に埋め込むことにする。使用するシェーダは以下の2つ。

// バーテックスシェーダ。
// attributeは頂点ごとの変数。
// varyingはフラグメントシェーダへ渡す値。
// gl_Positionに頂点座標の演算結果を代入する。
// ここでは受け取った座標をそのまま代入しているだけ。
// 座標はxyzwの4次元になるが、今のところはw=1.0と固定して考えても良い。
const VERTEX_SHADER_SOURCE = """
attribute vec3 vertexPosition;
attribute vec4 color;

varying vec4 vColor;

void main() {
  vColor = color;
  gl_Position = vec4(vertexPosition, 1.0);
}
""";
// フラグメントシェーダ。
// 初めにfloatの精度の指定が必要になる。
// lowp・mediump・highpなどがある。
// gl_FragColorに値を代入することでピクセルに色が書き込まれる。
// ここではバーテックスシェーダから受け取った色をそのまま書き込んでいる。
const FRAGMENT_SHADER_SOURCE = """
precision mediump float;
varying vec4 vColor;

void main() {
  gl_FragColor = vColor;
}
""";

バーテックスシェーダではプログラムから受け取った頂点座標を変換し、gl_Positionへと変換結果を代入する。ここでは特に変換などは行わず、頂点座標をそのまま代入している。なお、座標はxyzwの4次元で指定するが、今のところはw=1.0で固定していて良い。

フラグメントシェーダはバーテックスシェーダからいくつか情報を受け取り、それを元にgl_FragColorへピクセルを書き込む。ここでは頂点色をそのまま書き込んでいる。

GLSLで変数として使える型にはint, uint, float, double, boolの他に、vec2, vec3といったベクトルや、mat4, mat2x3などの行列がある。 また、GLSLの変数にはattribute, uniform, varyingの3種類が存在する。 shader_variables attribute変数は頂点ごとに異なる値を扱うのに使用する。例えば座標などは頂点ごとに異なるのでattribute変数として宣言する。今回は色も頂点ごとに変化させたいのでattribute変数にしている。バーテックスシェーダのみから利用できる。

uniform変数は頂点に依存しない共通の値を扱うのに使用する。例えば変換行列やテクスチャ画像データなどがこれにあたる。バーテックスシェーダ・フラグメントシェーダの両方から利用できる。

varying変数はバーテックスシェーダからフラグメントシェーダへ値を受け渡すのに使用する。フラグメントシェーダはattribute変数にアクセスできないので、バーテックスシェーダ側で受け取りvarying変数を使ってフラグメントシェーダへ送ろう。

 シェーダプログラムの作成

WebGLではコンパイル済みバイナリがサポートされていないので、実行のたびにシェーダをコンパイルする必要がある。まずはそれぞれのシェーダをコンパイルしよう。コンパイルが終わったら、コンパイルが成功したか否かを確認しておく。

// シェーダ作成→ソースを渡す→コンパイルの流れ。
Shader vertexShader = context.createShader(VERTEX_SHADER);
context.shaderSource(vertexShader, VERTEX_SHADER_SOURCE);
context.compileShader(vertexShader);
  
// コンパイルが成功しているか確認しておこう。
var isVsCompileSuccessful = context.getShaderParameter(vertexShader, COMPILE_STATUS);
if(!isVsCompileSuccessful) {
  var message = context.getShaderInfoLog(vertexShader);
  throw new Exception("Failed to compile vertex shader: $message");
}
  
// フラグメントシェーダも同様に。
Shader fragmentShader = context.createShader(FRAGMENT_SHADER);
context.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE);
context.compileShader(fragmentShader);
  
var isFsCompileSuccessful = context.getShaderParameter(fragmentShader, COMPILE_STATUS);
if(!isFsCompileSuccessful) {
  var message = context.getShaderInfoLog(fragmentShader);
  throw new Exception("Failed to compile fragment shader: $message");
}

シェーダの準備ができたら、次はシェーダプログラムを作成する。シェーダプログラムを作成するために、コンパイルの済んだバーテックスシェーダとフラグメントシェーダをリンクする。シェーダのコンパイルと同様、シェーダプログラムのリンクが成功したかどうかを確認しておこう。

// シェーダの準備が済んだらプログラムを作成する。
Program program = context.createProgram();
context.attachShader(program, vertexShader);
context.attachShader(program, fragmentShader);
context.linkProgram(program);

// プログラムのリンクが成功したか確認する。
var isProgramLinkSuccessful = context.getProgramParameter(program, LINK_STATUS);
if(!isProgramLinkSuccessful) {
  var message = context.getProgramInfoLog(program);
  throw new Exception("Failed to link program: $message");
}

シェーダプログラムのリンクがうまくいったら、このプログラムを使用することを指定しよう。

// 問題なければこのプログラムを使用する。
context.useProgram(program);

これでシェーダの準備は終わりだ。

ところでWebGLのAPIはかなり奇妙に見えるのではないだろうか。これはC言語由来のAPIであるためだ。 何もかもをコンテキストオブジェクトのメソッドで操作するので、慣れるまでは読み書きに苦労するはずだ。この記事では素のWebGLAPIを直接利用するが、実際にWebGLを利用したアプリケーションを作成するときには、オブジェクト主体で操作できるようなラッパを自作するか、有志の作成したライブラリを使用するのがいいだろう。

バッファの準備

シェーダの準備が済んだので、あとは必要なデータをGPUに流し込んで描画するだけだ。データを転送するにはバッファという仕組みを使用する。今回バーテックスシェーダで使用するのは頂点座標と色情報なので2つのバッファを用意する。

Buffer vertexBuffer = context.createBuffer();
Buffer colorBuffer  = context.createBuffer();

バッファが用意できたら、次は各バッファをバーテックスシェーダのattribute変数に結びつける。結びつけるにはシェーダプログラムからattributeの位置を取得する必要がある。

バッファの操作は、操作対象のバッファをバインドしてから行う。バッファの形式にはARRAY_BUFFERELEMENT_ARRAY_BUFFERがあるが、通常は前者を使う。

あとはattribute変数を有効化して、バインドしたバッファとattribute変数を結びつける。これでバッファを経由してシェーダにデータを送る準備ができた。

const VERTEX_SIZE = 3; // vec3
const COLOR_SIZE  = 4; // vec4

// attribute変数の位置を取得する。
var vertexAttribLocation = context.getAttribLocation(program, "vertexPosition");
var colorAttribLocation  = context.getAttribLocation(program, "color");

// バッファを操作する前には必ずバインドする必要がある。
// ARRAY_BUFFER形式としてバインドしている。
context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
// attribute変数の有効化
context.enableVertexAttribArray(vertexAttribLocation); 
// 現在バインドしているバッファと変数を結びつける。
// サイズはvec3を指定してるので3。型はFLOATを使用する。
// うしろ3つの引数は今は気にしなくてもいい
context.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, FLOAT, false, 0, 0);
  
context.bindBuffer(ARRAY_BUFFER, colorBuffer);
context.enableVertexAttribArray(colorAttribLocation);
context.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, FLOAT, false, 0, 0);

vertexAttribPointerには引数が6つ存在するが、今のところは最後の3つについては考えなくても良い。第2引数で指定するサイズは、バーテックスシェーダで指定した変数のサイズと食い違わないように注意しよう。

 データの描画

バッファに書き込む準備ができたので、次は描画データを用意しよう。3Dグラフィックスでは三角形が基本単位となるので、それを組み合わせて描画する。データはTypedDataとして用意する。

WebGLでは慣習として右手座標系を使用することが多い。ここでもそれに従うことにする。よってZ軸はプラス方向が手前になる。また、デフォルトの設定では、頂点を反時計回りに配置すると表向きとなる。

right-handed-coord

右手座標系

試しに四角形を描画してみよう。四角形を描画するには三角形が2つ必要なので、6頂点分のデータを用意すれば良い。Dartにfloatは無いのでdoubleで代用することになる。

// 頂点情報。vec3で宣言しているので、xyzxyzxyz…と並べていく。
var vertices = new Float32List.fromList(<double>[-0.5, 0.5,  0.0, 
                                                 -0.5, -0.5, 0.0,
                                                 0.5,  0.5,  0.0,
                                                 -0.5, -0.5, 0.0,
                                                 0.5,  -0.5, 0.0,
                                                 0.5,  0.5,  0.0]);
// 色情報。vec4で宣言してるのでrgbargbargba…と並べていく。
// すべて[0.0, 1.0]で指定する。attribute変数なので頂点と同じ数だけ必要。
var colors = new Float32List.fromList(<double>[1.0, 0.0, 0.0, 1.0,
                                               0.0, 1.0, 0.0, 1.0,
                                               0.0, 0.0, 1.0, 1.0,
                                               0.0, 1.0, 0.0, 1.0,
                                               0.0, 0.0, 0.0, 1.0,
                                               0.0, 0.0, 1.0, 1.0]);

データの準備ができたらバッファへ転送していこう。

// バインドしてデータを転送する。
context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);
  
context.bindBuffer(ARRAY_BUFFER, colorBuffer);
context.bufferDataTyped(ARRAY_BUFFER, colors, STATIC_DRAW);

bufferDataTypedの第三引数ではバッファの使用方法を指定する。指定できる値は{STATIC, DYNAMIC, STREAM}_{DRAW, READ, COPY} の9つ。ただの指標でしかないのでどれを指定してもかまわないが、WebGLが最適化する際のヒントになるので、できるだけ実際の用途に近いものを選ぼう。

データの転送が終わったら、描画命令を呼び出す。

// 四角形を描画。
const VERTEX_NUMS = 6;
context.drawArrays(TRIANGLES, 0, VERTEX_NUMS);

// WebGLに描画を促す。
context.flush();

描画の際には、頂点をどのような図形で描画するかを、プリミティブと呼ばれる基本図形の中から選ぶ。プリミティブにはPOINTS, LINES, LINE_LOOP, LINE_STRIP, TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FANが存在する。ここでは三角形を描画したいのでTRIANGLESを選択している。また、三角形を2つ描画するので頂点数は6になる。

flush()メソッドはWebGLに描画を強制させる命令だ。WebGLはある程度の描画命令をため込んでから一度に実行することがあるので、必要な描画命令を全て発行した後は、明示的に描画の開始を命令しておいた方がいい。

ここまでが描画までの一連の流れとなる。あとは実行すればブラウザに四角形が表示されるはずだ。

実行結果

実行結果

コード全体

import 'dart:html';
import 'dart:web_gl';
import 'dart:typed_data';

void main() {
  // 今回使うcanvas要素。
  var display = new CanvasElement(width: 500, height: 500);
  document.body.append(display);

  // WebGL用のコンテキストを取得する。
  RenderingContext context = display.getContext3d();

  // ビューポートの設定。
  // 普通はcanvasと同じサイズに設定する。
  context.viewport(0, 0, display.width, display.height);
  
  // バーテックスシェーダ。
  // attributeは頂点ごとの変数。
  // varyingはフラグメントシェーダへ渡す値。
  // gl_Positionに頂点座標の演算結果を代入する。
  // ここでは受け取った座標をそのまま代入しているだけ。
  // 座標はxyzwの4次元になるが、今のところはw=1.0と固定して考えても良い。
  const VERTEX_SHADER_SOURCE = """
  attribute vec3 vertexPosition;
  attribute vec4 color;
  
  varying vec4 vColor;
  
  void main() {
    vColor = color;
    gl_Position = vec4(vertexPosition, 1.0);
  }
  """;
  
  // フラグメントシェーダ。
  // 初めにfloatの精度の指定が必要になる。
  // lowp・mediump・highpなどがある。
  // gl_FragColorに値を代入することでピクセルに色が書き込まれる。
  // ここではバーテックスシェーダから受け取った色をそのまま書き込んでいる。
  const FRAGMENT_SHADER_SOURCE = """
  precision mediump float;
  varying vec4 vColor;
  
  void main() {
    gl_FragColor = vColor;
  }
  """;
  
  // シェーダ作成→ソースを渡す→コンパイルの流れ。
  Shader vertexShader = context.createShader(VERTEX_SHADER);
  context.shaderSource(vertexShader, VERTEX_SHADER_SOURCE);
  context.compileShader(vertexShader);
  
  // コンパイルが成功しているか確認しておこう。
  var isVsCompileSuccessful = context.getShaderParameter(vertexShader, COMPILE_STATUS);
  if(!isVsCompileSuccessful) {
    var message = context.getShaderInfoLog(vertexShader);
    throw new Exception("Failed to compile vertex shader: $message");
  }
  
  // フラグメントシェーダも同様に。
  Shader fragmentShader = context.createShader(FRAGMENT_SHADER);
  context.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE);
  context.compileShader(fragmentShader);
  
  var isFsCompileSuccessful = context.getShaderParameter(fragmentShader, COMPILE_STATUS);
  if(!isFsCompileSuccessful) {
    var message = context.getShaderInfoLog(fragmentShader);
    throw new Exception("Failed to compile fragment shader: $message");
  }
  
  // シェーダの準備が済んだらプログラムを作成する。
  Program program = context.createProgram();
  context.attachShader(program, vertexShader);
  context.attachShader(program, fragmentShader);
  context.linkProgram(program);
  
  // プログラムのリンクが成功したか確認する。
  var isProgramLinkSuccessful = context.getProgramParameter(program, LINK_STATUS);
  if(!isProgramLinkSuccessful) {
    var message = context.getProgramInfoLog(program);
    throw new Exception("Failed to link program: $message");
  }
  
  // 問題なければこのプログラムを使用する。
  context.useProgram(program);
  
  // データを転送するためのバッファを作成する。
  Buffer vertexBuffer = context.createBuffer();
  Buffer colorBuffer = context.createBuffer();
  
  const VERTEX_SIZE = 3; // vec3
  const COLOR_SIZE  = 4; // vec4

  // attribute変数の位置を取得する。
  var vertexAttribLocation = context.getAttribLocation(program, "vertexPosition");
  var colorAttribLocation  = context.getAttribLocation(program, "color");

  // バッファを操作する前には必ずバインドする必要がある。
  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  // attribute変数の有効化
  context.enableVertexAttribArray(vertexAttribLocation); 
  // 現在バインドしているバッファと変数を結びつける。
  // サイズはvec3を指定してるので3。型はFLOATを使用する。
  // うしろ3つの引数は今は気にしなくてもいい
  context.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, FLOAT, false, 0, 0);
    
  context.bindBuffer(ARRAY_BUFFER, colorBuffer);
  context.enableVertexAttribArray(colorAttribLocation);
  context.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, FLOAT, false, 0, 0);
  
  // 頂点情報。vec3で宣言しているので、xyzxyzxyz…と並べていく。
  var vertices = new Float32List.fromList(<double>[-0.5, 0.5,  0.0, 
                                                   -0.5, -0.5, 0.0,
                                                   0.5,  0.5,  0.0,
                                                   -0.5, -0.5, 0.0,
                                                   0.5,  -0.5, 0.0,
                                                   0.5,  0.5,  0.0]);
  
  // 色情報。vec4で宣言してるのでrgbargbargba…と並べていく。
  // すべて[0.0, 1.0]で指定する。attribute変数なので頂点と同じ数だけ必要。
  var colors = new Float32List.fromList(<double>[1.0, 0.0, 0.0, 1.0,
                                                 0.0, 1.0, 0.0, 1.0,
                                                 0.0, 0.0, 1.0, 1.0,
                                                 0.0, 1.0, 0.0, 1.0,
                                                 0.0, 0.0, 0.0, 1.0,
                                                 0.0, 0.0, 1.0, 1.0]);
  
  // バインドしてデータを転送する。
  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);
  
  context.bindBuffer(ARRAY_BUFFER, colorBuffer);
  context.bufferDataTyped(ARRAY_BUFFER, colors, STATIC_DRAW);
  
  // 四角形を描画。
  const VERTEX_NUMS = 6;
  context.drawArrays(TRIANGLES, 0, VERTEX_NUMS);

  // WebGLに描画を促す。
  context.flush();
}

最適化

無事に四角形を描画することができたが、このプログラムには無駄が多い。そこで、いくつかの最適化を行いたい。最適化と言ってもまだデータが小さいため効果を実感することはできないが、早めのうちに学んでおくべきだろう。

インターリーブドバッファ

attribute変数へデータを転送する際に2つのバッファを作成したが、変数ごとにバッファを作成すると、ひとつの頂点についての情報がバラバラに散らばってしまう。こういった状況は、メモリアクセスの関係でパフォーマンスに悪影響があると言われている。

そこで、インターリーブという手法を用いて複数のデータをひとつのバッファに纏める方法を紹介する。 通常のバッファでは一種類だけのデータを並べていたが、インターリーブされたバッファでは複数のデータを頂点ごとにまとめて配置する。

非インターリーブバッファ

非インターリーブバッファ

インターリーブバッファ

インターリーブバッファ

このときstrideoffsetという値が重要になる。strideは頂点ひとつあたりのデータサイズを表している。今回の場合であれば、vec3とvec4をあわせたサイズがstrideになる。offsetは先頭からのサイズを表している。Positionのoffsetは0で、Colorのoffsetはvec3ひとつ分になる。データを並べる順番に決まりはないので、自分がやりやすいようにすると良い。

以上のことを踏まえて、プログラムへ反映していこう。 まずはバッファを2つ作成していたところを、ひとつだけ作成するように変更しよう。

// データを転送するためのバッファを作成する。
Buffer vertexBuffer = context.createBuffer();
Buffer colorBuffer = context.createBuffer();

ここから片方を削除する。

// データを転送するためのバッファを作成する。
Buffer vertexBuffer = context.createBuffer();

次に変更が必要なのはバッファとattribute変数を結びつける箇所になる。

// バッファを操作する前には必ずバインドする必要がある。
context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
// attribute変数の有効化
context.enableVertexAttribArray(vertexAttribLocation); 
// 現在バインドしているバッファと変数を結びつける。
// サイズはvec3を指定してるので3。型はFLOATを使用する。
// うしろ3つの引数は今は気にしなくてもいい
context.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, FLOAT, false, 0, 0);
    
context.bindBuffer(ARRAY_BUFFER, colorBuffer);
context.enableVertexAttribArray(colorAttribLocation);
context.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, FLOAT, false, 0, 0);

これを次のように変更する。変更前はそれぞれ違うバッファを用いていたが、変更後はインターリーブされたひとつのバッファへ両方のattribute変数を結びつけている。

// strideはvec3+vec4で7。バイト数で指定する。
const STRIDE       = Float32List.BYTES_PER_ELEMENT * 7;
// poisionがvec3なのでcolorのoffsetは3。
const COLOR_OFFSET = Float32List.BYTES_PER_ELEMENT * 3;
  
// バッファをバインドする。
context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
// attribute変数の有効化
context.enableVertexAttribArray(vertexAttribLocation);
context.enableVertexAttribArray(colorAttribLocation);
// attribute変数と結びつける。
// 第5引数にstride、第6引数にoffsetを指定する。
context.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, FLOAT, false, STRIDE, 0);  
context.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, FLOAT, false, STRIDE, COLOR_OFFSET);

strideとoffsetの値はバイト数で指定することに注意しよう。 次に頂点データを変更する。頂点座標と色で別の配列を用意していたが、これをひとつにまとめる。

// 頂点情報。vec3で宣言しているので、xyzxyzxyz…と並べていく。
var vertices = new Float32List.fromList(<double>[-0.5, 0.5,  0.0, 
                                                 -0.5, -0.5, 0.0,
                                                 0.5,  0.5,  0.0,
                                                 -0.5, -0.5, 0.0,
                                                 0.5,  -0.5, 0.0,
                                                 0.5,  0.5,  0.0]);

// 色情報。vec4で宣言してるのでrgbargbargba…と並べていく。
// すべて[0.0, 1.0]で指定する。attribute変数なので頂点と同じ数だけ必要。
var colors = new Float32List.fromList(<double>[1.0, 0.0, 0.0, 1.0,
                                               0.0, 1.0, 0.0, 1.0,
                                               0.0, 0.0, 1.0, 1.0,
                                               0.0, 1.0, 0.0, 1.0,
                                               0.0, 0.0, 0.0, 1.0,
                                               0.0, 0.0, 1.0, 1.0]);

順番や要素数を間違えないようにまとめていく。

// 頂点情報。xyz, rgba, xyz, rgba…のインターリーブ。
var vertices = new Float32List.fromList(<double>[-0.5, 0.5,  0.0,      // xyz
                                                 1.0,  0.0,  0.0, 1.0, // rgba
                                                 -0.5, -0.5, 0.0,
                                                 0.0,  1.0,  0.0, 1.0,
                                                 0.5,  0.5,  0.0,
                                                 0.0,  0.0,  1.0, 1.0,
                                                 -0.5, -0.5, 0.0,
                                                 0.0,  1.0,  0.0, 1.0,
                                                 0.5,  -0.5, 0.0,
                                                 0.0,  0.0,  0.0, 1.0,
                                                 0.5,  0.5,  0.0,
                                                 0.0,  0.0,  1.0, 1.0]);

最後に不要になった部分を削って完成だ。

// バインドしてデータを転送する。
context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);

context.bindBuffer(ARRAY_BUFFER, colorBuffer);
context.bufferDataTyped(ARRAY_BUFFER, colors, STATIC_DRAW);
// バインドしてデータを転送する。
context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);

変更が終わったらプログラムを動かして同じ結果が得られるか確認してみよう。

以下は全ての変更を適用したコード。

import 'dart:html';
import 'dart:web_gl';
import 'dart:typed_data';

void main() {
  // 今回使うcanvas要素。
  var display = new CanvasElement(width: 500, height: 500);
  document.body.append(display);

  // WebGL用のコンテキストを取得する。
  RenderingContext context = display.getContext3d();

  // ビューポートの設定。
  // 普通はcanvasと同じサイズに設定する。
  context.viewport(0, 0, display.width, display.height);
  
  // バーテックスシェーダ。
  // attributeは頂点ごとの変数。
  // varyingはフラグメントシェーダへ渡す値。
  // gl_Positionに頂点座標の演算結果を代入する。
  // ここでは受け取った座標をそのまま代入しているだけ。
  // 座標はxyzwの4次元になるが、今のところはw=1.0と固定して考えても良い。
  const VERTEX_SHADER_SOURCE = """
  attribute vec3 vertexPosition;
  attribute vec4 color;
  
  varying vec4 vColor;
  
  void main() {
    vColor = color;
    gl_Position = vec4(vertexPosition, 1.0);
  }
  """;
  
  // フラグメントシェーダ。
  // 初めにfloatの精度の指定が必要になる。
  // lowp・mediump・highpなどがある。
  // gl_FragColorに値を代入することでピクセルに色が書き込まれる。
  // ここではバーテックスシェーダから受け取った色をそのまま書き込んでいる。
  const FRAGMENT_SHADER_SOURCE = """
  precision mediump float;
  varying vec4 vColor;
  
  void main() {
    gl_FragColor = vColor;
  }
  """;
  
  // シェーダ作成→ソースを渡す→コンパイルの流れ。
  Shader vertexShader = context.createShader(VERTEX_SHADER);
  context.shaderSource(vertexShader, VERTEX_SHADER_SOURCE);
  context.compileShader(vertexShader);
  
  // コンパイルが成功しているか確認しておこう。
  var isVsCompileSuccessful = context.getShaderParameter(vertexShader, COMPILE_STATUS);
  if(!isVsCompileSuccessful) {
    var message = context.getShaderInfoLog(vertexShader);
    throw new Exception("Failed to compile vertex shader: $message");
  }
  
  // フラグメントシェーダも同様に。
  Shader fragmentShader = context.createShader(FRAGMENT_SHADER);
  context.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE);
  context.compileShader(fragmentShader);
  
  var isFsCompileSuccessful = context.getShaderParameter(fragmentShader, COMPILE_STATUS);
  if(!isFsCompileSuccessful) {
    var message = context.getShaderInfoLog(fragmentShader);
    throw new Exception("Failed to compile fragment shader: $message");
  }
  
  // シェーダの準備が済んだらプログラムを作成する。
  Program program = context.createProgram();
  context.attachShader(program, vertexShader);
  context.attachShader(program, fragmentShader);
  context.linkProgram(program);
  
  // プログラムのリンクが成功したか確認する。
  var isProgramLinkSuccessful = context.getProgramParameter(program, LINK_STATUS);
  if(!isProgramLinkSuccessful) {
    var message = context.getProgramInfoLog(program);
    throw new Exception("Failed to link program: $message");
  }
  
  // 問題なければこのプログラムを使用する。
  context.useProgram(program);
  
  // データを転送するためのバッファを作成する。
  Buffer vertexBuffer = context.createBuffer();
  
  const VERTEX_SIZE = 3; // vec3
  const COLOR_SIZE  = 4; // vec4

  // attribute変数の位置を取得する。
  var vertexAttribLocation = context.getAttribLocation(program, "vertexPosition");
  var colorAttribLocation  = context.getAttribLocation(program, "color");
  
  // strideはvec3+vec4で7。バイト数で指定する。
  const STRIDE       = Float32List.BYTES_PER_ELEMENT * 7;
  // poisionがvec3なのでcolorのoffsetは3。
  const COLOR_OFFSET = Float32List.BYTES_PER_ELEMENT * 3;
  
  // バッファをバインドする。
  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  // attribute変数の有効化
  context.enableVertexAttribArray(vertexAttribLocation);
  context.enableVertexAttribArray(colorAttribLocation);
  // attribute変数と結びつける。
  // 第5引数にstride、第6引数にoffsetを指定する。
  context.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, FLOAT, false, STRIDE, 0);  
  context.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, FLOAT, false, STRIDE, COLOR_OFFSET);
  
  // 頂点情報。xyz, rgba, xyz, rgba…のインターリーブ。
  var vertices = new Float32List.fromList(<double>[-0.5, 0.5,  0.0,      // xyz
                                                   1.0,  0.0,  0.0, 1.0, // rgba
                                                   -0.5, -0.5, 0.0,
                                                   0.0,  1.0,  0.0, 1.0,
                                                   0.5,  0.5,  0.0,
                                                   0.0,  0.0,  1.0, 1.0,
                                                   -0.5, -0.5, 0.0,
                                                   0.0,  1.0,  0.0, 1.0,
                                                   0.5,  -0.5, 0.0,
                                                   0.0,  0.0,  0.0, 1.0,
                                                   0.5,  0.5,  0.0,
                                                   0.0,  0.0,  1.0, 1.0]);
  
  // バインドしてデータを転送する。
  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);
  
  // 四角形を描画。
  const VERTEX_NUMS = 6;
  context.drawArrays(TRIANGLES, 0, VERTEX_NUMS);

  // WebGLに描画を促す。
  context.flush();
}

インデックスバッファ

四角形を描画するには6頂点が必要だった。しかし本来ならば4頂点あれば十分なはずなので、2頂点分の無駄が発生していることになる。これを解決するのがインデックスバッファと呼ばれるバッファだ。

インデックスバッファは各頂点へ番号を振り、描画する頂点を座標の値などではなく番号で指定する。インデックスバッファを使えば、4頂点分のデータだけ用意しておいて、実際の描画には0-1-2, 1-3-2などと番号で指定した頂点データを使用することができる。 indexed_quad   これをプログラムに反映してみよう。まずはインデックスバッファに使用するバッファを作成する。

// データを転送するためのバッファを作成する。
Buffer vertexBuffer = context.createBuffer();

ここでバッファをひとつ追加する。

// データを転送するためのバッファを作成する。
Buffer vertexBuffer = context.createBuffer();
Buffer indexBuffer  = context.createBuffer();

次に6頂点分あった頂点データを4頂点分に減らそう。

// 頂点情報。xyz, rgba, xyz, rgba…のインターリーブ。
var vertices = new Float32List.fromList(<double>[-0.5, 0.5,  0.0,      // xyz
                                                 1.0,  0.0,  0.0, 1.0, // rgba
                                                 -0.5, -0.5, 0.0,
                                                 0.0,  1.0,  0.0, 1.0,
                                                 0.5,  0.5,  0.0,
                                                 0.0,  0.0,  1.0, 1.0,
                                                 -0.5, -0.5, 0.0,
                                                 0.0,  1.0,  0.0, 1.0,
                                                 0.5,  -0.5, 0.0,
                                                 0.0,  0.0,  0.0, 1.0,
                                                 0.5,  0.5,  0.0,
                                                 0.0,  0.0,  1.0, 1.0]);

4番目、6番目はそれぞれ2番目、3番目と同じなので削除できる。

// 頂点情報。xyz, rgba, xyz, rgba…のインターリーブ。
var vertices = new Float32List.fromList(<double>[-0.5, 0.5,  0.0,      // xyz
                                                 1.0,  0.0,  0.0, 1.0, // rgba
                                                 -0.5, -0.5, 0.0,
                                                 0.0,  1.0,  0.0, 1.0,
                                                 0.5,  0.5,  0.0,
                                                 0.0,  0.0,  1.0, 1.0,
                                                 0.5,  -0.5, 0.0,
                                                 0.0,  0.0,  0.0, 1.0,]);

次にインデックスを作成しよう。元は0-1-2, 3-4-5の三角形を描画していたが、0-1-2, 1-3-2に置き換えることができる。

// 描画に使うインデックス。int型を使う。
var indices = new Uint16List.fromList(<int>[0, 1, 2,
                                            1, 3, 2]);

インデックスはuint型で指定することに注意したい。Dartにはuint型はないので、int型となる。

インデックスが作成できたらバッファへ転送する。

// バインドしてデータを転送する。
context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);

ここへインデックスバッファへデータを転送する処理を加えよう。

// バインドしてデータを転送する。
context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);
  
context.bindBuffer(ELEMENT_ARRAY_BUFFER, indexBuffer);
context.bufferDataTyped(ELEMENT_ARRAY_BUFFER, indices, STATIC_DRAW);

インデックスバッファにはARRAY_BUFFERではなくELEMENT_ARRAY_BUFFERを使用する。他は普通のバッファと同じだ。

最後に、インデックスバッファを使用するときは描画命令に手を加える必要がある。

// 四角形を描画。
const VERTEX_NUMS = 6;
context.drawArrays(TRIANGLES, 0, VERTEX_NUMS);

ここでdrawArraysではなくdrawElementsに変更する。

// 四角形を描画。
// unsigned short = uint16
var vertexNum = indices.length;
context.drawElements(TRIANGLES, vertexNum, UNSIGNED_SHORT, 0);

大きくは変わらないが、引数の順番が異なることや、インデックスバッファの形式を指定する必要があることに注意。ここではunsigned shortを使用している。また、インデックスはリストの長さがそのまま頂点数と一致するので、頂点数を指定するときにもlengthを取得して使うといいだろう。

ここまでの変更ができたらもう一度実行してみよう。しっかり四角形が表示されるはずだ。

以下は全ての変更を適用したコード。

import 'dart:html';
import 'dart:web_gl';
import 'dart:typed_data';

void main() {
  // 今回使うcanvas要素。
  var display = new CanvasElement(width: 500, height: 500);
  document.body.append(display);

  // WebGL用のコンテキストを取得する。
  RenderingContext context = display.getContext3d();

  // ビューポートの設定。
  // 普通はcanvasと同じサイズに設定する。
  context.viewport(0, 0, display.width, display.height);
  
  // バーテックスシェーダ。
  // attributeは頂点ごとの変数。
  // varyingはフラグメントシェーダへ渡す値。
  // gl_Positionに頂点座標の演算結果を代入する。
  // ここでは受け取った座標をそのまま代入しているだけ。
  // 座標はxyzwの4次元になるが、今のところはw=1.0と固定して考えても良い。
  const VERTEX_SHADER_SOURCE = """
  attribute vec3 vertexPosition;
  attribute vec4 color;
  
  varying vec4 vColor;
  
  void main() {
    vColor = color;
    gl_Position = vec4(vertexPosition, 1.0);
  }
  """;
  
  // フラグメントシェーダ。
  // 初めにfloatの精度の指定が必要になる。
  // lowp・mediump・highpなどがある。
  // gl_FragColorに値を代入することでピクセルに色が書き込まれる。
  // ここではバーテックスシェーダから受け取った色をそのまま書き込んでいる。
  const FRAGMENT_SHADER_SOURCE = """
  precision mediump float;
  varying vec4 vColor;
  
  void main() {
    gl_FragColor = vColor;
  }
  """;
  
  // シェーダ作成→ソースを渡す→コンパイルの流れ。
  Shader vertexShader = context.createShader(VERTEX_SHADER);
  context.shaderSource(vertexShader, VERTEX_SHADER_SOURCE);
  context.compileShader(vertexShader);
  
  // コンパイルが成功しているか確認しておこう。
  var isVsCompileSuccessful = context.getShaderParameter(vertexShader, COMPILE_STATUS);
  if(!isVsCompileSuccessful) {
    var message = context.getShaderInfoLog(vertexShader);
    throw new Exception("Failed to compile vertex shader: $message");
  }
  
  // フラグメントシェーダも同様に。
  Shader fragmentShader = context.createShader(FRAGMENT_SHADER);
  context.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE);
  context.compileShader(fragmentShader);
  
  var isFsCompileSuccessful = context.getShaderParameter(fragmentShader, COMPILE_STATUS);
  if(!isFsCompileSuccessful) {
    var message = context.getShaderInfoLog(fragmentShader);
    throw new Exception("Failed to compile fragment shader: $message");
  }
  
  // シェーダの準備が済んだらプログラムを作成する。
  Program program = context.createProgram();
  context.attachShader(program, vertexShader);
  context.attachShader(program, fragmentShader);
  context.linkProgram(program);
  
  // プログラムのリンクが成功したか確認する。
  var isProgramLinkSuccessful = context.getProgramParameter(program, LINK_STATUS);
  if(!isProgramLinkSuccessful) {
    var message = context.getProgramInfoLog(program);
    throw new Exception("Failed to link program: $message");
  }
  
  // 問題なければこのプログラムを使用する。
  context.useProgram(program);
  
  // データを転送するためのバッファを作成する。
  Buffer vertexBuffer = context.createBuffer();
  Buffer indexBuffer  = context.createBuffer();
  
  const VERTEX_SIZE = 3; // vec3
  const COLOR_SIZE  = 4; // vec4

  // attribute変数の位置を取得する。
  var vertexAttribLocation = context.getAttribLocation(program, "vertexPosition");
  var colorAttribLocation  = context.getAttribLocation(program, "color");
  
  // strideはvec3+vec4で7。バイト数で指定する。
  const STRIDE       = Float32List.BYTES_PER_ELEMENT * 7;
  // poisionがvec3なのでcolorのoffsetは3。
  const COLOR_OFFSET = Float32List.BYTES_PER_ELEMENT * 3;
  
  // バッファをバインドする。
  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  // attribute変数の有効化
  context.enableVertexAttribArray(vertexAttribLocation);
  context.enableVertexAttribArray(colorAttribLocation);
  // attribute変数と結びつける。
  // 第5引数にstride、第6引数にoffsetを指定する。
  context.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, FLOAT, false, STRIDE, 0);  
  context.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, FLOAT, false, STRIDE, COLOR_OFFSET);
  
  // 頂点情報。xyz, rgba, xyz, rgba…のインターリーブ。
  var vertices = new Float32List.fromList(<double>[-0.5, 0.5,  0.0,      // xyz
                                                   1.0,  0.0,  0.0, 1.0, // rgba
                                                   -0.5, -0.5, 0.0,
                                                   0.0,  1.0,  0.0, 1.0,
                                                   0.5,  0.5,  0.0,
                                                   0.0,  0.0,  1.0, 1.0,
                                                   0.5,  -0.5, 0.0,
                                                   0.0,  0.0,  0.0, 1.0,]);
  
  // 描画に使うインデックス。int型を使う。
  var indices = new Uint16List.fromList(<int>[0, 1, 2,
                                              1, 3, 2]);
  
  // バインドしてデータを転送する。
  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);
  
  context.bindBuffer(ELEMENT_ARRAY_BUFFER, indexBuffer);
  context.bufferDataTyped(ELEMENT_ARRAY_BUFFER, indices, STATIC_DRAW);
  
  // 四角形を描画。
  // unsigned short = uint16
  var vertexNum = indices.length;
  context.drawElements(TRIANGLES, vertexNum, UNSIGNED_SHORT, 0);

  // WebGLに描画を促す。
  context.flush();
}

DartでWebGL入門-3D編へ続く

続き:DartでWebGL入門-3D編