no-image

WebGL2入門 テクスチャ編

今度はポリゴンだけではなく、テクスチャも描画してみましょう。

WebGL2入門 記事一覧

テクスチャとして使える画像

WebGL2でテクスチャとして使えるのは、img要素、canvas要素、video要素、ImageBitmap、ImageData、ArrayBufferViewです。今回はこのうちimg要素を使った方法を紹介します。

テクスチャの読み込み

通常のバッファのように、テクスチャを作成してバインド、そして転送という流れはテクスチャでも変わりません。使うメソッドが少し異なるだけです。

    const texture = gl.createTexture();     // テクスチャの作成
    gl.bindTexture(gl.TEXTURE_2D, texture); // テクスチャのバインド

createTextureメソッドでテクスチャを作成し、bindTextureメソッドでバインドします。

次にテクスチャデータを転送します。

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

テクスチャデータの転送にはtexImage2Dメソッドを使用します。このメソッドに指定する引数は、形式、ミップマップのレベル、内部色フォーマット、色フォーマット、データ形式、テクスチャ要素、の6つになります。形式はTEXTURE_2Dを指定します。ミップマップ(後述)のレベルは今のところ0でかまいません。色フォーマットにはRGBAを、データ形式にはUNSIGNED_BYTEを指定します。

テクスチャ要素にはimg要素かcanvas要素、またはvideo要素、あるいはImageBitmap、ImageData、ArrayBufferViewを指定します。テクスチャ要素は読み込み終わっている必要があります(loadイベントなどを使用して読み込みを待ちましょう)。

転送し終わったらミップマップを生成します。ミップマップを使用しない場合でもする必要があります。ミップマップというのは遠くのテクスチャには低解像度のものを用いて、近くのものには高解像度のものを用いる技術です。今回はミップマップは使用しません。

テクスチャの割り当て

テクスチャはポリゴンに貼り付ける必要があります。そしてそのポリゴンの、どこに、どのように貼り付けるのかを指定しなければなりません。

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

texture-coordinate

このことを考慮しつつ「どの頂点に」「テクスチャのどの部分を割り当てるか」ということを指定しなければいけません。

たとえば正方形にテクスチャを貼り付けるには以下のようにします。

    const vertices = new Float32Array([
        -1.0, 1.0, 0.0, // 頂点座標
        0.0, 0.0,       // テクスチャ座標
        -1.0, -1.0, 0.0,
        0.0, 1.0,
        1.0, 1.0, 0.0,
        1.0, 0.0,
        1.0, -1.0, 0.0,
        1.0, 1.0
    ]);

シェーダ側の処理

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

#version 300 es

in vec3 vertexPosition;
in vec2 texCoord;

out vec2 textureCoord;

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

テクスチャの処理は主にフラグメントシェーダ側で行います。

#version 300 es
precision highp float;

uniform sampler2D tex;
in vec2 textureCoord;
out vec4 fragmentColor;

void main() {
  fragmentColor = texture(tex, textureCoord);
}

まずはsampler2D型のuniform変数としてテクスチャ用の変数を宣言します。次にtexture関数にテクスチャと座標を指定し、テクスチャの色を取り出します。取り出した色を好きに処理して、最終的な結果を出力すれば完成です。今回は特に処理はせずに、テクスチャの色をそのまま出力しています。

シェーダ側ですることは、これだけです。

テクスチャを使用したプログラムを組む

ここまでのことをまとめて、テクスチャを描画するだけのプログラムを組んでみましょう。以下のテクスチャを表示するプログラムを書きます。

texture

このテクスチャを表示領域いっぱいに表示するプログラムを書きます。テクスチャのことに専念したいので、3D表示は諦めてシンプルなプログラムにしましょう。これを実際に書くと以下のようになります:

#version 300 es

in vec3 vertexPosition;
in vec2 texCoord;

out vec2 textureCoord;

void main() {
  textureCoord = texCoord;
  gl_Position = vec4(vertexPosition, 1.0);
}
#version 300 es
precision highp float;

uniform sampler2D tex;
in vec2 textureCoord;
out vec4 fragmentColor;

void main() {
  fragmentColor = texture(tex, textureCoord);
}
// Canvasを作成してbodyに追加します。
const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);

const gl = canvas.getContext('webgl2');

// シェーダを読み込みPromiseを返します。
function loadShaders() {
    const loadVertexShader = fetch('vertex_shader.glsl').then((res) => res.text());
    const loadFragmentShader = fetch('fragment_shader.glsl').then((res) => res.text());
    return Promise.all([loadVertexShader, loadFragmentShader]);
}

