Skip to content

Self-Hosting Tours

Host your own 360° VR experiences using the @xrgallery/viewer npm package.

Installation

npm install @xrgallery/viewer
yarn add @xrgallery/viewer
<script src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>

Quick Start

Create an index.html file:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My VR Tour</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; }
    #app { width: 100%; height: 100%; }
  </style>
</head>
<body>
  <div id="app"></div>
  <script src="https://unpkg.com/@xrgallery/viewer/dist/viewer-bundle.iife.js"></script>
  <script>
    XRGallery.create({
      element: '#app',
      config: {
        experience: {
          title: "My Virtual Tour"
        },
        stages: [
          {
            id: "lobby",
            name: "Welcome",
            skybox: {
              type: "image",
              url: "./panoramas/lobby.jpg"
            },
            hotspots: []
          }
        ]
      }
    });
  </script>
</body>
</html>

That's it! Open the file in a browser to view your tour.

Using a scene from XRGallery cloud?

You can also pass a sceneId instead of a config object:

XRGallery.create({ element: '#app', sceneId: 'YOUR_SCENE_ID' });

Framework integration

For React, Vue, Angular, Svelte, and Next.js examples, see the Framework Guides.

Configuration Reference

Experience Settings

experience: {
  title: "Museum Tour",           // Tour title (required)
  description: "Virtual museum",  // Optional description
  defaultFOV: 1.0                 // Camera field of view (0.3-2.5, default: 1.0)
}

You can also add global audio that plays across all stages:

{
  experience: { ... },
  stages: [ ... ],
  globalAudio: {
    url: "https://example.com/background-music.mp3",
    volume: 0.3,
    loop: true
  }
}

Stages

Each stage is a 360° location in your tour:

{
  id: "gallery-1",                // Unique ID (required)
  name: "Main Gallery",           // Display name (required)
  skybox: { ... },                // Background (required)
  hotspots: [ ... ],              // Interactive elements (optional)
  models: [ ... ],                // 3D models (optional)
  planes: [ ... ],                // 2D images/videos in space (optional)
  lights: [ ... ],                // Custom lighting (optional)
  audioUrl: "https://...",        // Ambient audio URL (optional)
  audioVolume: 0.5,               // 0-1 ambient audio volume (optional)
  initialCameraTarget: { x, y, z } // Where camera looks on load (optional)
}

Skybox Types

skybox: {
  type: "image",
  url: "https://example.com/panorama.jpg",
  rotation: 90  // Optional: rotate in degrees
}
Use equirectangular panorama images (2:1 aspect ratio).

skybox: {
  type: "video",
  url: "https://example.com/360-video.mp4",
  // Or use HLS for streaming:
  hlsUrl: "https://stream.mux.com/xxx.m3u8",
  thumbnailUrl: "https://example.com/preview.jpg",
  autoplay: true,
  loop: true
}
Supports MP4 and HLS streaming (.m3u8).

skybox: {
  type: "color",
  color: "#1a1a2e"
}
Solid color background.

Hotspot Types

Info Hotspot

Displays information when clicked:

{
  id: "info-1",
  type: "info",
  position: { x: 0, y: 1.6, z: -5 },
  label: "Learn More",
  color: "#4A90E2",
  infoTitle: "About This Artwork",
  infoDescription: "This piece was created in 1920...",
  infoImageUrl: "https://example.com/detail.jpg",  // Optional

  // Optional: Add links or embed content
  linkUrl: "https://example.com",
  linkText: "Visit Website",
  videoUrl: "https://example.com/video.mp4",
  iframeUrl: "https://example.com/embed"
}

Links to another stage:

{
  id: "nav-1",
  type: "navigation",
  position: { x: 3, y: 0, z: -4 },
  label: "Go to Gallery 2",
  targetStageId: "gallery-2",

  // Portal customization
  portalEffectStyle: "enhanced",  // "clean", "enhanced", "minimal", "alien", "floor-arrow"
  portalColor: "#4A90E2",
  portalRestSize: 1.0,
  portalHoverSize: 1.2
}

