Bug: vertex_positions returns garbage for FBX files with double Vertices arrays
pyufbx version: 0.0.7 (Windows, Python 3.13, installed from PyPI wheel)
Summary
When loading an FBX 7.4 binary file whose Vertices array uses type code d (double), mesh.vertex_positions returns garbage values (denormals / huge powers of two) instead of the actual coordinates. The topology (faces/indices) is correct — only vertex data is corrupted.
Root cause
In src/ufbx_wrapper.c L213-221:
const float* ufbx_wrapper_mesh_get_vertex_positions(const ufbx_mesh *mesh, size_t *out_count) {
...
*out_count = mesh->vertex_position.values.count;
return (const float*)mesh->vertex_position.values.data; // ← blind cast
}
ufbx's ufbx_real defaults to double (float requires opt-in via UFBX_REAL_IS_FLOAT). If the extension is not compiled with UFBX_REAL_IS_FLOAT, values.data points to a double buffer, and casting it to const float* makes the Cython layer reinterpret the raw bytes as float32 — a bit-level reinterpretation, not a value conversion.
The same blind cast exists in the other vertex attribute getters (vertex_normals L223, vertex_uvs L233, vertex_tangents L243, vertex_bitangents L253).
Evidence (bit-exact)
Test file: Meshy-exported FBX, Vertices array = 15954 doubles, zlib-compressed.
- Manual parse of the array header
[ArrayLength=15954][Encoding=1][CompressedLength=50087], zlib decompress, read as <f8 → 5318 vertices, all coordinates in ±0.5 (correct).
ufbx output vs raw double bytes reinterpreted as float32: bit-for-bit identical (e.g. both produce 0x20000000 = 1.0842022e-19 for the first component; correct value is -0.367188).
ufbx output vs correct values cast to float32: all 15954 components differ.
Bit-identical to a byte reinterpretation can only happen with a memcpy-style cast — a real double→float value conversion would produce values close to the correct ones.
Note: world_transform getters correctly use double*, which is why node transforms are fine — only the vertex attribute getters are affected.
Suggested fix
Either:
- compile the extension with
UFBX_REAL_IS_FLOAT (then the cast is correct and ufbx does the value conversion internally), or
- keep
ufbx_real as double and convert properly in the wrapper (copy into a float buffer, or expose the real element size).
The first option is the one-line fix if float32 precision is acceptable for the Python API.
Repro
import ufbx
import numpy as np, struct, zlib
scene = ufbx.load_file("model.fbx") # FBX with double Vertices array
vp = np.array(scene.meshes[0].vertex_positions, dtype=np.float64)
print(np.abs(vp).max()) # expect ~0.5, get ~1e19 garbage for ~12% of components
Happy to provide the test FBX if useful.
Bug:
vertex_positionsreturns garbage for FBX files withdoubleVertices arrayspyufbx version: 0.0.7 (Windows, Python 3.13, installed from PyPI wheel)
Summary
When loading an FBX 7.4 binary file whose
Verticesarray uses type coded(double),mesh.vertex_positionsreturns garbage values (denormals / huge powers of two) instead of the actual coordinates. The topology (faces/indices) is correct — only vertex data is corrupted.Root cause
In
src/ufbx_wrapper.cL213-221:ufbx's
ufbx_realdefaults todouble(float requires opt-in viaUFBX_REAL_IS_FLOAT). If the extension is not compiled withUFBX_REAL_IS_FLOAT,values.datapoints to adoublebuffer, and casting it toconst float*makes the Cython layer reinterpret the raw bytes as float32 — a bit-level reinterpretation, not a value conversion.The same blind cast exists in the other vertex attribute getters (
vertex_normalsL223,vertex_uvsL233,vertex_tangentsL243,vertex_bitangentsL253).Evidence (bit-exact)
Test file: Meshy-exported FBX,
Verticesarray = 15954 doubles, zlib-compressed.[ArrayLength=15954][Encoding=1][CompressedLength=50087], zlib decompress, read as<f8→ 5318 vertices, all coordinates in ±0.5 (correct).ufbxoutput vs raw double bytes reinterpreted as float32: bit-for-bit identical (e.g. both produce0x20000000=1.0842022e-19for the first component; correct value is-0.367188).ufbxoutput vs correct values cast to float32: all 15954 components differ.Bit-identical to a byte reinterpretation can only happen with a memcpy-style cast — a real double→float value conversion would produce values close to the correct ones.
Note:
world_transformgetters correctly usedouble*, which is why node transforms are fine — only the vertex attribute getters are affected.Suggested fix
Either:
UFBX_REAL_IS_FLOAT(then the cast is correct and ufbx does the value conversion internally), orufbx_realas double and convert properly in the wrapper (copy into a float buffer, or expose the real element size).The first option is the one-line fix if float32 precision is acceptable for the Python API.
Repro
Happy to provide the test FBX if useful.