Add Apple AR Quick Look (iOS) so web AR works on iPhone/iPad#34
Add Apple AR Quick Look (iOS) so web AR works on iPhone/iPad#34jasonkneen wants to merge 5 commits into
Conversation
The WebXR surface/float/inside flow only runs where navigator.xr exists
(Android Chrome, Quest — the "Google" AR path). iOS Safari has no WebXR,
so iPhone/iPad users could never place the world in AR.
Apple's only on-device web AR is AR Quick Look: open a .usdz through an
<a rel="ar"> and the system viewer finds a real surface, drops the model
on it, and lets the user walk around while it stays anchored — the same
"place the world on a surface and move around it" behaviour WebXR gives.
- Vendor THREE.USDZExporter (r128) as a classic global script using the
already-loaded global fflate.
- Build the USDZ on demand from worldGroup: snapshot meshes, convert
Lambert/Basic/Shader materials to MeshStandardMaterial (the only type
the exporter supports), recentre with the base at y=0, and scale to a
tabletop size for a sane first placement.
- Add an "Apple AR" panel button gated on relList.supports('ar'); hide the
WebXR buttons where there is no WebXR runtime so iOS shows just one
button. Show the panel if either path is available.
- Add a visible XR status line for AR progress/errors that auto-dismisses
outside an immersive session.
✅ Deploy Preview for tiny-world-builder ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThis PR adds Apple AR Quick Look support to TinyWorld as a fallback for iOS/iPadOS devices without WebXR. The changes include a new USDZ scene exporter library, UI controls and status display, integration logic that detects Quick Look capability and exports/launches AR, and developer documentation describing the feature. ChangesApple AR Quick Look via USDZ Export
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The first iOS export came out washed to white with holes and missing batched geometry. Root causes and fixes: - Wrong colour: TinyWorld layers a grayscale detail/noise map over each flat-coloured Lambert material, but UsdPreviewSurface wires diffuseColor straight to the texture and ignores the base colour it should multiply, so every tinted surface (grass, paths, walls) exported as the grayscale map = white. Drop maps and export the flat base colour, which also matches the low-poly look. Vertex-coloured meshes now average their geometry colour, and ShaderMaterials pull their first colour-valued uniform. - Holes: exported meshes are now double-sided. THREE DoubleSide alone is not enough — UsdPreviewSurface meshes are single-sided unless the prim sets `uniform bool doubleSided`, so the vendored r128 exporter now emits it. - Missing geometry: InstancedMeshes (batched voxel builds / crops / fences) were skipped entirely; they are now expanded into individual meshes with baked instance matrices, with a safety cap on total mesh count. Dropping textures also makes the USDZ smaller and removes any tainted-canvas export risk.
Per-frame JS animations (sway/water) don't carry into a static USDZ, so the AR model looked dead. Add an optional turntable to the vendored exporter: nest every mesh under one Xform driven by an `xformOp:rotateY` time sample (0 -> 360 deg over a 16s loop) and declare stage timeCodesPerSecond. Because the diorama is already centred on the origin, the spin pivots about its centre. One animated node (two keyframes) keeps the file tiny versus per-mesh keyframing, and using rotateY in degrees sidesteps matrix-convention pitfalls. Material/texture bindings use absolute paths, so nesting the meshes is safe. Static export is unchanged when no turntable option is passed.
The baked rotateY animation hung AR Quick Look's place-in-room (AR) mode on device. The Object viewer handled it, but AR mode is the priority, so revert to the static export confirmed working in AR. The colour and double-sided fixes are unaffected. Restores exporter + caller to their pre-turntable state.
The exporter inlined full point/normal/UV/index arrays for every mesh, and TinyWorld expands instanced props (crops, fences, voxels) into many copies that share one geometry object. Since USDZ must be stored uncompressed (ARKit requirement), all that duplicated geometry text made the file large and slow for AR Quick Look to load. Now each unique geometry+material is emitted once as an abstract `class` prototype, and every placement is a tiny `def Xform` that carries only its transform and references the prototype. The class never images on its own; each reference composes the shared mesh in at the instance's location. Also drop primvars:st (we export untextured flat colours, so UVs are dead weight) and trim float precision 7 -> 5. Same visuals, much smaller file. Verified offline: 502 placements collapse to 3 prototypes (geometry written once each), references resolve, no UV data emitted, braces balanced.
There was a problem hiding this comment.
Pull request overview
Adds an Apple AR Quick Look fallback so Tiny World Builder’s “place on a real surface and walk around it” AR flow works on iPhone/iPad Safari (no WebXR), while keeping the existing WebXR path for Android/Quest.
Changes:
- Vendors a classic-script
THREE.USDZExporter(three.js r128) and loads it after globalfflate. - Builds a USDZ from the live
worldGroupon-demand and launches AR Quick Look via<a rel="ar">on supported Safari. - Updates XR UI to show Apple AR when available, and hides WebXR buttons when no WebXR runtime exists; adds an
#xr-statusline with auto-dismiss behavior.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| vendor/three/USDZExporter.r128.js | Adds a vendored USDZ exporter adapted to globals, plus local instancing/prototype optimizations. |
| engine/world/18-scene-pick-xr.js | Implements Quick Look capability detection, world snapshot/export, and XR UI gating/status behavior. |
| tiny-world-builder.html | Adds the Apple AR button/status element and loads the USDZ exporter script. |
| styles/tiny-world.css | Styles the new XR status toast. |
| .codex/skills/tinyworld-webxr/SKILL.md | Documents the Apple AR Quick Look path and gating patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| worldGroup.traverse(obj => { | ||
| if (truncated || !obj.visible || !obj.isMesh) return; | ||
| const geom = obj.geometry; | ||
| if (!geom || !geom.attributes || !geom.attributes.position) return; | ||
| if (geom.attributes.position.isInterleavedBufferAttribute) return; |
What & why
Tiny World already supports Google AR on the web: the existing WebXR
surface("AR Desk") mode requestsimmersive-ar+hit-test, finds a real surface, anchors the world to it, and lets you move around with the device camera while it stays in place. That path only runs wherenavigator.xrexists — Android Chrome, Quest — which means iPhone/iPad users got nothing, because iOS Safari has no WebXR at all.Apple's only on-device web AR is AR Quick Look: open a
.usdzthrough an<a rel="ar">and the system viewer finds a surface, drops the model on it, and lets the user walk around while it stays anchored — exactly the "locate a surface and place the world on it, then move around it" behaviour requested. This PR adds that path so the feature works on both Apple and Google web AR.Changes
THREE.USDZExporter(r128) as a classic global script (vendor/three/USDZExporter.r128.js), adapted from the official three.js exporter to drop the ES-module imports and use the already-loaded globalfflate. Wired in right afterfflate.min.js.engine/world/18-scene-pick-xr.js):worldGroupinto a fresh group (shared geometry, no clones).MeshLambert/Basic/Shadermaterial toMeshStandardMaterial(the only type USDZExporter understands), preserving colour / emissive / map, deduped via a cache.y=0and scale the longest horizontal edge to ~0.5 m so the first placement reads as a tabletop diorama.model/vnd.usdz+zipBlob → object URL →<a rel="ar">with the single<img>child Safari requires, clicked programmatically.relList.supports('ar'); WebXR buttons are now hidden where there's no WebXR runtime, so iOS shows just the one button. The panel appears if either path is available. Added a visible#xr-statusline for AR progress/errors that auto-dismisses outside an immersive session..codex/skills/tinyworld-webxr/SKILL.md.Platform matrix
immersive-ar(unchanged)Testing
npm run check✅ andnpm run smoke✅;node --checkclean on the module and the vendored exporter.fflateagainst a stub scene and verifies the output is a valid USDZ zip containing a well-formedmodel.usda(header, mesh defs, material bindings, points, noundefinedleakage).tests/db-schema-errors.test.mjsfailure is environmental (thepostgresdependency isn't installed) and unrelated to this change.Generated by Claude Code
Summary by CodeRabbit