モダンなOpenGLでティーポットを描画したい -- COLLADAをOpenGLで描画する際の勘所

「モダンなOpenGLでティーポットを描画したい」という動機から、いろいろ調べた結果を残しておきます。シェーダーやglDrawElements*1は一応使えます、という人向けです。

モダンなOpenGL

OpenGL 3.x以降のシャーダーを使うことを前提とした仕様のOpenGLのことを、ここではモダンなOpenGLと呼ぶことにします。ただ、私はOpenGL 3.xは使ったこと無いので、OpenGL 2.xで固定機能を使わなかった場合もモダンに含めることとします。

モダンなOpenGLでは、行列演算からライティングまで全てをユーザが書くことになっています。そのため昔ながらの関数の多くが廃止されています。一応、モダンなOpenGLにもGLUやGLUTはあるようですが、GLUもGLUTも、もはや使う意味があまりないのが実情です*2

ティーポットを描きたい!

前述のとおり、モダンなOpenGLでは、古いglutSolidTeapotのような楽しい関数は基本的には使えないと考えたほうが良いです。なぜ使えないかというと、オリジナルなティーポットの実装*3では、glMapやglEvalMeshなどを使ってベジエ曲線で補完するようなコードになっているためです。残念ながらこれらのベジエ曲線系(OpenGL的に言えばEvaluator)は廃止されています。ちなみに、現在のfreeglut(v1.8)での実装では、Windows CE*4向けに古い実装とは別に頂点属性*5を用意して描画しようとしています。が、これも固定機能を使ってるので、残念ながらモダンな環境では使えません。

では、どうすれば良いのか。ということで、モダンなチュートリアルやWebGL界隈*6を少し調べてみたところ………普通にモデルを読み込んで描画していました。

例えば、WebGLのラッパーの1つであるC3DLでは、チュートリアルの中にteapot.daeというのがありました。".dae"というのはCOLLADAという3Dアセットのためのファイルフォーマットで使われる拡張子です。ライセンスについては特に明記されていないことから、恐らくライブラリと同様にMITライセンスで使って良さそうなので、今回は、このteapot.daeをモダンなOpenGLで描画することにチャレンジします。

COLLADAフォーマット

COLLADAは、初めにSonyが考案し、今はKhronosに公式で認定されているXML準拠のフォーマットです。まずは、以下のスライドを一読するのを強くおすすめします。思想なども含めてCOLLADAに関して詳しく書かれています。

特に、「3Dアセットの交換フォーマット」であるということは理解しておいたほうが良さそうです。

仕様は、案の定、非常に複雑なものとなっているので、全部を理解しようとするのはやめたほうが良いでしょう。フォーマットの主要な部分に関しては、以下のサイトが参考になります。

インデックスの謎*7

COLLADAは、頂点属性情報とインデックスが分離されています。teapot.daeの場合、

  <library_geometries>
    <geometry id="Teapot-mesh" name="Teapot">
      <mesh>
        <source id="Teapot-mesh-positions">
          <float_array id="Teapot-mesh-positions-array" count="1590">29.4787 0 50.5349 ...
        ...
        <source id="Teapot-mesh-normals">
          <float_array id="Teapot-mesh-normals-array" count="1590">-0.966742 0 -0.255753 -0.893014 ...
        ...
        <source id="Teapot-mesh-map-channel1">
          <float_array id="Teapot-mesh-map-channel1-array" count="2400">2 2 0 ...
        ...
        <triangles material="ColorMaterial" count="1024">
          <input semantic="VERTEX" source="#Teapot-mesh-vertices" offset="0"/>
          <input semantic="NORMAL" source="#Teapot-mesh-normals" offset="1"/>
          <input semantic="TEXCOORD" source="#Teapot-mesh-map-channel1" offset="2" set="1"/>
          <p>0 0 0 5 1 5 6 2 6 ...
        ...

triangles要素を見ると、頂点・法線・UVのループで、p要素内にインデックスが並んでいる、といった表現になっています。インデックスの部分を属性ごとに分離すれば、

  • 頂点インデックス:0 5 6 ...
  • 法線インデックス:0 1 2 ...
  • テクスチャインデックス:0 5 6 ...

となります。

さて、ここで疑問となるのが「なぜ属性ごとにインデックスを持っているのか」という点です。ファイルフォーマットの観点からすると、同じ値のデータを減らすことができるので、ファイルサイズを減らすことが出来る、というのは何となく理解できますが、描画という観点からすると少し困った事になります。

というのも、普通にインデックスバッファを利用して描画する事を考えると、各属性のデータ列をVBOで、頂点のインデックスをIBOで送っておき、VBOをbindしてattributeし、頂点のIBOをbindし、glDrawElementsを呼びます。すると、インデックスの順で各属性のデータ列から値が取り出され、シェーダに送られるわけです。これは、各属性のインデックスが統一されていることが前提となっています。

