magic-pixels
    Preparing search index...

    Typed vertex attributes

    A vertex attribute does not have to be a 32 bit float. WebGL2 can read a position, a UV or a colour straight out of a smaller integer type, and optionally rescale it on the way in. glTF leans on this heavily - it is how KHR_mesh_quantization shrinks a model's vertex data several times over with no visible loss of quality. This page is about the GPU feature behind it, not about the loader; the loader (step 7) only has to read the accessor metadata and hand the right typed array to BufferAttribute.

    gl.vertexAttribPointer(location, size, type, normalized, stride, offset) takes a type naming the numbers in the buffer and a normalized flag saying what to do with them. WebGL2 accepts eight type values; magic-pixels now supports all of them because each maps one-to-one to a JavaScript typed array:

    type bytes JS array signed range
    BYTE 1 Int8Array -128..127
    UNSIGNED_BYTE 1 Uint8Array 0..255
    SHORT 2 Int16Array -32768..32767
    UNSIGNED_SHORT 2 Uint16Array 0..65535
    INT 4 Int32Array -2³¹..2³¹−1
    UNSIGNED_INT 4 Uint32Array 0..2³²−1
    FLOAT 4 Float32Array any

    Without normalized, the GPU casts the stored integer straight to a float in the shader: a Uint8Array value of 200 arrives as 200.0, which is only useful for things that are genuinely small integers (a bone index, a material ID). With normalized, the same byte is rescaled to the unit range instead, so it can stand in for a float that happens to only need 8 or 16 bits of precision:

    unsigned: value / (2^bits1)              → 0 .. 1
    signed: max(value / (2^(bits1) − 1), −1) → −1 .. 1

    A normalized Uint8 UV coordinate of 255 becomes 1.0, 128 becomes ≈0.502; a normalized Int16 position of 32767 becomes ≈1.0. The max(..., −1) in the signed formula exists because the negative range has one more representable value than the positive one (-32768 vs 32767); without the clamp -32768 / 32767 would slip fractionally past -1.

    A Float32Array position costs 12 bytes per vertex; a normalized Int16Array costs 6, at a precision of about 1 part in 32000 across whatever range the mesh occupies - once the mesh is scaled to fit in -1..1 (which the exporter does, storing the true scale separately), that is finer than a screen pixel for anything but extreme close-ups. UVs in 0..1 are an even better fit for a normalized Uint8 or Uint16: there is no meaningful precision lost, because the source texture only has so many pixels to sample anyway. Halving or quartering every vertex halves or quarters the file size and the memory bandwidth spent reading it - free performance, which is why glTF exporters default to it and KHR_mesh_quantization exists to make it official.

    BufferAttribute.data accepts any of the seven typed arrays above (not Float32Array only, as before), and a third constructor argument sets BufferAttribute.normalized. WebGL2Renderer reads the GL type off data's constructor - Int16Array always means SHORT, so there is nothing to keep in sync - and forwards normalized unchanged to vertexAttribPointer. Nothing else about a geometry changes: count, needsUpdate, dynamic and the buffer-rebuild rules from Geometry all work the same regardless of which typed array backs an attribute.

    The index buffer got the same treatment as a side effect: BufferGeometry.index now stores a Uint16Array/Uint32Array directly instead of a plain number[] that the renderer had to copy into a typed array on every upload. setIndex() keeps its ArrayLike<number> signature, so existing calls are unaffected.

    Where it lives:

    • src/geometries/buffer-geometry.ts: the TypedArray union, BufferAttribute's normalized field, and BufferGeometry.index's new type.
    • src/webgl/gl-constants.ts: glComponentType(), a typed array constructor → GL type lookup.
    • src/webgl/webgl2-renderer.ts: createGeometryResources() passes the derived type and normalized to vertexAttribPointer; the index buffer upload no longer re-wraps geometry.index in a fresh typed array.

    Typed vertex attributes renders the same box twice: once with the usual Float32Array attributes, once quantized into normalized Int16Array/Int8Array/Uint8Arrays built by hand from the same source data. The two are visually identical - open the source and change one of the multipliers (* 32767, * 127, * 255) to something smaller to see the quantization error appear as the box's surface starts to facet.