Skip to main content

Project Setup

This guide walks through setting up an XR Blocks project for local development. You can get started immediately with zero build tooling using an import map, or install XR Blocks via npm to build with TypeScript and a bundler like Vite.

Installation & Import Options​

XR Blocks can be imported directly into a webpage using an import map, or installed via npm and bundled.

Option A: Using an Import Map (No Build Step)​

This is the fastest way to get started. You can use an import map to load XR Blocks and three.js directly from a CDN.

For a starter template, see templates/00_basic/index.html (note that while templates in the repository use relative paths for local development, your project should use the CDN URLs below).

To use an import map, add the following script tag to your index.html page. This minimal map supports the core engine. Feature templates include the extra mappings they require.

<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.0/examples/jsm/",
"xrblocks": "https://cdn.jsdelivr.net/gh/google/xrblocks@build/xrblocks.js",
"xrblocks/addons/": "https://cdn.jsdelivr.net/gh/google/xrblocks@build/addons/"
}
}
</script>
tip

Using xrblocks@build loads the latest development version of XR Blocks every time your web app loads. To pin a build of XR Blocks, replace build with an SHA from the build branch or load a version from the xrblocks npm package using a URL such as https://cdn.jsdelivr.net/npm/xrblocks@0.24.0/build/xrblocks.js.

Checking versions...

Option B: Using npm & a Bundler​

Use a supported Node.js LTS release for the TypeScript/Vite template.

Install XR Blocks and its required peer dependencies with:

npm install xrblocks three @pmndrs/uikit @preact/signals-core

For a pre-configured template using TypeScript and Vite, check out templates/13_typescript_vite/src/main.ts (and its associated templates/13_typescript_vite/package.json and templates/13_typescript_vite/tsconfig.json).

Handling Optional Dependencies​

XR Blocks keeps large providers external so applications can select the features they use. The built-in spatial UI uses @pmndrs/uikit and @preact/signals-core; it is not a separate XR Blocks add-on. Lit supplies the optional simulator browser interface.

To install the optional feature dependencies together, run:

npm install @google/genai openai lit @pmndrs/uikit @preact/signals-core @mediapipe/tasks-vision @mediapipe/tasks-audio @sparkjsdev/spark three-mesh-bvh @litertjs/core three-pathfinding

Otherwise, manage dependencies based on your tooling:

  • TypeScript: Because xrblocks declaration files import types from optional dependencies (like @google/genai or lit), the TypeScript compiler will complain about missing types. You can handle this in two ways:

    • Set "skipLibCheck": true (Recommended): Adding "skipLibCheck": true to the compilerOptions section of your tsconfig.json tells the compiler to skip type checking of declaration files (.d.ts files) inside node_modules/. This is the easiest way to use xrblocks without installing any unused optional dependencies.
    • Check declarations with skipLibCheck: false: Install the optional packages listed above and @types/three. These declaration dependencies are listed in package.json as optional peers. Provider peer ranges are intentionally broad so installing XR Blocks does not force an application to upgrade its provider's major version. No repository paths aliases, ambient speech-recognition types, or esnext.temporal library are needed. The SDK includes structural speech, duration, and Rapier types; native Temporal.Duration objects are also accepted as timeouts.
  • Bundler Configuration (Rollup, Vite, Webpack): If you are not using certain features and do not want to install their corresponding optional dependencies, you should configure your bundler to mark them as external so it skips trying to resolve them:

    Rollup (rollup.config.js):

    export default {
    external: [
    '@google/genai',
    '@mediapipe/tasks-vision',
    '@pmndrs/uikit',
    // Add other unused optional dependencies here
    ],
    };

    Vite (vite.config.js): Use build.rollupOptions.external for production builds and optimizeDeps.exclude for dependency pre-bundling in development, as in templates/13_typescript_vite/vite.config.ts:

    import {defineConfig} from 'vite';

    const unusedDependencies = [
    '@google/genai',
    '@mediapipe/tasks-vision',
    // Add other unused optional dependencies here
    ];

    export default defineConfig({
    optimizeDeps: {exclude: unusedDependencies},
    build: {
    rollupOptions: {external: unusedDependencies},
    },
    });

    Webpack 5.94+ (webpack.config.mjs): Use externals, not external. For a browser ESM build, enable both experiments.outputModule and output.module. Use externalsType: 'module-import' to preserve the SDK's lazy import() calls for optional providers; 'module' would turn them into static imports and require unused providers at load time:

    export default {
    experiments: {outputModule: true},
    output: {module: true},
    externalsType: 'module-import',
    externals: {
    '@google/genai': '@google/genai',
    '@mediapipe/tasks-vision': '@mediapipe/tasks-vision',
    // Add other unused optional dependencies here
    },
    };

    Externalizing a dependency does not supply it at runtime. Install or provide import-map entries for dependencies of features you actually enable.

