Subterranean Flower

WebGL2入門 最適化編

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

WebGL2入門 基礎編では、簡単なWebGL2の使い方について学びました。しかし無駄が多いプログラムでした。そこで、次はいくつかの最適化を行いたいと思います。最適化と言っても、まだ規模が小さいため効果は実感しにくいはずです。ですが、早めのうちに学んでおくべきでしょう。

WebGL2入門 記事一覧

最適化手法

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

基礎編ではバーテックスシェーダへ転送するin変数ごとにバッファを作成しました。しかし別々にバッファを作成すると、メモリ上の配置が散らばってしまい、メモリアクセスの関係でパフォーマンスに影響が出る可能性があります。

そこで、複数のデータをひとつのバッファにまとめるインターリーブという手法があります。通常のバッファではひとつのデータだけを並べていましたが、インターリーブされたバッファでは、頂点ごとに複数のデータをごちゃ混ぜに配置します。

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

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

インターリーブバッファ

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

ここで重要になるのがstrideoffsetという値です。ここでstrideは頂点ひとつあたりのデータ長を表します。ここではstrideはvec3とvec4を合わせた長さになります。また、offsetはstride内での先頭からのサイズをあわらしています。この図ではPositionのoffsetは0で、Colorのoffsetはvec3ひとつ分になります。

インターリーブドバッファの作り方に決まりはありませんが、通常は、頂点1座標、頂点1色、頂点2座標、頂点2色…などというように、頂点ごとにまとめて配置します。頂点ごとにまとめておけば、メモリアクセスの観点から有利だからです。

これらのことを踏まえてインターリーブドバッファを作成しましょう。まずはふたつあったバッファをひとつに減らします。

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

次はバッファとin変数を結びつける部分の変更が必要になります。

        const VERTEX_SIZE = 3; // vec3
        const COLOR_SIZE  = 4; // vec4
 
        // バッファ操作前には必ずバインドします。
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        // in変数を有効化します
        gl.enableVertexAttribArray(vertexAttribLocation);
        // 現在バインドしているバッファと変数を結びつけます。
        // サイズはvec3を指定してるので3にします。型はFLOATを使用します。
        // うしろ3つの引数は今は気にしないでください。
        gl.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, gl.FLOAT, false, 0, 0);
 
        // 頂点色についても同様にします。
        gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
        gl.enableVertexAttribArray(colorAttribLocation);
        gl.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, gl.FLOAT, false, 0, 0);

この部分を変更します。変更前はふたつのバッファをそれぞれin変数に結び付けていましたが、これをインターリーブされたひとつのバッファへ変更します。

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

        const STRIDE = (3 + 4) * Float32Array.BYTES_PER_ELEMENT;
        const POSITION_OFFSET = 0;
        const COLOR_OFFSET = 3 * Float32Array.BYTES_PER_ELEMENT;

        // バッファをバインドします。
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

        // in変数を有効化します。
        gl.enableVertexAttribArray(vertexAttribLocation);
        gl.enableVertexAttribArray(colorAttribLocation);

        // in変数とバッファを結び付けます。
        // 第5引数にstride、第6引数にoffsetを指定します。
        gl.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, gl.FLOAT, false, STRIDE, POSITION_OFFSET);
        gl.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, gl.FLOAT, false, STRIDE, COLOR_OFFSET);

このとき重要なのがstrideとoffsetです。strideはvec3 + vec4なので、(3 + 4) * バイト数、になります。また、vertexPositionのoffsetは先頭なので0、colorのoffsetはposition(vec3)のあとなので3 * バイト数、となります。これをvertexAttribPointerメソッドの引数に指定します。

次に頂点データを変更しましょう。

        // 頂点情報。vec3で宣言しているので、xyzxyzxyz…と並べていきます。
        // WebGL2では基本的にfloat型を使うので、Float32Arrayを使用します。
        const vertices = new Float32Array([
            -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の範囲で指定します。
        // 頂点と同じ数だけ(今回は6つ)必要です。
        const colors = new Float32Array([
            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
        ]);

頂点座標と頂点色が別のデータになっていました。これをインターリーブドバッファとするために、ひとつのバッファに交互に並べた形にしましょう。

        // インターリーブされた頂点情報です。
        const vertices = new Float32Array([
            -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
        ]);

これでインターリーブドバッファができました。

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

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

colorBufferの部分は不要になったので削除します。

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

これで完成です。コードの変更が終わったら、同じ結果が得られるか実行してみましょう。

ここまでのコードまとめ

// Canvasを作成してbodyに追加します。
const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);

// WebGL2のコンテキストを取得します。
const gl = canvas.getContext('webgl2');

const loadVertexShader = fetch('vertex_shader.glsl');
const loadFragmentShader = fetch('fragment_shader.glsl');

Promise.all([loadVertexShader, loadFragmentShader])
    .then((responses) => Promise.all([responses[0].text(), responses[1].text()]))
    .then((shaderSources) => {
        const vertexShaderSource = shaderSources[0];
        const fragmentShaderSource = shaderSources[1];

        // バーテックスシェーダをコンパイルします。
        const vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexShaderSource);
        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, fragmentShaderSource);
        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);

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

        // バーテックスシェーダのin変数の位置を取得します。
        const vertexAttribLocation = gl.getAttribLocation(program, 'vertexPosition');
        const colorAttribLocation  = gl.getAttribLocation(program, 'color');

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

        const STRIDE = (3 + 4) * Float32Array.BYTES_PER_ELEMENT;
        const POSITION_OFFSET = 0;
        const COLOR_OFFSET = 3 * Float32Array.BYTES_PER_ELEMENT;

        // バッファをバインドします。
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

        // in変数を有効化します。
        gl.enableVertexAttribArray(vertexAttribLocation);
        gl.enableVertexAttribArray(colorAttribLocation);

        // in変数とバッファを結び付けます。
        // 第5引数にstride、第6引数にoffsetを指定します。
        gl.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, gl.FLOAT, false, STRIDE, POSITION_OFFSET);
        gl.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, gl.FLOAT, false, STRIDE, COLOR_OFFSET);

        // インターリーブされた頂点情報です。
        const vertices = new Float32Array([
            -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
        ]);

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

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

        // WebGLに描画を促します。
        gl.flush();
    });

