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

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

在之前的教程里,我们已经看到如何使用Libgdx加载一个3D场景。现在我们来快速看一下,在这背后,究竟发生了什么。你在使用Libgdx 3D Api的时候,若不关心它的实现机理,可以无需了解这些信息,你可以放心了忽略掉这一章。但我想,了解背后的运行机制是较好的做法,并且还能知道,你可以使用它获得哪些好处。因此,这一章会讲的很基础,这些都是New 3D Api中,我觉得你应该了解的。

这篇教程有两部分,第一部分会讲到G3DB/G3DJ的文件格式,Model类,并且这些如果作用在你的模型应用中。第二部分会讲到包括渲染管线,重加载一个模型,到通过Shader渲染。

我们从fbx-conv开始,确保下载到最新版本fbx-conv,让我们将之前创建的invaders.fbx转化一下。不过这次不同的是,我们要添加一个命令行选项,生成更易读了字符型json文件格式,而不要默认的二进制格式。

fbx-conv -o G3DJ invaders.fbx

这会生成一个名为invaders.g3dj的文件。用你常用的文件工具打开它(notepad就行)。不要被那里面乱七八糟的数字弄晕了头,主要看一下总体的结构:

{
    "version": [  0,   1], 
    "id": "", 
    "meshes": [
...
    ], 
    "materials": [
...
    ], 
    "nodes": [
...
    ], 
    "animations": []
}

我假设你熟悉json这种文件格式。这里我们看到文件中有一个对象,它定义了6个成员。第一个是version(版本号),这样模型加载器就知道怎么加载这个文件。第二个ID(在一些建模工具中,允许你去指定),我们暂时还用不到。然后有4个数组,meshes, materials, nodes和animations。现在,如果你简单的看过我们之前使用过的 LibGDX Model 类的话,你会看到这样的定义。

public class Model implements Disposable {
    public final Array<Mesh> meshes = new Array<Mesh>();
    public final Array<MeshPart> meshParts = new Array<MeshPart>();
    public final Array<Material> materials = new Array<Material>();
    public final Array<Node> nodes = new Array<Node>();
    public final Array<Animation> animations = new Array<Animation>();
    ...
}

模型文件中,也有如meshes, materials, nodes 和 animations这样的数组定义。它还有一个meshParts的数组,这个我们很快就会说到。但现在,我们可以说,g3dj/g3db文件里面定义的就是一个模型所有的信息。实际上,这就是fbx-conv的工作,将一个fbx文件转化成一个可运行的文件格式,从而去渲染。同时,意味着,我们打开了g3dj文件,就看到了这个模型类,将会包含的信息。这对于debugging和testing是非常有用的。看一下在g3dj文件中的meshes数组定义。还是,别被里面的数字吓到,我们只看结构。

{
    "version": [  0,   1], 
    "id": "", 
    "meshes": [
        {
            "attributes": ["POSITION", "NORMAL", "TEXCOORD0"], 
            "vertices": [
                 25.000017, -95.105652, -18.163574, -0.269870,  0.942723,  0.196072,  0.050000,  0.900000, 
                 ...
            ], 
            "parts": [
                {
                    "id": "mpart1", 
                    "type": "TRIANGLES", 
                    "indices": [
                          0,   1,   2,   1,   0,   3,   3,   4,   5,   4,   3,   0, 
                         ...
                    ]
                }, 
                {
                    "id": "mpart2", 
                    "type": "TRIANGLES", 
                    "indices": [
                         ...
                    ]
                }, 
                {
                    "id": "mpart4", 
                    "type": "TRIANGLES", 
                    "indices": [
                         ...
                    ]
                }
            ]
        }, 
        {
            "attributes": ["POSITION", "NORMAL"], 
            "vertices": [
                ...
            ], 
            "parts": [
                {
                    "id": "mpart3", 
                    "type": "TRIANGLES", 
                    "indices": [
                         ...
                    ]
                }
            ]
        }
    ], 
    "materials": [
        ...
    ], 
    "nodes": [
        ...
    ], 
    "animations": []
}

这里我们可以看到,meshes array包含两个项目(两个mesh)。每个项目都包含三个数组:attributes(属性),vertices(顶点)和parts(零件)。Attributes数组指定包含的mesh顶点属性。如果您看了Libgdx 3D 基础 这一章教程,在我们通过ModelBuilder创建BOX的时候,你已经使用过VertexAttributes(顶点属性)的Usage.Position,和Usage.Normal。而“TEXCOORD0”属性指定的mesh包含纹理坐标。


顶点数组,就是一个很大的fload值数组,表示mesh中的各个顶点。请注意,每一行是怎么样表示一个顶点的信息的。在每一行的前三个值表示的位置,接下来的三个值代表法线,最后的最后两个值表示的纹理坐标(UV)。

