WWDC Quick Look 💓 By SwiftGGTeam
What’s new for the spatial web

What’s new for the spatial web

Watch original video

Highlight

Safari on visionOS 26 turns on the HTML <model> element by default. It renders USDZ files inline as real 3D, and adds Website Environments so a site can supply its own immersive backdrop.


Main content

For more than a decade, the only way to show a 3D model on the web was a JS library like Three.js or Babylon.js, drawing geometry into a 2D canvas. The user still saw a flat picture. There was no depth, and no way to walk over and look from another angle. It looked like 3D, but felt like 2D.

Safari on visionOS 26 ships a proposal that has been brewing for years: the HTML <model> element. The syntax is as simple as <img>, but the rendering is completely different. It opens “a window” on the page surface and places a USDZ model in real 3D inside that virtual space. The user can turn their head to look from a different angle, pinch the model and drag it into Quick Look to see it at real size, or even drop it into another app. The element fits naturally into the document, CSS, and JavaScript: you can set background-color so the virtual space matches the page, use backdrop-filter to lay a frosted-glass panel over the model, or use position: absolute to stack other DOM elements with the model.

Beyond <model>, the session also covers immersive media — 180/360-degree video, Apple Immersive Video, and spatial photos — playing inline and full-screen inside <video> and <img>, plus a developer preview of <link rel="spatial-backdrop">, which lets a site name a USDZ environment that visitors can opt to step into while they browse.


Details

The simplest form of <model> (01:00)

<model src="teapot.usdz"></model>

Key points:

  • src points to a USDZ file. The <model> element must have an explicit closing tag (unlike <img>, which is a void element).
  • The browser centers the bounding box on its x-y face and scales it to fit the element. The model always renders behind the page surface, so it never “pokes out” when scrolled out of view.

The server needs the right MIME type (05:30)

# NGINX mime.types
types {
  ...
  model/vnd.usdz+zip usdz;
}

Key points:

  • Older CDNs and servers may serve .usdz as application/octet-stream. The browser then refuses to render it.
  • Use AddType model/vnd.usdz+zip .usdz on Apache. Python’s built-in http.server needs you to edit Handler.extensions_map.

Provide a fallback (05:51 ~ 06:17)

<!-- <model-viewer> library from https://modelviewer.dev/ -->
<script type="module"
  src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.0.0/model-viewer.min.js">
</script>

<model src="camera.usdz">
  <!-- Fallback experience for backward compatibility -->
  <model-viewer src="camera.glb"></model-viewer>
</model>

Key points:

  • Put the fallback content inside <model>. Browsers that support <model> ignore it; browsers that don’t will render the inner content.
  • The simplest fallback is an <img>. To keep the rotate experience, embed a 2D library like <model-viewer>.

Detect the feature, not the user agent (06:52)

if (window.HTMLModelElement) {
  // Supported by this browser
} else {
  // Not supported by this browser
}

Key points:

  • Check for the window.HTMLModelElement global.
  • Don’t sniff the user agent string. Other browsers will eventually ship this standard, and a UA check will be wrong.

Loading indicator (07:32)

<model src="camera.usdz" id="mymodel"></model>

<script>
const mymodel = document.getElementById("mymodel");

if (window.HTMLModelElement) {
  mymodel.ready.then(result => {
    // Hide the loading indicator
    // Show the model
  }).catch(error => {
    // Loading error occurred, show a retry button
  });
}
</script>

Key points:

  • model.ready returns a Promise that resolves once the asset is fully loaded.
  • USDZ files often run past 10 MB, which means several seconds on a slow network. A loading indicator keeps users from closing the page.
  • Use the .catch branch to show a retry button on network failure.

Control lighting with IBL (10:56)

<model src="camera.usdz" environmentmap="sunset.exr"></model>

Key points:

  • The environmentmap attribute points to an image-based lighting map (equirectangular projection).
  • Prefer OpenEXR or Radiance HDR. They cover several orders of magnitude of brightness and produce real-looking reflections. JPEG makes the model look flat.
  • The IBL file is not bundled into the USDZ, so you can apply different lighting in different scenes.

Drive pose from JavaScript (13:49)

<model src="teapot.usdz" id="mymodel"></model>
<a onclick="turnRight()">Right</a>

<script>
const mymodel = document.getElementById("mymodel");
function turnRight() {
  const matrix = mymodel.entityTransform; // DOMMatrixReadOnly
  const newMatrix = matrix.rotateAxisAngle(0, 1, 0, 90);
  mymodel.entityTransform = newMatrix;
}
</script>

