Libgdx New 3D API 教程之 -- 加载3D场景的背后-第二部分

http://bbs.9ria.com/thread-221699-1-1.html

在本教程的第一部分,我们已经看过LibGDX 3D API中Model类的总体结构。在第2部分中,我们将会分析渲染管道,从加载模型开始,到真正的渲染模型。我们将不会在渲染管道的某个问题上进行深入探讨。我们只会介绍一些非常基本的内容,这是我觉得你使用3D API时,应该了解的。

  在这一部分,我们要分析渲染究竟做了什么。明白我们在渲染时所做的事很重要。在前一部分本教程,我们已经看到,一个Model是由很多个Node组成,而Node由NodePart组成。一个NodePart是组成模型最小的部分,包含了所有在渲染时所需要的信息。它包含一个MeshPart,描述要渲染什么(形状),它包含一个Material,描述应该如何渲染。阅读这一部分教程时,一定要记得这些概念。

  我们以Loading a scene with Libgdx的教程为基础(参考译文:使用Libgdx加载3D场景)。我们需要拆开代码,来看一看场景后面的实际情况,因此,你可以需要备份,或复制一份以进行新的工作,这里给出参考代码:

  1. public class SceneTest implements ApplicationListener {
  2. public PerspectiveCamera cam;
  3. public CameraInputController camController;
  4. public ModelBatch modelBatch;
  5. public AssetManager assets;
  6. public Array<ModelInstance> instances = new Array<ModelInstance>();
  7. public Lights lights;
  8. public boolean loading;
  9. public Array<ModelInstance> blocks = new Array<ModelInstance>();
  10. public Array<ModelInstance> invaders = new Array<ModelInstance>();
  11. public ModelInstance ship;
  12. public ModelInstance space;
  13. @Override
  14. public void create () {
  15. modelBatch = new ModelBatch();
  16. lights = new Lights();
  17. lights.ambientLight.set(0.4f, 0.4f, 0.4f, 1f);
  18. lights.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));
  19. cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
  20. cam.position.set(0f, 7f, 10f);
  21. cam.lookAt(0,0,0);
  22. cam.near = 0.1f;
  23. cam.far = 300f;
  24. cam.update();
  25. camController = new CameraInputController(cam);
  26. Gdx.input.setInputProcessor(camController);
  27. assets = new AssetManager();
  28. assets.load("data/invaders.g3db", Model.class);
  29. loading = true;
  30. }
  31. private void doneLoading() {
  32. Model model = assets.get("data/invaders.g3db", Model.class);
  33. for (int i = 0; i < model.nodes.size; i++) {
  34. String id = model.nodes.get(i).id;
  35. ModelInstance instance = new ModelInstance(model, id);
  36. Node node = instance.getNode(id);
  37. instance.transform.set(node.globalTransform);
  38. node.translation.set(0,0,0);
  39. node.scale.set(1,1,1);
  40. node.rotation.idt();
  41. instance.calculateTransforms();
  42. if (id.equals("space")) {
  43. space = instance;
  44. continue;
  45. }
  46. instances.add(instance);
  47. if (id.equals("ship"))
  48. ship = instance;
  49. else if (id.startsWith("block"))
  50. blocks.add(instance);
  51. else if (id.startsWith("invader"))
  52. invaders.add(instance);
  53. }
  54. loading = false;
  55. }
  56. @Override
  57. public void render () {
  58. if (loading && assets.update())
  59. doneLoading();
  60. camController.update();
  61. Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
  62. Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  63. modelBatch.begin(cam);
  64. for (ModelInstance instance : instances)
  65. modelBatch.render(instance, lights);
  66. if (space != null)
  67. modelBatch.render(space);
  68. modelBatch.end();
  69. }
  70. @Override
  71. public void dispose () {
  72. modelBatch.dispose();
  73. instances.clear();
  74. assets.dispose();
  75. }
  76. @Override public void resume () {}
  77. @Override public void resize (int width, int height) {}
  78. @Override public void pause () {}
  79. @Override public void dispose () {}
  80. }
