上一篇:GLtf读写库的初步研究
1.glTF-SDK现状及问题
1、接口使用较为方便;有简单示例;
2、研究源码及示例,仅支持连续的addAccessor(),即连续的add绑定同一bufferView的Accessors,
比如很多个mesh,
(1)只能统一获取所有的indices(positions、uvs)再连续的addAccessories,绑定到一个indices的bufferView上;然后再绑定positions的,其次再绑定uvs...,这样流程不太 方便,占内存较大;
(2)对于每一个mesh的indices,都创建一个bufferView,这样也是官方示例所示,问题是会有很多的bufferView;
(3)结合上面两点:对于一部分meshes,创建一个bufferView,这样在上面两点之间做一个平衡;
(4)如果想实现第一点的效果,但是不想用哪种方式,那么可以在glTF-SDK的源码基础上新增一些接口和行为,来满足,具体的,行为上来说,可以交叉的add不同类型的(比如mesh0_indices,mesh0_positions,mesh0_uvs,mesh1_indices,mesh1_positions,mesh1_uvs...);在具体的,addAccessor()时不需要立即写入到stream,而是记录下来(将二进制数据和bufferView的offset等信息更新),最后统一(或自动的)写入stream;
目前在glTF-SDK基础是上做了个尝试,但是工作量有点大,还是暂时搁置;
2.glTF结构
建议要先看官方的specification:https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy
了解了格式之后,使用glTF-SDK会方便很多,
3.代码示例
其实glTF-SDK有序列化及反序列化gltf文件的简单示例(https://github.com/Microsoft/glTF-SDK),对于入门还好,但是对于实际使用还是不够的,现结合实际使用进行总结,
注意:结合官方示例看本文会理解的更好,本文也是基础官方示例,结合使用总结的,
resourceWriter:
std::unique_ptr<ResourceWriter> resourceWriter; // If the file has a '.gltf' extension then create a GLTFResourceWriter if (pathFileExt == MakePathExt(GLTF_EXTENSION)) { resourceWriter = std::make_unique<GLTFResourceWriter>(std::move(streamWriter)); } // If the file has a '.glb' extension then create a GLBResourceWriter. This class derives // from GLTFResourceWriter and adds support for writing manifests to a GLB container's // JSON chunk and resource data to the binary chunk. if (pathFileExt == MakePathExt(GLB_EXTENSION)) { resourceWriter = std::make_unique<GLBResourceWriter>(std::move(streamWriter)); } if (!resourceWriter) { throw std::runtime_error("Command line argument path filename extension must be .gltf or .glb"); }
Initialize Buffer & AddBuffer:
// The Document instance represents the glTF JSON manifest Document document; // Use the BufferBuilder helper class to simplify the process of // constructing valid glTF Buffer, BufferView and Accessor entities BufferBuilder bufferBuilder(std::move(resourceWriter)); // Create all the resource data (e.g. triangle indices and // vertex positions) that will be written to the binary buffer const char* bufferId = nullptr; // Specify the 'special' GLB buffer ID. This informs the GLBResourceWriter that it should use // the GLB container's binary chunk (usually the desired buffer location when creating GLBs) if (dynamic_cast<const GLBResourceWriter*>(&bufferBuilder.GetResourceWriter())) { bufferId = GLB_BUFFER_ID; } // Create a Buffer - it will be the 'current' Buffer that all the BufferViews // created by this BufferBuilder will automatically reference bufferBuilder.AddBuffer(bufferId);
Prepare Data:
// get data from mesh std::vector<unsigned short> indexes; std::vector<float> vertexes, normals, uvs; GetMeshData(mesh, indexes, vertexes, normals, uvs); std::vector<unsigned char> batchIds((unsigned char)(vertexes.size() / 3), batchId);
AddBufferView:
// Create a BufferView with target ARRAY_BUFFER (as it will reference vertex attribute data) bufferBuilder.AddBufferView(BufferViewTarget::ARRAY_BUFFER); std::vector<float> minValues(3U, (std::numeric_limits<float>::max)()); std::vector<float> maxValues(3U, std::numeric_limits<float>::lowest()); const size_t positionCount = vertexes.size(); // Accessor min/max properties must be set for vertex position data so calculate them here for (size_t i = 0U, j = 0U; i < positionCount; ++i, j = (i % 3U)) { minValues[j] = (std::min)(vertexes[i], minValues[j]); maxValues[j] = (std::max)(vertexes[i], maxValues[j]); } accessorIdPositions = bufferBuilder.AddAccessor(vertexes, { TYPE_VEC3, COMPONENT_FLOAT, false, std::move(minValues), std::move(maxValues) }).id; // normals bufferBuilder.AddBufferView(BufferViewTarget::ARRAY_BUFFER); accessorIdNormals = bufferBuilder.AddAccessor(normals, { TYPE_VEC3, COMPONENT_FLOAT }).id; // uvs bufferBuilder.AddBufferView(BufferViewTarget::ARRAY_BUFFER); accessorIdUvs = bufferBuilder.AddAccessor(uvs, { TYPE_VEC2, COMPONENT_FLOAT }).id; // batchId bufferBuilder.AddBufferView(BufferViewTarget::ELEMENT_ARRAY_BUFFER); accessorIdBatchIds = bufferBuilder.AddAccessor(batchIds, { TYPE_SCALAR, COMPONENT_BYTE }).id; // Create a BufferView with a target of ELEMENT_ARRAY_BUFFER (as it will reference index // data) - it will be the 'current' BufferView that all the Accessors created by this // BufferBuilder will automatically reference bufferBuilder.AddBufferView(BufferViewTarget::ELEMENT_ARRAY_BUFFER); // Copy the Accessor's id - subsequent calls to AddAccessor may invalidate the returned reference accessorIdIndices = bufferBuilder.AddAccessor(indexes, { TYPE_SCALAR, COMPONENT_UNSIGNED_SHORT/*COMPONENT_UNSIGNED_SHORT*/ }).id;
node:
{ "nodes": [ { "name": "Car", "children": [1, 2, 3, 4] }, { "name": "wheel_1" }, { "name": "wheel_2" }, { "name": "wheel_3" }, { "name": "wheel_4" } ] }
支持如下两种transform形式,当然需要结合实际情况选用,博主用的就是第二种矩阵形式的,
需要注意矩阵存储是列优先的
{ "nodes": [ { "name": "Box", "rotation": [ 0, 0, 0, 1 ], "scale": [ 1, 1, 1 ], "translation": [ -17.7082, -11.4156, 2.0922 ] } ] }
{ "nodes": [ { "name": "node-camera", "camera": 1, "matrix": [ -0.99975, -0.00679829, 0.0213218, 0, 0.00167596, 0.927325, 0.374254, 0, -0.0223165, 0.374196, -0.927081, 0, -0.0115543, 0.194711, -0.478297, 1 ] } ] }
scene:
All nodes listed in
scene.nodes
array must be root nodes// scene & root node Scene scene; Node rootNode; SetRootNodeTransform(rootNode.matrix); rootNode.name = new char[5] { 'r', 'o', 'o', 't', '\0' }; ...... // element node Node nodeElement; nodeElement.name = itrItem->first; ...... // mesh node Node node; node.meshId = meshId; ConvertToMatrix(itrMesh->meshTrs, node.matrix); // Add it to the Document and store the generated ID auto nodeId = document.nodes.Append(std::move(node), AppendIdPolicy::GenerateOnEmpty).id; nodeElement.children.push_back(nodeId); auto elemNodeId = document.nodes.Append(std::move(nodeElement), AppendIdPolicy::GenerateOnEmpty).id; rootNode.children.push_back(elemNodeId); // All nodes listed in scene.nodes array must be root nodes auto rootNodeId = document.nodes.Append(std::move(rootNode), AppendIdPolicy::GenerateOnEmpty).id; scene.nodes.push_back(rootNodeId); // Add it to the Document, using a utility method that also sets the Scene as the Document's default document.SetDefaultScene(std::move(scene), AppendIdPolicy::GenerateOnEmpty);
那么 meshId从何而来?
// Construct a MeshPrimitive. Unlike most types in glTF, MeshPrimitives are direct children // of their parent Mesh entity rather than being children of the Document. This is why they // don't have an ID member. MeshPrimitive meshPrimitive; meshPrimitive.materialId = materialId; meshPrimitive.indicesAccessorId = itrMesh->accessorIdIndices; meshPrimitive.attributes[ACCESSOR_POSITION] = itrMesh->accessorIdPositions; meshPrimitive.attributes[ACCESSOR_NORMAL] = itrMesh->accessorIdNormals; meshPrimitive.attributes["_BATCHID"] = itrMesh->accessorIdBatchIds; meshPrimitive.attributes[ACCESSOR_TEXCOORD_0] = itrMesh->accessorIdUvs; meshPrimitive.mode = MESH_TRIANGLES; // Construct a Mesh and add the MeshPrimitive as a child Mesh mesh; mesh.primitives.push_back(std::move(meshPrimitive)); // Add it to the Document and store the generated ID auto meshId = document.meshes.Append(std::move(mesh), AppendIdPolicy::GenerateOnEmpty).id;
material & texture:
// initialize material Material material; // image used by texture Image image; image.uri = fileName.c_str(); auto imageId = document.images.Append(image, AppendIdPolicy::GenerateOnEmpty).id; // sampler used by texture Sampler sampler; sampler.magFilter = MagFilterMode::MagFilter_LINEAR; sampler.minFilter = MinFilterMode::MinFilter_LINEAR_MIPMAP_LINEAR; sampler.wrapS = WrapMode::Wrap_REPEAT; sampler.wrapT = WrapMode::Wrap_REPEAT; auto samplerId = document.samplers.Append(sampler, AppendIdPolicy::GenerateOnEmpty).id; // texture Texture texture; texture.imageId = imageId; texture.samplerId = samplerId; auto textureId = document.textures.Append(texture, AppendIdPolicy::GenerateOnEmpty).id; TextureInfo textureInfo; textureInfo.texCoord = 0; textureInfo.textureId = textureId; // texture transform static string strKHRTextureTrs = "KHR_texture_transform"; if (document.extensionsUsed.find(strKHRTextureTrs) == document.extensionsUsed.end()) document.extensionsUsed.insert(strKHRTextureTrs); if (document.extensionsRequired.find(strKHRTextureTrs) == document.extensionsRequired.end()) document.extensionsRequired.insert(strKHRTextureTrs); ...... string textureTrsContent; GetTextureTransformExtension(textureData, textureTrsContent); textureInfo.extensions["KHR_texture_transform"] = textureTrsContent; material.metallicRoughness.baseColorTexture = textureInfo; material.metallicRoughness.metallicFactor = 0.4f; material.metallicRoughness.roughnessFactor = 0.3015f; material.alphaMode = abs(itrMesh->material->getTransparency()) < 1e-5 ? ALPHA_OPAQUE : ALPHA_BLEND/*ALPHA_OPAQUE*/; material.doubleSided = false; //material.alphaCutoff = 1.0f - itrMesh->material->getTransparency(); // Add it to the Document and store the generated ID auto materialId = document.materials.Append(std::move(material), AppendIdPolicy::GenerateOnEmpty).id;
看不懂?没关系,结合数据格式继续看~
![]()
这部分最好详细了解gltf2.0关于材质部分的格式之后再看代码,理解的更深,
上面表示texture transform用到了extension数据,glTF格式预定义了一些扩展格式:
https://github.com/KhronosGroup/glTF/blob/master/extensions/README.md
4.示例文件
{
"asset": {
"version": "2.0"
},
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 180,
"type": "VEC3",
"max": [
-0.8864412307739258,
3.537320137023926,
26.694475173950197
],
"min": [
-55.02029800415039,
-49.61228561401367,
20.26502227783203
]
},
{
"bufferView": 1,
"componentType": 5126,
"count": 180,
"type": "VEC3"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 180,
"type": "VEC2"
},
{
"bufferView": 3,
"componentType": 5120,
"count": 180,
"type": "SCALAR"
},
{
"bufferView": 4,
"componentType": 5123,
"count": 348,
"type": "SCALAR"
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 2160,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 2160,
"byteLength": 2160,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 4320,
"byteLength": 1440,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 5760,
"byteLength": 180,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 5940,
"byteLength": 696,
"target": 34963
}
],
"buffers": [
{
"byteLength": 6636
}
],
"images": [
{
"uri": "Thermal_Moisture.Roof.Tiles.Spanish.png"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0,
"extensions": {
"KHR_texture_transform": {
"offset": [
0.0,
0.0
],
"rotation": 0.0,
"scale": [
0.6000000238418579,
0.6000000238418579
],
"texCoord": 0
}
}
},
"metallicFactor": 0.4000000059604645,
"roughnessFactor": 0.30149999260902407
}
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 0,
"_BATCHID": 3,
"TEXCOORD_0": 2
},
"indices": 4,
"material": 0
}
]
}
],
"nodes": [
{
"mesh": 0
},
{
"children": [
0
],
"name": "356468"
},
{
"children": [
1
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
-1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0
],
"name": "root"
}
],
"samplers": [
{
"magFilter": 9729,
"minFilter": 9987
}
],
"scenes": [
{
"nodes": [
2
]
}
],
"textures": [
{
"sampler": 0,
"source": 0
}
],
"scene": 0,
"extensionsUsed": [
"KHR_texture_transform"
],
"extensionsRequired": [
"KHR_texture_transform"
]
}
5.总结
- 需要学习glTF2.0 specification:https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy
- 需要下载glTF-SDK,并结合官方示例进行初步理解:https://github.com/Microsoft/glTF-SDK
- 需要结合本文代码示例及示例文件进行进一步的理解;
- 当然还可以使用其他的glTF生成库来生成glTF;
- 多学习,多实操~