Audio Hotspot

Plays audio when clicked:

{
  id: "audio-1",
  type: "audio",
  position: { x: -2, y: 1.5, z: -3 },
  label: "Audio Guide",
  audioUrl: "https://example.com/narration.mp3",
  audioVolume: 0.8,
  audioLoop: false,

  // 3D spatial audio settings
  audioSpatial: true,
  audioRolloffFactor: 1,
  audioMaxDistance: 100
}

Combined Hotspot (Info + Audio)

Shows information AND plays audio:

{
  id: "both-1",
  type: "both",
  position: { x: 1, y: 1.6, z: -4 },
  label: "Guided Info",
  infoTitle: "The Starry Night",
  infoDescription: "Vincent van Gogh, 1889...",
  audioUrl: "https://example.com/starry-night-narration.mp3"
}

Lead Capture Hotspot

Collect visitor information with customizable forms:

{
  id: "contact-1",
  type: "lead_capture",
  position: { x: 0, y: 1.5, z: -5 },
  label: "Get in Touch",
  leadForm: {
    formType: "contact",
    title: "Contact Us",
    fields: [
      { name: "name", type: "text", label: "Name", required: true },
      { name: "email", type: "email", label: "Email", required: true },
      { name: "message", type: "textarea", label: "Message" }
    ]
  }
}

Ambient Audio

Add background music or ambience to a stage:

{
  id: "forest",
  name: "Forest Trail",
  skybox: { ... },
  audioUrl: "https://example.com/forest-ambience.mp3",
  audioVolume: 0.5  // 0 to 1
}

3D Models

Add glTF/GLB models to your scenes:

{
  id: "gallery",
  name: "Gallery",
  skybox: { ... },
  models: [
    {
      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
    }
  ]
}

2D Planes

Embed images or videos as floating planes in 3D space:

{
  planes: [
    {
      type: "image",
      url: "https://example.com/poster.jpg",
      position: { x: 0, y: 2, z: -6 },
      scale: { width: 3, height: 4 }
    },
    {
      type: "video",
      url: "https://example.com/promo.mp4",
      position: { x: 4, y: 1.5, z: -5 },
      scale: { width: 4, height: 2.25 },
      autoplay: true,
      loop: true
    }
  ]
}

Custom Lighting

Override the default scene 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,
      range: 20
    }
  ]
}

Add floor plans or maps with markers:

{
  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 } }
    ]
  }
}
{
  navigation: {
    type: "map",
    showMinimap: true,
    map: {
      style: "streets",
      defaultZoom: 15
    },
    markers: [
      {
        stageId: "entrance",
        label: "Main Entrance",
        geoPosition: { lat: 40.7128, lng: -74.0060 }
      }
    ]
  }
}

Complete Example

