インターリーブ配列 VBO

実行結果

今回のサンプルの実行結果

メモリアクセスの効率化

前回はフラットシェーディングと題して、WebGL の拡張機能を利用した特殊なシェーディングについて解説しました。

少し古めかしいような、懐かしいような、独特なレンダリング結果を得られるフラットシェーディングですが、現代においてはむしろ拡張機能を利用しないといけないケースもあるというのが、なんとも皮肉めいていて面白い技術でした。

さて今回ですが、インターリーブ配列 を用いた処理について見ていきたいと思います。

3DCG 独自の用語ではなく、一般的なコンピュータ用語として「メモリインターリーブ」というキーワードがあります。これはメモリアクセスを効率化する手法を指して使われる言葉ですが、WebGL の場合も、VBO の生成に工夫を凝らすことにより GPU 上のグラフィックスメモリのアクセスを効率化するインターリーブが利用できます。

OpenGL などのネイティブ開発では一般的なテクニックのひとつですが、当サイトのサンプルは、いずれもこのインターリーブを 用いない やり方で記述されています。

今回はひとつ上のレベルを目指してインターリーブ配列を用いた処理に挑戦してみましょう。

グラフィックスメモリ

インターリーブについて、まずは外堀から埋めていきましょう。

そもそもインターリーブとは、先述のとおり「メモリアクセスを効率化する技術」のことを言いますが、WebGL に関連の深いメモリとはなんでしょうか。

これは、言うまでもなく GPU 上にあるグラフィックスメモリですね。グラフィックスメモリは VRAM(Video Random Access Memory)とも呼ばれるグラフィックス処理に主に利用されるメモリ領域です。いわゆるデジタルデバイスに搭載されている RAM の、グラフィックス用特化版とでも言えばいいでしょうか。マシン構成などによってグラフィックスメモリの性能はもちろん様々ですが、CPU と GPU が共存しているようなオンボード GPU よりも、当然ながら独立したグラフィックスボードのほうが、潤沢なグラフィックスメモリを持っていることが多いです。

WebGL でメモリインターリーブを用いるとすれば、これは VBO の生成 のフェーズになります。VBO は、頂点の情報をまるっと格納したバッファですが、この VBO の情報はバッファにデータを格納した段階で GPU 上に転送されます。つまり言い換えれば、VBO の生成方法(VBO へのデータの格納方法)を工夫することにより、GPU 上でのメモリの配置に影響を与えることができるというわけですね。

より具体的には、メモリ上にデータを配置する際に、頂点単位でデータがまとまって格納される ように工夫します。と、言葉で説明されてもわかりにくいと思いますので、図解して考えてみましょう。

これまでのサンプルの VBO 生成イメージ

インターリーブ配列を用いた場合のイメージ

当サイトのサンプルのほとんどすべてが、上の画像で示したような手順でバッファを生成しています。

もう少し具体的に言うと、全てのデータが「頂点属性ごとにひとまとまり」になっているわけですね。

頂点属性のそれぞれを個別に配列に格納し、それを元に VBO を生成しているため、上の画像で示したようにそれぞれの頂点属性ごとにデータがまとまった状態になります。この方法はもちろん悪くはありませんし、深刻な悪影響を及ぼす方法というわけではありません。

しかし、インターリーブ配列を用いれば、さらに効率的にデータを配置できます。インターリーブを用いた場合はどんなイメージになるのか、こちらもやはり同じように図解してみましょう。

インターリーブ配列を用いた場合のイメージ

インターリーブ配列を用いた場合のイメージ

さてどうでしょうか。

ここで注目してほしいのは、メモリ上のデータの配置が 頂点ごとにまとまっている という点です。先ほどの VBO の生成方法では、頂点単位ではなく全体のジオメトリのデータが、それぞれの 頂点属性ごと にまとまった配置になっていました。しかしインターリーブ配列の場合は、属性ごとではなく頂点ひとつひとつのデータが寄り添うようにまとまっているのがわかりますね。

このように、一見するとバラバラにデータが分散されたように見えても、実際には用途に合わせて合理的にメモリ配置がなされた状態になることでメモリキャッシュ効率が向上します。これがインターリーブ配列を用いた場合のメリットなのですね。GPU のメモリアクセスは局所性が高いと言われているので、近いところにデータがまとまっていたほうが効率よく処理を行うことができるというわけです。

VBO の生成

さて、それでは概念を理解できたところで、実際にインターリーブ配列を用いた VBO の生成について見ていきましょう。

基本的に、インターリーブ配列を使うからといって VBO の生成に必要となるメソッドの種類が変わったりするわけではありません。これまで利用してきたメソッドを使い、引数だけを変化させつつ、VBO を生成します。

まず最初に、今回のサンプルの仕様を簡単に説明します。

サンプルでは、頂点属性として「頂点座標」、「頂点法線」、「頂点カラー」という一般的な属性を持つ状態を想定しています。いつもどおりの方法なら、VBO をそれぞれの頂点属性ごとにみっつ生成するところですが……今回はこれをひとつにまとめてから VBO を生成します。

当サイトのサンプルでたびたび登場している、トーラスのモデルデータを生成する関数を使い、頂点の各属性のデータを取得したあと、これをひとつの配列に格納していきます。具体的には、以下のようにすればいいですね。

モデルデータの生成と配列への格納

// トーラスモデル
var torusData = torus(32, 32, 2.0, 3.0);
var tIndex    = create_ibo(torusData.i);

// インターリーブ配列を作る
var stride = 0;
attStride.map(function(value){
    // ストライドをすべて合計する(position + normal + color = 10 要素)
    stride += value;
});