Key points:

  • entityTransform is a DOMMatrixReadOnly that holds the model’s current pose.
  • Call rotateAxisAngle(0, 1, 0, 90) to rotate 90° around the y-axis, then assign it back to entityTransform to trigger a re-render.
  • To reset to the initial pose: model.entityTransform = new DOMMatrix();

Seek along the animation timeline (17:35)

<model src="camera.usdz" id="mymodel"></model>

<script>
const mymodel = document.getElementById("mymodel");

function openFlash() {
  mymodel.currentTime = 1; // Unit is seconds
}

function openScreen() {
  mymodel.currentTime = 3; // Unit is seconds
}
</script>

Key points:

  • currentTime is in seconds and maps to the animation timeline inside the USDZ file.
  • Pair it with <input type="range"> to build a draggable progress bar.
  • Auto-play with <model loop autoplay>. Drive playback from JS with model.play() and model.pause(), and read model.paused for state.

Generate USDZ on the fly with Three.js (19:35)

import * as THREE from "three";
import { USDZExporter } from "three/examples/exporters/USDZExporter.js";

async function generateModel() {
  const scene = new THREE.Scene();
  // ... create a really nice scene procedurally ...

  const bytes = await new USDZExporter().parseAsync(scene);
  const objURL = URL.createObjectURL(new Blob([bytes]));

  const mymodel = document.getElementById("mymodel");
  mymodel.setAttribute("src", objURL);
}

Key points:

  • Three.js ships a USDZExporter that turns a procedurally built scene into a USDZ byte stream.
  • Wrap it in a blob URL with URL.createObjectURL and assign it to the <model> src. The browser renders it in 3D.
  • This is a good fit for parametric product configurators: the user changes color or size, you regenerate the USDZ, and the preview updates.

Inline immersive media (23:10)

<video src="spatial_video.mov"></video>  <!-- Single file -->
<video src="360_video.m3u8"></video>  <!-- HTTP Live Streaming -->

Key points:

  • The existing <video> tag handles 180/360-degree, wide-angle, and Apple Immersive Video.
  • It plays inline as 2D inside the page. After player.requestFullScreen(), it goes full-screen and wraps the user with the correct projection.
  • For spatial photos, just use <img src="spatial.heic" controls>. The controls attribute adds a button to open the immersive view.

Website Environments (developer preview, 26:49)

<link rel="spatial-backdrop" href="office.usdz" environmentmap="lighting.hdr">

Key points:

  • Use <link> to declare a USDZ immersive backdrop for the whole page.
  • Once the user opts to immerse, the room around them is replaced by the office.usdz environment.
  • This is a developer preview today. Users have to turn it on by hand in Safari settings.

Takeaways

  • What to do: upgrade product 3D from 2D canvas to <model>

    • Why it’s worth it: e-commerce, furniture, cameras, and toys can show real 3D in Safari on visionOS. Turning your head to see another angle feels an order of magnitude more present than a “360-degree photo set”, and you don’t need a native app.
    • How to start: shoot a sample as USDZ with iPhone + Reality Composer → export a compressed-texture version from Mac Preview, keep it under 5 MB → drop <model src> into the existing product page, with <img> as a fallback.
  • What to do: build a product configurator with <model> + DOM

    • Why it’s worth it: you don’t need to build a WebGL rendering pipeline. CSS already gives you frosted-glass panels, text layers, and buttons. Switching configurations is just changing src, or generating USDZ on the fly with Three.js.
    • How to start: get procedural scene generation working with Three.js on the existing page, then plug in USDZExporter to emit a blob URL. Use model.ready to drive the loading state.
  • What to do: put your brand’s immersive short film on the site

    • Why it’s worth it: Apple Immersive Video plays inline and full-screen in Safari on visionOS without an App Store submission. Regular visitors see 2D inline; Vision Pro users get a 180-degree immersive view full-screen.
    • How to start: cut the source into HLS (.m3u8) → serve both clients from one <video> tag → prepare a poster fallback for desktop.
  • What to do: try spatial-backdrop for a brand-site environment

    • Why it’s worth it: as an early developer-preview experiment, it lets visitors “step into your store”. Being early can earn press attention.
    • How to start: build a simple storefront or showroom USDZ in Reality Composer Pro → pair it with an HDR lighting map → add <link rel="spatial-backdrop"> and verify it with the experimental feature flag in Safari.

Comments

GitHub Issues · utterances