This blog is a chinese version of xoppa's Libgdx new 3D api tutorial. For English version, please refer to >>LINK<<
在本教程的第一部分,我们已经看过LibGDX 3D API中Model类的总体结构。在第2部分中,我们将会分析渲染管道,从加载模型开始,到真正的渲染模型。我们将不会在渲染管道的某个问题上进行深入探讨。我们只会介绍一些非常基本的内容,这是我觉得你使用3D API时,应该了解的。
在这一部分,我们要分析渲染究竟做了什么。明白我们在渲染时所做的事很重要。在前一部分本教程,我们已经看到,一个Model是由很多个Node组成,而Node由NodePart组成。一个NodePart是组成模型最小的部分,包含了所有在渲染时所需要的信息。它包含一个MeshPart,描述要渲染什么(形状),它包含一个Material,描述应该如何渲染。阅读这一部分教程时,一定要记得这些概念。
我们以Loading a scene with Libgdx的教程为基础(参考译文:使用Libgdx加载3D场景)。我们需要拆开代码,来看一看场景后面的实际情况,因此,你可以需要备份,或复制一份以进行新的工作,这里给出参考代码:
public class SceneTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public ModelBatch modelBatch;
public AssetManager assets;
public Array<ModelInstance> instances = new Array<ModelInstance>();
public Lights lights;
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();
lights = new Lights();
lights.ambientLight.set(0.4f, 0.4f, 0.4f, 1f);
lights.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 = 0.1f;
cam.far = 300f;
cam.update();
camController = new CameraInputController(cam);
Gdx.input.setInputProcessor(camController);
assets = new AssetManager();
assets.load("data/invaders.g3db", Model.class);
loading = true;
}
private void doneLoading() {
Model model = assets.get("data/invaders.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);
for (ModelInstance instance : instances)
modelBatch.render(instance, lights);
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 () {}
@Override public void dispose () {}
}
在代码中,我们通过AssetManager来加载Model,在大多数情况下,这都是最好的办法。但有时,你可能需要对加载过程有更多的控制。所以,这次我们把AssetMnager删掉。
public class SceneTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public ModelBatch modelBatch;
public Model model;
public Array<ModelInstance> instances = new Array<ModelInstance>();
public Lights lights;
...
@Override
public void create () {
...
Gdx.input.setInputProcessor(camController);
ModelLoader modelLoader = new G3dModelLoader(new JsonReader());
ModelData modelData = modelLoader.loadModelData(Gdx.files.internal("data/invaders.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,并通过手动的方式来加载Model,所以,我们在Model加载完成后,调用了doneLoading()。我们这里还是调用了在之前教程中创建的invaders.g3dj,而不是invaders.g3db。所以,确保你把这个文件拷贝到了项目里的assets文件夹中。现在看一下加载部分代码:
ModelLoader modelLoader = new G3dModelLoader(new JsonReader());
ModelData modelData = modelLoader.loadModelData(Gdx.files.internal("data/invaders.g3dj"));
model = new Model(modelData, new TextureProvider.FileTextureProvider());
我们创建了一个ModelLoader,这个在之前的使用Libgdx加载3D场景一讲中已经用到过。但我们不再使用ObjLoader,而是创建G3dModelLoader。我们传一个JsonReader参数给构造函数,因为invader.g3dj就是一个json文件。如果是g3db,那你可以使用UBJsonReader。
然后,加载ModelData。ModelData类中,包含了模型的原始数据,本质上,这个类与我们之前分析过的文件格式是一一对应的。它不含有任何源文件,如它有一个float数组来表示Mesh,用文件名来指定纹理,而不是包含文件本身。所以,现阶段,不管Model class,或其他资源文件如何,你都可以随意修改它。
结尾,在create()方法的最后一行,我们通过刚刚得到的ModelData,创建了一个Model对象。我们还传进去一个TextureProvider参数,我们要用它来加载纹理文件。想要更多的控制加载过程,你可以自己实现TextureProvider接口。如果通过AssetManager来加载模型,那加载纹理也可以通过AssetManager。现在Model和它的资源,如Meshes和Textures,都已经加载了。还有,Model也可用于最后的资源回收(disposing)。
现在来看看Materials怎么玩:
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++) {
...
}
}
第一行,我们得到了模型的block1节点(Node),这是我们已知的。得到它的第一个node-part,与它的material。我们在前几章看到过,这个material其实就是block_default1的一个引用,而它会被所有的block节点共享使用。所以,改变这个值,所有的block就都跟着变了。第二行,我们拿到material的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直接得到了material。并且,我们没有去得到当前的diffuse color值,而只是设置。这样,如果这个材质里没有这个属性,就添加上去,如果有,就覆盖。
改变模型材质,会影响到改变后创建的ModelInstance。你可以为每一个instance做改变:
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));
}
}
没有通过节点,也没有通过ID,我们只是拿到第一个材质,因为ModelInstance也需要指定一个Material:
之前,通过查看G3DJ文件,我们看过Model的结构了,现在来看看ModelInstance class.
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差不多,它有Material,Node,和Animation的数组各一个。这些是在构建ModelInstance对象时,从Model对象中复制过来的,这样,你在改变ModelInstance的时候,不会影响到Model对象。若你在创建ModelInstances的时候,指定了Node ID,将仅有指定的material和animation,被复制到ModelInstance中,也仅作用于这一个ModelInstance对象。因此,好像我们已经创建的Block ModelInstance对象一样,我们知道,第一个material,只会对指定的block node有影响。
注意,与Model类不同的地方,ModelInstance不包括Mesh和MeshPart数组。这些没被复制过来,取而代之的是指定Node(NodePart)的引用。所以,Meshes中的信息是被多个Model Instances共享的。对于材质中可能包含的纹理也是这样。
在ModelInstance类中的Model,是在创建ModelInstance时建立的指向Model的一个引用。transform代表了这个ModelInstance的position(位置), rotation(旋转), 和scale(缩放)信息。这些知识,在之前加载场景的那一篇教程中都看过了。值得一提的是,这个值不是final的,所以,需要的话,你可以指定一个Matrix4引用。最后,userData是一个用户可以自定义的值,设置任何你想要的数据在这里。比如,你可以为你的shader放一些instructions.(我对shader不了解,不会译咯: supply extra instructions to your shader)。
ModelInstance是由RenderableProvider接口实现而来。当我们调用modelBatch.render(instance, lights)时,ModelBatch看的是这是不是一个RenderableProvider对象,而不是ModelInstance。任何一个继承于RenderableProvider的类,都可以提供可渲染的对象给ModelBatch。看一看 Renderable 类:
public class Renderable {
/** the model transform **/
public final Matrix4 worldTransform = new Matrix4();
/** the mesh to render **/
public Mesh mesh;
/** the offset into the mesh's indices **/
public int meshPartOffset;
/** the number of indices/vertices to use **/
public int meshPartSize;
/** the primitive type, encoded as an OpenGL constant, like {@link GL20#GL_TRIANGLES} **/
public int primitiveType;
/** 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 lights to be used to render this Renderable, may be null **/
public Lights lights;
/** the Shader to be used to render this Renderable, may be null **/
public Shader shader;
/** user definable value. */
public Object userData;
}
对比以前看到的NodePart,它是是一个模型最小的单位,描述了应该怎么渲染这个模型,模型含有一个MeshPart和一个Material。而Renderable对象,也有这三个值,同时,还定义了transform, lights, shader和userData。所以,当你调用ModelBatch.render(ModelInstance)时,这个ModelInstance中所有的node parts都会被转换成Renderable实例,并传送给ModelBatch. 我们手动实现一下:
public class RenderableTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public ModelBatch modelBatch;
public Model model;
public Lights lights;
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.mesh = blockPart.meshPart.mesh;
renderable.meshPartOffset = blockPart.meshPart.indexOffset;
renderable.meshPartSize = blockPart.meshPart.numVertices;
renderable.primitiveType = blockPart.meshPart.primitiveType;
renderable.material = blockPart.material;
renderable.lights = lights;
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();
}
...
}
这里,我们像以前一样,加载了,invaders.g3dj,但我们要取得ship节点的第一个NodePart。通过它,我们创建了一个Renderable对象,设定了一系列相应的值。同时我们设定了light,将worldTransform的位移设定回了原点。没有旋转,和缩放。我们移除了ModelInstances,取而代之的是,在render方法中,我们只将renderable对象,传给了ModelBatch。我把镜头向原点拉近了一些,可以看得清楚些。
一个ModelInstance限制了它所包含的renderable对象,而ModelBatch负责渲染这些renderable对象。事实上,渲染还不是ModelBatch做的,它仅仅是将Renderable排个序,最优化Renderable的渲染顺序,然后将他们传给可渲染的shader。如果没指定,或者指定了不合适的shader,那ModelBatch会帮你创建一个。通过调用ShaderProvider来获得一个shader。这里我们暂时还不深入,但是你可以记下,你可以在创建ModelBatch时,指定你自己的ShaderProvider。
所以,我们知道了,Shader才是负责渲染Renderable对象的。它会负责一切渲染的工作,来呈现你的Renderable对象。叫法可能不同,建议称之为OpenGL ES 1.x shader。对于OpenGL ES 2.0来说,它还封装了一个ShaderProgram,并且对于不同的Renderable对象,可以设计相应的uniforms和attributes。
public class RenderableTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public Shader shader;
public RenderContext renderContext;
public Model model;
public Lights lights;
public Renderable renderable;
@Override
public void create () {
lights = new Lights();
...
model = new Model(modelData, new TextureProvider.FileTextureProvider());
NodePart blockPart = model.getNode("ship").parts.get(0);
renderable = new Renderable();
renderable.mesh = blockPart.meshPart.mesh;
renderable.meshPartOffset = blockPart.meshPart.indexOffset;
renderable.meshPartSize = blockPart.meshPart.numVertices;
renderable.primitiveType = blockPart.meshPart.primitiveType;
renderable.material = blockPart.material;
renderable.lights = lights;
renderable.worldTransform.idt();
renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1));
shader = new DefaultShader(renderable.material,
renderable.mesh.getVertexAttributes(),
true, false, 1, 0, 0, 0);
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。RenderContext保存了OpenGL的状态信息,从而避免了shader切换时的状态改变。例如,已经绑定了一个Texture,不用再次绑定了,我们使用一个包含了纹理绑定信息的DefaultTextureBinder来构造一个RenderContext,从而避免了再次绑定纹理。然后, 我们通过一些参数,如灯光什么的,新建一个shader作为DefaultShader。注意,DefaultShader是OpenGL ES 2.0的,所以你要启用你的GLES20,才能让这个有效。
在Render方法中,我们调用了renderContext.begin(),这可以保证context是在初始化的状态,然后,我们调用了shader.begin()来告诉shader,工作开始了,你需要做好渲染对象的准备。这会设置一些全局的uniforms,比如透视矩阵什么的。之后,使用shader来渲染renderable对象。最后调用shader.end()和renderContext.end()来结束渲染。
总结:
ModelInstance包含了:一组nodes的复本、模型的材质。但比如Meshes和Textures,这些属性都只是那些资源的引用。ModelInstance会为自己包含的每一个NodePart创建Renderable实例。
Renderable,是传给渲染管道的最小的渲染单位。
ModelBatch保存了渲染每一个Renderable对象的Shader,将renderable对象排序,是为了最优化渲染顺序。
Shader负责渲染Renderable对象。每多应用中,都要使用到多个shader,每个shader都负责一个唯一的ShaderProgram(GLSL program)。
RenderContext是用来保存OpenGL 上下文的,比如纹理的绑定状态。