no-image

DartでWebGL入門-テクスチャ編

単なるポリゴンだけでなく、テクスチャも描画してみよう。

DartでWebGL入門

テクスチャの仕様

WebGLで使えるテクスチャにはサイズ制限がある。テクスチャ画像のサイズは縦横ともに2のべき乗でなければならない。例えば、64×32や、128×128といったサイズの画像のみが使える。

画像をWebGLで扱えるテクスチャとして読み込むには<img>要素を使用する。なお、<canvas>や<video>もテクスチャとして使用することができる。

テクスチャの読み込み

テクスチャを作成してバインド、データを転送という流れはバッファと変わらない。

Texture texture = context.createTexture(); // テクスチャを作成
context.bindTexture(TEXTURE_2D, texture);  // テクスチャをバインド

<img>要素をテクスチャデータとして転送するにはtexImage2DImageメソッドを使用する。同様に<canvas>の場合はtexImage2DCanvas、<video>の場合はtexImage2DVideoになる。このメソッドに指定する引数は、形式、ミップマップのレベル、内部色フォーマット、色フォーマット、データ形式、ソース、の6つになる。今回は一般的な2Dテクスチャを使用するので、形式にはTEXTURE_2Dを指定する。ミップマップは使用しないのでレベルは0になる。色フォーマットには通常はRGBRGBAを指定すればいいだろう。

context.texImage2DImage(TEXTURE_2D, 0, RGBA, RGBA, UNSIGNED_BYTE, textureSource); // テクスチャデータの転送
context.generateMipmap(TEXTURE_2D); // ミップマップの作成

このとき、ソースとして指定した要素の読み込みが終わっていないとエラーが出る。<img>要素のonLoadイベントをlistenして読み込みが終わってからにしよう。

転送し終わったらgenerateMipmapメソッドを呼び出してミップマップを作成しよう。ミップマップを使用しない場合でも呼び出しておく必要がある。ミップマップというのは、遠くのテクスチャには縮小した画像を使用して、無駄を減らし高速化する仕組みだ。

使用するテクスチャが1枚だけならこれだけでいいが、複数枚になると少し複雑になる。複数枚を扱う場合については後述する。

テクスチャのマッピング

ポリゴンにテクスチャを貼り付けるには、頂点ごとに「テクスチャのどの部分を割り当てるか」を指定する。

テクスチャ座標には少しクセがある。テクスチャ座標は0.0から1.0へと正規化されており、左下を(0.0, 0.0)、右上を(1.0, 1.0)とする座標系になっている。ただしテクスチャ画像は上下逆さまになっている。

texture-coordinate

例えば単純に板ポリゴンにそのままテクスチャを貼り付ける場合は、以下のようになる。

var vertices = new Float32List.fromList(<double>[-1.0, 1.0, 0.0, // 頂点座標
                                                 0.0, 0.0,       // テクスチャ座標
                                                 1.0, 1.0, 0.0,
                                                 1.0, 0.0,
                                                 -1.0, -1.0, 0.0,
                                                 0.0, 1.0,
                                                 1.0, -1.0, 0.0,
                                                 1.0, 1.0]);

シェーダ側の処理

頂点シェーダはテクスチャ座標を受け取り、フラグメントシェーダへと渡すだけでいい。

attribute vec3 vertexPosition;
attribute vec2 texCoord;

varying vec2 textureCoord;

void main() {
  textureCoord = texCoord;
  gl_Position = vec4(vertexPosition, 1.0);
}

テクスチャは主にフラグメントシェーダ側で処理する。まずはsampler2D型のuniform変数としてテクスチャ用の変数を宣言する。次にtexture2D関数にテクスチャと座標を指定し、テクスチャの色を取り出す。取り出した色を好きに処理して、最終的な出力結果をgl_FragColorに代入してやる。

precision mediump float;

uniform sampler2D texture;
varying vec2 textureCoord;

void main() {
  gl_FragColor = texture2D(texture, textureCoord);
}

これでテクスチャを描画することができる。

サンプル

texture-sample

attribute vec3 vertexPosition;
attribute vec2 texCoord;

varying vec2 textureCoord;

void main() {
  textureCoord = texCoord;
  gl_Position = vec4(vertexPosition, 1.0);
}
precision mediump float;

uniform sampler2D texture;
varying vec2 textureCoord;

void main() {
  gl_FragColor = texture2D(texture, textureCoord);
}
import 'dart:html';
import 'dart:async';
import 'dart:web_gl';
import 'dart:typed_data';