インデックスバッファ

これまで四角形を作るのに三角形をふたつ使用していました。しかし、四角形は4頂点で十分なはずなのに6頂点分のデータを用意せねばならず、いくつかの無駄が発生していました。

これを解決するのがインデックスバッファです。インデックスバッファは頂点に番号を振り、描画データを番号で指定する機能です。インデックスバッファを使えば、データを4頂点分だけ用意しておき、実際の描画には「0-1-2と1-3-2を描画」というふうに番号だけで指定することができます。今回のように頂点数が少ない場合はわずかな節約にしかなりませんが、規模が大きくなってくると大幅な最適化が期待できます。

indexed_quad

インデックスバッファを使用するには、まずはバッファを作成します。インターリーブドバッファのときにひとつ削りましたが、今回またひとつ追加する形になります。

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

ここにインデックスバッファを追加します。

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

続いて6頂点分あったデータを4頂点分に減らします。

        // インターリーブされた頂点情報です。
        const vertices = new Float32Array([
            -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
        ]);

ここから重複するデータを削除します。

        // インターリーブされた頂点情報です。
        const vertices = new Float32Array([
            -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
        ]);

次にインデックスバッファの要となるインデックスを作成しましょう。もともとは6頂点分のデータを用いて1-2-3, 4-5-6とふたつの三角形を描画していましたが、インデックスを用いることで0-1-2, 1-3-2に置き換えることができます。

        // インターリーブされた頂点情報です。
        const vertices = new Float32Array([
            -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と1-3-2というふたつの三角形を指定します。
        const indices = new Uint16Array([
            0, 1, 2,
            1, 3, 2
        ]);

インデックスの指定にはuint型を使います。JavaScriptプログラマによってuint型というのは馴染みが薄いと思いますが、これはunsigned int(符号なし整数)型の略です。簡単に説明すると、正の整数を表せる型ということです。

インデックスの準備ができたらバッファへ転送します。

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

ここにインデックスバッファの転送を追加します。

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

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

ここで、インデックスバッファの転送にはARRAY_BUFFERではなくELEMENT_ARRAY_BUFFERを使用します。

データが転送できたら描画しましょう。

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

この描画コードを以下のように変更します。

        // 四角形を描画します。
        const indexSize = indices.length;
        gl.drawElements(gl.TRIANGLES, indexSize, gl.UNSIGNED_SHORT, 0);

注意点としてはdrawArraysではなくdrawElementsを使用するということです。引数もいくつか異なるので注意しましょう。

ここまでの変更ができたら一度動作するか確認してみましょう。しっかりできていれば四角形が描画されるはずです。

ここまでのコードまとめ

// Canvasを作成してbodyに追加します。
const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);

// WebGL2のコンテキストを取得します。
const gl = canvas.getContext('webgl2');

const loadVertexShader = fetch('vertex_shader.glsl');
const loadFragmentShader = fetch('fragment_shader.glsl');

Promise.all([loadVertexShader, loadFragmentShader])
    .then((responses) => Promise.all([responses[0].text(), responses[1].text()]))
    .then((shaderSources) => {
        const vertexShaderSource = shaderSources[0];
        const fragmentShaderSource = shaderSources[1];

        // バーテックスシェーダをコンパイルします。
        const vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexShaderSource);
        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, fragmentShaderSource);
        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);

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

        // バーテックスシェーダのin変数の位置を取得します。
        const vertexAttribLocation = gl.getAttribLocation(program, 'vertexPosition');
        const colorAttribLocation  = gl.getAttribLocation(program, 'color');

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

        const STRIDE = (3 + 4) * Float32Array.BYTES_PER_ELEMENT;
        const POSITION_OFFSET = 0;
        const COLOR_OFFSET = 3 * Float32Array.BYTES_PER_ELEMENT;

        // バッファをバインドします。
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

        // in変数を有効化します。
        gl.enableVertexAttribArray(vertexAttribLocation);
        gl.enableVertexAttribArray(colorAttribLocation);

        // in変数とバッファを結び付けます。
        // 第5引数にstride、第6引数にoffsetを指定します。
        gl.vertexAttribPointer(vertexAttribLocation, VERTEX_SIZE, gl.FLOAT, false, STRIDE, POSITION_OFFSET);
        gl.vertexAttribPointer(colorAttribLocation, COLOR_SIZE, gl.FLOAT, false, STRIDE, COLOR_OFFSET);

        // インターリーブされた頂点情報です。
        const vertices = new Float32Array([
            -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と1-3-2というふたつの三角形を指定します。
        const indices = new Uint16Array([
            0, 1, 2,
            1, 3, 2
        ]);

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

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

        // 四角形を描画します。
        const indexSize = indices.length;
        gl.drawElements(gl.TRIANGLES, indexSize, gl.UNSIGNED_SHORT, 0);

        // WebGLに描画を促します。
        gl.flush();
    });

WebGL2入門 3D知識編へ続く

続き:WebGL2入門 3D知識編