Daniel's been poking around in GLB files and wants to know what's actually inside them. Here's what he sent us.
Two GLBs of the same object — one's two hundred kilobytes and looks like grey clay, the other's eighty megabytes and looks photoreal. Same format, wildly different contents. He wants us to walk through the binary structure: the JSON chunk that describes the scene graph, nodes, meshes, and materials, versus the binary buffer chunk holding the actual vertex positions, normals, UV coordinates, and indices. Then the big one — textured versus untextured. What does textured actually mean? Embedded image data plus a UV map telling the renderer how to wrap that image onto the geometry, versus a model with only material definitions, or vertex colors baked into the mesh, or nothing but geometry with a default grey PBR material. He wants the full PBR material set: base color slash albedo, metallic-roughness, normal map, ambient occlusion, emissive — and what happens visually when each one is missing. Why a GLB can reference textures externally instead of embedding them, and how that breaks when you hand the file to someone else. What Draco and Meshopt compression do to the geometry. And why polygon count, texture resolution, and compression are the three dials that explain almost any file size difference. Then practical advice: how do you inspect an unknown GLB before you drop it into a project.
That's a full syllabus.
It is, and I love it. So let's open the box. Literally — we're going to hex dump a GLB and walk through every byte. But first, the big picture, because the format is deceptively simple.
GLB is the binary container for glTF two point oh — the so-called JPEG of three D. The Khronos Group released glTF two point oh in June twenty seventeen, and the whole idea was a self-contained, portable three D asset format. Something you can drop into any engine, any viewer, and it just works. The GLB is the single-file version of that.
And the single-file part is what makes it interesting, because it's not actually a single blob of data. It's a container.
No, wait, I said I wasn't going to do that. It's a container with at least two chunks stuffed inside. A JSON chunk and a binary buffer chunk. The JSON chunk is the scene graph — it's human-readable text describing every node, mesh, material, and how they connect. The binary chunk is raw vertex data, index buffers, and optionally embedded images. The split is the key to understanding everything about file size.
So one chunk you can open in a text editor and read. The other is opaque bytes.
Right. And here's why that matters: if you want to know why a GLB is big, you look at the binary chunk. If you want to know what's in it, you look at the JSON. The JSON is like a recipe — it says there's a mesh with four hundred triangles, it uses material number three, the position data starts at byte offset zero in buffer view zero. The binary chunk is the ingredients — here are twelve hundred floats for those positions.
Recipe and ingredients. I'll allow it.
Thank you. Alright, let's crack open the actual binary format. If you hex dump a GLB, the first thing you see is the twelve-byte header. Four bytes for the magic number, four bytes for the version, four bytes for the total file length.
And the magic number is...
Zero x four six five four six C six seven. Which is the ASCII string glTF. Every GLB starts with those four bytes. It's how parsers know what they're looking at. Then the version — for glTF two point oh it's the integer two, stored as a little-endian unsigned int. Then the total length of the entire file in bytes.
So the header alone tells you this is a GLB, it's version two, and it's this big.
That's it. Twelve bytes and you already know the basics. Right after the header comes the first chunk. Every chunk has an eight-byte header of its own: four bytes for the chunk length, four bytes for the chunk type. The JSON chunk always comes first, and its type is zero x four E four F five three four A — that's the ASCII string JSON.
And the chunk length tells you exactly how many bytes of JSON to read.
So you read those eight bytes, then you read chunk-length bytes of JSON text. That text is the entire scene description. Nodes, meshes, accessors, buffer views, materials, textures, images, samplers, animations, skins — all of it in one JSON object. Then, if there's a binary buffer, the next chunk has type zero x zero zero four E four nine four two — that's B I N null in ASCII, four bytes with a null terminator.
Wait, the type field is four bytes and B I N is only three letters, so they null-pad it?
Yeah, it's B I N backslash zero. The chunk type field is always exactly four ASCII characters or null-padded. JSON is exactly four. B I N is three plus a null. It's tidy.
So you've got header, JSON chunk, binary chunk. That's a minimal valid GLB.
And the JSON chunk is where the detective work happens. Let me walk through what's actually in there. The top-level JSON object has arrays: nodes, meshes, accessors, buffer views, materials, textures, images. The nodes array defines the transform hierarchy — each node has a name, maybe a mesh reference, maybe children, and a matrix or translation-rotation-scale. The meshes array defines the actual drawable geometry — each mesh has primitives, and each primitive references an accessor for its indices and attributes.
So a node says where something is in space, a mesh says what to draw.
Right. And the accessor is the bridge between the JSON and the binary. An accessor says: I point to buffer view zero, starting at byte offset twelve, I contain three hundred unsigned shorts, they represent triangle indices. Or: I contain twelve hundred floats, they represent vertex positions, three per vertex. The accessor also stores min and max bounds — the bounding box of the positions, for example, which is incredibly useful for culling.
And the buffer view is the slice of the binary chunk.
A buffer view says: I cover bytes zero through fourteen thousand four hundred of the binary buffer, and I'm meant to be used as an index buffer or vertex attribute buffer. It can specify a byte stride too — if your vertex data is interleaved, the stride tells you how many bytes to skip between vertices.
Interleaved meaning positions, normals, and UVs packed together for each vertex, rather than all positions in one block, all normals in another.
Yes. Interleaved is: position, normal, UV, position, normal, UV, position, normal, UV. Non-interleaved is: all positions, then all normals, then all UVs. Interleaved is better for GPU cache coherence, but it means the buffer view stride is thirty-two bytes — eight floats at four bytes each.
That's the number Daniel was asking about. Thirty-two bytes per vertex.
That's the canonical number for a vertex with position, normal, and one UV set. Three floats for position, three for normal, two for UV. Eight floats times four bytes each. Thirty-two bytes. If you have tangents, that's another four floats — sixteen more bytes. Two UV sets, four more floats. Vertex colors, four more. It adds up fast.
So a hundred thousand vertices at thirty-two bytes each is three point two megabytes just for the vertex data, before you even count indices.
And indices are typically two bytes each for unsigned short, or four bytes for unsigned int if you have more than sixty-five thousand vertices. A hundred thousand triangles at three indices each is three hundred thousand indices. At two bytes per index, that's six hundred kilobytes. So a hundred thousand triangle mesh with positions, normals, and UVs is roughly three point eight megabytes uncompressed. That's your baseline.
And that's before we even talk about textures.
Which brings us to the big one. Textured versus untextured. Daniel asked what textured actually means, and it's more specific than most people think.
Textured means the material references a texture object. That texture object references an image. That image references either a buffer view — meaning the PNG or JPEG is embedded right in the binary chunk — or an external URI, meaning the image is a separate file on disk. And the material also defines how that texture maps onto the geometry via UV coordinates stored in the vertex data.
So textured equals image data plus UV map plus material reference. All three have to be there.
All three. And if any one is missing, you don't get a textured model. But here's where it gets interesting — there are actually four levels of visual information a GLB can carry. Level one: embedded images with UV maps. The full pipeline. This is your eighty megabyte photoreal model. Level two: material definitions only. The JSON has PBR values — base color as a float array, metallic factor, roughness factor — but no image data at all. The renderer applies those values uniformly across the whole surface. Level three: baked vertex colors. The mesh has a COLOR underscore zero attribute — every vertex carries its own RGB value. No textures needed, but you can get surprisingly detailed surface appearance. Level four: geometry only. No materials, no vertex colors. The renderer falls back to a default grey PBR material.
Level four is the grey clay Daniel mentioned.
That's the one. And here's what's counterintuitive: level two and level three can look really good. A model with well-tuned PBR values — the right base color, the right roughness, the right metalness — can look like polished copper or rough concrete without a single texture pixel. Add vertex colors and you can paint detail right onto the mesh. It's not photoreal, but it's not grey clay either.
And it'll be tiny compared to the textured version.
Two hundred kilobytes versus eighty megabytes tiny. All the visual weight is in the textures.
So let's go through the PBR material set. What are the maps, and what breaks when they're missing?
The standard PBR metallic-roughness model has five texture slots. Base color — that's your albedo or diffuse map, the surface color ignoring lighting. Metallic-roughness — this is a packed texture where the blue channel is the metalness value and the green channel is the roughness. The red channel is unused, or sometimes holds occlusion. Normal map — tangent-space surface normals that fake small-scale geometric detail. Occlusion — ambient occlusion in the red channel, which darkens crevices and corners. And emissive — parts of the surface that glow independently of scene lighting.
And each one missing has a specific visual consequence.
Missing base color and the surface is white or whatever the material's base color factor defaults to. Missing metallic-roughness and everything looks uniformly shiny or uniformly matte — you lose the variation that makes leather look different from chrome on the same object. Missing the normal map and the surface goes flat — all those little bumps and scratches disappear, and it looks plastic and smooth. Missing occlusion and you lose contact shadows — the darkening where two surfaces meet, under bolts, in crevices. The model looks like it's floating, ungrounded.
The occlusion one is subtle but it's what makes things feel physically present.
It really is. Ambient occlusion is the unsung hero of PBR. You don't notice it when it's there, but you definitely notice when it's gone. Everything looks like a video game from twenty years ago.
And emissive?
Emissive is the most optional. It's for things like LED panels, glowing eyes, headlights. If it's missing, those surfaces just don't glow. For most models it's not even included.
That's the material side. Now the other big piece Daniel asked about: external texture references. Why would you not embed the textures?
Three reasons. Development speed, version control, and CDN delivery. During development, if you're exporting from Blender every five minutes, re-encoding all your four K textures into the GLB on every export is painfully slow. Keeping them external means the GLB export is instant — it just writes URIs. For version control, you can track the geometry changes separately from the texture changes. And for web delivery, external textures can be cached independently by the browser — change the geometry, the cached textures still load instantly.
The breakage happens when you hand someone the GLB without the textures folder.
Classic scenario. You export from Blender, textures go to a folder called textures next to the GLB. The URIs in the JSON are relative paths: textures slash albedo dot png. You zip up the GLB and email it. The recipient opens it and gets a grey blob because the URIs point to files that don't exist on their machine. There's no standard fallback, no embedded thumbnail, nothing. The GLB is technically valid — the JSON is fine, the geometry is fine — but it's visually broken.
There's no warning. The file just opens grey.
No warning at all. Unless your viewer logs missing resources, you might not even know it's supposed to be textured. That's why the inspection step Daniel asked about is so important — you need to check for external URIs before you assume the file is self-contained.
Alright, let's talk compression. Draco and Meshopt. What are they actually doing?
Both compress the geometry — the vertex data and index buffers in the binary chunk. They don't touch textures at all. Draco is Google's library, and it's a bit-level compressor. It uses Edgebreaker connectivity compression on the triangle indices — that's an algorithm that walks the mesh surface and encodes the triangle connectivity in about one to two bits per triangle. Then it quantizes vertex attributes — positions, normals, UVs — to a specified bit depth and entropy-encodes them.
It's lossy compression on the vertex data.
Lossy in a controlled way. You specify the quantization bits — typically eight to twelve bits per component. At ten bits, positions are quantized to about a thousand steps per axis, which for most models is visually indistinguishable from the original thirty-two-bit floats. The compression ratios are dramatic: ten to one to fifteen to one on geometry data is typical. A fifteen megabyte geometry buffer becomes one to one and a half megabytes.
The cost is decompression time.
CPU cost at load time, yes. Draco decoding is not trivial — for a large mesh it can take a few hundred milliseconds. And the extension is KHR underscore draco underscore mesh underscore compression. It's a Khronos extension, widely supported. But it's all-or-nothing for the mesh — if your runtime doesn't support Draco, the mesh simply won't load.
Versus Meshopt.
Meshopt is different. It's a byte-level compressor using the meshoptimizer library by Arseny Kapoulkine. Instead of bit-level entropy encoding, it uses filter-based compression. It applies a filter to reorder the bytes for better compressibility, then runs a standard LZ compressor. The filters are the clever part.
There are three filter modes.
Three. Octahedral for normals — that encodes a unit vector in just two bytes by mapping the octahedron to a square, and it's remarkably precise. Quaternion for rotations — encodes rotation quaternions in a compressed form. Exponential for positions — encodes floating point values with a shared exponent, good when positions have a limited dynamic range. After filtering, the data is compressed with a standard LZ compressor.
It tends to beat Draco on ratio and speed.
Often, yes. On the same mesh, Draco might get a fifteen megabyte geometry buffer down to one point two megabytes, while Meshopt gets it to nine hundred kilobytes. And Meshopt decompression is generally faster — it's simpler, byte-aligned operations rather than bit-unpacking. The extension is EXT underscore meshopt underscore compression — a vendor extension, but it's supported in three dot js, Babylon, and most web engines.
If you control the runtime, Meshopt is usually the better choice.
If you control the runtime and you're optimizing for web delivery, Meshopt is fantastic. If you need maximum compatibility — say you're uploading to a platform that might use different engines — Draco has broader support because it's a Khronos extension.
Which brings us to Daniel's three dials. Polygon count, texture resolution, compression.
This is the framework that explains almost any GLB file size difference. Dial one: polygon count. Each triangle costs roughly fifty to eighty bytes uncompressed — three indices plus three positions plus three normals plus two UVs. A five hundred thousand triangle model is around thirty to forty megabytes of geometry before compression. Dial two: texture resolution. A four K PNG is fifteen to twenty megabytes. A one K PNG is one to two megabytes. A five twelve PNG is about three hundred kilobytes. If your model has five PBR maps at four K, that's seventy-five to a hundred megabytes right there. Dial three: compression. Draco or Meshopt on the geometry gets you ten to fifteen to one. JPEG or PNG compression on textures — or better, KTX two slash Basis Universal for GPU-compressed textures — can get you another five to ten to one.
That eighty megabyte GLB Daniel mentioned — break it down.
Seventy megabytes of four K textures plus ten megabytes of five hundred thousand triangle geometry. Compress the textures to two K and switch to JPEG — that's about five megabytes of textures. Enable Draco on the geometry — that's about one megabyte. Total: six megabytes. Same visual quality at normal viewing distances, ninety-two percent smaller.
If someone just sees the eighty megabyte file and the six megabyte file side by side, they assume the six megabyte one is lower quality.
That's the misconception. Smaller doesn't mean worse. It means someone turned the dials intelligently. The three dials let you trade off size versus quality systematically, and most exporters default to the safest, least optimized settings — maximum resolution, no compression — because they'd rather give you a huge file that definitely works than a small file that might have compatibility issues.
How do you actually inspect an unknown GLB? Daniel wanted the practical workflow.
Four methods, from quickest to deepest. Method one: drag it into glTF Transform Inspect. That's a command-line tool — gltf-transform inspect model dot glb. It prints chunk sizes, vertex counts, texture dimensions, compression extensions, everything. You get a one-page summary of exactly what's inside.
If you don't want to install a command-line tool?
Method two: the glTF Viewer at gltf-viewer dot don mccurdy dot com. Drag your GLB in, it renders it and shows the full scene graph in a panel. You can click through every node, every mesh, every texture, see the UV maps, see the material values. It's the easiest way to visually explore a file.
If you want to get your hands dirty?
Method three: open it in a text editor. A GLB is binary, but if you rename it to dot gltf, many editors will show you the JSON chunk. It won't be pretty — there's binary garbage around it — but you can find the JSON and read the materials, the URIs, the accessor counts. Or use a hex editor and look at the magic number, the chunk headers, the total length. Method four: if you're in a three dot js project, use GLTF Loader with manager dot set URL modifier to log every resource the loader requests. That catches external texture URIs in real time.
The checklist before dropping a GLB into a project.
Five questions. One: is it self-contained? Check for external URIs in the images array. Two: what's the texture resolution? Four K is overkill for web — resize to two K or one K. Three: what's the triangle count? Over a hundred thousand needs level-of-detail variants. Four: is it compressed? If not, run it through gltf-transform with Draco or Meshopt. Five: does it use compression extensions your runtime supports? Check for KHR underscore draco or EXT underscore meshopt and make sure your engine can decode them.
And the golden rule?
File size equals polygons times bytes per vertex, plus texture pixels times bytes per pixel times compression ratio. Everything else is noise. Understand those three dials and you can predict and control any GLB's size.
Next time someone sends you a GLB, you're not just getting a file — you're getting a JSON scene graph, a binary geometry buffer, possibly some compressed textures, and a whole lot of decisions about what to embed and what to reference.
Now you know how to read those decisions. What about animated GLBs, though? That's a whole other can of worms — skinning, morph targets, animation samplers. The binary chunk gets a lot more complicated when you add vertex joints and weights.
Maybe we'll open that box another time.
Hilbert: If I'm downloading random GLBs from Sketchfab and half of them show up grey, is that broken files or missing textures?
Almost certainly missing textures. The geometry is fine, the JSON is fine, but the images array has URIs pointing to files that aren't on your machine. Check for external references — if they're there, the file isn't self-contained. You need the textures folder next to the GLB, or you need to find a version with embedded images.
Good question, Hilbert. Thanks.
Thanks, Hilbert.
Alright. If you've got a GLB sitting on your desktop right now, go inspect it. You'll see the format completely differently once you know what the chunks are doing.
If you've got a weird prompt of your own — technical, philosophical, whatever — send it in. Show at my weird prompts dot com. We read everything.
This has been My Weird Prompts. I'm Corn.
I'm Herman Poppleberry. We'll be back soon.