# 360 Tour — Unity 6 Plan & Build Guide A node-based 360° photo/video navigation experience with time travel. Targets: Meta Quest (standalone), Windows .exe, WebGL. All scripts in `Assets/Scripts`, shader in `Assets/Shaders`. Everything lives in the `Tour360` namespace. ## Core design Each vantage point is a **PhotoNode** GameObject placed in the Unity scene at the real capture position (X/Z from your site plan, Y = tripod/eye height). A node holds a list of **MediaVariants** — one per time **epoch** (e.g. "2021 parking lot", "2026 building"), each with a photo or video and a per-capture yaw calibration (`northOffsetDegrees`). The panorama is rendered as the **skybox** using a custom shader (`Custom/SkyboxEquirectBlend`) that holds two equirect textures and a blend value — every transition (node-to-node or time change) is a crossfade between the two slots. Because the skybox is infinitely distant, "moving" is just teleporting the rig and swapping the panorama; hotspot directions stay correct as long as each panorama's yaw is calibrated. **TourManager** is the single source of truth: current node, current epoch, rig movement, crossfade, video players, hotspot spawning. Everything else (map, time slider, hotspots) talks to it via `MoveToNode()`, `SetEpoch()`, and the `NodeChanged`/`EpochChanged` events. **Time behavior:** when the epoch changes, if the current node has media for that epoch it crossfades in place; otherwise the rig jumps to the *nearest node* (scene-space distance) that does — this handles your "vantage points don't match across eras" case. Hotspots and map markers are filtered/dimmed for nodes with no media in the current epoch. ## Script inventory | File | Role | |---|---| | `PhotoNode.cs` | Node data: neighbors, media variants per epoch, gizmos | | `TourManager.cs` | Navigation, epoch logic, skybox crossfade, video, hotspot spawning | | `Hotspot.cs` | Billboarded clickable marker toward a neighbor node | | `DesktopCameraController.cs` | Mouse look + click raycast (exe/WebGL) | | `MapController.cs` | Top-down clickable map (RenderTexture + RawImage), marker colors per epoch | | `TimeControlUI.cs` | Slider + label bound to the epoch list | | `XRI/HotspotXRIBridge.cs` | Makes hotspots selectable by VR controller rays (import only after installing XRI) | | `Editor/PhotoNodeTools.cs` | Menu items to auto-link/clear neighbors by distance | | `Shaders/SkyboxEquirectBlend.shader` | Two-slot equirect skybox with per-slot rotation and blend | ## Project setup (once) 1. **Unity 6 (6000.x)**, URP template ("Universal 3D"). 2. Packages (Window > Package Manager): **XR Plugin Management**, **OpenXR Plugin**, **XR Interaction Toolkit** (3.x, import its Starter Assets sample), **TextMeshPro** (usually present). 3. Project Settings > XR Plug-in Management: enable **OpenXR** on the Android tab (Quest) and Standalone tab (PC VR via Link, optional). Add the Meta Quest interaction/touch controller profiles under OpenXR settings. 4. Project Settings > Player > **Active Input Handling: Both** (XRI uses the new Input System; `DesktopCameraController` uses the legacy API for simplicity). 5. Copy `Assets/Scripts` and `Assets/Shaders` in. Hold back `Scripts/XRI/HotspotXRIBridge.cs` until XRI is installed. ## Asset pipeline (Insta360 → Unity) - Export from Insta360 Studio as **equirectangular** JPEG (photos) and H.264 MP4 (videos). 2:1 aspect (e.g. 5760×2880). - Photo import settings: sRGB on, **Generate Mip Maps OFF** (prevents the vertical seam artifact where the equirect UV wraps), Wrap Mode: Repeat (U), Max Size 8192 (desktop/WebGL) — see per-platform overrides below. - Compression: desktop = DXT/BC7 default; **Android tab override: ASTC 6x6, Max Size 5760 or 4096** (Quest GPU memory is limited); WebGL tab: Max Size 4096 recommended. - Videos for Quest/desktop: import as VideoClip, disable transcode if the source is already H.264. Videos for **WebGL: do NOT use VideoClips** — put the .mp4 in `Assets/StreamingAssets/` and set the variant's Video Source to **Url** with just the file name (the code resolves StreamingAssets automatically), or host it on a server and paste the full URL. Keep WebGL video ≤4K; browser decoders vary. - Memory note: a 5760×2880 uncompressed-in-VRAM texture is large; with more than ~20–30 nodes per scene, move to Addressables and load textures on demand (see Roadmap). ## Scene construction (step by step) Scene hierarchy target: ``` TourScene ├── TourManager (TourManager component) ├── Nodes/ (empty parent) │ ├── Node_Lobby (PhotoNode) │ ├── Node_Entrance (PhotoNode) │ └── ... ├── XR Origin (XR Rig) (from XRI Starter Assets — VR builds) ├── DesktopRig (empty; child: Camera + DesktopCameraController — flat builds) ├── VideoPlayers (2× VideoPlayer components: A and B) ├── Map/ │ ├── MapCamera (ortho, looks down, culls "Map" layer, outputs RenderTexture) │ └── MapBackdrop (optional) (quad with site-plan image, layer "Map") └── UI Canvas ├── MapImage (RawImage + MapController) └── TimeSlider + Label (TimeControlUI) ``` 1. **Skybox material.** Create a Material from `Custom/SkyboxEquirectBlend`; assign it in Lighting > Environment > Skybox Material AND to TourManager's `skyboxMaterial` slot. 2. **Site plan underlay.** Drop your site plan/floor plan on a big ground quad (temporarily) to place nodes against. Scale it so 1 Unity unit = 1 metre. 3. **Place nodes.** Create empty GameObjects with `PhotoNode`, position at each capture spot, Y = camera height at capture (~1.6 m tripod). Fill `variants`: one entry per epoch the spot was shot in, oldest first (last entry doubles as fallback). 4. **Link neighbors.** Run Tools > 360 Tour > Auto-Link Neighbors, then prune links that pass through walls. Gizmos show links as yellow lines. 5. **Epochs.** On TourManager, define the epoch list oldest-first (id + label), set `startNode` and `startEpochIndex`. 6. **Hotspot prefab.** Sphere or ring mesh + SphereCollider + `Hotspot` + a child TMP text for the destination name. Add `HotspotXRIBridge` for VR. Assign to TourManager. 7. **Video players.** Two VideoPlayer components (Play On Awake OFF); assign as `videoPlayerA/B` on TourManager. Only needed if you use video variants — two players let video↔video transitions crossfade. 8. **Map.** Create a "Map" layer. MapCamera: orthographic, rotation (90,0,0), culling mask = Map, target = a new RenderTexture (512×512). RawImage in the canvas shows that RT and carries `MapController` (assign map camera + a marker prefab: small quad rotated flat, **URP/Unlit** material, layer Map, no collider). Camera framing is automatic. 9. **Rig wiring.** VR build: TourManager `rig` = XR Origin, and set the XR Origin's Camera Y Offset / tracking origin to **Floor** with the node Y representing eye height — simplest is Tracking Origin Mode = **Device** with Camera Y Offset 0, so the head sits exactly at the node point. Flat build: `rig` = DesktopRig (camera at local 0,0,0). Keep both rigs in the scene and enable exactly one per build (a one-line bootstrap script or two scene variants — see Builds). ### Yaw calibration (`northOffsetDegrees`) For each variant: enter Play mode at that node, look toward a landmark whose direction you know in the scene (e.g. a neighbor node's hotspot), and adjust the offset until the photo's view of that landmark sits behind its hotspot. Tip: Insta360 exports usually put the camera's power-button direction at the image center; if you noted your camera heading during capture you can precompute the offset. Do it once per capture; it's what makes hotspots appear "at" the real doorway or path. ## Time navigation UX - The slider (or two buttons calling `TourManager.SetEpoch(i±1)`) switches epochs globally. - Same spot, both eras → in-place crossfade: a powerful "then/now" effect. - No coverage at the current spot → jump to the nearest covered node; the map shows dimmed markers for uncovered nodes so users understand coverage per era. - Optional upgrade: a "time portal" hotspot variant that appears only where another epoch exists at (nearly) the same spot. ## Builds **Quest (standalone).** Platform: Android. IL2CPP, ARM64 only. Texture compression ASTC. OpenXR enabled on Android tab with Meta Quest Support feature. Use the XR Origin rig; disable DesktopRig. XRI ray interactors hit hotspot colliders via `HotspotXRIBridge`; add a `TrackedDeviceGraphicRaycaster` to the canvas and make it **world-space** (attach near the left controller or floating at 1.4 m) so map + slider work with the ray. **Windows .exe.** Same scene. Non-VR: enable DesktopRig, disable XR Origin, screen-space canvas. PC VR (optional): keep OpenXR enabled on Standalone tab and use the XR rig — same as Quest wiring. **WebGL.** No VR in a stock Unity WebGL build (Unity doesn't ship WebXR; the community De-Panther WebXR Export package can add it later). Use DesktopRig + screen-space canvas. Videos must be URL-based (StreamingAssets or hosted). Build settings: compression Brotli, and watch total texture memory — prefer 4096-wide panoramas. Practical approach for three targets: one scene, plus a tiny `Bootstrap` step — keep both rigs and both canvases in the scene, and enable the right pair per build with `#if UNITY_ANDROID` / `XRSettings.isDeviceActive` in a 10-line script, or just maintain two scene variants (VR / flat) that share all nodes via a prefab. ## Roadmap / scaling up 1. **MVP:** 3–5 nodes, one epoch, photos only, desktop build. 2. Add second epoch + time slider; calibrate offsets. 3. Add Quest build + XRI; then WebGL. 4. **Scale:** move panoramas to Addressables and load/unload per node (keeps Quest memory flat); store node data in JSON if you want to author outside Unity. 5. Polish: teleport fade-to-black comfort option, footstep "ground ring" hotspots, then/now split-slider shader (the blend shader already supports a screen-side wipe with a small modification), spatial audio per node. ## Known gotchas - Vertical seam line in the panorama → mipmaps are on; turn them off (or accept it). - Blend never changes → the material in Lighting settings is a different instance than the one on TourManager; assign the *same asset*. - Hotspots misaligned with the imagery → `northOffsetDegrees` not calibrated for that variant. - WebGL video silent/black → autoplay policy: require one user click before first video plays (start on a photo node), and serve .mp4 with correct MIME type. - Editing `skyboxMaterial` at runtime permanently changes the asset in the editor — that's normal; blend/textures get overwritten on next Play.