GLSL
openFrameworks
OpenGL

Instanced Drawを行う

はじめに

 ここでは数あるOpenGLの描画方法の内の一つ、Instanced Drawについて解説を行う。
まずは、glDrawArraysを利用する場合の問題点(というほどでもないんだけど)を指摘した上で、それを解消する手段として、Instanced Drawの解説を進める。

解説用ソース
ModernOpenGL-AdventCal2017/03-instancedrender

Instanced Drawの前に

 glDrawArrays()で大量のオブジェクトを描画したとする。例えば、パーティクルをレンダリングしたい時、以下のようなコードになるだろう。

glBindVertexArray(vao);
for(int i = 0; i < PARTICLE_NUM; i++) {
    glDrawArrays(GL_TRIANGLE_STRIPE, 0, 4);
}
glBindVertexArray(0);

ドローコール数そのものがボトルネックになる

 上記は、glDrawArrays()がパーティクルの数だけ発行されることになる。特に問題があるわけではないが、CPUからGPUへのドローコール数自体がボトルネックになってくるという問題が潜在している。

2月-28-2018 23-22-49.gif
1つのプリミティヴを8回描画するイメージ

Instanced Drawは、似た形状をドローコール1回で描画する機能

 これから紹介するInstanced Drawを行うと、ドローコールを1回に抑えることができ、パフォーマンスの改善が見込める。パーティクルなど似た形状のオブジェクトを大量に描画する際によく採用されている。
2月-28-2018 23-19-01.gif
8個のプリミティヴを一気に描画するイメージ

Instanced Drawを行うには

 Instanced Drawを行うには、「ここからここまでが一つのプリミティヴ(インスタンス)だよ」とGPUに伝え、専用の描画コマンドを利用することで実現できる。

下記を利用する。

  • glVertexAttribDivisor()
  • glDrawArraysInstanced()
  • glDrawElementsInstanced()

実装&解説

 ランダムに散りばめられたポリゴンをInstanced Drawするプログラムを用意した。このプログラムを元に、実装と解説を進めていく。
ModernOpenGL-AdventCal2017 / 03-instancedrender
3月-01-2018 12-13-38.gif
10000個の正方形ポリゴンをInstanced Drawする

方針

 まず、以下の方針で実装する。

  • 頂点/位置/色を属性としてVAOにバインドする
  • 頂点情報は同じなので使いまわす
  • 位置属性を利用して、Vertex Shader側で並行移動させる
  • EBOを利用する

(本来ならば位置(vec3)だけでなく、モデル行列をバインドするのだがここでは割愛する)

1. VBOを作成し、VAOにバインドする

 以下のバインドポイントでVAOにバインドした。

0→頂点属性
1→位置属性
2→色属性

source code

2. glVertexAttribDivisor()でインスタンス毎に分割

 ここがキモになる部分、glVertexAttribDivisor()はその名の通り、VAO内のそれぞれの属性をインスタンス毎に分割する関数。

glVertexAttribDivisor( GLuint index, GLuint divisor);

index
 VAOにバインドしている属性のバインドポイントを指定

divisor
 glVertexAttribPointerの第2引数で指定している頂点数で分割する。

以下の様になる

glVertexAttribDivisor(0, 0); //頂点属性(0)は、1つしかないので分割しない
glVertexAttribDivisor(1, 1);   //位置属性(1)は、3頂点分で分割する
glVertexAttribDivisor(2, 1);  //色属性(2)は、4頂点分で分割する

スクリーンショット 2018-03-01 11.26.23.png

glVertexAttribDivisor()を行うことで、1つのVAO内のデータを複数のインスタンスとして分割することができる。

スクリーンショット 2018-03-01 17.07.35.png

source code

 

もし第2引数を2に指定するとどうなるか
 例えば、10000インスタンスを生成する想定でglVertexAttribDivisor(2, 2);すると、5001インスタンス目から、予期せぬ色でプリミティヴがレンダリングされる。(ほとんどの場合真っ黒になると思う)

glVertexAttribDivisor(0, 0);
glVertexAttribDivisor(1, 1);
glVertexAttribDivisor(2, 2);

スクリーンショット 2018-03-01 11.45.59.png

3. 描画する

描画も非常にシンプル。EBOを使っているか否かで、以下を使い分けるだけ。glDrawElementsInstanced()の第5引数に描画するインスタンスの数を指定する。

  • glDrawArraysInstanced()
  • glDrawElementsInstanced()
glBindVertexArray(vao);
glDrawElementsInstanced(GL_TRIANGLE_STRIP, 6, GL_UNSIGNED_INT, NULL, 10000);
glBindVertexArray(0);

oFで同じことをする

 ofVboのみで完結できる。
oFは、0,1,2,3のバインドポイントを予約しているので、位置属性は「4」でバインドしている。
source code

まとめ

 ここでは、Instanced Drawについての解説を行った。
大量のオブジェクトを描画する場合、Instanced Drawを用いるとドローコールが1回に軽減でき、パフォーマンスの向上が見込める。また、Instanced Drawは、glVertexAttribDivisor()glDrawArraysInstanced()glDrawElementsInstanced()を駆使することで実現できる。