// 各属性のデータをまとめて格納する配列
var vertexBufferData = [];
(function(){
    var i, j, k, l, m;
    var position = torusData.p;
    var normal = torusData.n;
    var color = torusData.c;
    for(i = 0, j = position.length / 3; i < j; ++i){
        k = attStride[0] * i; // attStride[0] === 3
        l = attStride[1] * i; // attStride[1] === 3
        m = attStride[2] * i; // attStride[2] === 4
        vertexBufferData.push(
            position[k], position[k + 1], position[k + 2],
            normal[l],   normal[l + 1],   normal[l + 2],
            color[m],    color[m + 1],    color[m + 2],    color[m + 3]
        );
    }
})();

即時関数を作って実行していますが、要は全部の頂点属性を順番に配列にプッシュしなおしているのがわかるでしょうか。

各属性のデータをまとめて格納するための配列が vertexBufferData という変数名になっており、ここにドカドカと各属性のデータを直列で突っ込んでいきます。あくまでも一次元の、単純なひとつの配列データにしてしまうわけです。

配列の中身は、頂点属性 xyz に続き頂点法線 xyz、そして最後に頂点カラー rgba という具合に、連続してデータが連なった状態になります。まとめてデータを格納した配列ができたら、次にこれを元に VBO を生成します。

VBO の生成自体は、これまでに当サイトで利用してきたものと全く同じ手順で行えます。空のバッファを生成して、データを割り当てるだけです。ちょっと特殊なことをしないといけないのは、その次の手順になります。

これも、コードを見ながら考えてみましょう。

attribute location を有効化する

// VBO を生成する
var tVBO = create_vbo(vertexBufferData);

// VBO をバインドする
// (create_vbo 関数では汎用性のため最後にバッファをアンバインドしているため)
gl.bindBuffer(gl.ARRAY_BUFFER, tVBO);

// attributeLocationを有効化し登録する
var byteLength = stride * 4; // 32bit === 4byte
gl.enableVertexAttribArray(attLocation[0]);
gl.vertexAttribPointer(attLocation[0], attStride[0], gl.FLOAT, false, byteLength, 0);
gl.enableVertexAttribArray(attLocation[1]);
gl.vertexAttribPointer(attLocation[1], attStride[1], gl.FLOAT, false, byteLength, 12);
gl.enableVertexAttribArray(attLocation[2]);
gl.vertexAttribPointer(attLocation[2], attStride[2], gl.FLOAT, false, byteLength, 24);

生成した VBO をバインドした状態で、各頂点属性を有効化していきます。

これまでのケースでは gl.vertexAttribPointer メソッドの第六引数には、無条件で 0 を指定していました。言い換えると、この引数の意味について、特に考えずに使っていたわけです。インターリーブ配列を用いる場合は、この引数の意味を正しく理解しておく必要があります。

まず最初に第五引数に注目してみます、こちらは 頂点のストライド を渡します。上のコードを見ると、この第五引数に与えている値の算出では、全ての頂点属性のストライドを合計した値( stride という変数)を、さらに 4 倍したものが使われているのがわかりますね。これはコメントにもあるとおりで gl.FLOAT が 32bit として扱われることから、ひとつの要素あたり 4byte のデータを使うことに由来します。

インターリーブ配列を用いているので、メモリ領域のなかで 40byte 分でちょうどひとつの頂点という感じで、データが固まった状態になっているのですね。

同様の考え方(一要素で 32bit === 4byte)で、第六引数も考えます。

ここには、各属性のオフセット値をバイト単位で指定します。たとえば頂点座標は、配列のなかの最初に配置されているので第六引数は 0 です。一方で、法線はその頂点座標の次に配置されているので、3 要素 x 4byte で、頂点座標が消費しているバイト数分オフセットしていることを引数で示すのですね。これは続く頂点カラーの場合も同様に考えてやればいいでしょう。

このように、インターリーブを用いている場合はバイト単位でのメモリ管理が必要です。javascript ではバイトレベルでのデータ管理を意識することはそれほど多くありませんが、WebGL のような低レベル API では、こういった次元での細かな指定もしっかりと行ってやる必要があります。

ちょっと面倒な感じがするかもしれませんが、慣れてしまえば、どうということもないでしょう。パフォーマンス向上のためには、必要な経費だと思って割りきりましょう(笑)

まとめ

インターリーブ配列を用いた描画では、これまで見てきたような事前の準備だけがこれまでの手順と異なります。つまり描画命令の発行や uniform 変数のプッシュなどは、これまでとまったく同じで問題ありません。

振り返ってみると、案外とインターリーブ配列を用いた描画も難しくないことがわかりますね。当サイトは一応入門サイトという体裁なので、基本的にはわかりやすさを重視してテキストを書いていることもあり、開設当初からずっと VBO は頂点属性ごとに用意する方法で解説してきました。しかし、一度わかってしまえば、パフォーマンスに優れたインターリーブ配列を用いることに、抵抗を抱く人はそれほどいないのではないでしょうか。

インターリーブ配列がどういうものなのか、その意味がしっかりわかっていれば、頂点データを生成するような処理を自前で書いている場合でも、効率よく配列内にデータを格納することができるでしょう。そして、バイト単位で細かな調整が必要とは言っても、そこはいったん汎用性の高いロジックを組んでしまえば、どうとでも再利用ができるはずです。

今回のサンプルはインターリーブ配列を用いたトーラスのレンダリングを行っていますが、描画に関しては特別なことは何もしていません。動作確認程度にしか使えないかもしれませんが、実際に動いている様子を確かめてみてください。

サンプルは以下のリンクから参照できます。

entry

PR

press Z key