ModelBatch (コード) は描写呼び出しを管理するために使用されるクラスです。 通常はモデルのインスタンスを描写するのに使われますが、モデルに限定されません。 ModelBatch クラスは全ての描写コードを抽象化し、その上にレイヤーを用意して、あなたがゲーム固有のロジックにより集中できるようにします。 ModelBatch の機能の全ての部分は、設計によってカスタマイズ可能です。
注意:
ModelBatch は描写呼び出しを管理してその結果レンダリングコンテキストも管理しているので、 ModelBatch.begin()
呼び出しとModelBatch.end()
呼び出しの間であなたが手動でレンダリングコンテキストを編集すべきではありません(例えば、シェーダーやテクスチャやメッシュの紐付け、gl
で始める機能の呼び出しなど)。
これは正常に機能せず、予期せぬ動作を引き起こす可能性があります。そうではなく、ModelBatch が提供するカスタマイズオプションを使用してください。
ModelBatch.render()
を呼び出す度に、少なくとも一回の描写呼び出しが発生します。
ModelBatch では、これをカスタマイズして、実際の描写を行う前に視錐台カリングを実行するようにできます。
しかし一般的には、ModelBatch.render()
を呼び出す前に(視錐台)カリングを実行すべきです。
では、 ModelBatch は実際には何をしているのでしょう?
これで終わりです。それ以上でも以下でもありません。そしてこれの全ての部分はカスタマイズ可能です。 この設計思想のため、同一の基本的なタスクを実行する方法が複数あるかもしれないので注意してください。
シェーダーを作成する可能性があるため、ModelBatchは比較的重いオブジェクトです。
可能であれば再利用をするよう心掛けましょう。
通常はcreate()
メソッド内でModelBatchを作成します。
ModelBatchにはネイティブリソース(自身が使用するシェーダーのような)が含まれているので、不要になった時は dispose()
メソッドを呼び出す必要があります。
描写処理は毎フレームごとに行う必要があり、通常はrender()
メソッド内で行います。
描写を開始するには、modelBatch.begin(camera);
を呼び出す必要があります。
次にmodelBatch.render(...);
メソッドを使用して、1つ以上の描写呼び出しを追加します。
描写呼び出しの追加が完了したら、指定した呼び出しを実際に描写するために modelBatch.end();
を呼び出す必要があります。
ModelBatch modelBatch; ... @Override public void create () { modelBatch = new ModelBatch(); ... } @Override public void render () { ... // call glClear etc. modelBatch.begin(camera); modelBatch.render(...); ... // add other render calls modelBatch.end(); ... } @Override public void dispose () { modelBatch.dispose(); ... }
modelBatch.render(...)
の呼び出しは、modelBatch.begin(camera)
と modelBatch.end()
の間でのみ有効です。
実際の描写はend();
を呼び出した時に実行されます。
modelBatch.begin(camera)
と modelBatch.end()
の間で描写を強制実行したい場合は、modelBatch.flush();
メソッドを使用できます。
引数として渡す Camera
は参照によって保持されます。
つまり、 begin 呼び出しと end呼び出しの間でカメラを変更してはいけません。
begin 呼び出しと end呼び出しの間でカメラを切り替える必要がある場合は、modelBatch.setCamera(camera);
を呼び出すことができます。
必要であればbatchのflush()
を実行します。
ModelBatchの目的は描写呼び出しを管理することです。では描写呼び出しとは正確には何のことで、それらはどうやって設定するのでしょうか?
"描写呼び出し" (しばしば "ドローコール"とも言われます)は 、基本的にはGPUへ何かを描写(表示)するよう指示する命令です。 簡単にいれば、各 "描写呼び出し" ではいくつかのプロパティ(例えば、位置や画像や色など)が付いた形状が表示されます。 もしくはもっと正確に言うと、"描写呼び出し"はGPUに対して、渡されたコンテキストの渡されたシェーダーを使って、渡されたメッシュのパーツを描写するよう指示します。 これについては後で詳しく見ていくことにします。
基本的な使い方の場合は、描写呼び出しの詳細についてまで知る必要はありません。 ModelBatchがそれらを抽象化しているからです。 しかし、描写呼び出しはパフォーマンスは影響を与えます。 通常、各描写呼び出しはGPU上で実行されます。つまり、CPUのコードと並行して実行されるのです。 新たに描写呼び出しが実行される度に、GPUとCPUを同期させる必要があるかもしれないのです。 言い換えれば、小さな描写呼び出しが大量にあるのより、大きな描写呼び出しが少しだけある方が、通常はパフォーマンスがよくなるのです。
描写呼び出しを設定するために、libGDXにはRenderable (コード) クラスが実装されています。 これには、一回の描写呼び出しを実行するのに必要なもの(cameraを除く)がほぼ全て含まれています。 基本的には、どのコンテキストで(environment とmaterial)、どのように(シェーダー)、どこに(変換情報)、形状(メッシュパーツ)を描写するのか、といった情報が含まれています。
ModelBatch
の render(...)
メソッドは、たくさんのシグネチャ (メソッド引数の異なるバリエーション)を持っています。
そのうちの1つが ModelBatch.render(Renderable);
です。
これを使うと、ModelBatchに追加したい描写呼び出しを直接指定することができます。
他の render(...)
メソッドでは、RenderableProvider
を使って一つ以上の描写呼び出しを指定できます。
これらのメソッドでは、他の引数を使って描写呼び出しの既定値を渡すことができます。
例えば ModelBatch#render(RenderableProvider, Environment, Shader)
メソッドを使用する時は、RenderableProvider
が提供する全ての Renderable
の environment
メンバと shader
メンバを設定(上書き)します。
RenderableProvider
(コード)は、1つ以上のRenderable
インスタンスを提供するための実装をすることができるインタフェースです。
おそらく、この印てフェースの実装で最も一般的なものは ModelInstance
クラスです。
このクラスは全てのnodes (parts) にまたがり、それらをRenderable
インスタンスに変換します。しかし、必要に応じてRenderableProvider
を使用することができます。
例えばボクセルエンジンを作成している時、各塊ごとに Renderable
を作成できます。
またはエンティティコンポーネントシステムを使用している時、コンポーネントとして RenderableProvider
を使用できます。
RenderableProvider
はメソッドを1つだけ持っています:
public void getRenderables (Array<Renderable> renderables, Pool<Renderable> pool);
これは非常にオープンな API ですが(例えば、これまで追加した全てのRenderable
の Array
にアクセスできます)、配列に要素を追加するだけの使い方にとどめておいた方が良いでしょう。
必要に応じて、割り当てを避けるためにプールを使用できます。
プールを無視するのも、必要に応じて動的 Renderable
にプールを使用するのもあなたの自由です。
obtain()
を使ってRenderableProviderから取得したRenderable
はModelBatch
によって自動的に解放されるので、あなたが解放について気にする必要はありません。
Shader
(コード) は、描写呼び出しを実際に実行するための実装を抽象化するインタフェースです。
通常この実装ではa ShaderProgram
を使用します。ShaderProgram
とは描写呼び出しを実行するために必要な GPU プログラム(例えば the vertex shader と fragment shader)です。
Shader
の実装にも、このShaderProgram
を使うのに必要なもの(uniform値の設定のような)が全て含まれています。
実際にどのような描写を行っているかに関わらず、ModelBatch ではRenderable
ごとに1つのShader
が必要です。
このため、ModelBatchは ShaderProvider
(コード) インタフェースを使用します。
batch に追加された全てのRenderable
ごとに(例えシェーダーが含まれていたとしても)、ShaderProvider
のgetShader
が呼び出されてシェーダーを取得してRenderable
を描写します。
public Shader getShader (Renderable renderable);
既定では、DefaultShaderProvider (code)が使用されます。
これにより、既に作成したシェーダーが再利用できない場合は常にDefaultShaderが作成されます。
しかし、独自のShaderProvider
を渡すかDefaultShaderProvider
を拡張することでこれをカスタマイズできます。
ModelBatch
は Shader
の管理を ShaderProviders
へ委譲します。
Shader
は通常 ShaderProgram
を使用しているので、不要になったら破棄をする必要があります。
modelBatch.dispose();
が呼び出された時、ModelBatch
は ShaderProvider
のdispose()
メソッドを呼び出します。
シェーダーの管理と再利用を手助けするため、libGDX には抽象クラス BaseShaderProvider
(コード)が用意されています。
このクラスは作成された全てのシェーダーを追跡し、可能であればそれらを再利用し、不要になったらそれらを破棄します。
このクラスを継承すると、再利用できるシェーダーがない場合にはcreateShader(Renderable)
メソッドが呼び出されます。
Shader
を再利用できるかどうかはshader.canRender(Renderable)
を呼び出すことで分かります。
一般的な使用例は、 DefaultShaderProvider
( BaseShaderProviderを継承している)を継承し、必要に応じてカスタムシェーダーを渡し、カスタムシェーダーが使用できない場合はDefaultShader へ切り戻すというものです。
public static class MyShaderProvider extends DefaultShaderProvider { @Override protected Shader createShader (Renderable renderable) { if (renderable.material.has(CustomColorTypes.AlbedoColor)) return new MyShader(renderable); else return super.createShader(renderable); } }
上記コードでは Material を使ってカスタムシェーダーを使用するかどうか決めています。
これが推奨される最も簡単な方法です。
しかし、渡されたrenderableでのshader.canRender(renderable)
メソッドがtrue を返すのであれば、汎用のrenderable.userData
を含む任意の値を使って、どのシェーダーを使用するか決めることができます。
カスタムShaderProvider
を指定しない時、 ModelBatch
は DefaultShaderProvider
を使用します。
DefaultShaderProvider
は必要な場合は新規にDefaultShader
のインスタンスを作成します。
DefaultShader
クラスでは標準的なmaterial と environment の属性(lightingと normal mapsと reflection cubemapsなどを含む)のほとんどのデフォルト実装が用意されています。
つまり:属性値と対応するuniform
値を紐付けます。
ユニフォーム変数名の一覧はここで見ることができます。
メモ: 既定では、シェーダープログラム ( glsl ファイル) は頂点ごとの照明 (グーローシェーディング)を使用します。 Normal mappingや reflection などは既定では適用されません。
このクラスの動作は、DefaultShader.Config
インスタンスを DefaultShaderProvider
に渡すことで設定できます。
DefaultShader.Config config = new DefaultShader.Config(); config.numDirectionalLights = 1; config.numPointLights = 0; config.numBones = 16; modelBatch = new ModelBatch(new DefaultShaderProvider(config));
メモ: 既定の設定が各ユースケースにとって最適になっていることはめったにありません。 例えば、あなたが point lightを1つとdirectional lightを1つだけ使用している場合でさえ、既定の設定では point lightを5つとdirectional lightを2つを使用するのです。 DefaultShader をあなた固有のユースケースに合わせて調整し、最大限活用してください。 スキン処理を使用している場合、ボーンの数はモデルと一緒に作成されたボーンの数と一致しなければなりません。
使用するGPU シェーダー (頂点シェダーとフラグメントシェーダー)もこのconfigを使って設定できます。
このシェーダーは属性の様々な組み合わせに使用できるので、通常は ubershaderと呼ばれています。
これは、プリプロセッサマクロを使って、Renderable
に基づいてパーツを有効や無効にする、シェーダーglsl コードです。
例:
#ifdef blendedFlag gl_FragColor.a = diffuse.a * v_opacity; #ifdef alphaTestFlag if (gl_FragColor.a <= v_alphaTest) discard; #endif #else gl_FragColor.a = 1.0; #endif
上記では、シェーダーで実際に使用されるコードはblendedFlag
と alphaTestFlag
が定義されているかどうかによって変わります。
DefaultShader
クラスは
Renderable
の値に基づいてこれらを定義します。
カスタム ubershaderを指定しない場合、既定の ubershader が使用されます (vertex shader と fragment shaderのソースを参照してください)。 このシェーダーはほとんどの基本的な属性をサポートしていますが (スキン処理やディヒューズや頂点ごとの鏡面照明などのような)、 非常に汎用的すぎて、考えられる全ての属性の組み合わせをサポートすることはできません。
フラグメントごとの照明や法線マッピングやリフレクションを使用したい場合は、この "非公式" シェーダーを使用できます。 しかしこのシェーダーは、directional lightが1つに制限されるといったことなどと引き換えにこの機能を追加しているので、注意してください。
既定のubershaderのソースを見るとおそらく、そのコード量は膨大でコードを管理するどころか読むことすらほぼ不可能であることに気づくでしょう。
幸いなことに考えられる全ての属性の組み合わせをサポートする必要はなく、前回の段落で見たようにDefaultShaderProvider
を継承して複数のシェーダーに分割し、管理を簡単にすることができます。例えば:
public static class MyShaderProvider extends DefaultShaderProvider { DefaultShader.Config albedoConfig; public MyShaderProvder(DefaultShader.Config defaultConfig) { super(defaultConfig); albedoConfig = new DefaultShader.Config(); albedoConfig.vertexShader = Gdx.files.internal("data/albedo.vertex.glsl").readString(); albedoConfig.fragmentShader = Gdx.files.internal("data/albedo.fragment.glsl").readString(); } @Override protected Shader createShader (Renderable renderable) { if (renderable.material.has(CustomColorTypes.AlbedoColor)) return new DefaultShader(renderable, albedoConfig); else return super.createShader(renderable); } }
ランダムな順番で描写呼び出しが実行された場合、描写がおかしくなる上にパフォーマンスが低下します。 例えば、透明なオブジェクトがその後ろにあるオブジェクトよりも先に描写された場合、後ろにあるオブジェクトが見えなくなります。 これは、深度バッファによってオブジェクトの描写が妨げられるためです。 描写呼び出しを並び替えることでこれを解決できます。
既定では、ModelBatch
はDefaultRenderableSorter (コード) を使って描写呼び出しを並び替えます。
この実装ではまず不透明なオブジェクトが前から後ろにある順に描写され、その後に透明のオブジェクトが後ろから前にある順番で描写されます。
オブジェクトが透明かどうかを判断するため、既定の実装では BlendingAttribute#blended 値を確認します。
並び替えをカスタマイズするとパフォーマンスが向上します。
例えば、シェーダーやメッシュが使用するテクスチャに基づいて並び替えをすると、シェーダーやメッシュやテクスチャの切り替えを減らすのに貢献します。
この種の最適化は、非常にアプリケーション固有のものになります。
ModelBatch
のインスタンスを作成する時に独自のRenderableSorter
(コード) 実装を指定することで並び替えをカスタマイズできます。このインタフェースはメソッドを1つだけ持っています:
public void sort (Camera camera, Array<Renderable> renderables);
このメソッドは、ModelBatch
が実際に描写する直前に持っている全ての情報を引数として渡します。
これも非常にオープンな APIで、必要に応じて配列を編集することができます。
このインタフェースを使って、ラスト数分のアクション(視錐台カリングのような、並び替えとは関係ないものでさえ)を実行することが可能です。
このメソッドが完了した後の renderables
の順番が、実際に描写呼び出しが実行される順番になります。
ModelBatch
を使うことで、複数のShader
実装にまたがるテクスチャの紐付けといった、冗長なOpenGL呼び出しを避けることができます。
例えば Shader
で背面カリング処理が必要でなおかつ前回のシェーダーで背面カリング処理が有効になっている場合は、 glEnable
とglCullFace
の冗長な呼び出しを避けることができます。
RenderContext
クラスはこうした不要な呼び出しを避けようとします。
このクラスはOpenGL ES上の薄いレイヤーとして動作して以前まで呼び出しを追跡し続け、冗長な呼び出しが発生するのを防ぎます。
GL呼び出しの小さなサブセットのみ実装されていますが、それを継承して追加の呼び出しを追加できます。
コンストラクタに引数として設定されなかった時は、 ModelBatch
があなたに代わってRenderContext
を作成して管理します。
注意: 一目瞭然ですが、これはShader
の実装がRenderContext
を使っている場合のみ動作します。
GL呼び出しを直接呼び出している場合は動作しません。
可能であれば、対応するGL呼び出しを直接実行するのではなく、常に RenderContext
を使用してください。
例: RenderContextを使って深度テストを有効にした場合、RenderContextがあなたに代わって深度テストを有効にします。
次に、例えばSpriteBatchを使用する場合、SpriteBatchは深度テストを無効にしてもRenderContextの更新まではしません。
これにより予期せぬ結果が引き起こされます。
これを避けるために、既定では (あなたが自分で RenderContext を指定しなかった場合は)ModelBatchは begin()
メソッドとend()
メソッドの両方で、RenderContextの同名のメソッドを呼び出すことでRenderContextをリセットします。
これは、ModelBatch外のコンテキストの切り替えが描写を妨げないようにするためです。
しかし、独自のRenderContext
(その実装をカスタムする必要はありません) を指定した場合は、
あなたが責任を持ってcontext.begin()
メソッドと context.end()
メソッドを呼び出す必要があります。
これにより複数のModelBatch インスタンスで同じコンテキストを使用することができ、コンテキストを全て一緒にリセットする必要さえなくなります。
インスタンス作成時にRenderContext を指定した場合、あなたがRenderContext を保持しているので必要に応じてあなたがリセット (begin() メソッドとend() メソッドの呼び出し) を行うことが求められます。 modelBatch.ownsRenderContext メソッドを呼び出した、RenderContextの保持と管理をModelBatch が行っているかどうかを確認できます。
現在紐付けされているテクスチャを追跡し続けるため、 RenderContext は textureBinder メンバを持っています。
TextureBinder
(コード) は、
テクスチャの紐付けとテクスチャコンテキスト (例えば minification/magnification フィルタ)を追跡し続けるのに使用されるインタフェースです。
既定ではDefaultTextureBinder
(コード)が使用されます。
独自の実装を指定することもできますが、そのようなことはめったに必要ありません。
DefaultTextureBinder は全ての使用可能な(指定した範囲内の)テクスチャユニットを使って不要なテクスチャの紐付けを防ぎます。
一般的な最新の携帯用GPU では、だいたい16個か32個のテクスチャユニットにテクスチャを紐付けることができます。
OpenGL ES ではユニット数は 32個に制限されます。
offset
引数と count
引数を使って、DefaultTextureBinderのインスタンス作成時に使用する範囲を指定できます。
offsetを指定しない場合、0を指定したと見なされます。
count を指定しない場合、使用可能な残り全てのユニットが使用されます。
既定では、ModelBatch は0番のテクスチャユニットを範囲から除外します。0番はGUIでよく使用されるためです。
そのためGPU がサポートするテクスチャユニットが少ない場合を除き、
既定では 1
番から 31
番までのテクスチャユニットが 使用されます。
DefaultTextureBinder
は二つのメソッドをサポートしています:
既定では、 ModelBatch は WEIGHTED のやり方を使用します。
bind(...)
メソッドを呼び出すことで、 TextureBinder
を使ってテクスチャを紐付けることができます。
このメソッドはテクスチャが紐付けられたユニットを戻り値として返します。
そのため実際には、以下のようにしてShader
のテクスチャとuniformを紐付けることができます:
program.setUniformi(uniformLocation, context.textureBinder.bind(texture));
テクスチャを指定する時、この api ではしばしば TextureDescriptor
を使用します。
これは、テクスチャを使用したいが特定のコンテクストプロパティが必要、といった場合があるためです。
こうしたプロパティには、現時点ではminification filter とmagnification filter と horizontal wrapping と vertical wrapping があります。
そのため、 TextureDescriptor は例えばTextureAttribute や CubemapAttributeでも使用されます。
利便性のために、TextureBinderではTextureDescriptorを直接指定することができます:
program.setUniformi(uniformLocation, context.textureBinder.bind( (TextureAttribute)(renderable.material.get(TextureAttribute.Diffuse))) .textureDescription));