このチュートリアルの 第一部では、 LibGDX 3D APIのModelクラスの全体構造を見てきました。 この第二部では、モデルの読み込みからモデルを実際に描写するまでの、レンダリングパイプラインを見ていきます。 レンダリングパイプラインの各固有の部分については深く説明しません。 3D apiを使う際に知っておくべき非常に基本的な部分を見ていくだけです。
このチュートリアルの全 ソース、 素材、実行可能なテストはこの github リポジトリにあります。
このページでは実際の描写について見ていきます。我々が実際に何を描写しているのかを理解することは重要です。 前回はそれについて見てきました。 Modelはノードで構成され、ノード自身はノードパーツで構成されています。 NodePartはModelの最小パーツで、どのように描写するかに関する全ての情報を保持しています。 ノードパーツにはMeshPartが含まれています。MeshPartは何を(どんな形状を)描写するのかの情報を持ち、どのように描写するかを決めるMaterialを持っています。 このページを呼んでいる間は、その内容を頭に留めておいてください。
libGDXでシーンを読み込む のチュートリアルから説明を再開します。 コードを読み解いて、シーンの舞台裏を見ることにします。 そのため、作業をするためにバックアップやコピーを取っておいた方が良いかもしれません。 今回参照するのは以下のコードです。:
public class BehindTheScenesTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public ModelBatch modelBatch;
public AssetManager assets;
public Array<ModelInstance> instances = new Array<ModelInstance>();
public Environment environment;
public boolean loading;
public Array<ModelInstance> blocks = new Array<ModelInstance>();
public Array<ModelInstance> invaders = new Array<ModelInstance>();
public ModelInstance ship;
public ModelInstance space;
@Override
public void create () {
modelBatch = new ModelBatch();
environment = new Environment();
environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));
cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
cam.position.set(0f, 7f, 10f);
cam.lookAt(0,0,0);
cam.near = 1f;
cam.far = 300f;
cam.update();
camController = new CameraInputController(cam);
Gdx.input.setInputProcessor(camController);
assets = new AssetManager();
assets.load("data/invaderscene.g3db", Model.class);
loading = true;
}
private void doneLoading() {
Model model = assets.get("data/invaderscene.g3db", Model.class);
for (int i = 0; i < model.nodes.size; i++) {
String id = model.nodes.get(i).id;
ModelInstance instance = new ModelInstance(model, id);
Node node = instance.getNode(id);
instance.transform.set(node.globalTransform);
node.translation.set(0,0,0);
node.scale.set(1,1,1);
node.rotation.idt();
instance.calculateTransforms();
if (id.equals("space")) {
space = instance;
continue;
}
instances.add(instance);
if (id.equals("ship"))
ship = instance;
else if (id.startsWith("block"))
blocks.add(instance);
else if (id.startsWith("invader"))
invaders.add(instance);
}
loading = false;
}
@Override
public void render () {
if (loading && assets.update())
doneLoading();
camController.update();
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
modelBatch.render(instances, environment);
if (space != null)
modelBatch.render(space);
modelBatch.end();
}
@Override
public void dispose () {
modelBatch.dispose();
instances.clear();
assets.dispose();
}
@Override public void resume () {}
@Override public void resize (int width, int height) {}
@Override public void pause () {}
}
上記コードではAssetManagerを使ってほとんどの場面で最適なModelを読み込んでいます。 しかし、場合によっては読み込み処理をもっと制御したいかもしれません。 したがってこのチュートリアルではAssetManagerを削除します:
public class BehindTheScenesTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public ModelBatch modelBatch;
public Model model;
public Array<ModelInstance> instances = new Array<ModelInstance>();
public Environment environment;
...
@Override
public void create () {
...
Gdx.input.setInputProcessor(camController);
ModelLoader modelLoader = new G3dModelLoader(new JsonReader());
ModelData modelData = modelLoader.loadModelData(Gdx.files.internal("data/invaderscene.g3dj"));
model = new Model(modelData, new TextureProvider.FileTextureProvider());
doneLoading();
}
private void doneLoading() {
for (int i = 0; i < model.nodes.size; i++) {
...
}
}
@Override
public void render () {
camController.update();
...
}
@Override
public void dispose () {
modelBatch.dispose();
instances.clear();
model.dispose();
}
...
}
上記コードでは、AssetManagerを削除してその代わりに手動でモデルを読み込んでいます。したがって、モデルの読み込みが完了した後すぐに doneLoading()メソッドを呼び出しています。 また、invaderscene.g3dbファイルの代わりに前回のパートで作成したinvaderscene.g3djファイルを読み込みます。 では、そのファイルをプロジェクトのassetsフォルダー内にあるdata フォルダにコピーしてください。 それでは、実際の読み込み部分について見てください。:
ModelLoader modelLoader = new G3dModelLoader(new JsonReader());
ModelData modelData = modelLoader.loadModelData(Gdx.files.internal("data/invaderscene.g3dj"));
model = new Model(modelData, new TextureProvider.FileTextureProvider());
ModelLoaderを作成します。これは以前のlibGDXを使ったモデルの読み込みで見たものです。 しかし以前に使用したObjLoader ではなく、今回はG3dModelLoaderを作成しています。 invaderscene.g3dj のファイル形式はjsonなので、コンストラクタには引数としてJsonReader を渡しています。 g3db ファイルの場合は UBJsonReaderを使用できます。
次に ModelDataを読み込みます。ModelData クラスには生のモデルデータが含まれています。 これは基本的には、前回のパートで見たファイル形式を一対一で表したものです。 リソースは含まれていません。 Mesh の代わりにfloat型とshort型の配列を含んでいるだけで、テクスチャの場合は実際のテクスチャの代わりにファイル名を含んでいるだけです。 そのためこの段階では、Model クラスやリソースを考慮する必要がなく好きなようにデータを処理できます。
最後に、create() メソッド内の最後の行で、読み込んだModelData を使ってModel のコンストラクタを実行します。 また、引数としてTextureProvider を追加しています。これでテクスチャを内部ファイルとして読み込みます。 テクスチャの読み込みをさらに制御したい場合は、TextureProvider インタフェースを実装できます。 AssetManager を使用してModelを読み込む場合、AssetManager はテクスチャを読み込むのにも使用されます。 Model とModel のリソース(メッシュやテクスチャなど)が読み込まれたら、Model はそれらを破棄する責任も負います。
今度はマテリアルについて見てみましょう:
private void doneLoading() {
Material blockMaterial = model.getNode("block1").parts.get(0).material;
ColorAttribute colorAttribute = (ColorAttribute)blockMaterial.get(ColorAttribute.Diffuse);
colorAttribute.color.set(Color.YELLOW);
for (int i = 0; i < model.nodes.size; i++) {
...
}
}
最初の行では、modelのblock1 ノードを呼び出し(このmodel内にblock1 ノードがあるのは既に知っていると思いますが)、そのノードの一つ目のノードパーツを呼び出し、そしてそのノードパーツのマテリアルを取得しています。 これは全てのblock ノードで共有される "block_default1"マテリアルへの参照だということを前回のパートで見てきました。 そのため、その参照を変更すると全てのblock のマテリアルが変更されます。 二行目では、マテリアルのDiffuse ColorAttributeを呼び出しています(このマテリアル内にDiffuse ColorAttributeがあるのも既に知っていると思いますが)。 そして最後に、属性の色を黄色に変更します。
しかし、このコードだと意味を理解するために実際のモデルに関する多くに知識が必要になるので、別の書き方をしてみましょう:
private void doneLoading() {
Material blockMaterial = model.getMaterial("block_default1");
blockMaterial.set(ColorAttribute.createDiffuse(Color.YELLOW));
for (int i = 0; i < model.nodes.size; i++) {
...
}
}
同じ結果になりますが、前のコードと違って今回はIDを使ってマテリアルに直接アクセスしています。 そして現在のディフューズ色の取得は行わずに、ディフューズ色の設定のみを行っています。 そのためマテリアルにディフューズ色が設定されていない場合はそのまま追加され、ディフューズ色が設定されている場合は上書きがされます。
Model のマテリアルを変更すると、その変更後に作成される全てのModelInstanceに影響を及ぼします。 インスタンスごとにマテリアルを変更することもできます:
private void doneLoading() {
for (int i = 0; i < model.nodes.size; i++) {
...
}
for (ModelInstance block : blocks) {
float r = 0.5f + 0.5f * (float)Math.random();
float g = 0.5f + 0.5f * (float)Math.random();
float b = 0.5f + 0.5f * (float)Math.random();
block.materials.get(0).set(ColorAttribute.createDiffuse(r, g, b, 1));
}
}
このModelInstance に付けられているマテリアルを一つしかないので、ノードを介したりIDを使用したり(IDでも取得可能です)してマテリアルを取得するのではなく、 今回はmaterials配列の最初にあるマテリアルを取得しているだけです。
前回はG3DJ ファイルを調べることでModel クラスの構造を見てきました。 今回は ModelInstance クラスを見てみましょう。
public class ModelInstance implements RenderableProvider {
public final Array<Material> materials = new Array<Material>();
public final Array<Node> nodes = new Array<Node>();
public final Array<Animation> animations = new Array<Animation>();
public final Model model;
public Matrix4 transform;
public Object userData;
...
}
Model と同じ様に、ModelInstance にもmaterials 配列とnodes配列 とanimations 配列が含まれています。 これらはModelInstanceの作成時にModel からコピーされており、これらの値を変更しても同じModelから作成された他のModelInstanceには影響を与えません。 ModelInstance作成時にノードIDを設定した場合、そのノードに影響を与えるmaterials とanimations のみがコピーされます。 そのためblock のModelInstanceの時と同様に、指定したblock ノードには1つ目のマテリアルが常に影響を与えるということは知っての通りです。
Model クラスと違い、 ModelInstance は Mesh の配列と MeshPart の配列を持っていないので注意してください。 それらはコピーされませんが、代わりにNode (NodePart)を介して参照します。 したがって、メッシュは複数のmodel インスタンスで共有されます。 これはマテリアルが保持しているテクスチャにも当てはまります。
ModelInstance クラスの modelフィールドは、作成時に使用されたModel への参照です。 transformフィールドは、このModelInstanceの位置情報と回転情報とスケール情報を表します。 以前のシーンを読み込むチュートリアルでそれを見てきました。 このフィールドはfinal定義ではないので、必要な場合は別のMatrix4 値を設定することができるので覚えておいてください。 最後に、userData フィールドはユーザーで定義可能な値で、必要なものを自由に設定できます。 例えば、この値を使って追加の指示をシェーダーに与えることができます(詳細は後で説明します)。
ModelInstance は RenderableProvider インタフェースを実装しているのことに注目してください。
modelBatch.render(instance, lights);
を呼び出す時、 ModelBatch は実際にはModelInstanceではなくRenderableProvider が引数として渡されることを想定しているのです。
RenderableProvider を実装しているクラスは、複数の Renderable オブジェクトを ModelBatchへ渡します。
Renderable クラスを見てみましょう:
public class Renderable {
/** the model transform **/
public final Matrix4 worldTransform = new Matrix4();
/** The MeshPart that contains the shape to render **/
public MeshPart meshPart;
/** the material to be applied to the mesh **/
public Material material;
/** the bones transformations used for skinning, or null if not applicable */
public Matrix4 bones[];
/** the Environment to be used to render this Renderable, may be null **/
public Environment environment;
/** the Shader to be used to render this Renderable, may be null **/
public Shader shader;
/** user definable value. */
public Object userData;
}
以前にも見たように、NodePart は設定するModel の最小パーツで、Model をどのように描写するかを指定しています。 NodePart は MeshPart と Materialを持っています。 Renderableも、渡された transformと lightsと shader とuserDataに加えてそれらの値を持っています。 そのため、ModelBatch.render(ModelInstance)を呼び出した時、ModelInstance 内の全てのノードパーツはRenderable インスタンスに変換されてModelBatchへ渡されます。 では、それを手動で行ってみましょう:
public class BehindTheScenesTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public ModelBatch modelBatch;
public Model model;
public Environment environment;
public Renderable renderable;
@Override
public void create () {
...
cam.position.set(2f, 2f, 2f);
...
model = new Model(modelData, new TextureProvider.FileTextureProvider());
NodePart blockPart = model.getNode("ship").parts.get(0);
renderable = new Renderable();
renderable.meshPart.set(blockPart.meshPart);
renderable.material = blockPart.material;
renderable.environment = environment;
renderable.worldTransform.idt();
}
@Override
public void render () {
camController.update();
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
modelBatch.render(renderable);
modelBatch.end();
}
@Override
public void dispose () {
modelBatch.dispose();
model.dispose();
}
...
}
上記コードでは前回と同じ様に invaderscene.g3djを読み込んでますが、今回はship Node内の一番目にあるNodePart を取得しています。 NodePart からRenderableを作成し、各値にNodePartの対応する値を設定します。 またRenderable の光源を設定し、位置は(0,0,0)で回転はせず拡大縮小もしないということを意味する識別子をworldTransform に設定します。 ModelInstancesは削除して、今回は代わりにrender メソッド内でrenderable をModelBatchへ渡すだけです。 またカメラを少し原点へ近づけてもっと良く見えるようにしました。
ModelInstance はrenderableを渡す処理を司り、ModelBatch はRenderable インスタンスを描写する処理を司ります。 しかし、ModelBatch はそれらの描写を実際にはしません。 代わりにそれらが最も最適化された順番で描写されるように並び替えを行い、renderableのシェーダーへ渡します。 シェーダーが引数として渡されていない場合(または渡されたシェーダーが適切でない場合)、代わりにModelBatch がシェーダーを作成します。 必要なシェーダーを ShaderProvider に要求することによってこれを行います。 今のところこれ以上は説明しませんが、ModelBatchのインスタンスを作成する際は独自に拡張したShaderProvider を渡すことができるので覚えておいてください。
そして、 Shader は Renderableの描写を司っています。渡されたRenderableを描写するのに必要なことは全て行います。 その名前からは分かりませんが、ShaderはOpenGL ES 1.x系のシェーダーです。 OpenGL ES 2.0 の場合、 ShaderProgram をカプセル化して、渡されたRenderableに基づいてユニフォーム変数と属性を設定します。
public class BehindTheScenesTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public Shader shader;
public RenderContext renderContext;
public Model model;
public Environment environment;
public Renderable renderable;
@Override
public void create () {
environment = new Environment();
...
model = new Model(modelData, new TextureProvider.FileTextureProvider());
NodePart blockPart = model.getNode("ship").parts.get(0);
renderable = new Renderable();
renderable.meshPart.set(blockPart.meshPart);
renderable.material = blockPart.material;
renderable.environment = environment;
renderable.worldTransform.idt();
renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1));
shader = new DefaultShader(renderable);
shader.init();
}
@Override
public void render () {
camController.update();
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
renderContext.begin();
shader.begin(cam, renderContext);
shader.render(renderable);
shader.end();
renderContext.end();
}
@Override
public void dispose () {
shader.dispose();
model.dispose();
}
...
}
上記コードでは、 ModelBatch を削除して RenderContext と Shaderを追加しています。 A RenderContext keeps tracks of the OpenGL state to eliminate state switching between shader switches. 例えば、テクスチャが既に紐付けされている場合、それはもう紐付け処理を行う必要はありません。 どのテクスチャがどのテクスチャユニットに紐付けされているかのトレースを保持しているDefaultTextureBinder を使ってRenderContext のインスタンスを作成し、 テクスチャユニットの再利用をすることでテクスチャの紐付けを省くようにします。 次に、DefaultShader に引数renderable を渡してshader のインスタンスを作成します。 DefaultShader は OpenGL ES 2.0 のシェーダーのため、これを動作させるには GLES20 を有効にする必要があることを覚えておいてください。
render メソッド内では renderContex.begin() を呼び出し、コンテクストを初期状態にします。 次に shader.beginを呼び出して、描写を開始するために必要な処理をshader へ指示します。 これはプロジェクションマトリクスprojection matrixなどのようなグローバルユニフォーム変数を設定します。 それからshaderを使ってrenderable を描写します。 そして最後に shader.end と renderContext.endを呼び出してこれらをクリーンアップします。
要約すると: