How to manually read GLTF files

I’m currently working on a game engine, and one very important part of that is being able to import 3D models. And at the moment GLTF files are a great method for doing that due to their open-source nature. There’s also the benefit that a good bit of GLTF data is human-readable, allowing us to easily poke at their inner workings.

A quick disclaimer before we get started, what’s covered here is essentially just a hello world as we will only be reading vertex data but it should be enough for you to get accustomed to the format and understand what other documentation is saying.

File types

GLTF files have two different main file types: .gltf and .glb.

GLTF files are essentially just a rebranded json file, they usually come pared with a .bin file that contains stuff like vertex data but that stuff can also be contained directly in the json.

GLB files are like GLTF files, but everything is contained in the same file. It is split into three sections, a small header, the json string, and the binary buffer.

Image from official gltf github

The GLTF format

In GLTF, everything to do with meshes, animations, and skinning is stored in buffers, and though at first, it may seem daunting to read from a raw binary file without a library, it’s not actually too hard. We’ll take it step by step.

In this article, we’re going to go over how to read the vertex position data from a single mesh from both a .gltf and .glb file.

Before we can get to the actual code we need to understand how to use the json part of the file to find what we want, since we have to hop around a ton to find anything. You can start at the scene level and work your way down if you want, but as I plan to only use the format for single meshes, I’m going to start at the meshes node of the graph.

Let’s say our GLTF file looks something like this (Note that an actual file will have a lot more data):

{
    "accessors" : [
        {
         "bufferView": 0,
         "byteOffset": 0,
         "componentType": 5126,
         "count": 197,
         "max": [ -0.004780198, 0.0003038254, 0.007360002 ],
         "min": [ -0.008092392, -0.008303153, -0.007400591 ],
         "type": "VEC3"
      }
    ],
   "buffers": [
      {
         "byteLength": 2460034,
         "uri": "example.bin"
      }
   ],
    "bufferViews": [
      {
         "buffer": 0,
         "byteLength": 306642,
         "target": 34963,
         "byteOffset": 2153392
      },
    ],
    "meshes": [
      {
         "name": "example mesh",
         "primitives": [
            {
               "attributes": {
                  "POSITION": 0,
                  "NORMAL": 1,
                  "TEXCOORD_0": 2,
                  "TANGENT": 3
               },
               "indices": 4,
               "material": 0,
               "mode": 4
            }
         ]
      }
    ]
}

To find the position data for our mesh we first need to access the “meshes” key at index 0, then access the first primitive. (From what I can tell primitives are essentially just submeshes.) Then we’ll retrieve “attributes”->”POSITION”. This will give us the index of an accessor. Plugging that in we can get the “bufferView” value from the first accessor. This then gives us the index of a buffer view, which we can finally use to get the buffer to retrieve data from. In this case the buffer is stored in the external file “example.bin”. Once we open that file we go to the position given to us by “byteOffset” in the accessor and finally read the buffer data.

Ok, maybe that didn’t make any sense at all when said that way… but let’s see it in code. That might help.

Reading from a GLTF file

I’m going to be using c++ for my example code along with jsoncpp for parsing json, but the steps should be about the same for any other language.

If you want to download a gltf file to mess around with, the Unity Adam Head is what I’m using.

// First define our filname, would probbably be better to prompt the user for one
const std::string& gltfFilename = "example.gltf"

// open the gltf file
std::ifstream jsonFile(gltfFilename, std::ios::binary);

// parse the json so we can use it later
Json::Value json;

try{
    jsonFile >> json;
}catch(const std::exception& e){
    std::cerr << "Json parsing error: " << e.what() << std::endl;
}
jsonFile.close();

// Extract the name of the bin file, for the sake of simplicity I'm assuming there's only one
std::string binFilename = json["buffers"][0]["uri"].asString();

// Open it with the cursor at the end of the file so we can determine it's size,
// We could techincally read the filesize from the gltf file, but I trust the file itself more
std::ifstream binFile = std::ifstream(binFilename, std::ios::binary | std::ios::ate);

// Read file length and then reset cursor
size_t binLength = binFile.tellg();
binFile.seekg(0);


std::vector<char> bin(binLength);
binFile.read(bin.data(), binLength);
binFile.close();



// Now that we have the files read out, let's actually do something with them
// This code prints out all the vertex positions for the first primitive

// Get the primitve we want to print out: 
Json::Value& primitive = json["meshes"][0]["primitives"][0];


// Get the accessor for position: 
Json::Value& positionAccessor = json["accessors"][primitive["attributes"]["POSITION"].asInt()];


// Get the bufferView 
Json::Value& bufferView = json["bufferViews"][positionAccessor["bufferView"].asInt()];


// Now get the start of the float3 array by adding the bufferView byte offset to the bin pointer
// It's a little sketchy to cast to a raw float array, but hey, it works.
float* buffer = (float*)(bin.data() + bufferView["byteOffset"].asInt());

// Print out all the vertex positions 
for (int i = 0; i < positionAccessor["count"].asInt(); ++i)
{
    std::cout << "(" << buffer[i*3] << ", " << buffer[i*3 + 1] << ", " << buffer[i*3 + 2] << ")" << std::endl;
}

// And as a cherry on top, let's print out the total number of verticies
std::cout << "vertices: " << positionAccessor["count"].asInt() << std::endl;

Reading from a GLB file:

Reading from a .glb file is a little bit harder since we can’t just throw it into a JSON parser, but it’s doable. Referring back to the image above in the file type sections we can find all the information about the file format that we need:

std::ifstream binFile = std::ifstream(glbFilename, std::ios::binary); 

binFile.seekg(12); //Skip past the 12 byte header, to the json header
uint32_t jsonLength;
binFile.read((char*)&jsonLength, sizeof(uint32_t)); //Read the length of the json file from it's header

std::string jsonStr;
jsonStr.resize(jsonLength);
binFile.seekg(20); // Skip the rest of the JSON header to the start of the string
binFile.read(jsonStr.data(), jsonLength); // Read out the json string

// Parse the json
Json::Reader reader;
if(!reader.parse(jsonStr, _json))
	std::cerr << "Problem parsing assetData: " << jsonStr << std::endl;

// After reading from the json, the file cusor will automatically be at the start of the binary header

uint32_t binLength;
binFile.read((char*)&binLength, sizeof(binLength)); // Read out the bin length from it's header
binFile.seekg(sizeof(uint32_t), std::ios_base::cur); // skip chunk type

std::vector<char> bin(binLength);
binFile.read(bin.data(), binLength);


//Now you're free to use the data the same way we did above

Summary

Hope this helped you out somewhat. I know it’s a little lacking in details in some areas so once I learn more I might come back and update this with more info on things like animations and skinning. But until then, see you around.

Liked it? Take a second to support WireWhiz on Patreon!
Become a patron at Patreon!