q26335804 阅读(290) 评论(0)

This blog is a chinese version of xoppa's Libgdx new 3D api tutorial. For English version, please refer to >>LINK<<

  在本教程的第一部分,我们已经看过LibGDX 3D APIModel类的总体结构。在第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 上下文的,比如纹理的绑定状态。