void main() {
  var loadVs = HttpRequest.getString("vertex_shader.glsl");
  var loadFs = HttpRequest.getString("fragment_shader.glsl");

  var completer = new Completer<ImageElement>();
  var texture = new ImageElement(src:"texture.png");
  texture.onLoad.listen((e)=>completer.complete(texture));

  Future.wait([loadVs, loadFs, completer.future])
        .then((values) => start(values[0], values[1], values[2]));
}

void start(String vsSource, String fsSource, ImageElement textureSource) {
  const WIDTH  = 512;
  const HEIGHT = 128;
  var canvas = new CanvasElement(width:WIDTH, height:HEIGHT);
  document.body.append(canvas);

  var context = canvas.getContext3d();

  var vs = context.createShader(VERTEX_SHADER);
  context.shaderSource(vs, vsSource);
  context.compileShader(vs);

  var fs = context.createShader(FRAGMENT_SHADER);
  context.shaderSource(fs, fsSource);
  context.compileShader(fs);

  var program = context.createProgram();
  context.attachShader(program, vs);
  context.attachShader(program, fs);
  context.linkProgram(program);

  context.useProgram(program);

  Texture texture = context.createTexture(); // テクスチャを作成
  context.bindTexture(TEXTURE_2D, texture);  // テクスチャをバインド
  context.texImage2DImage(TEXTURE_2D, 0, RGBA, RGBA, UNSIGNED_BYTE, textureSource); // テクスチャデータの転送
  context.generateMipmap(TEXTURE_2D); // ミップマップの作成

  var vertexBuffer = context.createBuffer();
  var indexBuffer  = context.createBuffer();

  var vertexPositionLocation = context.getAttribLocation(program, "vertexPosition");
  var texCoordLocation       = context.getAttribLocation(program, "texCoord");

  const VERTEX_SIZE    = 3;
  const TEXTURE_SIZE   = 2;
  const STRIDE         = (VERTEX_SIZE + TEXTURE_SIZE) * Float32List.BYTES_PER_ELEMENT;
  const VERTEX_OFFSET  = 0;
  const TEXTURE_OFFSET = 3 * Float32List.BYTES_PER_ELEMENT;

  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);

  context.enableVertexAttribArray(vertexPositionLocation);
  context.enableVertexAttribArray(texCoordLocation);

  context.vertexAttribPointer(vertexPositionLocation, VERTEX_SIZE, FLOAT, false, STRIDE, VERTEX_OFFSET);
  context.vertexAttribPointer(texCoordLocation, TEXTURE_SIZE, FLOAT, false, STRIDE, TEXTURE_OFFSET);

  var vertices = new Float32List.fromList(<double>[-1.0, 1.0, 0.0, // 頂点座標
                                                   0.0, 0.0,       // テクスチャ座標
                                                   1.0, 1.0, 0.0,
                                                   1.0, 0.0,
                                                   -1.0, -1.0, 0.0,
                                                   0.0, 1.0,
                                                   1.0, -1.0, 0.0,
                                                   1.0, 1.0]);

  var indices = new Uint16List.fromList(<int>[0, 1, 2, 2, 3, 1]);

  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);

  var vertexNum = indices.length;
  context.drawElements(TRIANGLES, vertexNum, UNSIGNED_SHORT, 0);
}

テクスチャを複数枚使う場合

さて、これが少しややこしい。テクスチャにはテクスチャユニットというものがあり、0から31までの32個を使用することができる。といっても常に32枚使用できるというわけではなく、最大枚数は実行環境に依存する。使用できる枚数はgetParameterメソッドで取得することができる。

var maxTextures = context.getParameter(MAX_COMBINED_TEXTURE_IMAGE_UNITS);

環境にもよるが、せいぜい16ぐらいではないだろうか。 少し古い環境だと8枚だということもあるので、16枚使えると決めつけていると痛い目を見るかもしれない。

テクスチャユニットを切り替えはactiveTextureメソッドでできる。バッファをバインドするのと同じ感覚だ。テクスチャユニットを指定するためにはTEXTURE0、TEXTURE1、…、TEXTURE31という定数を使用する。

context.activeTexture(TEXTURE0);

シェーダ側へどのテクスチャユニットを使用するのかを伝えるには、sampler2D型の変数にテクスチャユニット番号を転送してやればよい。番号の転送にはuniform1iを使用する。

var textureUnitID = 0;
var textureLocation = context.getUniformLocation(program, "texture");
context.uniform1i(textureLocation, textureUnitID);

あとはこれを適宜切り替えつつ処理を実行していこう。もちろんsampler2D型の変数を複数用意すれば複数枚同時に使うこともできる。