Formats & platforms
The one page worth reading before you write any code. Nearly every integration bug traces back to it, and the failure mode is a blank canvas with nothing in the console.
There is no format both platforms read
- SceneKit and AR Quick Look read USDZ. They cannot open glTF.
- Filament and Scene Viewer read GLB. They cannot open USDZ.
So every model is stored as both. The GLB is produced when the restaurant uploads; the USDZ is derived from it by a background worker and arrives a minute or two later.
iOS
| Inline preview | SceneKit USDZ |
| AR | Quick Look USDZ |
| No USDZ yet | Poster image |
| Camera permission | Not required |
Android
| Inline preview | Filament GLB |
| AR | Scene Viewer GLB |
| No USDZ yet | Unaffected |
| Camera permission | Not required |
Web
| Inline preview | WebGL GLB |
| AR on Android | WebXR / Scene Viewer GLB |
| AR on iOS | Quick Look USDZ |
| Desktop | 3D only, no AR |
The consequence people miss
You never have to reason about this per call site. resolvePresentation() makes the decision and the components call it for you. The rule is simply: never reach for model.glbUrl yourself on iOS.
const { inline, ar } = resolvePresentation({
platform: "ios",
model: item.model,
});
// inline.kind is "usdz" when the conversion finished,
// and "poster" when it has not — never "glb" on iOS.
if (inline.kind === "poster") showPhoto(inline.url);Telling “not yet” from “never”
ar.blockedOnConversion is true only while the USDZ is still being made — a state that resolves by itself. Every other unsupported reason (a desktop browser, a handset without ARCore, a failed conversion) is permanent for that session, and showing a hopeful message there is a lie the interface repeats forever.
usdzStatus carries the underlying detail: pending, processing, ready or failed.