さて、この記事を書き始めたのは 2019年12月2日の1時あたりでしょうか。
まだまだ、WebGL のアドベントカレンダーがスカスカであると知り、なぜにと思いよく見ると Three.js に分散がしたがゆえの過疎化が起きている状態でした。哀しみ。
日頃から WebGL やるなら Three.js 使えばよいと思うよ。と皆に進めておきながら自分は基本的にライブラリなしで書いており、Three.js はあまりわからない始末。いつしか何故かネイティブ愛のような謎の感情も湧いてきている気さえする。それ故に書かなくては、書かなくては、と心の奥底から囁く声にいざなわれて、気づけばキーを叩き初めていたのです。
とうことで、表題のとおり WebGL 2.0 で VAOとインターリーブ配列とインスタンシングをすることについて書いていきます。言い訳ですが、急遽書き始めたので既にあったデモをもとに雑に書いていきます。あしからず。
それぞれについてはお馴染みの以下のサイトをみると分かりやすいです。
そして VAO によるインスタンシングについても同様にまとまっています。
とういことで、これから説明するのは上記をインターリーブ配列で対応する場合の話です。まあ、インターリーブ配列のパフォーマンスについては色々な話がありますし、私も詳しくはないので、ご自身で調べてみてください。
デモ
https://syuji-higa.github.io/sketchbook-webgl/191122a/
パーティクルになっている drawArraysInstanced
と、画像になっている drawElementsInstanced
が両方とも使われていますが、今回はどちらかというとシンプルな drawArraysInstanced
だけ説明します。
説明するコードに関連するファイル
描画に関するメインの処理(JavaScript)
https://github.com/syuji-higa/sketchbook-webgl/blob/master/projects/191122a/src/views/webgl-objects/draw-arrays-object.ts
drawElementsInstanced
との共通部分は他に記載していますが、今回お伝えしたいことにはあまり関係ないので、とりあえずこちらだけ見てもらえればと思います。
モジュール化された関数たち(JavaScript)
https://github.com/syuji-higa/sketchbook-webgl/blob/master/projects/191122a/src/utils-webgl/util.ts
汎用性のある処理は関数にしてモジュール化しているので、ちょいちょいこの中の関数がでてきます。重要な箇所については説明しますが、それ以外はサラッと流して進めます。
バーテックスシェーダの処理(GLSL)
https://github.com/syuji-higa/sketchbook-webgl/blob/master/projects/191122a/src/views/webgl-objects/shaders/draw-arrays-default.vert
パーティクルなのでフラグメントシェーダは大したことしてない為、バーテックスシェーダのみ載せておきます。
雑な説明
ほんと雑です。重要な箇所以外はコメントすらしません。わからない箇所が言葉がでてきたら、上部に記載したお馴染みのサイトの記事を読んでみてください。
1. attribute の生成
// 1つ目に位置データ
_attLocs[0] = 'position'
_attStrides[0] = 3
_attDivisors[0] = 0 // インスタンシングしないデータは `0` を指定しておく
// 2つ目にインスタンシング用のオフセットデータ
_attLocs[1] = 'offset'
_attStrides[1] = 3
_attDivisors[1] = 1 // インスタンシングする場合の除数を設定
参照箇所
分かりやすいように先に説明しておきますが、インスタンシング用のデータはインターリーブ配列にはいれません。インターリーブ配列やインスタンシングの仕組みが理解できていれば、その理由は明白なのですが、分からなくてもとりあえず、インスタンシング用のデータはインターリーブ配列にいれないとだけ覚えておいてください。
2. インターリーブ配列用のストライドだけをまとめる
const _interleaveStrides = _attStrides.filter((val, index) => {
return _attDivisors[index] === 0
})
今回は position
しかインターリーブ配列にいれるものはないので、それだけですが、他にもあれば追加されます。
3. プログラムの生成
// プログラム生成の関数
_programData.default = createProgramData(
this._gl,
_attLocs, // 全ての attribute
_uniLocs,
_attStrides,
defaultVertexShader,
defaultFragmentShader
)
uniform
の生成もすっ飛ばしてるしココは説明不要かと思ったのですが、かるく説明します。当たり前ではあるのですが、attribute はインターリーブ配列にいれるものもインスタンシング用のデータも全て登録します。
4. 頂点データの生成してインターリーブ配列に追加
// モデルの生成の関数
const { position } = square(0.2, 0.2, 10, 10)
const _len: number /* int[0,inf) */ = position.length / _attStrides[0]
_vertexLen += _len
// インターリーブ配列に追加する関数
addInterleaveVertexAttr(
_vartexAtts,
_interleaveStrides, // 先ほど用意したインターリーブ配列用のストライド
_len,
[position]
)
ここで重要なのはインターリーブ配列に追加する際に、インターリーブ配列用のストライドだけを追加するということです。あくまでこの関数において追加するとう表現をつかっていますが、ストライドを何につかっているかというと、生成した配列をインターリーブ配列用に一次元にまとめて並べ直す為につかっています。
つまり、ここではインスタンシング用の attribute
が含まれておらず、 attribute
毎に正しいストライドで並び直せていれば問題ないということです。
5. インスタンシング用のデータを生成
// オフセットデータを生成
const _instanceAttrOffset: number[] = []
for (let z = -1; z <= 1; z += 0.1) {
for (let y = -1; y <= 1; y++) {
for (let x = -1; x <= 1; x++) {
_instanceAttrOffset.push(x, y, z)
}
}
}
// インスタンスの数を取得
_instanceLen += _instanceAttrOffset.length / _attStrides[1]
インスタンシング用のデータは attribute のストライド数 × インスタスの数 を用意します。この時点でよくわかっていなかった方も、インスタンシング用のデータをインターリーブ配列に含めいない理由がわかってきたのではないでしょうか?だって数が合わないですもんね。
6. attribute を有効化する為に必要なデータを生成
// 有効化時に設定するオプションのリストを生成する関数
const _enableAttributeOptionsList = createEnableAttributeOptionsList(
_programData.default.attLocs,
byte, // インターリーブ配列のストライドのバイト数
offsets, // インターリーブ配列のストライドのオフセットのバイト数のリスト
_attDivisors // インスタンシングの除数
)
// 有効化に必要なデータを生成する関数
const _enableAttributeDataList = createEnableAttributeDataList(
_programData.default.attLocs,
_attStrides,
_interleaveStrides,
_enableAttributeOptionsList // 上記で生成したオプションのリスト
)
ここは複雑なんですが、まとめたほうがわかりやすそうなので一緒にしました。それぞれの関数について説明します。
6-1. 有効化時に設定するオプションのリストを生成する関数
export const createEnableAttributeOptionsList = (
attLocs: GLuint[],
byte: GLsizei,
offsets: GLintptr[],
divisors: GLuint[]
): EnableAttributeOptions[] => {
const _optionsList = []
for (let i = 0; attLocs.length > i; i++) {
const _options: EnableAttributeOptions = divisors[i]
// インスタンシング用は除数を設定
? { divisor: divisors[i] }
// インターリーブ配列用はストライドとそのオフセットを設定
: { stride: byte, offset: offsets[i] }
_optionsList.push(_options)
}
return _optionsList
}
有効化する際に vertexAttribPointer()
を使うのですが、それに設定する引数が違います。また、インスタンシングする attribute
は vertexAttribDivisor()
を実行する必要があります。そのために振り分けて設定しています。
6-2. 有効化に必要なデータを生成する関数
export const createEnableAttributeDataList = (
attLocs: GLuint[],
attStrides: GLsizei[],
interleaveStrides: GLsizei[],
enableAttributeOptionsList: EnableAttributeOptions[]
): EnableAttributeData[][] => {
const _attrLenList = []
for (let i = 0; attLocs.length - interleaveStrides.length + 1 > i; i++) {
i === 0
// インターリーブ配列に追加した attribute の数を追加
? _attrLenList.push(interleaveStrides.length)
// インスタンシング用の attribute は個別に処理するので、それぞれ 1 を設定
: _attrLenList.push(1)
}
// _attrLenList は配列の
// 1つ目にインターリーブ配列の attribute の数が
// 2つ目以降はインスタス attribute の数だけ 1 が入っている
return _attrLenList.map((val, index) => {
const _dataList = []
for (let i = 0; val > i; i++) {
// インターリーブ配列の場合はインターリーブ配列内のインデックスをカウント
// インスタンシングの場合は _attrLenList のインデックスをカウント
// (なんかわざわざ複雑に書いてる気がする。。。のでその辺はスルーしてください)
const _count = index === 0 ? i : _attrLenList[0] + index - 1
_dataList.push([
attLocs[_count],
attStrides[_count],
enableAttributeOptionsList[_count]
])
}
return _dataList
})
}
ごちゃごちゃしてますが、重要な箇所だけ説明すると、インターリーブ配列はまとめて VBO に登録するんですが、 attribute
は個別に有効化する必要があるのです。さらに vertexAttribPointer()
に入れる値も attribute
毎に違うので、マジめんどいよねってことです。
まあ、僕はこのあとの VAO の処理が楽になるとういか汎用性を持たせたかったのでまとめていますが、個々に処理してもなんら問題はないです。なので、大事なのは太字のところだと思っておけばよいです。
7. VAO の生成
// VAO を生成する関数
createVao(
this._gl,
// インターリーブ配列&個別にインスタンシング用の配列を追加
[_vartexAtts, _instanceAttrOffset],
// 先ほど生成した attribute の有効化に必要なデータのリスト
_enableAttributeDataList
)
やっと VAO の生成ですね。ここだけ見てみもなんにもわからないので関数のなかをみましょう。
export const createVao = (
gl: WebGL2RenderingContext,
dataList: number[][],
enableAttributeDataList: EnableAttributeData[][],
options: CreateVaoOptions = {}
): WebGLVertexArrayObject => {
const _vao: WebGLVertexArrayObject = gl.createVertexArray()
const { draw, indexes } = {
draw: gl.STATIC_DRAW,
indexes: null,
...options
}
gl.bindVertexArray(_vao)
for(let i = 0; dataList.length > i; i++) {]
// VBO への登録はインターリーブ配列とインスタンシング用の attribute 毎に処理
const _vbo: WebGLBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, _vbo)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(dataList[i]), draw)
for(const data of enableAttributeDataList[i]) {
// attribute の有効化は attribute 毎に処理
enableAttribute(gl, ...data)
}
}
if(indexes) {
const _ibo: WebGLBuffer = gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, _ibo)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(indexes), draw)
}
gl.bindVertexArray(null)
gl.bindBuffer(gl.ARRAY_BUFFER, null)
if(indexes) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null)
}
return _vao;
}
既に必要なデータを作る際にもざっくり説明していますが、重要なことはコメントで書いてることです。一応以下にも書いておきます。
- VBO への登録はインターリーブ配列とインスタンシング用の
attribute
毎に処理 attribute
の有効化はattribute
毎に処理
まあ、僕はまとめて処理しようとしすぎてハマりましたが、個別に記述していけばそこまでハマるポイントではないのかもしれません。
8. 描画
// プログラムを有効化する
useProgram(this._gl, prg)
// VAO をバインドする
this._gl.bindVertexArray(vao)
// ドローコール
this._gl.drawArraysInstanced(this._gl.POINTS, 0, vertexLen, instanceLen)
// VAO をバインドを解除する
this._gl.bindVertexArray(null)
もうこれ以上は説明いらないと思う(お馴染みのサイト見たほうがわかりすい)のですが、一応ここまで書いたので描画まではかるく触れておきます。
やはり VAO の魅了が溢れてでてますね。VBO の登録や attribute
の有効化をまとめて設定しておけるので、モデルの切り替えなども簡単です。あとは、既に出てきてますがドローコールが drawArraysInstanced()
となっており、インスタンシング用の関数になっています。まあ、このあたりは VAO とインスタンシングの話なので詳しくは説明しません。
まとめ
シェーダ側は特に説明はしていませんが、ここでは必要ないかなと思ったので説明を省いています。それにもう夜明けが近づいていますし。そろそろ5時になりそう。
なにげに WebGL に関する記事を書くのは初めてかもしれません。記事の一部でちょっととかはありますが。社内では WebGL ができる人間的に扱わてはいたのですが、いかんせん周りをみると凄い人がたちが溢れていて、なかなか自身の言葉で語るということを恐れていたのかもしれないです。まあ、語るほどのことも無かっただけとうこともありますが。
独自の関数だらけで分かりにくい記事になって申し訳ない感じなんですが、それでもアドベントカレンダーなんてお祭りみたいなもんだし、書きたいと思ってしまったこの気持はもう止められなかったので、それはそれで良しとして自分の中では収めたいと思っています。
では明日も誰かが WebGL Advent Calendar 2019 に投稿することを願ってます。おやすみなさい。