// テクスチャを読み込みPromiseを返します。
function loadTextureImage(srcUrl) {
    const texture = new Image();
    return new Promise((resolve, reject) => {
        texture.addEventListener('load', (e) => {
            resolve(texture);
        });

        texture.src = srcUrl;
    });
}

// シェーダのソースからシェーダプログラムを作成し、
// Programを返します。
function createShaderProgram(vsSource, fsSource) {
    // バーテックスシェーダをコンパイルします。
    const vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, vsSource);
    gl.compileShader(vertexShader);

    const vShaderCompileStatus = gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS);
    if(!vShaderCompileStatus) {
        const info = gl.getShaderInfoLog(vertexShader);
        console.log(info);
    }

    // フラグメントシェーダについても同様にします。
    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, fsSource);
    gl.compileShader(fragmentShader);

    const fShaderCompileStatus = gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS);
    if(!fShaderCompileStatus) {
        const info = gl.getShaderInfoLog(fragmentShader);
        console.log(info);
    }

    // シェーダプログラムを作成します。
    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);

    const linkStatus = gl.getProgramParameter(program, gl.LINK_STATUS);
    if(!linkStatus) {
        const info = gl.getProgramInfoLog(program);
        console.log(info);
    }

    // プログラムを使用します。
    gl.useProgram(program);

    return program
}

// バッファを作成し返します。
function createBuffer(type, typedDataArray) {
    const buffer = gl.createBuffer();
    gl.bindBuffer(type, buffer);
    gl.bufferData(type, typedDataArray, gl.STATIC_DRAW);
    gl.bindBuffer(type, null); // バインド解除

    return buffer;
}

// シェーダとテクスチャを読み込み終わったら開始します。
Promise.all([loadShaders(), loadTextureImage('texture.png')]).then((assets) => {
    const shaderSources = assets[0];
    const textureImage = assets[1];

    const vertexShaderSource = shaderSources[0];
    const fragmentShaderSource = shaderSources[1];

    const program = createShaderProgram(vertexShaderSource, fragmentShaderSource);

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

    const vertices = new Float32Array([
        -1.0, 1.0, 0.0, // 頂点座標
        0.0, 0.0,       // テクスチャ座標
        -1.0, -1.0, 0.0,
        0.0, 1.0,
        1.0, 1.0, 0.0,
        1.0, 0.0,
        1.0, -1.0, 0.0,
        1.0, 1.0
    ]);

    const indices = new Uint16Array([0, 1, 2, 1, 3, 2]);

    const vertexBuffer = createBuffer(gl.ARRAY_BUFFER, vertices);
    const indexBuffer = createBuffer(gl.ELEMENT_ARRAY_BUFFER, indices);

    const vertexAttribLocation = gl.getAttribLocation(program, 'vertexPosition');
    const textureAttribLocation  = gl.getAttribLocation(program, 'texCoord');

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

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

    gl.enableVertexAttribArray(vertexAttribLocation);
    gl.enableVertexAttribArray(textureAttribLocation);

    gl.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, gl.FLOAT, false, STRIDE, VERTEX_OFFSET);
    gl.vertexAttribPointer(textureAttribLocation, TEXTURE_SIZE, gl.FLOAT, false, STRIDE, TEXTURE_OFFSET);

    // 描画します。
    const indexSize = indices.length;
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.drawElements(gl.TRIANGLES, indexSize, gl.UNSIGNED_SHORT, 0);
    gl.flush();
});

これを実行すると以下のように表示されます。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-09-08-11-02-59

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

テクスチャは複数枚使用することもできます。ただし上限枚数があり、getParameterメソッドで取得できます。

const vertexShaderTextureUnit = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
const shaderTextureUnit = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
const totalTextureUnit = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);

console.log(vertexShaderTextureUnit);
console.log(shaderTextureUnit);
console.log(totalTextureUnit);

テクスチャはテクスチャユニットという単位で管理されています。WebGL2では、バーテックスシェーダに16ユニット、各シェーダに16ユニット、すべてのシェーダ合わせて32ユニット使えることが保証されています。実際の最大枚数は環境によって異なりますが、少なくともこれだけはどの環境でも使えます。

使用するテクスチャユニットを切り替えるにはactiveTextureメソッドを使用します。

gl.activeTexture(gl.TEXTURE0);

TEXTURE0〜TEXTURE31まであるので、好きに切り替えて利用しましょう。

シェーダ側でどのテクスチャユニットを使用するかについては、sampler2D型の変数にどのテクスチャユニットを使用するかの番号を転送してやれば指定できます。

const textureUnitID = 0;
const textureLocation = context.getUniformLocation(program, 'tex');
context.uniform1i(textureLocation, textureUnitID);

また、もちろんsampler2D型の変数を複数用意すれば、複数同時に使うこともできます。