第一个mesh的parts数组,包含三个对象。这些被称为mesh-parts(网格零件)。我们已经看到了上面的模型类有一个单独的meshParts数组。该数组保存所有网格内的所有部件。现在看的第一部分。它包含三个成员。第一个是id值,这是一个唯一的标识符,在内部使用,以确定part。接下来是type,它定义了如何渲染part(原始类型)。从理论上讲,这可以是不同的值,但在实践中,将始终是“TRIANGLES”,这意味着该部分是由多个三角形组成,而每个三角形有三个顶点。最后有索引数组。同样的,这是一个很大的数组,每个数字都是之前vertices数组中的ID值。因此,如一个0值,表示的顶点数组中的第一行,1值表示在第二行。由于我们设置的类型是TRIANGLES,因此前三个值(0,1,2)表示的,就是一个三角形,接下来第二个值(1,0,3)指定的是第二个,等等。请注意,每行由12个值,这意味着这一行定义了有四个三角形。


现在,如果快速看一下LibGDX Mesh类,你会看到,它包含以下几行:

public class Mesh implements Disposable {
    ...
    final VertexData vertices;
    final IndexData indices;
    ...

在这段代码中,可以看到VertexData是一个很大的浮点数数组,这刚好用来存放g3dj文件中的顶点数据。IndexData也一样是个大数组,但是他只存储short值。这个数组将要存储g3dj文件中,每一个mesh part的顶点索引值,刚好对应用parts部分(flattening out the parts,不知道译的对不对)。好像第一个mesh这种情况,这里将要保存的,是第一个mesh part的顶点索引数据(即,mpart1那一部分),然后,紧跟着第二个(mpart2)的顶点索引数据,然后第三个(mpart4)的顶点索引数据。为了识别mesh中的每一个part,我们需要知道,他在indexData的哪些位置,现在,我们看一下MeshPart类

public class MeshPart {
    /** unique id within model **/
    public String id;
    /** the primitive type, OpenGL constant like GL_TRIANGLES **/
    public int primitiveType;
    /** the offset into a Mesh's indices array **/
    public int indexOffset;
    /** the number of vertices that make up this part **/
    public int numVertices;
    /** the Mesh the part references, also stored in {@link Model} **/
    public Mesh mesh;
}

嗯,这里正有我们所需要的,indexOffset和numVertices值可以帮我们定们使用的是IndexData中的哪一部分。 LibGDX3D API中,我们不渲染mesh,我们渲染的是meshparts。这是有用的,因为现在多个不同ModelInstances,可以共享相同的mesh。这实际上降低了mesh的绑定次数,从而提高性能。事实上,注意看我们开始时使用四个模型(ship,block,invader和spacesphere),而不是现在只有两个mesh,但总共有四个mesh part。FBX-CONV为我们合并了mesh,让他们共享了相同的属性。第二mesh不包含TEXCOORD0,因为block不包含纹理(Texture)。我们可以够优化这个block模型,为其添加上纹理坐标,但却不为其指定纹理。这样,可以将mesh的绑定次数减少到只有一次。这里我就不再演示这个过程了,这会根据不同的建模工具而不同。但请记住,具有相同的顶点属性将有助于合并mesh。您可以随时转换你的场景成G3DJ的格式,从而快速检查这些类型的优化。


让我们继续看看添加的materials array:

"materials": [
    {
        "id": "sphere2_auv1", 
        "diffuse": [ 1.000000,  1.000000,  1.000000], 
        "textures": [
            {
                "id": "file3", 
                "filename": "invader.png", 
                "type": "DIFFUSE"
            }
        ]
    }, 
    {
        "id": "lambert2", 
        "diffuse": [ 1.000000,  1.000000,  1.000000], 
        "textures": [
            {
                "id": "file1", 
                "filename": "space.jpg", 
                "type": "DIFFUSE"
            }
        ]
    }, 
    {
        "id": "cube1_auv1", 
        "diffuse": [ 1.000000,  1.000000,  1.000000], 
        "textures": [
            {
                "id": "file2", 
                "filename": "ship.png", 
                "type": "DIFFUSE"
            }
        ]
    }, 
    {
        "id": "block_default1", 
        "diffuse": [ 0.000000,  0.000000,  1.000000]
    }
],

在这里,我们可以看到该文件中包含四种material。每个material具有一个唯一的id,这与建模工具中指定的material名称刚好对应。正如你可以看到的,这些id的名称,字面上没什么意义。因为他们是从obj文件转化过来的。 不论怎么样,我都建议你给你的材料起一些有用的名称。它可以方便你在Model类中,找到materials array内的material。接着,该material包含有diffuse(漫反射)颜色值,它以数组的形式,定义了取值范围从0到1的红色R,绿色G和蓝色B分量的材料的 diffuse 颜色。所以值[0.5,0.5,0.5]代表灰色,值[1.0,0.0,0.0]代表红色。看一下最后一种material,这是block的材料定义,被指定了蓝色的diffuse颜色。最后,前三个材料定义中,还有一个textures的数组定义块,其中定义了要使用的纹理。同样,纹理的ID是与建模应用程序中指定的一样,我又建议你起一些有意义的名称。 filename值很明显,就是纹理的文件名。type值指定了texture应该怎样被应用,现在这里,指定的是“diffuse”,但可以例如另一个值“NORMALMAP(法线)”。我们现在不更多的深入材料的定义了,但注意到,除了ID,每个值都是可选的。末定义的值,就不会被应用。


接下来,看看nodes数组:

"nodes": [
    {
        "id": "space", 
        "parts": [
            {
                "meshpartid": "mpart1", 
                "materialid": "lambert2", 
                "uvMapping": [[  0]]
            }
        ]
    }, 
    {
        "id": "ship", 
        "rotation": [ 0.000000,  1.000000,  0.000000,  0.000000], 
        "translation": [ 0.000000,  0.000000,  6.000000], 
        "parts": [
            {
                "meshpartid": "mpart2", 
                "materialid": "cube1_auv1", 
                "uvMapping": [[  0]]
            }
        ]
    }, 
    {
        "id": "block1", 
        "translation": [-5.000000,  0.000000,  3.000000], 
        "parts": [
            {
                "meshpartid": "mpart3", 
                "materialid": "block_default1"
            }
        ]
    }, 
    ...
    {
        "id": "block6", 
        "translation": [ 5.000000,  0.000000,  3.000000], 
        "parts": [
            {
                "meshpartid": "mpart3", 
                "materialid": "block_default1"
            }
        ]
    }, 
    {
        "id": "invader1", 
        "translation": [-5.000000,  0.000000,  0.000000], 
        "parts": [
            {
                "meshpartid": "mpart4", 
                "materialid": "sphere2_auv1", 
                "uvMapping": [[  0]]
            }
        ]
    }, 
    ...
    {
        "id": "invader30", 
        "translation": [ 5.000000,  0.000000, -8.000000], 
        "parts": [
            {
                "meshpartid": "mpart4", 
                "materialid": "sphere2_auv1", 
                "uvMapping": [[  0]]
            }
        ]
    }
],

嗯,看起来差不多。包含了每一个,我们在建模工具中生成了模型实例(大部分都被省略了)。每一个node,都有一个ID值,这个和我们在上一篇文章中,使用建模工具为每一个模型实例指定的名称一样。有一些实例会保含位移,或旋转的值。对于这些值,如果没定义的话,就不会被应用。所以,看到space这一node(节点),就没有定义任何位移(translate)和旋转(rotated)的值,而对于ship,两者都有,这就像我们在建模工具中指定的一样。接下来是part数组,这里所有的东西汇聚到一起。它描述了node的渲染方式。每一个项目,被称为node-part,包含了id与mesh和material,表示每一个特定的mesh part,将使用哪个material来渲染。UV Mapping值描述了对应的纹理,使用相应的纹理坐标。记得我们之前有看到"TEXCOORD0",的顶点属性,与"DIFFUSE"的纹理。现在,考虑一种情况,我们有两个"TEXCOORD0"和“TEXCOORD1”的纹理属性,并且有"DIFFUSE"和"NORMALMAP"两种纹理,而UV Mapping的数组指定了,对于“TEXCOORD0”要使用的纹理(如:DIFFUSE)与对于"TEXCOORD1"要使用的纹理(如NORMALMAP).


我们现在这种情况,parts array只有一个node-part,但想想,如一辆车的模型,你想要使用预定的纹理来渲染这两车,但只有车窗想换成黑色。这样,你就可能需要两个部分,一部分是车窗,使用黑色的材质,另一部分是其他车体,与相就的材料纹理。你要一直记得这点。不论什么时候,当你在建模工具中,对一个模型应用两种或两种以上的材质纹理时,它就将被分成两个不同的节点零件(node parts)。在车的例子中,我们可以通过添加一个小的黑色矩形到纹理中,并对车窗设置相应的纹理坐标,这就可以只覆盖黑色矩形中间的部分。


做一个简单的旁注,如果你使用的是旧的Libgdx 3d Model 类,这两个可以比较一下。旧的StillModel类,有一个数组,包含了一个SubMesh类的数组,这里面有一个Material,Mesh,和 Primitive-type。NodePart类,包含了相用的信息,但不同点就在于,没有直接引用Mesh,而是引用了MeshPart。

同样的,在G3dj文件中的nodes,与Model类中的nodes是一个对应一个的。所以,Model类中的nodes与建模工具中的nodes,也是一一对应的。很多建模工具允许这些节点之前分层,而在Model类中,也可以。每一个Node都有一个子数组,包含他的子节点。现在我们不深入这里,不过记注,如果你在建模工具中使用了层级关系,那这些也会反映到Model类。

在G3DJ文件中还有一个数组,叫做animations,这里是定义模型动画的,我们迟点会讨论动画,现在就知道在这里有个动画的定义就好了。

现在,如果你喜欢的话,可以拷贝invaders.g3dj文件到你的asset/data文件夹中,并在代码中,用它替换原来的invaders.g3db。你将可以简单的通过改变一些数据,看看在场景中,都有什么变化。比如,改变ship材质的diffuse颜色,或是换一个不同的材质。








没有更多推荐了,返回首页