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场景)。我们需要拆开代码,来看一看场景后面的实际情况,因此,你可以需要备份,或复制一份以进行新的工作,这里给出参考代码:
- 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,也是我们一早知道的。最后设置成黄色。
<ignore_js_op>
这看起来需要对模型文件有很详细的了解,让我们看看另一种方法:
- 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:
<ignore_js_op>
之前,通过查看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。我把镜头向原点拉近了一些,可以看得清楚些。
<ignore_js_op>
一个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 上下文的,比如纹理的绑定状态。