How to Export 3D Models to USDZ Using Three.js USDZExporter

Step-by-step guide to convert GLB/GLTF scenes to USDZ with Three.js USDZExporter — from browser export to AR Quick Look and Apple Vision Pro workflows

How to Export 3D Models to USDZ Using Three.js USDZExporter

How to Export 3D Models to USDZ Using Three.js USDZExporter

USDZ is Apple’s official packaged 3D format designed specifically for Augmented Reality. It powers AR Quick Look on iPhone and iPad and serves as the primary format for RealityKit on Apple Vision Pro.

This comprehensive, step-by-step guide will teach you how to convert GLB models into optimized .usdz files directly in the browser using Three.js official USDZExporter, without needing any desktop software.

Step 1: Installation

First, install Three.js in your project. This is the only core dependency required for the conversion:

npm install three

Then import the necessary modules in your React file:

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { USDZExporter } from 'three/addons/exporters/USDZExporter.js';

Step 2: Scene Preparation (Material & Texture Fixes)

USDZ only supports MeshStandardMaterial. The following function traverses your model to convert materials and fix ORM (Occlusion/Roughness/Metalness) maps, which is the most common reason for models appearing black or incorrect sau khi xuất.

const prepareSceneForUSDZ = async (scene) => {
  scene.traverse((obj) => {
    if (!obj.isMesh) return;

    let materials = obj.material;
    if (!Array.isArray(materials)) materials = [materials];

    materials.forEach((mat, index) => {
      if (!(mat instanceof THREE.MeshStandardMaterial)) {
        const oldMat = mat;
        const newMat = new THREE.MeshStandardMaterial({
          color: oldMat.color || 0xffffff,
          map: oldMat.map,
          normalMap: oldMat.normalMap,
          roughnessMap: oldMat.roughnessMap,
          metalnessMap: oldMat.metalnessMap,
          emissiveMap: oldMat.emissiveMap,
          alphaMap: oldMat.alphaMap,
          transparent: !!oldMat.transparent,
          opacity: oldMat.opacity ?? 1,
          side: oldMat.side || THREE.FrontSide,
        });

        if (Array.isArray(obj.material)) {
          obj.material[index] = newMat;
        } else {
          obj.material = newMat;
        }
      }
    });

    // Fix ORM maps (Occlusion/Roughness/Metalness)
    const mat = Array.isArray(obj.material) ? obj.material[0] : obj.material;
    if (mat.metalnessMap) {
      mat.metalness = 1;
      if (!mat.roughnessMap) mat.roughnessMap = mat.metalnessMap;
    }
    if (mat.roughnessMap) mat.roughness = 1;
  });
};

Step 3: Handling File Upload and Auto-Scaling

This logic loads the GLB file, centers it at the origin, and normalizes its scale (ensuring it's between 0.3m and 2.0m) to make it ready for AR environments.

const [downloadInfo, setDownloadInfo] = useState(null);

const handleFileUpload = async (event) => {
  const file = event.target.files[0];
  if (!file) return;

  const url = URL.createObjectURL(file);
  const loader = new GLTFLoader();
  
  loader.load(url, async (gltf) => {
    const scene = new THREE.Scene();
    const model = gltf.scene;
    scene.add(model);

    // === Center and Normalize Scale ===
    const box = new THREE.Box3().setFromObject(model);
    const center = box.getCenter(new THREE.Vector3());
    const size = box.getSize(new THREE.Vector3());
    model.position.sub(center);

    const maxDim = Math.max(size.x, size.y, size.z);
    if (maxDim > 2 || maxDim < 0.3) {
      const targetSize = maxDim > 2 ? 2 : 0.8;
      model.scale.setScalar(targetSize / maxDim);
    }

    // === Prepare & Export ===
    const usdzFileName = file.name.replace(/\.(glb|gltf)$/, '') + '.usdz';
    await prepareSceneForUSDZ(scene);
    
    const exporter = new USDZExporter();
    const arrayBuffer = await exporter.parseAsync(scene, {
      maxTextureSize: 1024,
      quickLookCompatible: true,
      onlyVisible: true,
      includeAnchoringProperties: true,
    });

    // === Create Download URL and update State ===
    const blob = new Blob([arrayBuffer], { type: 'model/vnd.usdz+zip' });
    setDownloadInfo({
      url: URL.createObjectURL(blob),
      fileName: usdzFileName
    });
    
    URL.revokeObjectURL(url);
  });
};

Step 4: Minimalist User Interface (UI)

Here is the React structure for the conversion tool. It uses standard HTML elements and a hidden file input triggered by a click event.

return (
  <div className="converter-card">
    <header>
      <h1>GLB to USDZ</h1>
    </header>

    <main>
      <div className="upload-zone" onClick={() => fileInputRef.current.click()}>
        <input 
          type="file" 
          ref={fileInputRef} 
          onChange={handleFileUpload} 
          accept=".glb,.gltf" 
          hidden 
        />
        <div className="status-content">
          <p>Click to upload GLB or GLTF file</p>
        </div>
      </div>

      {downloadInfo && (
        <div className="result-section">
          <a href={downloadInfo.url} download={downloadInfo.fileName} className="action-button primary">
             Download {downloadInfo.fileName}
          </a>
        </div>
      )}
    </main>
  </div>
);

Common Issues & Solutions

Issue Solution
Model is invisible after exportEnsure materials are converted to MeshStandardMaterial and use quickLookCompatible: true
Model appears too large or smallAlways center the model and normalize the scale (0.3m to 2m) before exporting.
Textures look dark or incorrectRun the traversal step to fix metalness and roughness values before triggering the exporter.
File size is too bigReduce maxTextureSize to 768 or 512 in the exporter options.

Conclusion: With this workflow, you have a production-ready, minimal pipeline to convert GLB models to USDZ directly in your web app.

Final Interface

After finishing the code and adding CSS, your interface will look like this:

GLB to USDZ Converter - Final Beautiful UI