我们先将上一篇中的 spacesphere.obj 模型使用 fbx-conv 将其转为g3dj格式,因为g3dj为文本格式,方便我们查看里面的内容:
fbx-conv -o G3DJ spacesphere.obj
得到一个 spacesphere.g3dj 文件,使用一个文本编辑器打开它,它的整体结构如下:
{
"version": [ 0, 1],
"id": "",
"meshes": [
...
],
"materials": [
...
],
"nodes": [
...
],
"animations": []
}
g3dj 格式是一个 json 文件格式。主要包含六个成员的对象。第一个是版本。第二个是id(一些建模应用程序允许您指定的名称),我们现在不使用它。然后依次是四个数组:网格、材质、节点和动画。现在,我们查看下 Model(com.badlogic.gdx.graphics.g3d.Model) 类的结构:
public class Model implements Disposable {
public final Array<Mesh> meshes = new Array();
public final Array<Material> materials = new Array();
public final Array<Node> nodes = new Array();
public final Array<Animation> animations = new Array();
public final Array<MeshPart> meshParts = new Array();
protected final Array<Disposable> disposables = new Array();
...
}
Model类主要包括了网格、材质、节点和动画数组属性。它还有一个meshParts阵列。所以g3dj(和g3db)文件和 Model 类实际上包含的内容的是一对一对应的。事实上,这就是fbx conv的主要功能,它将obj/fbx等文件转换为可供 libGDX框架渲染的运行时格式。这也意味着,通过 g3dj/g3db 文件,我们直接可以得到一个model类。
{
...
"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": [
...
]
}
]
}
],
...
}
网格数组里的每个对象主要包含三个数组:属性数组(attributes)、顶点数组(vertices)和部件数组(parts)。
属性数组(attributes) 指定网格包含哪些顶点属性。数组第一位为位置(POSITION)。第二位为VertexAttribute的值,第三位为“TEXCOORD0”,指定网格包含纹理坐标。
顶点数组(vertices) 只是代表网格的一个庞大的浮点值数组。在每一项上,前三个值代表位置,后三个值代表法线,最后两个值代表纹理坐标(UV)。
部件数组(parts) 的每一项被称为网格部件,主要包含了三个属性。在 Model 类中有一个单独的meshParts数组。该数组包含所有网格中的所有部件。第一个属性是id,这是一个唯一的标识符,在内部用于标识部件。接下来第二个属性是类型,它定义了部件应该如何呈现(基本类型)。理论上这可能是一个不同的值,但在实践中它的值基本上是“三角形(TRIANGLES)”,也就是部件是由三角形组成,其中每个三角形由三个顶点指定。最后一个属性是索引数组。同样,这是一个庞大的数字数组,其中每个数字用于标识顶点数组中的一个顶点。例如,值0表示顶点数组中的第0项,值1表示第1项。由于类型设置为三角形,所以前三个值(0、1、2)指定第一个三角形,后三个值(1、0、3)指定第二个三角形,依此类推。请注意,每条线由12个值组成,这意味着一条线上有四个三角形。
接下来看下Mesh类(com.badlogic.gdx.graphics.Mesh):
public class Mesh implements Disposable {
...
final VertexData vertices;
final IndexData indices;
...
}
在 Mesh 类中,VertexData可以被视为一个庞大的浮点值数组,它将与g3dj文件中顶点数组(vertices)相匹配。IndexData也可以被视为一个巨大的数组,但现在它是一个 short 值。该数组将填充g3dj文件中 meshes 中的部件属性(parts)的下的indices索引数组,把部件铺平。因此,对于第一个网格,它将包含第一个网格部分(mpart1)的索引,紧接着是第二个网格部分(mpart2)的索引,然后是第三个网格部分(mpart4)的索引。要识别网格中的部件,我们需要知道它在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 offset;
/** the number of vertices that make up this part **/
public int size;
/** the Mesh the part references, also stored in {@link Model} **/
public Mesh mesh;
}
这正是我们需要的,indexOffset和numVertices值告诉我们IndexData的哪个部件用于这个网格部分。所以在LibGDX 3d api中,我们不渲染网格,而是渲染网格部件。这很有用,因为现在多个不同的ModelInstance可以共享同一个网格。这实际上减少了网格需要绑定的次数,从而提高了性能。事实上,在我们前几篇中的四个模型中来看,现在只有两个网格,但总共有四个网格部分。fbx-conv 已经合并了为我们共享相同属性的网格。第二个网格不包含TEXCOORD0,因为 block 模型上没有纹理。我们可以通过向 block 模型添加纹理坐标,而不应用纹理来优化它。这将使网格绑定的次数仅减少一次。我现在不介绍这个过程,因为它是特定于应用程序的建模。但请记住,具有相同的顶点属性将有助于合并网格。将其使用的模型转换为G3DJ/G3DB格式的模型可以得到明显的优化。
{
...
"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]
}
],
...
}
在上面的结构中,可以看到该文件包含了四种材质。每个材质都有一个唯一的id,该id与建模应用程序中材质的名称相同。最好还是给材质起一个有用的名字比较好。它允许您在 Model 类的 材质(materials)数组中标识材质。接下来,材质包含一个漫反射值(diffuse),该值将材质的漫反射颜色表示为从0到1的红色、绿色和蓝色(RGB)数组。所以[0.5,0.5,0.5]的值为灰色,[1.0,0.0,0.0]的值为红色。请注意,最后一种材质是块模型的材质,具有蓝色漫反射颜色。最后,前三种材质还有一个纹理数组,其中定义了模型的纹理。同样,纹理的id与建模应用程序中纹理的名称相同,这里也是建议给出一个有用的名称。filename的值显然是纹理的文件名。type的值指定如何应用纹理,在本例中是“漫反射”,但另一个值可以是“法线贴图”。我们现在不会深入讨论材质,但请注意,除了id之外的所有值都是可选的。
{
...
"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]]
}
]
}
],
...
}
节点数据包含我们在建模应用程序中创建的每个模型实例。每个节点都有一个id,该id与我们在建模应用程序中使用的名称相匹配,我们在前面的教程中使用了该id来创建每个ModelInstance。某些节点还具有平移(translation)或旋转(rotation)值。下一个值是parts数组,它是所有内容组合在一起的地方。它描述了应该如何呈现节点。meshpartid为网格的部件ID,materialid为材质的ID。uvMapping值用于指定哪个纹理应使用哪个纹理坐标。记住我们有一个“TEXCOORD0”顶点属性和一个“漫反射”纹理。现在考虑一个场景,我们将同时拥有“TEXCOORD0”和“TEXCOORD1”顶点属性,以及“DIFFUSE”和“NORMALMAP”纹理。uvMapping数组指定哪个纹理应用于TEXCOORD0(例如DIFFUSE),哪个纹理应用于TEXCOORD1(例如NORMALMAP)。
在这种情况下,部件数组只包含一个节点零件。但请考虑一个汽车模型,在该模型中,您希望使用应用的纹理渲染整个模型,但窗口除外,窗口应使用黑色渲染。在这种情况下,您将有两个部分,一个包含窗户的网格部分和黑色材质,另一个包含汽车剩余部分的网格部分和具有纹理的材质。你应该永远记住这一点。无论何时将两种或更多不同材质应用于建模应用程序中的模型,它都将被拆分为多个节点部件。以汽车为例,我们可以通过向纹理中添加一个小的黑色矩形,并设置窗口的纹理坐标,使其仅覆盖该小黑色矩形的中间部分来优化此设置。
同样,g3dj文件中的节点与模型类中的节点一一匹配。因此,模型中的节点与建模应用程序中的节点匹配。大多数建模应用程序都允许这些节点具有层次结构,模型类也是如此。每个节点都有一个子数组,其中包含其子节点。现在我们将不再深入探讨这个问题,但请记住,如果在建模应用程序中使用层次结构,它也将反映在模型类中。
它显然用于动画模型。后面教程将讨论动画。