Supported TypeScript configuration​

Use TypeScript 5.9 or later with moduleResolution: "bundler", module: "ESNext", and the ES2023, DOM, and DOM.Iterable libraries. node16/nodenext resolution is not currently supported for the complete dependency graph: providers such as Spark and Rapier publish extensionless relative declaration imports.

When using the LiteRT addon with TypeScript 5.9, also install @webgpu/types and include it in compilerOptions.types. TypeScript 6+ includes WebGPU in lib.dom; do not include @webgpu/types there, since the declarations conflict.

Contributors can run npm run build:check-types after building. It installs consumer dependencies from the package's declared peers, then compiles the root and every emitted addon declaration with skipLibCheck: false using TypeScript 5.9, the repository compiler, and the registry's latest stable compiler. The consumer fixture also assigns both Rapier compat packages to options.physics.RAPIER.

List of Optional Dependencies​

  • AI Integration: @google/genai (Gemini), openai (OpenAI)
  • Physics: choose @dimforge/rapier3d, @dimforge/rapier3d-compat, or @dimforge/rapier3d-simd-compat
  • Built-in spatial UI: @pmndrs/uikit, @preact/signals-core
  • Simulator browser interface: lit
  • Gaussian Splatting: @sparkjsdev/spark
  • Vision & Audio (MediaPipe): @mediapipe/tasks-vision (Hands/Face/Objects), @mediapipe/tasks-audio (Audio classification)
  • Performance & Utilities: three-mesh-bvh (Accelerated raycasting)
  • Navigation: three-pathfinding (Navmesh pathfinding)
  • LiteRT addon: @litertjs/core (plus @webgpu/types for TypeScript 5.9)

Deploy the complete build​

XR Blocks uses private lazy-loaded chunks for features such as the built-in UI and simulator. Copy and serve the complete package build/ directory:

build/
├── xrblocks.js
├── xrblocks.min.js
├── internal/
└── addons/

Do not deploy only xrblocks.js, and do not import files from build/internal/ directly. Those filenames are implementation details.


Creating an Interactive Scene​

XR Blocks uses a singleton engine driven by a script lifecycle. Rather than managing the renderer and canvas yourself, you write your app as one or more xb.Script subclasses and register them.

Upon importing, XR Blocks automatically creates the core singleton which manages the lifecycle, camera, and renderer.

Here is a complete, interactive example of a script-based scene:

import * as THREE from 'three';
import * as xb from 'xrblocks';

class InteractiveScene extends xb.Script {
init() {
// Runs once after registration; can be asynchronous.

// Add lighting to the scene
this.add(new THREE.HemisphereLight(0xffffff, 0x666666, 3));

// Create an interactive cube
this.cube = new THREE.Mesh(
new THREE.BoxGeometry(0.3, 0.3, 0.3),
new THREE.MeshStandardMaterial({color: 0x4285f4})
);

// Place the cube in front of the user (using user-physical context)
this.cube.position.set(0, xb.user.height - 0.3, -xb.user.objectDistance);
this.add(this.cube);
}

update(time, frame) {
// Runs on every animation frame.
this.cube.rotation.y += xb.getDeltaTime();
}

onSelectEnd() {
// Fired on a desktop click or a WebXR controller pinch/trigger release.
// Randomize the cube's color when selected.
this.cube.material.color.set(Math.random() * 0xffffff);
}

dispose() {
// Clean up three.js resources to prevent GPU memory leaks
this.cube.geometry.dispose();
this.cube.material.dispose();
}
}

// Bootstrap the application when the DOM is ready
document.addEventListener('DOMContentLoaded', () => {
xb.add(new InteractiveScene());
xb.init(new xb.Options());
});
  • init(): Sets up your lights, cameras, models, and UI. This is where you construct your 3D objects.
  • update(): Updates positions, rotations, animations, and runs any per-frame logic.
  • onSelectEnd(): Handles user clicks (desktop) and pinch/select releases (WebXR).
  • dispose(): Disposes of geometries, materials, and textures to prevent memory leaks on the GPU when the script is removed from the scene.