Signed char and half-floats for vertex data?

In my engine I am using signed chars for normals and tangents, and half-floats for UV coordinates. Reducing the vertex format size does provide a significant performance benefit in vertex-limited scenes. It would be nice to have these available in glTF. It seems odd that people are going to the lengths of adding mesh compression extensions (Draco) when tangents and normals are using 32-bit floats. (I know glTF supports unsigned shorts for texcoords, but half floats allow non-normalized coords).

Current layout:

  • position (12 bytes)
  • normal (12)
  • tangent (16)
  • texcoord (8)
    Total: 48 bytes

Compressed layout using char and half float:

  • position (12 bytes)
  • normal (3, round up to 4)
  • tangent (4)
  • texcoord (4)
    Total: 24 bytes

Some additional vertex attribute types are enabled by including the KHR_mesh_quantization extension: see extending mesh attributes. This extension can be used on its own and is very widely supported. It is also complementary to EXT_meshopt_compression for compression of the quantized values.

KHR_mesh_quantization doesn’t allow half floats (I’m not sure why that was omitted) but you can achieve the same size as your compressed layout example, using normalized uint8 or uint16 texture coordinates and a texture offset and scale.

Draco also does internal quantization — the encoding process requires specifying quantization bits for vertex attributes, with most attributes defaulting to 8-10 bits per component value. Decoding to float32 after decompression is common, probably most developers focus too much on transmission size and not memory. But the Draco library does allow you to decode to smaller data types as well.

Nice, but putting the textureTransform object into the material seems like a strange choice. I would expect this to be in the primitives structure. Looking at the extension, it seems it is written with texture atlases in mind, not mesh compression.

My solution is to check if the texcoord data contains coordinates outside of the range 0…1. If it does, 32-bit floats are used, otherwise a normalized unsigned short is used. Would prefer half-floats but this is alright.

Yes, the texture transform also supports use cases like texture atlases — there isn’t a separate texture transform specifically for dequantization parameters.

Some implementations don’t support arbitrary per-material-texture-slot transforms for complexity reasons, e.g. three.js will use up to one transform per UV slot, not per material texture slot. Generally that’s fine. There’s been some discussion of this in KhronosGroup/glTF#1422.

If you’re interested in transmission size (not just in-memory size) it can be worthwhile to quantize further than 16 bits, e.g. 12 or 14 bits, within a uint16 normalized attribute. Same size in memory, but when the file is served with additional compression like Gzip or Meshopt, there’s further size reduction.

Correction: with the model collapsed to a single mesh, Draco produces a 6.2 MB file. So I would just skip straight to that and skip the quantization options.

1 Like