glTF入门
glTF介绍
DCC生成的模型往往不能直接被渲染引擎使用,比如包含冗余数据、缺失场景数据、缺少动画状态机、压缩率不足等。许多引擎拥有一个Asset Pipeline,负责将不同DCC导出的各类数据转化为引擎可以直接使用的资源文件,比如UE的Cooking操作
glTF是一种表示3D场景的模型格式,它的目标就是尽可能保留3D场景相关的数据(使用json描述场景),为渲染引擎提供无需解码的模型数据(使用二进制存储buffer和image)
文本类型
glTF有二进制(.glb)和ASCII文本格式(.gltf)
一般而言,文本格式的glTF并不包含二进制内容(buffer和image),而是仅有一个uri链接,指向真正存储这些二进制数据的文件,这些数据已经被处理成GPU便于访问的格式
"buffer01": { "byteLength": 12352, "type": "arraybuffer", "uri": "buffer01.bin" }
|
"image01": { "uri": "image01.png" }
|
正是因为glTF这种描述和存储分离的特点,我们可以用很小的文件就能得到场景的描述,当我们需要使用到某个buffer或image时再懒加载这个资源,于是很适合Web项目。现在很多基于WebGL的渲染器都使用glTF格式
值得注意的是,对于一些小模型,我们也可以将这些二进制嵌入到json中
"buffers" : [ { "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=", "byteLength" : 44 } ],
|
json结构
scene
scene是解析一个glTF的入口,一个glTF可以包含多个scene
一个scene中会有一个nodes,nodes是一个数组,指向多个node
一个node中会有mesh等元素
mesh
一个meshes包含多个primitives(图元),每个图元中有一个attributes(属性)和indices
attributes中包含着模型的顶点数据(vertices),比如POSITION、NORMAL、TEXCOORD_0
"meshes" : [ { "primitives" : [ { "attributes" : { "POSITION" : 1 }, "indices" : 0 } ] } ],
|
accessor
accessor(访问器),这是对资源的一种抽象封装,它存储了bufferView的结构,以及访问方法
对于mesh、skin、animation等数据,我们要通过accessor来访问。accessor中有一个bufferView,bufferView又指向存储真正二进制数据的buffer
"accessors" : [ { "bufferView" : 0, "byteOffset" : 0, "componentType" : 5123, "count" : 3, "type" : "SCALAR", "max" : [ 2 ], "min" : [ 0 ] }, { "bufferView" : 1, "byteOffset" : 0, "componentType" : 5126, "count" : 3, "type" : "VEC3", "max" : [ 1.0, 1.0, 0.0 ], "min" : [ 0.0, 0.0, 0.0 ] } ],
|
buffer
A buffer defines a block of raw, unstructured data with no inherent meaning
为了便于渲染API的使用,glTF里的buffer是单纯的二进制,因此在解析时一定要明确读取的步长、类型
bufferView
A bufferView describes a “chunk” or a “slice” of the whole, raw buffer data
bufferView将一个完整的巨大的二进制内容切割为一个个小片,可以实现一个buffer存储多种类型的数据,还可以实现字节对齐等功能
一个简单的读mesh示例
这里我使用tinygltf库来加载glTF,这是一个基于cmake的纯头文件库
举一个简单的glTF读取mesh的例子,仅读取index buffer和vertex buffer(只有POSITION和TEXCOORD_0)
class GltfLoader { public: GltfLoader(std::string model_path); ~GltfLoader();
std::vector<Index> loaded_index_buffer; std::vector<Vertex> loaded_vertex_buffer;
private: tinygltf::Model m_gltf_model; tinygltf::TinyGLTF m_loader_context; };
|
GltfLoader::GltfLoader(std::string model_path) { std::string gltf_load_error; std::string gltf_load_warning; bool gltf_load_result = m_loader_context.LoadASCIIFromFile(&m_gltf_model, &gltf_load_error, &gltf_load_warning, model_path);
if (gltf_load_result) { for (auto& mesh : m_gltf_model.meshes) { for (auto& primitive : mesh.primitives) { int vertex_count = 0; std::vector<Vertex> local_vertex_buffer; if (primitive.attributes.find("POSITION") != primitive.attributes.end()) { const int accessor_index = primitive.attributes.at("POSITION"); const tinygltf::Accessor& accessor = m_gltf_model.accessors[accessor_index]; const tinygltf::BufferView& buffer_view = m_gltf_model.bufferViews[accessor.bufferView]; const tinygltf::Buffer& buffer = m_gltf_model.buffers[buffer_view.buffer]; const float* data_ptr = reinterpret_cast<const float*>(&buffer.data[buffer_view.byteOffset + accessor.byteOffset]); const int position_byte_stride = 3;
vertex_count = accessor.count;
for (int i = 0; i < vertex_count; i++) { Vertex local_vertex; local_vertex.pos = glm::vec3(data_ptr[i * position_byte_stride], data_ptr[i * position_byte_stride + 1], data_ptr[i * position_byte_stride + 2]); local_vertex_buffer.push_back(local_vertex); } } if (primitive.attributes.find("TEXCOORD_0") != primitive.attributes.end()) { const int accessor_index = primitive.attributes.at("TEXCOORD_0"); const tinygltf::Accessor& accessor = m_gltf_model.accessors[accessor_index]; const tinygltf::BufferView& buffer_view = m_gltf_model.bufferViews[accessor.bufferView]; const tinygltf::Buffer& buffer = m_gltf_model.buffers[buffer_view.buffer]; const float* data_ptr = reinterpret_cast<const float*>(&buffer.data[buffer_view.byteOffset + accessor.byteOffset]); const int uv_byte_stride = accessor.ByteStride(buffer_view) ? (accessor.ByteStride(buffer_view) / sizeof(float)) : tinygltf::GetNumComponentsInType(TINYGLTF_TYPE_VEC2); for (int i = 0; i < vertex_count; i++) { local_vertex_buffer[i].texture_coord = glm::vec2(data_ptr[i * uv_byte_stride], data_ptr[i * uv_byte_stride + 1]); } } for (int i = 0; i < vertex_count; i++) { loaded_vertex_buffer.push_back(local_vertex_buffer[i]); } if (primitive.indices >= 0) { const tinygltf::Accessor& accessor = m_gltf_model.accessors[primitive.indices]; const tinygltf::BufferView& buffer_view = m_gltf_model.bufferViews[accessor.bufferView]; const tinygltf::Buffer& buffer = m_gltf_model.buffers[buffer_view.buffer];
const void* dataPtr = &(buffer.data[buffer_view.byteOffset + accessor.byteOffset]); const int elements_count = accessor.count;
switch (accessor.componentType) { case TINYGLTF_PARAMETER_TYPE_UNSIGNED_INT: { const uint32_t* buf = static_cast<const uint32_t*>(dataPtr); for (int i = 0; i < elements_count; i++) { Index local_index; local_index.index = buf[i]; loaded_index_buffer.push_back(local_index); } break; } case TINYGLTF_PARAMETER_TYPE_UNSIGNED_SHORT: { const uint16_t* buf = static_cast<const uint16_t*>(dataPtr); for (int i = 0; i < elements_count; i++) { Index local_index; local_index.index = buf[i]; loaded_index_buffer.push_back(local_index); } break; } case TINYGLTF_PARAMETER_TYPE_UNSIGNED_BYTE: { const uint8_t* buf = static_cast<const uint8_t*>(dataPtr); for (int i = 0; i < elements_count; i++) { Index local_index; local_index.index = buf[i]; loaded_index_buffer.push_back(local_index); } break; } default: std::cerr << "Index component type " << accessor.componentType << " not supported!" << std::endl; return; } } } } } }
|
参考
glTF Tutorials