Thanks for the clear report and for putting up a test page — that made this much easier to dig into.
I pulled your model.glb. It has 574,189 vertices and uses 32-bit (UNSIGNED_INT) indices. Babylon needs the uintIndices capability to render that — it comes from WebGL2, or from WebGL1 via the OES_element_index_uint extension. If neither is available, we silently truncate the index buffer to 16-bit, and every index above 65,535 wraps around and points at an unrelated vertex.
I decoded your index buffer to see how much of the model that would affect: 978,533 of 1,103,457 triangles (88.7%) reference at least one vertex above 65,535. So if this is what’s happening, ~89% of the mesh gets rewired into garbage triangles stretching between unrelated parts of the model, leaving a small fraction recognizable — which is what your screenshot looks like to me.
That would also fit the rest of the symptoms: no console errors (the truncation is silent), fine in Chrome/Firefox/Opera on the same machine, and fine on Apple Silicon.
So my working theory is that Safari 26.5 on Intel isn’t giving us a usable WebGL2 context, and we’re falling back to WebGL1.
Two things would confirm it:
1. Does a smaller model (under ~65k vertices) render correctly on the same page?
2. Paste this into the Safari console on your test page, once the model has finished loading:
(async () => {
const out = {}, mk = t => { try { return document.createElement('canvas').getContext(t); } catch { return null; } };
const gl2 = mk('webgl2'), gl1 = mk('webgl');
out.webgl2Available = !!gl2;
out.webgl2IsReal = !!(gl2 && gl2.deleteQuery);
out.webgl1Available = !!gl1;
out.OES_element_index_uint = !!(gl1 && gl1.getExtension('OES_element_index_uint'));
const gl = gl2 || gl1;
if (gl) {
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
out.vendor = dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR);
out.renderer = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
out.glVersion = gl.getParameter(gl.VERSION);
}
const d = document.querySelector('babylon-viewer')?.viewerDetails;
if (!d) {
out.viewer = 'not ready — rerun once the model has loaded';
} else {
const e = d.scene.getEngine();
out.babylonEngine = e.constructor.name;
out.babylonGLVersion = e.webGLVersion;
out.uintIndicesCap = e.getCaps().uintIndices;
out.meshes = d.scene.meshes.filter(m => m.getTotalVertices() > 0).map(m => ({
name: m.name,
verts: m.getTotalVertices(),
indices: m.getTotalIndices(),
gpuIndexType: (() => { try { return m.geometry?.getIndexBuffer()?.is32Bits ? 'Uint32' : 'Uint16'; } catch { return '?'; } })()
}));
}
console.log(JSON.stringify(out, null, 2));
})();
The value I care about most is uintIndicesCap — if that comes back false, this is confirmed.
A couple of other things worth checking while you’re in there:
- Safari → Settings → Advanced → Feature Flags — is WebGL 2.0 enabled?
- Which GPU does the machine have (Intel Iris/UHD, or a discrete AMD)? And which Mac model?
One note on “it worked three months ago”: your page loads @babylonjs/viewer from jsDelivr unpinned, so it auto-updates. If you pin an older version and it still breaks, that points at Safari rather than at Babylon.
Separately, and regardless of what turns out to be happening on your machine — Babylon dropping index data silently here isn’t great. I’ll look into getting a warning logged for that case.