XRGallery.create({
  element: '#app',
  config: {
    experience: {
      title: "Art Gallery Virtual Tour",
      description: "Explore our collection from anywhere"
    },
    stages: [
      {
        id: "entrance",
        name: "Gallery Entrance",
        skybox: {
          type: "image",
          url: "./images/entrance-360.jpg"
        },
        initialCameraTarget: { x: 0, y: 0, z: -100 },
        hotspots: [
          {
            id: "welcome",
            type: "info",
            position: { x: 0, y: 1.6, z: -4 },
            label: "Welcome",
            infoTitle: "Welcome to the Gallery",
            infoDescription: "Explore our virtual exhibition."
          },
          {
            id: "to-modern",
            type: "navigation",
            position: { x: 3, y: 0, z: -3 },
            label: "Modern Art Wing",
            targetStageId: "modern-wing",
            portalEffectStyle: "enhanced"
          },
          {
            id: "to-classical",
            type: "navigation",
            position: { x: -3, y: 0, z: -3 },
            label: "Classical Art Wing",
            targetStageId: "classical-wing"
          }
        ]
      },
      {
        id: "modern-wing",
        name: "Modern Art",
        skybox: {
          type: "video",
          url: "./videos/modern-art-360.mp4"
        },
        audioUrl: "./audio/jazz-ambient.mp3",
        audioVolume: 0.3,
        models: [
          {
            url: "./models/sculpture.glb",
            position: { x: 0, y: 0, z: -4 },
            scale: 1.5
          }
        ],
        hotspots: [
          {
            id: "mondrian-info",
            type: "both",
            position: { x: 2, y: 1.5, z: -4 },
            label: "Mondrian",
            infoTitle: "Composition with Red, Blue, and Yellow",
            infoDescription: "Piet Mondrian, 1930.",
            infoImageUrl: "./images/mondrian-detail.jpg",
            audioUrl: "./audio/mondrian-guide.mp3"
          },
          {
            id: "back-to-entrance",
            type: "navigation",
            position: { x: -4, y: 0, z: 2 },
            label: "Back to Entrance",
            targetStageId: "entrance"
          }
        ]
      },
      {
        id: "classical-wing",
        name: "Classical Art",
        skybox: {
          type: "image",
          url: "./images/classical-360.jpg"
        },
        hotspots: [
          {
            id: "back-to-entrance-2",
            type: "navigation",
            position: { x: 4, y: 0, z: 2 },
            label: "Back to Entrance",
            targetStageId: "entrance"
          }
        ]
      }
    ],
    navigation: {
      type: "floorplan",
      showMinimap: true,
      floorPlans: [
        { floor: 1, name: "Gallery", imageUrl: "./images/floorplan.png" }
      ],
      markers: [
        { stageId: "entrance", label: "Entrance", floorPlanPosition: { x: 0.5, y: 0.2 } },
        { stageId: "modern-wing", label: "Modern", floorPlanPosition: { x: 0.8, y: 0.5 } },
        { stageId: "classical-wing", label: "Classical", floorPlanPosition: { x: 0.2, y: 0.5 } }
      ]
    }
  }
});

Positioning Hotspots

Hotspot positions use 3D coordinates relative to the camera center:

  • x: Left (-) / Right (+)
  • y: Down (-) / Up (+)
  • z: Behind (+) / In Front (-)

Positioning Tips

  • Place navigation hotspots at y: 0 to appear at floor level
  • Info hotspots work well at y: 1.5 to y: 1.8 (eye level)
  • Keep hotspots at distance 3-6 units from center (z: -3 to z: -6)
  • Use the XRGallery Editor to visually place hotspots

Hosting Options

Static File Hosting

The viewer works with any static file host:

  • Netlify: Drop your folder to deploy
  • Vercel: vercel deploy
  • GitHub Pages: Push to gh-pages branch
  • AWS S3: Upload to S3 bucket with static hosting
  • Any web server: Nginx, Apache, etc.

CORS Requirements

If your media files are hosted on a different domain, ensure CORS headers are set:

Access-Control-Allow-Origin: *

See CORS Troubleshooting for details.

Exporting from XRGallery

The easiest way to create a self-hosted tour:

  1. Create your tour at xrgallery.online
  2. Use the visual editor to add stages and hotspots
  3. Click Export in the dashboard
  4. Download the ZIP file
  5. Extract and host anywhere

The exported package includes:

  • index.html - Ready-to-use viewer
  • config.json - Your tour configuration
  • README.md - Instructions

Browser Support

Browser Version Notes
Chrome 79+ Full WebXR support
Firefox 79+ Full WebXR support
Safari 15.4+ WebXR support
Edge 79+ Full WebXR support
Mobile Chrome 79+ WebXR on supported devices
Mobile Safari 15.4+ Limited WebXR

VR Headset Support

Any WebXR-compatible device:

  • Meta Quest ⅔/Pro
  • HTC Vive / Vive Pro
  • Valve Index
  • Windows Mixed Reality
  • Pico 4
  • And more...

VR Mode

VR mode requires HTTPS in production. For local development, localhost works without HTTPS.

Bundle Size

The viewer bundle is approximately 3.1 MB gzipped and includes:

  • Babylon.js 3D engine
  • HLS.js for video streaming
  • All UI components
  • WebXR support

The bundle is fully self-contained with no external dependencies at runtime.

Further Reading