复制代码


  在代码中,我们通过AssetManager来加载Model,在大多数情况下,这都是最好的办法。但有时,你可能需要对加载过程有更多的控制。所以,这次我们把AssetMnager删掉。

  1. public class SceneTest implements ApplicationListener {
  2. public PerspectiveCamera cam;
  3. public CameraInputController camController;
  4. public ModelBatch modelBatch;
  5. public Model model;
  6. public Array<ModelInstance> instances = new Array<ModelInstance>();
  7. public Lights lights;
  8. ...
  9. @Override
  10. public void create () {
  11. ...
  12. Gdx.input.setInputProcessor(camController);
  13. ModelLoader modelLoader = new G3dModelLoader(new JsonReader());
  14. ModelData modelData = modelLoader.loadModelData(Gdx.files.internal("data/invaders.g3dj"));
  15. model = new Model(modelData, new TextureProvider.FileTextureProvider());
  16. doneLoading();
  17. }
  18. private void doneLoading() {
  19. for (int i = 0; i < model.nodes.size; i++) {
  20. ...
  21. }
  22. }
  23. @Override
  24. public void render () {
  25. camController.update();
  26. ...
  27. }
  28. @Override
  29. public void dispose () {
  30. modelBatch.dispose();
  31. instances.clear();
  32. model.dispose();
  33. }
  34. ...
  35. }
复制代码


  我们删掉了AssetManager,并通过手动的方式来加载Model,所以,我们在Model加载完成后,调用了doneLoading()。我们这里还是调用了在之前教程中创建的invaders.g3dj,而不是invaders.g3db。所以,确保你把这个文件拷贝到了项目里的assets文件夹中。现在看一下加载部分代码:

  1. ModelLoader modelLoader = new G3dModelLoader(new JsonReader());
  2. ModelData modelData = modelLoader.loadModelData(Gdx.files.internal("data/invaders.g3dj"));
  3. 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怎么玩:

  1. private void doneLoading() {
  2. Material blockMaterial = model.getNode("block1").parts.get(0).material;
  3. ColorAttribute colorAttribute = (ColorAttribute)blockMaterial.get(ColorAttribute.Diffuse);
  4. colorAttribute.color.set(Color.YELLOW);
  5. for (int i = 0; i < model.nodes.size; i++) {
  6. ...
  7. }
  8. }
复制代码


  第一行,我们得到了模型的block1节点(Node),这是我们已知的。得到它的第一个node-part,与它的material。我们在前几章看到过,这个material其实就是block_default1的一个引用,而它会被所有的block节点共享使用。所以,改变这个值,所有的block就都跟着变了。第二行,我们拿到material的Diffuse ColorAttribute,也是我们一早知道的。最后设置成黄色。

<ignore_js_op>

2013-6-27 18:08:34 上传
下载附件 (128.7 KB)
 


  这看起来需要对模型文件有很详细的了解,让我们看看另一种方法:

  1. private void doneLoading() {
  2. Material blockMaterial = model.getMaterial("block_default1");
  3. blockMaterial.set(ColorAttribute.createDiffuse(Color.YELLOW));
  4. for (int i = 0; i < model.nodes.size; i++) {
  5. ...
  6. }
  7. }
复制代码


  同样的结果,但我们通过ID直接得到了material。并且,我们没有去得到当前的diffuse color值,而只是设置。这样,如果这个材质里没有这个属性,就添加上去,如果有,就覆盖。

  改变模型材质,会影响到改变后创建的ModelInstance。你可以为每一个instance做改变:

  1. private void doneLoading() {
  2. for (int i = 0; i < model.nodes.size; i++) {
  3. ...
  4. }
  5. for (ModelInstance block : blocks) {
  6. float r = 0.5f + 0.5f * (float)Math.random();
  7. float g = 0.5f + 0.5f * (float)Math.random();
  8. float b = 0.5f + 0.5f * (float)Math.random();
  9. block.materials.get(0).set(ColorAttribute.createDiffuse(r, g, b, 1));
  10. }
  11. }
复制代码


  没有通过节点,也没有通过ID,我们只是拿到第一个材质,因为ModelInstance也需要指定一个Material:

<ignore_js_op>

2013-6-27 18:08:36 上传
下载附件 (128.36 KB)
 


  之前,通过查看G3DJ文件,我们看过Model的结构了,现在来看看ModelInstance class.

  1. public class ModelInstance implements RenderableProvider {
  2. public final Array<Material> materials = new Array<Material>();
  3. public final Array<Node> nodes = new Array<Node>();
  4. public final Array<Animation> animations = new Array<Animation>();
  5. public final Model model;
  6. public Matrix4 transform;
  7. public Object userData;
  8. ...
  9. }
复制代码


  和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 类:

  1. public class Renderable {
  2. /** the model transform **/
  3. public final Matrix4 worldTransform = new Matrix4();
  4. /** the mesh to render **/
  5. public Mesh mesh;
  6. /** the offset into the mesh's indices **/
  7. public int meshPartOffset;
  8. /** the number of indices/vertices to use **/
  9. public int meshPartSize;
  10. /** the primitive type, encoded as an OpenGL constant, like {@link GL20#GL_TRIANGLES} **/
  11. public int primitiveType;
  12. /** the material to be applied to the mesh **/
  13. public Material material;
  14. /** the bones transformations used for skinning, or null if not applicable */
  15. public Matrix4 bones[];
  16. /** the lights to be used to render this Renderable, may be null **/
  17. public Lights lights;
  18. /** the Shader to be used to render this Renderable, may be null **/
  19. public Shader shader;
  20. /** user definable value. */
  21. public Object userData;
  22. }
