@xrgallery/viewer
v1.1.0
Published
WebXR 360° VR viewer for self-hosting immersive tours
Maintainers
Readme
@xrgallery/viewer
A standalone, self-hostable WebXR viewer for immersive 360° virtual tours. Built on Babylon.js.
This is the open-source viewer component of XRGallery. Use it to embed and self-host tours on your own site. To create tours visually with a drag-and-drop editor, AI generation, and cloud hosting, use the XRGallery platform.
Features
- 360° panoramas (images, video, HLS streaming, solid colors)
- WebXR/VR headset support (Quest, Vive, Index, etc.)
- Interactive hotspots (info panels, navigation portals, audio, lead capture)
- 3D model loading (glTF/GLB)
- Multi-stage tours with portal transitions
- Floor plan and geographic map navigation
- Spatial audio
- Post-processing effects (bloom, DOF, film grain)
- Mobile touch controls
- Zero dependencies beyond the bundle (Babylon.js is included)
Install
npm:
npm install @xrgallery/viewerCDN:
<script src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>Quick Start
Scene ID (simplest — loads from XRGallery cloud)
<div id="app" style="width: 100%; height: 100vh;"></div>
<script src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>
<script>
(async () => {
const viewer = await XRGallery.create({
element: '#app',
sceneId: 'YOUR_SCENE_ID'
});
})();
</script>Inline Config (self-hosted, no cloud dependency)
<div id="app" style="width: 100%; height: 100vh;"></div>
<script src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>
<script>
(async () => {
const viewer = await XRGallery.create({
element: '#app',
config: {
experience: { title: "My Tour" },
stages: [
{
id: "lobby",
name: "Welcome",
skybox: { type: "image", url: "https://example.com/panorama.jpg" }
}
]
}
});
})();
</script>React / TypeScript
import { useEffect, useRef } from 'react';
function VRViewer({ sceneId }: { sceneId: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const viewer = XRGallery.create({ element: ref.current, sceneId });
return () => { viewer.then(v => v.dispose()); };
}, [sceneId]);
return <div ref={ref} style={{ width: '100%', height: '100%' }} />;
}Vanilla JS with Config
const viewer = await XRGallery.create({
element: document.getElementById('viewer-container'),
config: {
experience: { title: "Gallery Tour" },
stages: [
{
id: "main",
name: "Main Hall",
skybox: { type: "image", url: "/panorama.jpg" },
hotspots: [
{
id: "info-1",
type: "info",
position: { x: 0, y: 1.6, z: -5 },
label: "Welcome",
infoTitle: "Hello!",
infoDescription: "Look around by dragging."
}
]
}
]
}
});
// Later: clean up
viewer.dispose();API Reference
XRGallery.create(options): Promise<ViewerInstance>
| Option | Type | Description |
|--------|------|-------------|
| element | string \| HTMLElement \| null | CSS selector, DOM element, or React ref. Omit to use document.body. |
| sceneId | string | Scene ID to load from XRGallery cloud. |
| config | XRGalleryConfig | Inline config object (see below). |
| firebaseConfig | FirebaseConfig | Override Firebase config (advanced, only with sceneId). |
| title | string | Custom page title. |
Provide either sceneId or config, not both.
ViewerInstance
| Method | Description |
|--------|-------------|
| dispose() | Stops rendering and releases GPU/audio resources. |
| isDisposed() | Returns true if dispose() has been called. |
Configuration Reference
Top-Level Config
{
experience: { ... }, // Required: title, description, settings
stages: [ ... ], // Required: array of tour locations
navigation: { ... }, // Optional: floor plans or maps
globalAudio: { ... } // Optional: background music across all stages
}Experience
experience: {
title: "Museum Tour", // Required
description: "Virtual museum", // Optional
defaultFOV: 1.0 // Camera field of view (0.3-2.5, default: 1.0)
}Stage
Each stage is a location in the tour:
{
id: "gallery-1", // Unique ID (required)
name: "Main Gallery", // Display name (required)
skybox: { ... }, // 360 background (required)
hotspots: [ ... ], // Interactive elements (optional)
models: [ ... ], // 3D models (optional)
planes: [ ... ], // 2D images/videos in 3D space (optional)
lights: [ ... ], // Custom lighting (optional)
audioUrl: "https://example.com/music.mp3", // Per-stage ambient audio URL (optional)
audioVolume: 0.5, // 0-1 (optional)
initialCameraTarget: { x: 0, y: 0, z: -100 } // Where camera looks first
}Skybox Types
Image (equirectangular panorama):
skybox: { type: "image", url: "https://example.com/panorama.jpg", rotation: 90 }Video (360 video or HLS stream):
skybox: {
type: "video",
url: "https://example.com/360-video.mp4",
// Or HLS:
hlsUrl: "https://stream.mux.com/xxx.m3u8",
thumbnailUrl: "https://example.com/preview.jpg",
autoplay: true,
loop: true
}Solid color:
skybox: { type: "color", color: "#1a1a2e" }Hotspots
All hotspots share these base fields:
{
id: "unique-id", // Required
type: "info" | "navigation" | "audio" | "both" | "lead_capture",
position: { x: 0, y: 1.6, z: -5 }, // 3D position in meters
label: "Click Me", // Hover text
color: "#4A90E2" // Optional accent color
}Info hotspot — displays a panel with rich content:
{
type: "info",
infoTitle: "About This Artwork",
infoDescription: "Created in 1920...",
infoImageUrl: "https://example.com/detail.jpg",
linkUrl: "https://example.com", // Optional link button URL
linkText: "Website", // Optional link button text
videoUrl: "https://example.com/video.mp4", // Optional embedded video
iframeUrl: "https://example.com/embed", // Optional iframe
infoContentType: "video" // "image" | "video" | "iframe"
}Navigation hotspot — portal to another stage:
{
type: "navigation",
targetStageId: "gallery-2",
portalEffectStyle: "enhanced", // "clean" | "enhanced" | "minimal" | "alien" | "floor-arrow"
portalColor: "#4A90E2",
portalRestSize: 1.0,
portalHoverSize: 1.2,
targetViewDirection: { x: 0, y: 0, z: -1 } // Camera direction on arrival
}Audio hotspot — plays audio (optionally spatial/3D):
{
type: "audio",
audioUrl: "https://example.com/narration.mp3",
audioVolume: 0.8,
audioLoop: false,
audioSpatial: true,
audioRolloffFactor: 1,
audioMaxDistance: 100
}Combined hotspot — info panel + audio narration:
{
type: "both",
infoTitle: "The Starry Night",
infoDescription: "Vincent van Gogh, 1889...",
audioUrl: "https://example.com/narration.mp3"
}Lead capture hotspot — contact/inquiry form:
{
type: "lead_capture",
leadForm: {
formType: "contact", // "contact" | "email_capture" | "inquiry" | "custom"
title: "Contact Us",
description: "We'll get back to you within 24 hours",
submitButtonText: "Send Message",
fields: [
{ name: "name", type: "text", label: "Name", required: true },
{ name: "email", type: "email", label: "Email", required: true },
{ name: "message", type: "textarea", label: "Message", required: false }
]
}
}3D Models
models: [
{
id: "sculpture",
url: "https://example.com/sculpture.glb",
position: { x: 0, y: 0, z: -5 },
rotation: { x: 0, y: 1.57, z: 0 }, // Radians
scale: 2.0 // Or { x: 2, y: 2, z: 2 }
}
]2D Planes
Floating images or videos in 3D space:
planes: [
{
type: "image",
url: "https://example.com/poster.jpg",
position: { x: 0, y: 2, z: -6 },
rotation: { x: 0, y: 0, z: 0 },
scale: { width: 3, height: 4 }
},
{
type: "video",
url: "https://example.com/promo.mp4",
hlsUrl: "https://stream.mux.com/xyz.m3u8",
position: { x: 4, y: 1.5, z: -5 },
scale: { width: 4, height: 2.25 },
autoplay: true, loop: true, muted: false
}
]Custom Lighting
lights: [
{ type: "hemispheric", intensity: 0.7, diffuse: "#FFFFFF", groundColor: "#444444", direction: { x: 0, y: 1, z: 0 } },
{ type: "point", position: { x: 0, y: 5, z: 0 }, intensity: 1.5, diffuse: "#FFE4B5", range: 20 },
{ type: "spot", position: { x: 2, y: 4, z: -3 }, direction: { x: 0, y: -1, z: 0 }, intensity: 2, angle: 0.8, exponent: 2 }
]Navigation (Floor Plans & Maps)
Floor plan:
navigation: {
type: "floorplan",
showMinimap: true,
floorPlans: [
{ floor: 1, name: "Ground Floor", imageUrl: "https://example.com/floor1.png" }
],
markers: [
{ stageId: "lobby", label: "Lobby", floorPlanPosition: { x: 0.5, y: 0.3 } }
]
}Geographic map:
navigation: {
type: "map",
showMinimap: true,
map: { style: "streets", defaultZoom: 15 },
markers: [
{ stageId: "entrance", label: "Main Entrance", geoPosition: { lat: 40.7128, lng: -74.0060 } }
]
}Global Audio
Background music that plays across all stages:
globalAudio: {
url: "https://example.com/background-music.mp3",
volume: 0.3,
loop: true
}Positioning Guide
Positions use 3D coordinates in meters from the camera center:
| Axis | Negative | Positive | |------|----------|----------| | x | Left | Right | | y | Down | Up | | z | In front | Behind |
Rules of thumb:
- Navigation portals:
y: 0(floor level) - Info hotspots:
y: 1.5toy: 1.8(eye level) - Comfortable distance:
z: -3toz: -6
Migrating from v1.0
v1.1 is fully backward compatible. The window.__XRGALLERY_CONFIG__ approach still works. To adopt the new API:
Before (v1.0):
<script>
window.__XRGALLERY_CONFIG__ = {
experience: { title: "My Tour" },
stages: [...]
};
</script>
<script src="viewer-bundle.iife.js"></script>After (v1.1):
<div id="app"></div>
<script src="viewer-bundle.iife.js"></script>
<script>
(async () => {
const viewer = await XRGallery.create({
element: '#app',
config: {
experience: { title: "My Tour" },
stages: [...]
}
});
})();
</script>Benefits of the new API:
- No global variable pollution
- Works with React refs and any framework
- Returns a
ViewerInstancewithdispose()for cleanup sceneIdmode fetches config from XRGallery cloud automatically
Browser Support
| Browser | Version | WebXR | |---------|---------|-------| | Chrome | 79+ | Full | | Firefox | 79+ | Full | | Safari | 15.4+ | Partial | | Edge | 79+ | Full |
VR Headset Support
Any WebXR-compatible device: Meta Quest 2/3/Pro, HTC Vive, Valve Index, Windows Mixed Reality, Pico 4.
What This Package Is (and Isn't)
This package is: A self-contained viewer for rendering 360° tours in the browser. You provide the config JSON and media URLs, it renders the experience. MIT licensed, no server required, no tracking, no external calls.
This package is not: The XRGallery editor, dashboard, or cloud platform. Those are proprietary SaaS features available at xrgallery.online. If you want a visual editor, AI scene generation, cloud hosting, analytics, or team collaboration, sign up there.
Creating Tours
You have two options:
- Write config JSON by hand using this package (free, self-hosted)
- Use the XRGallery editor at xrgallery.online to build tours visually, then export them to self-host or use cloud hosting
License
MIT
