GLTF文件结构初探
GLTF (.gltf)
是JSON格式的文本文件。GLTF文件包含了3D模型的结构、材质、动画和其他元数据,可以直接打开并阅读(例如用文本编辑器)。
GLB (.glb)
二进制文件,GLB文件将3D模型的所有数据,包括几何、材质、动画和纹理,都打包在一个单一的二进制文件中。二进制格式,GLB文件通常比GLTF文件加载更快,占用空间更小。
GLTF适合需要手动编辑或调试3D模型文件的情况,GLB适合需要高效传输和加载3D模型的应用场景,尤其是在网络传输中。
GLTF文件样例
来源:网上找的一个.glb
龙的模型。
通过Blender转换成.gltf
结构之后查看。
具有纹理的 | 通过Blender转换后的 |
.gltf
文件内容:
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.28",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
3
]
}
],
"nodes":[
{
"mesh":0,
"name":"Mesh_0"
},
{
"mesh":1,
"name":"Mesh_1"
},
{
"children":[
0,
1
],
"name":"dragon_asian.1"
},
{
"children":[
2
],
"name":"RootNode.0",
"scale":[
0.009999999776482582,
0.009999999776482582,
0.009999999776482582
]
}
],
"materials":[
{
"alphaCutoff":0.5,
"alphaMode":"MASK",
"doubleSided":true,
"name":"Material_0",
"occlusionTexture":{
"index":0
},
"pbrMetallicRoughness":{}
},
{
"doubleSided":true,
"name":"Material_1",
"occlusionTexture":{
"index":1
},
"pbrMetallicRoughness":{}
}
],
"meshes":[
{
"name":"Mesh_0",
"primitives":[
{
"attributes":{
"POSITION":0,
"NORMAL":1,
"TEXCOORD_0":2
},
"indices":3,
"material":0
}
]
},
{
"name":"Mesh_1",
"primitives":[
{
"attributes":{
"POSITION":4,
"NORMAL":5,
"TEXCOORD_0":6
},
"indices":7,
"material":1
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
},
{
"sampler":0,
"source":1
}
],
"images":[
{
"bufferView":4,
"mimeType":"image/png",
"name":"Image_3"
},
{
"bufferView":9,
"mimeType":"image/png",
"name":"Image_7"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":101491,
"max":[
2.050593376159668,
6.596331596374512,
2.6598503589630127
],
"min":[
-2.3590798377990723,
0.09649088978767395,
-2.5816965103149414
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":101491,
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":101491,
"type":"VEC2"
},
{
"bufferView":3,
"componentType":5125,
"count":101496,
"type":"SCALAR"
},
{
"bufferView":5,
"componentType":5126,
"count":46404,
"max":[
2.4441118240356445,
5.79871940612793,
2.5048890113830566
],
"min":[
-2.445387840270996,
-0.005728369578719139,
-3.0280914306640625
],
"type":"VEC3"
},
{
"bufferView":6,
"componentType":5126,
"count":46404,
"type":"VEC3"
},
{
"bufferView":7,
"componentType":5126,
"count":46404,
"type":"VEC2"
},
{
"bufferView":8,
"componentType":5123,
"count":46407,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":1217892,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":1217892,
"byteOffset":1217892,
"target":34962
},
{
"buffer":0,
"byteLength":811928,
"byteOffset":2435784,
"target":34962
},
{
"buffer":0,
"byteLength":405984,
"byteOffset":3247712,
"target":34963
},
{
"buffer":0,
"byteLength":4686983,
"byteOffset":3653696
},
{
"buffer":0,
"byteLength":556848,
"byteOffset":8340680,
"target":34962
},
{
"buffer":0,
"byteLength":556848,
"byteOffset":8897528,
"target":34962
},
{
"buffer":0,
"byteLength":371232,
"byteOffset":9454376,
"target":34962
},
{
"buffer":0,
"byteLength":92814,
"byteOffset":9825608,
"target":34963
},
{
"buffer":0,
"byteLength":6688734,
"byteOffset":9918424
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":16607160,
"uri":"data:xxxx"
}
]
}
glTF就是把几个JSON数组放到了一个JSON对象里面去,主要包括:
Scene 场景 、 Node 结点、Texture 纹理、Material 材质、Mesh 网格、Primitive 图元、Accessor 访问器、Sampler 采样器、Buffer 缓冲、BufferView “缓冲视图” 和 Image 图像。
层级关系如图所示:
基础结构图 | gltf文件结构 |
解读 GLTF 最终要的方法
核心是 索引。只要明白一切的数据存储都是通过索引调用实现的,加上知道顶层是如何一步步索引到数据层(即需要了解层级结构),GLTF的文件结构就不难理解了。
首先抛开材质不谈,所有的顶点数据,法线数据和采样uv坐标数据都存在了数据层,即buffer,对应JSON文件中 Buffer 那一栏,可以是直接在GLTF文件中存储,也可以是直接外接作为一个.bin文件。
再来看最顶层的scene。
"scenes":[
{
"name":"Scene",
"nodes":[
3
]
}
]
这里secne中包含nodes的列表,nodes中写着3,表示从JSON的nodes中拿列表中的第三个。
"nodes":[
{
"mesh":0,
"name":"Mesh_0"
},
{
"mesh":1,
"name":"Mesh_1"
},
{
"children":[
0,
1
],
"name":"dragon_asian.1"
},
{
"children":[
2
],
"name":"RootNode.0",
"scale":[
0.009999999776482582,
0.009999999776482582,
0.009999999776482582
]
}
],
nodes[3]中有一个children列表,表示他有子节点,子节点的索引是2,也就是nodes[2],而nodes[2]中仍然有子节点,是nodes[0]和nodes[1],接着看下去,发现nodes[0]中有个mesh,nodes[1]中也有个mesh,分别对应mesh的索引 0 和 1 ,这时候再去看mesh数组。
"meshes":[
{
"name":"Mesh_0",
"primitives":[
{
"attributes":{
"POSITION":0,
"NORMAL":1,
"TEXCOORD_0":2
},
"indices":3,
"material":0
}
]
},
{
"name":"Mesh_1",
"primitives":[
{
"attributes":{
"POSITION":4,
"NORMAL":5,
"TEXCOORD_0":6
},
"indices":7,
"material":1
}
]
}
],
mesh[0]中有网格自己的名字,以及它的图元信息,具体看primitives(图元)中的内容,首先图元中有自己的位置、法线和贴图UV的索引,还有外层的 顶点 索引和材质索引。这些索引对应着的是在accessors (访问器)中拿对应的内容。
拿 indices (顶点索引)的索引举例,是7,代表从 accessors 的列表中拿到第七个数据,accessors中存放的内容是bufferView,bufferView可以理解为如何去拿取数据层中的信息。因为数据层都是以byte的形式存放的,而顶点数据、法线数据、uv数据等有的是float类型,有的是unsigned int类型,所以才会有bufferView这种东西来教你如何正确地去拿这些信息。
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":101491,
"max":[
2.050593376159668,
6.596331596374512,
2.6598503589630127
],
"min":[
-2.3590798377990723,
0.09649088978767395,
-2.5816965103149414
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":101491,
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":101491,
"type":"VEC2"
},
{
"bufferView":3,
"componentType":5125,
"count":101496,
"type":"SCALAR"
},
{
"bufferView":5,
"componentType":5126,
"count":46404,
"max":[
2.4441118240356445,
5.79871940612793,
2.5048890113830566
],
"min":[
-2.445387840270996,
-0.005728369578719139,
-3.0280914306640625
],
"type":"VEC3"
},
{
"bufferView":6,
"componentType":5126,
"count":46404,
"type":"VEC3"
},
{
"bufferView":7,
"componentType":5126,
"count":46404,
"type":"VEC2"
},
{
"bufferView":8,
"componentType":5123,
"count":46407,
"type":"SCALAR"
}
],
接着我们看到了 bufferView的索引是8,表示拿取bufferViews[8],componentType表示的是数据类型,5123代表的是16位无符号整数,还有5125等不同的数据类型,这个会在最后补充类型数据。count代表拿的这个数据有多少个,有46407个无符号整数,type代表拿的数据类型是一个什么类型,此处为SCALAR,是一个标量,标量有什么存的,就是5123,16位无符号整数。接着我们可以看最后的数据层了。
"bufferViews":[
{
"buffer":0,
"byteLength":1217892,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":1217892,
"byteOffset":1217892,
"target":34962
},
{
"buffer":0,
"byteLength":811928,
"byteOffset":2435784,
"target":34962
},
{
"buffer":0,
"byteLength":405984,
"byteOffset":3247712,
"target":34963
},
{
"buffer":0,
"byteLength":4686983,
"byteOffset":3653696
},
{
"buffer":0,
"byteLength":556848,
"byteOffset":8340680,
"target":34962
},
{
"buffer":0,
"byteLength":556848,
"byteOffset":8897528,
"target":34962
},
{
"buffer":0,
"byteLength":371232,
"byteOffset":9454376,
"target":34962
},
{
"buffer":0,
"byteLength":92814,
"byteOffset":9825608,
"target":34963
},
{
"buffer":0,
"byteLength":6688734,
"byteOffset":9918424
}
],
bufferViews[8]拿到的是buffer中的内容,buffer的索引是0,代表从buffer列表中拿到第0个,byteLength是92814,代表该部分数据长度是92814字节,byteOffset代表偏移的字节数,从buffers[0] + 9825608 处开始拿数据,"target": 34963
是在描述一个缓冲区对象(Buffer Object)的目标,通常与 OpenGL API 的常量对应。具体来说,34963
是 OpenGL 的常量 GL_ELEMENT_ARRAY_BUFFER
的数值。"target": 34962
代表 (GL_ARRAY_BUFFER),"target": 0
或未指定则代表未绑定到特定 OpenGL 缓冲区目标,由应用程序自行决定如何使用这些数据。
由此我们已经完成了一整个GLTF文件的内容解析。现在回头来看,最终要的两点,一索引, 而层级结构的调用顺序。其中还有材质的部分没有讲到,但是掌握这些已经足够应对大部分的情况了。
GLTF文件的略深解读
一个glTF文件包括一个或多个Scene,其中必定有一个是默认场景,用scene指定。每个场景就是一棵Node构成的树,因此肯定有一个根结点。有些Node含有Mesh,每个Mesh可能有一个或多个Primitive,Primitive其实就是一堆三角形+材质。每个Node有自己对应的变换矩阵,也就是MVP变换里面的model,这一矩阵说明了本Node在父结点的坐标系中怎么摆放。这一变换定义在matrix中(16x16的行主序的矩阵),或者也可以用TRS的方式给出,即分别说明移动translation、旋转rotation和缩放scale。translate是个三维向量(XYZ),rotation是个四元数(WXYZ),scale是个三维向量。
glTF的坐标系采用右手系,向上为Y。OpenGL则是左手系
因此,如果三角形的顶点数据存放在Buffer当中,同一个Buffer可以用不同的方式的方式看待,也就是BufferView(类似Vulkan的BufferView)。Accessor定义了我们怎样访问BufferView(涉及offset、stride、数据类型等等,和定义VAO时要考虑的很像)。Buffer中的数据可以直接在文件中用base64编码给出,也可以给定一个URI(可以理解为文件的路径)引用一个.bin文件。
顶点数据一般就是POSITION位置、NORMAL法向量、TEXCOORD纹理坐标,这三者对应的Accessor都会在Primitive当中指明。Primitive还要给出顶点索引对应的Accessor(对应我们调用draw call时要绑定的索引缓冲)。
每个Primitive一般有自己的Material。每个材质有可能有对应的Texture。Texture又引用对应的Sampler和Image。Sampler说明放大/缩小的时候采用的插值方式、纹理坐标超过[−1,1][−1,1]如何处理。Image则就是贴图,一般也是用URI应用外部的图片文件。
GLTF文件结构图
tinygltf库中定义的几个主要结构体
struct Material {
std::string name;
std::vector<double> emissiveFactor; // length 3. default [0, 0, 0]
std::string alphaMode; // default "OPAQUE"
double alphaCutoff; // default 0.5
bool doubleSided; // default false;
PbrMetallicRoughness pbrMetallicRoughness;
NormalTextureInfo normalTexture;
OcclusionTextureInfo occlusionTexture;
TextureInfo emissiveTexture;
// For backward compatibility
// TODO(syoyo): Remove `values` and `additionalValues` in the next release.
ParameterMap values;
ParameterMap additionalValues;
ExtensionMap extensions;
Value extras;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
Material() : alphaMode("OPAQUE"), alphaCutoff(0.5), doubleSided(false) {}
DEFAULT_METHODS(Material)
bool operator==(const Material &) const;
};
struct BufferView {
std::string name;
int buffer{-1}; // Required
size_t byteOffset{0}; // minimum 0, default 0
size_t byteLength{0}; // required, minimum 1. 0 = invalid
size_t byteStride{0}; // minimum 4, maximum 252 (multiple of 4), default 0 =
// understood to be tightly packed
int target{0}; // ["ARRAY_BUFFER", "ELEMENT_ARRAY_BUFFER"] for vertex indices
// or atttribs. Could be 0 for other data
Value extras;
ExtensionMap extensions;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
bool dracoDecoded{false}; // Flag indicating this has been draco decoded
BufferView()
: buffer(-1),
byteOffset(0),
byteLength(0),
byteStride(0),
target(0),
dracoDecoded(false) {}
DEFAULT_METHODS(BufferView)
bool operator==(const BufferView &) const;
};
struct Accessor {
int bufferView; // optional in spec but required here since sparse accessor
// are not supported
std::string name;
size_t byteOffset;
bool normalized; // optional.
int componentType; // (required) One of TINYGLTF_COMPONENT_TYPE_***
size_t count; // required
int type; // (required) One of TINYGLTF_TYPE_*** ..
Value extras;
ExtensionMap extensions;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
std::vector<double>
minValues; // optional. integer value is promoted to double
std::vector<double>
maxValues; // optional. integer value is promoted to double
struct {
int count;
bool isSparse;
struct {
int byteOffset;
int bufferView;
int componentType; // a TINYGLTF_COMPONENT_TYPE_ value
} indices;
struct {
int bufferView;
int byteOffset;
} values;
} sparse;
///
/// Utility function to compute byteStride for a given bufferView object.
/// Returns -1 upon invalid glTF value or parameter configuration.
///
int ByteStride(const BufferView &bufferViewObject) const {
if (bufferViewObject.byteStride == 0) {
// Assume data is tightly packed.
int componentSizeInBytes =
GetComponentSizeInBytes(static_cast<uint32_t>(componentType));
if (componentSizeInBytes <= 0) {
return -1;
}
int numComponents = GetNumComponentsInType(static_cast<uint32_t>(type));
if (numComponents <= 0) {
return -1;
}
return componentSizeInBytes * numComponents;
} else {
// Check if byteStride is a mulple of the size of the accessor's component
// type.
int componentSizeInBytes =
GetComponentSizeInBytes(static_cast<uint32_t>(componentType));
if (componentSizeInBytes <= 0) {
return -1;
}
if ((bufferViewObject.byteStride % uint32_t(componentSizeInBytes)) != 0) {
return -1;
}
return static_cast<int>(bufferViewObject.byteStride);
}
// unreachable return 0;
}
Accessor()
: bufferView(-1),
byteOffset(0),
normalized(false),
componentType(-1),
count(0),
type(-1) {
sparse.isSparse = false;
}
DEFAULT_METHODS(Accessor)
bool operator==(const tinygltf::Accessor &) const;
};
struct Primitive {
std::map<std::string, int> attributes; // (required) A dictionary object of
// integer, where each integer
// is the index of the accessor
// containing an attribute.
int material; // The index of the material to apply to this primitive
// when rendering.
int indices; // The index of the accessor that contains the indices.
int mode; // one of TINYGLTF_MODE_***
std::vector<std::map<std::string, int> > targets; // array of morph targets,
// where each target is a dict with attribues in ["POSITION, "NORMAL",
// "TANGENT"] pointing
// to their corresponding accessors
ExtensionMap extensions;
Value extras;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
Primitive() {
material = -1;
indices = -1;
mode = -1;
}
DEFAULT_METHODS(Primitive)
bool operator==(const Primitive &) const;
};
struct Mesh {
std::string name;
std::vector<Primitive> primitives;
std::vector<double> weights; // weights to be applied to the Morph Targets
ExtensionMap extensions;
Value extras;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
Mesh() = default;
DEFAULT_METHODS(Mesh)
bool operator==(const Mesh &) const;
};
class Node {
public:
Node() : camera(-1), skin(-1), mesh(-1) {}
DEFAULT_METHODS(Node)
bool operator==(const Node &) const;
int camera; // the index of the camera referenced by this node
std::string name;
int skin;
int mesh;
std::vector<int> children;
std::vector<double> rotation; // length must be 0 or 4
std::vector<double> scale; // length must be 0 or 3
std::vector<double> translation; // length must be 0 or 3
std::vector<double> matrix; // length must be 0 or 16
std::vector<double> weights; // The weights of the instantiated Morph Target
ExtensionMap extensions;
Value extras;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
};
struct Buffer {
std::string name;
std::vector<unsigned char> data;
std::string
uri; // considered as required here but not in the spec (need to clarify)
// uri is not decoded(e.g. whitespace may be represented as %20)
Value extras;
ExtensionMap extensions;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
Buffer() = default;
DEFAULT_METHODS(Buffer)
bool operator==(const Buffer &) const;
};
struct Asset {
std::string version = "2.0"; // required
std::string generator;
std::string minVersion;
std::string copyright;
ExtensionMap extensions;
Value extras;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
Asset() = default;
DEFAULT_METHODS(Asset)
bool operator==(const Asset &) const;
};
struct Scene {
std::string name;
std::vector<int> nodes;
ExtensionMap extensions;
Value extras;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
Scene() = default;
DEFAULT_METHODS(Scene)
bool operator==(const Scene &) const;
};
class Model {
public:
Model() = default;
DEFAULT_METHODS(Model)
bool operator==(const Model &) const;
std::vector<Accessor> accessors;
std::vector<Animation> animations;
std::vector<Buffer> buffers;
std::vector<BufferView> bufferViews;
std::vector<Material> materials;
std::vector<Mesh> meshes;
std::vector<Node> nodes;
std::vector<Texture> textures;
std::vector<Image> images;
std::vector<Skin> skins;
std::vector<Sampler> samplers;
std::vector<Camera> cameras;
std::vector<Scene> scenes;
std::vector<Light> lights;
int defaultScene = -1;
std::vector<std::string> extensionsUsed;
std::vector<std::string> extensionsRequired;
Asset asset;
Value extras;
ExtensionMap extensions;
// Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled.
std::string extras_json_string;
std::string extensions_json_string;
};
GLTF文件中的 componentType
在GLTF文件格式中,componentType用于指定数据类型。GLTF是一种开放的标准文件格式,用于存储3D模型。该格式设计紧凑、高效,并且易于传输和解析。GLTF文件通常用于WebGL应用程序以及其他需要高效传输3D数据的应用。
componentType 的定义
componentType 属性通常出现在访问器(Accessor)对象中,用于定义存储在缓冲区(Buffer)中的数据类型。它表示元素数组的类型,决定了顶点属性、法线、颜色等数据的基本类型。
常见的 componentType 值及其含义
GLTF规范中定义了一些常用的 componentType 值,每个值都对应一个特定的数据类型。以下是常见的 componentType 值及其含义:
- 5120 (GL_BYTE): 8位有符号整数
- 5121 (GL_UNSIGNED_BYTE): 8位无符号整数
- 5122 (GL_SHORT): 16位有符号整数
- 5123 (GL_UNSIGNED_SHORT): 16位无符号整数
- 5125 (GL_UNSIGNED_INT): 32位无符号整数
- 5126 (GL_FLOAT): 32位浮点数
GLTF 中的 mode 属性
GLTF(GL Transmission Format)文件格式是用于传输和存储3D模型的标准格式之一。mode 属性是 GLTF 文件中定义几何体绘制方式的重要部分。它决定了如何将顶点数据解释为图元,如点、线、三角形等。
mode 属性的定义
mode 属性出现在 primitive 对象中,用于指定绘制几何图元的方式。该属性接受整数值,每个值对应一种 OpenGL 图元类型。
常见的 mode 值及其含义
以下是 GLTF 规范中定义的 mode 值及其含义:
- 0 (POINTS): 绘制点
- 1 (LINES): 绘制独立的线段
- 2 (LINE_LOOP): 绘制线环
- 3 (LINE_STRIP): 绘制连续的线段
- 4 (TRIANGLES): 绘制独立的三角形
- 5 (TRIANGLE_STRIP): 绘制三角形条带
- 6 (TRIANGLE_FAN): 绘制三角形扇
文件的读取和管理
首先读取解析glTF的JSON文件格式,解析完成后,就能知道buffer和image的读取方式,然后在二进制数据块中读取即可。
(1).buffer中的二进制文件
一个buffer包含一个URI,指向包含实际数据的二进制文件(.bin文件),通过buffers、bufferview和accessors中,可以知道文件的数据类型,布局。读取出来的数据不需要解析,直接送进GPU进行渲染。
(2).图片文件
一个image中包含一个URI,通过这个ID去获取具体的图片文件。
参考资料
glTF-Tutorials/gltfTutorial/gltfTutorial_002_BasicGltfStructure.md at master · javagl/glTF-Tutorials