复制代码


  对比以前看到的NodePart,它是是一个模型最小的单位,描述了应该怎么渲染这个模型,模型含有一个MeshPart和一个Material。而Renderable对象,也有这三个值,同时,还定义了transform, lights, shader和userData。所以,当你调用ModelBatch.render(ModelInstance)时,这个ModelInstance中所有的node parts都会被转换成Renderable实例,并传送给ModelBatch. 我们手动实现一下:

  1. public class RenderableTest implements ApplicationListener {
  2. public PerspectiveCamera cam;
  3. public CameraInputController camController;
  4. public ModelBatch modelBatch;
  5. public Model model;
  6. public Lights lights;
  7. public Renderable renderable;
  8. @Override
  9. public void create () {
  10. ...
  11. cam.position.set(2f, 2f, 2f);
  12. ...
  13. model = new Model(modelData, new TextureProvider.FileTextureProvider());
  14. NodePart blockPart = model.getNode("ship").parts.get(0);
  15. renderable = new Renderable();
  16. renderable.mesh = blockPart.meshPart.mesh;
  17. renderable.meshPartOffset = blockPart.meshPart.indexOffset;
  18. renderable.meshPartSize = blockPart.meshPart.numVertices;
  19. renderable.primitiveType = blockPart.meshPart.primitiveType;
  20. renderable.material = blockPart.material;
  21. renderable.lights = lights;
  22. renderable.worldTransform.idt();
  23. }
  24. @Override
  25. public void render () {
  26. camController.update();
  27. Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
  28. Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  29. modelBatch.begin(cam);
  30. modelBatch.render(renderable);
  31. modelBatch.end();
  32. }
  33. @Override
  34. public void dispose () {
  35. modelBatch.dispose();
  36. model.dispose();
  37. }
  38. ...
  39. }
复制代码


  这里,我们像以前一样,加载了,invaders.g3dj,但我们要取得ship节点的第一个NodePart。通过它,我们创建了一个Renderable对象,设定了一系列相应的值。同时我们设定了light,将worldTransform的位移设定回了原点。没有旋转,和缩放。我们移除了ModelInstances,取而代之的是,在render方法中,我们只将renderable对象,传给了ModelBatch。我把镜头向原点拉近了一些,可以看得清楚些。

<ignore_js_op>

2013-6-27 18:08:34 上传
下载附件 (20.34 KB)
 


  一个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。

  1. public class RenderableTest implements ApplicationListener {
  2. public PerspectiveCamera cam;
  3. public CameraInputController camController;
  4. public Shader shader;
  5. public RenderContext renderContext;
  6. public Model model;
  7. public Lights lights;
  8. public Renderable renderable;
  9. @Override
  10. public void create () {
  11. lights = new Lights();
  12. ...
  13. model = new Model(modelData, new TextureProvider.FileTextureProvider());
  14. NodePart blockPart = model.getNode("ship").parts.get(0);
  15. renderable = new Renderable();
  16. renderable.mesh = blockPart.meshPart.mesh;
  17. renderable.meshPartOffset = blockPart.meshPart.indexOffset;
  18. renderable.meshPartSize = blockPart.meshPart.numVertices;
  19. renderable.primitiveType = blockPart.meshPart.primitiveType;
  20. renderable.material = blockPart.material;
  21. renderable.lights = lights;
  22. renderable.worldTransform.idt();
  23. renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1));
  24. shader = new DefaultShader(renderable.material,
  25. renderable.mesh.getVertexAttributes(),
  26. true, false, 1, 0, 0, 0);
  27. shader.init();
  28. }
  29. @Override
  30. public void render () {
  31. camController.update();
  32. Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
  33. Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
  34. renderContext.begin();
  35. shader.begin(cam, renderContext);
  36. shader.render(renderable);
  37. shader.end();
  38. renderContext.end();
  39. }
  40. @Override
  41. public void dispose () {
  42. shader.dispose();
  43. model.dispose();
  44. }
  45. ...
  46. }
复制代码


  上面的代码中,我们删掉了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 上下文的,比如纹理的绑定状态。



  原文地址:http://blog.csdn.net/q26335804/article/details/9162461