しかし、前述の通り、COLLADAでは属性毎に別のインデックスを持っていることが有ります。ということは、source要素から取り出した生データのままでは、glDrawElementsでの描画は出来ないということです*8

そこで、私なりの解決策としては、以下の2つを考えました。

  1. 頂点インデックスに合うように、他の属性のデータを並び替える。
  2. いっそのこと、インデックスを全て展開して、glDrawArraysで描画してしまう。

1の方法は、考えたはいいのですが、うまく並べ変える方法がパッと思いつかなかったのと、わざわざデータを並べ変えるという事自体が、ひねくれた方法に感じ無くもないです。かといって、2の方法は、単純ですがメモリをバカ食いします*9

また「属性ごとにインデックスを設定できれば万事解決じゃないか」というそもそも論も浮かびました。調べてみたところ、Direct 3D 10以降では、シェーダー側で特定のインデックスのデータを取得することで複数インデックスに対応できるという情報が見つかりました。

OpenGLにも似た機能があるのかなーと思い調べた所、TBO(テクスチャバッファオブジェクト)を利用すればデータサイズに制限があるものの出来ないこともないことが分かりました。

しかし、、、

頼みの綱 StackOverflow先生

やはり日本語では、CG関係の情報はあまり出てきませんので、英語でちまちま検索していたのですが、例に違わずStackOverflowに、ズバリな質問と回答が載っていました。

回答を見ると、

Your best bet is to simply accept that your data will be larger. A great many model formats will use multiple indices; you will need to fixup this vertex data before you can render with it.

(意訳「でっかくなるけど展開しちゃえばいいじゃない。」)

先程の2の方法が推奨されていました。また、

For D3D10/OpenGL 3.x-class hardware, it is possible to avoid performing fixup and use multiple indexed attributes directly. However, be advised that this will likely decrease rendering performance.
...
Please note again that this will decrease overall vertex processing performance. Therefore, it should only be used in the most memory-limited of circumstances, after all other options for compression or optimization have been exhausted.
OpenGL ES does not as of yet offer similar functionality to this.


(意訳「というか、そもそも個別のインデックスバッファ使った描画はパフォーマンス的に不利なんだよねー。だから、よっぽどメモリに制約がある時以外はTBO使って解決するのはやめといたほうがいいよ。OpenGL ESではサポートされてないしねー。」)

ということだそうです*10

コード断片

文章だけというのもアレなので、少しだけコードを載せておきます。ただし、言語はPython(2.7)ですので、あまり参考にならないかもしれません。COLLADAの読み込みにはpycolladaというのを利用しました。

import collada

def create_teapot(teapot_dae_path):
    mesh = collada.Collada(teapot_dae_path)
    triangles = mesh.geometries[0].primitives[0]
   
    def extract(vertices, indices):
        result = [None] * indices.size
        for i in xrange(indices.size):
            result[i] = vertices[indices[i]]
        assert not None in result
        return np.float32(result)
       
    pos = extract(triangles.vertex, triangles.vertex_index.flatten())
    nor = extract(triangles.normal, triangles.normal_index.flatten())
    tex = extract(triangles.texcoordset[0],
        triangles.texcoord_indexset[0].flatten())
    return pos, nor, tex

あとはゴニョゴニョしてglDrawArraysすれば、以下の様な感じに描画できます*11

f:id:s-shin:20130924155932p:plain

テクスチャを張った場合。

f:id:s-shin:20130924155940p:plain

おしまい。

*1:面倒なのでサフィックスは省略。以降同様。

*2:GLUT自体もAPI仕様を定めたものにすぎないので、GLUTの実装によっては使える可能性もあります。現に、freeglutなんかでは、固定機能を回避しようという努力は見られます。実際に使えるかどうかは別ですが。

*3:Silicon Graphicsによる実装

*4:組み込み向けWindows

*5:「頂点属性」と書いたら頂点座標、法線ベクトル、UV座標など頂点に関する色々情報を表すこととします

*6:GLESですが、ほぼモダンな実装が強要されています

*7:実は、この記事で一番書きたかったのは、この項目です。どうにもネットに情報が少ないので悩んでしまいました。

*8:頂点座標だけで描画するなら普通にglDrawElementsを使えます。ネット上には、そういうサンプルも転がったりしているので、余計に迷ってしまいました。

*9:そもそもインデックスバッファは、この手のムダを無くすために導入されたのに、完全に逆行してる

*10:適当に英語は読んでるので違うかも

*11:若干ライティングが、特にスペキュラがおかしい気がする。多分ライティングの理論の理解が間違ってるんだろなぁ。