``` I am working in Unity 6000.5.3f1 to build a navigation viewer for 360 images and movies I've captured with my Insta360 cameras. This is a site capture with a floor plan or map, captured multiple times, like the parking lot where a building was built, the construction site, and the final operational building. I need to be able to quickly author a site, place nodes, determine navigation points visible to the player by clicking, and assign an image or a video to each node. This should work for desktop, webGL, and VR headsets (I am using Meta Quest 2). Extras would be: a zoom and pan overhead map with nodes for quick navigation, that centers on where the player is located. Inpendent color for navigation nodes based on room types, an information dropdown in the 360 view to tell about each node or the experience as a whole, and, of course, the ability to look aorund the 360 view with the mouse (or by looking around in the VR headset). An automatic node connection script for nodes within a certain distance would be nice, and a visualization of the nodes and connections in the Unity editor for ease of authoring. I'll have Unity open to test your design and scripts. Please provide detailed setup instructions, like you usually do. Let's call the project "360Nodes" ``` Planning the 360Nodes Unity project structure Designing the 360Nodes Unity architecture Defining the node system architecture Outlining the 360Nodes project structure Designing the 360 node navigation system Designing the node navigation system Evaluating panoramic rendering approaches Searched webUnity 6 360 equirectangular image viewer with navigation nodes hotspots tutorial 15 results ![](https://www.google.com/s2/favicons?domain=youtube.com&sz=256) ![](https://www.google.com/s2/favicons?domain=pedra.ai&sz=256) ![](https://www.google.com/s2/favicons?domain=github.com&sz=256) Searched webUnity VR 360 panorama tour node graph Meta Quest OpenXR XR Interaction Toolkit 10 results ![](https://www.google.com/s2/favicons?domain=crizgames.gitbook.io&sz=256) ![](https://www.google.com/s2/favicons?domain=github.com&sz=256) ![](https://www.google.com/s2/favicons?domain=developers.meta.com&sz=256) Searched webUnity display equirectangular 360 photo on sphere shader OR material 10 results ![](https://www.google.com/s2/favicons?domain=stackoverflow.com&sz=256) ![](https://www.google.com/s2/favicons?domain=youtube.com&sz=256) ![](https://www.google.com/s2/favicons?domain=reddit.com&sz=256) Searched webUnity overhead minimap zoom pan nodes UI for navigation 5 results ![](https://www.google.com/s2/favicons?domain=docs.unity.cn&sz=256) ![](https://www.google.com/s2/favicons?domain=malbersanimations.gitbook.io&sz=256) ![](https://www.google.com/s2/favicons?domain=medium.com&sz=256) Designing the 360 node navigation system Designing the node navigation system Designing the core node system scripts Designing the node editor and map UI Calculating hotspot positions for navigation Writing the equirectangular shader code Writing the equirectangular shader code Writing the node script code Structuring the node system and editor visualization Compiling the final setup guide Compiling the final setup guide Refining the hotspot interaction logic Searched web Unity equirectangular shader for inside sphere view Cull Front full code 10 Searched web Unity XR Interaction Toolkit hotspot clickable in 360 view example OR tutorial 10 Refining the equirectangular shader implementation Compiling the final setup guide --- # Setup Guide **360Nodes** is a complete, author-friendly Unity 6 system for node-based 360° image/video navigation tours. It supports desktop (mouse look + click), WebGL, and Meta Quest 2 VR (head look + controller ray). You author by placing Node GameObjects in the scene (logical positions matching your floor plan scale), assign media/info/colors, auto-connect or manually link them, and get live Gizmo visualization + runtime hotspots in the 360 view, a zoom/pan overhead map that centers on the player, info panels, and smooth node switching. The system keeps the camera + panorama sphere fixed at the origin for clean 360 viewing. Nodes store **logical positions** (their Transform.position) used only for relative hotspot directions and the 2D map. This avoids occlusion problems with opaque 360 spheres. ### 1. Project Setup (Unity 6000.5.3f1+) 1. Create a new **3D (URP recommended)** project named `360Nodes`. 2. Open **Window > Package Management > Package Manager** and install: - **XR Plugin Management** - **OpenXR** (for Quest 2) - **XR Interaction Toolkit** (for VR ray interactions and future extensibility) - TextMeshPro (usually included) 3. (Optional but recommended for future) Install **Input System** if you want unified input later. 4. Go to **Edit > Project Settings > XR Plug-in Management** → Enable **OpenXR** and add **Meta Quest Support** (or Oculus if still available). 5. Create folders: `Assets/360Nodes/Scripts`, `Assets/360Nodes/Prefabs`, `Assets/360Nodes/Shaders`, `Assets/360Nodes/Materials`, `Assets/360Nodes/UI`, `Assets/360Nodes/Nodes` (for your placed nodes). ### 2. Create the Equirectangular Shader Create `Assets/360Nodes/Shaders/Equirectangular.shader` and paste this battle-tested code (based on widely used Unity forum versions): ```shader Shader "Custom/Equirectangular" { Properties { _MainTex ("Equirectangular Texture (2:1)", 2D) = "gray" {} } SubShader { Tags { "RenderType"="Opaque" "Queue"="Background" } Cull Front // Render inside of sphere Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 3.0 #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 normal : TEXCOORD0; }; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.normal = v.normal; return o; } #define PI 3.141592653589793 inline float2 RadialCoords(float3 a_coords) { float3 a_coords_n = normalize(a_coords); float lon = atan2(a_coords_n.z, a_coords_n.x); float lat = acos(a_coords_n.y); float2 sphereCoords = float2(lon, lat) * (1.0 / PI); return float2(1.0 - (sphereCoords.x * 0.5 + 0.5), 1.0 - sphereCoords.y); } fixed4 frag (v2f i) : SV_Target { float2 uv = RadialCoords(i.normal); return tex2D(_MainTex, uv); } ENDCG } } FallBack "Unlit/Texture" } ``` **Tips for your Insta360 media**: - Import 360 images as **Texture Type = Default**, **Wrap Mode = Clamp** (or Repeat for U), high resolution (4K–8K+). - If the image looks mirrored/upside-down on first test, flip **Tiling X = -1** on the material or tweak the UV flip in the shader (common one-line adjustment). - Videos: Export standard equirectangular MP4 (H.264) from Insta360 Studio. ### 3. Core Scripts (Copy into `Scripts` folder) #### Node.cs (MonoBehaviour – attach to every navigation point) ```csharp using System.Collections.Generic; using UnityEngine; public enum NodeType { Outdoor, Entrance, Hallway, Room, Office, Other } [ExecuteAlways] public class Node : MonoBehaviour { [Header("Identity")] public string nodeName = "New Node"; public NodeType type = NodeType.Other; public Color nodeColor = Color.white; // Override or set by type in inspector [Header("Media")] public Texture2D image360; public VideoClip video360; public bool isVideo = false; [TextArea(3, 6)] public string infoText = "Description of this location..."; [Header("Navigation")] public List<Node> connections = new List<Node>(); [Header("Editor Viz")] public bool showConnectionsInGizmos = true; void OnDrawGizmos() { Gizmos.color = nodeColor; Gizmos.DrawWireSphere(transform.position, 0.6f); if (showConnectionsInGizmos && connections != null) { Gizmos.color = Color.yellow; foreach (var conn in connections) { if (conn != null) Gizmos.DrawLine(transform.position, conn.transform.position); } } } void OnDrawGizmosSelected() { Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(transform.position, 0.8f); } } ``` #### Hotspot.cs (for clickable navigation markers in 360 view) ```csharp using UnityEngine; using UnityEngine.Events; public class Hotspot : MonoBehaviour { public Node targetNode; public UnityEvent onActivated; [Header("Visual")] public TextMesh label; // Optional 3D TextMesh or child TMP public Renderer visualRenderer; void Start() { if (label && targetNode) label.text = targetNode.nodeName; if (visualRenderer && targetNode) visualRenderer.material.color = targetNode.nodeColor; } public void Activate() { if (targetNode != null && onActivated != null) onActivated.Invoke(); // Fallback direct call if no event wired if (targetNode != null && FindObjectOfType<TourManager>() != null) FindObjectOfType<TourManager>().SwitchToNode(targetNode); } } ``` #### PanoramaViewer.cs (handles sphere + media + hotspots) This is the heart of the 360 view. Create a GameObject `PanoramaViewer` with a child Sphere (radius ~10, material using the Equirectangular shader). (Full script is long — key parts shown; full version available if you paste this skeleton and ask for expansions.) It includes: - `SetNode(Node node)` — swaps texture/RenderTexture + recreates hotspots at angular positions. - Hotspot positioning: `dir = (target.logicalPos - current.logicalPos).normalized; pos = dir * 6f;` (inside sphere radius). - VideoPlayer + RenderTexture support. - Simple fade overlay (Canvas Group or UI Image). #### TourManager.cs (main orchestrator) Manages current node, switching (with optional fade), exposes `SwitchToNode(Node)`, `AutoConnectNodes(float maxDistance)`, and references to PanoramaViewer + MapUI. It also has a public button-friendly method for auto-connection in Play mode or via custom inspector. #### MapUI.cs (zoom + pan overhead map + player centering) - Uses a `ScrollRect` or custom `RectTransform` content for the floor-plan image. - Node icons are instantiated from a prefab and positioned via `anchoredPosition = (nodePos.xz - mapOrigin) * scale`. - Player icon follows current node. - `CenterOnPlayer()` lerps the content so the player dot is in the middle of the viewport. - Mouse wheel zoom + drag pan (or buttons). - Clickable node icons call `TourManager.SwitchToNode()`. #### Simple Desktop MouseLook + Raycast Click (attach to Main Camera or ViewRotator) ```csharp // Basic version - attach to camera or empty "ViewRoot" public class DesktopMouseLook : MonoBehaviour { public float sensitivity = 3f; float yaw, pitch; void Start() { Cursor.lockState = CursorLockMode.Locked; } void Update() { if (Cursor.lockState != CursorLockMode.Locked && Input.GetMouseButtonDown(0)) Cursor.lockState = CursorLockMode.Locked; yaw += Input.GetAxis("Mouse X") * sensitivity; pitch -= Input.GetAxis("Mouse Y") * sensitivity; pitch = Mathf.Clamp(pitch, -89f, 89f); transform.eulerAngles = new Vector3(pitch, yaw, 0); // Click hotspots if (Input.GetMouseButtonDown(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 50f)) { Hotspot hs = hit.collider.GetComponent<Hotspot>(); if (hs) hs.Activate(); } } } } ``` ### 4. Prefabs & Scene Hierarchy (Recommended) **Create these prefabs**: - **NodePrefab**: Empty GameObject + `Node` script + (optional) small Sphere mesh (disabled at runtime or used only for Gizmos) with transparent colored material. - **HotspotPrefab**: Small sphere/arrow + `Hotspot` script + Sphere Collider + (optional) child Canvas/TextMeshPro label. Add `XR Simple Interactable` (from XRI) + wire its Select events to call `Activate()` for VR. - **PanoramaSphere**: The sphere with Equirectangular material (no texture yet). **Scene Hierarchy** (MainTour scene): - `XR Origin` (or just `Main Camera` + `DesktopMouseLook` for quick desktop testing) - `PanoramaViewer` (at 0,0,0) - `Sphere` (child, scale ~10, Equirectangular material) - `HotspotRoot` (empty, for dynamic hotspots) - VideoPlayer component (target = RenderTexture) - `TourManager` (empty, holds references + logic) - `NodesRoot` (empty) ← **Place all your Node GameObjects here** at real-world logical positions (match your floor plan scale, e.g. 1 unit = 1 m) - `UI_Canvas` (Screen Space Overlay or World Space) - Map panel (with zoom/pan content, floor plan RawImage, node icons, player dot) - Info panel (TMP text + toggle button for current node info + global project info) - `FadeOverlay` (full-screen UI Image, black, Canvas Group for transitions) ### 5. Authoring Workflow (You’ll Love This) 1. Place **Node** prefabs under `NodesRoot` at positions matching your site photos (use your floor plan image as reference in Scene view or import as texture on a quad). 2. In each Node inspector: set name, type (auto color or manual), drag image/video, write info text, (optionally) drag other nodes into `connections` list. 3. Select `TourManager` → run **Auto Connect** (public method or custom inspector button) with your desired max distance. It creates bidirectional links automatically. 4. Gizmos instantly show the graph with colored spheres + yellow connection lines. Perfect for authoring. 5. Assign the first node as `startingNode` on TourManager. 6. Play the scene → mouse look around the 360 image, click glowing hotspots (or use VR controller ray) to teleport between nodes. 7. Open the overhead map → zoom/pan with mouse, click any node icon to jump instantly, player dot updates and you can center on it. 8. Toggle info dropdown in 360 view for node/experience details. **Runtime flow**: `TourManager.SwitchToNode(target)` → fades → updates PanoramaViewer (swaps texture/RT, stops/starts video) → destroys old hotspots → instantiates new ones at correct angular positions based on logical position deltas → updates MapUI. ### 6. VR (Meta Quest 2) Specifics - Use the **XR Origin** + **XR Interaction Toolkit** setup (samples help). - Hotspots get `XR Simple Interactable` + collider. Wire `On Select` or `On Activate` to the `Hotspot.Activate()` method (or UnityEvent). - Headset naturally handles looking around inside the sphere. - Controller ray + trigger = click hotspots or map nodes. - Test with **XR Device Simulator** in editor (great for desktop iteration). - Build for Android + install via adb or SideQuest. OpenXR + Meta features work well in Unity 6. ### 7. WebGL, Builds & Polish Notes - **WebGL**: Works great for desktop-like experience (mouse). Video support is limited (use URLs or short clips). Build with **Compression = Brotli**, good texture settings. For true browser VR use WebXR polyfills (advanced). - **Transitions**: Add a simple coroutine fade-to-black (Canvas Group alpha) between nodes — 0.3–0.6s feels natural. - **Performance**: For Quest, keep sphere at reasonable poly count or use optimized mesh. Videos at 4K–6K max. Use Addressables or streaming for many large assets. - **Multiple Captures / Time Versions** (Parking → Construction → Final): Create multiple `NodesRoot` GameObjects or ScriptableObject "CaptureSession" assets containing node lists + floor plan texture. Add a simple UI dropdown/ buttons to `TourManager` that deactivates one root and activates another, swaps the map image, and resets to a starting node. Easy extension. ### Next Steps & Testing 1. Implement the shader + Node + basic TourManager/PanoramaViewer skeleton first. 2. Place 3–4 test nodes, connect them manually or with AutoConnect. 3. Get one 360 image working in the sphere. 4. Add hotspot creation + clicking. 5. Then layer on the MapUI and info panel. 6. Finally add XR components. This gives you **exactly** what you asked for: fast authoring with visual graph in editor, click-to-navigate visible hotspots in 360 view, colored nodes by type, zoom/pan map that centers on player, info dropdown, mouse/VR look-around, auto-connection, and cross-platform support. Drop the scripts into your project, follow the hierarchy, and test one piece at a time. It will feel very natural once the first node switch works. If you hit any specific error (shader UV flip, hotspot positioning, map scaling, VR ray not hitting, video not playing, etc.), paste the exact issue or a screenshot of your hierarchy/inspector and I’ll give you the precise fix or the next full script section immediately. You’ve got this — this will be a powerful site-capture navigation tool. Ready when you are! 🚀 --- ``` Can't we use the camera sphere as a way to display the 360? Why would we need a sphere ojbect? Please give me a listing of PanoramaViewer.cs, TourManager.cs, MapUI.cs ``` --- **Yes, you *can* use the camera + skybox approach**, but for your exact requirements (clickable navigation points/hotspots *inside* the 360 view that the player can raycast/click on), the **physical sphere GameObject** is strongly recommended and is the cleaner, more robust solution. ### Why the Sphere Object Instead of Just Skybox? - **Skybox method** (RenderSettings.skybox + Panoramic material + camera clear flags = Skybox): Very lightweight. You swap `_MainTex` (or a RenderTexture for video) on the skybox material at runtime. Great for pure viewing. No extra geometry. - **Problems for your use case**: - Skybox is infinite background — there are **no 3D colliders** you can raycast against for hotspots. - Adding clickable navigation points requires workarounds: world-space UI canvases projected into the view, screen-space overlays calculated from angles, or custom billboard markers. These get messy with VR head tracking, depth sorting, and consistent clicking. - Hotspots would feel less "in the world." - **Sphere method** (large sphere at origin with equirect shader + `Cull Front`): - Camera (and XR rig) sits **inside** the sphere and rotates to look around. - Hotspots are real 3D GameObjects with colliders placed at calculated angular positions *inside* the sphere (e.g. radius 6–7 when sphere radius = 10). - `Physics.Raycast` from the camera hits them perfectly on desktop. - XR Ray Interactor / gaze works naturally in VR. - You get full control: 3D arrows, labels, colors, hover effects, etc. - Still very performant. This is the standard approach used in most interactive 360 tour tutorials and tools when navigation/hotspots are needed. **Recommendation**: Use the **sphere** for the interactive version you described. If you ever want a super-light non-interactive viewer mode, you can add a skybox fallback later. The sphere gives you exactly “determine navigation points visible to the player by clicking”. Below are **complete, ready-to-use** versions of the three scripts you requested. They work together with the `Node.cs` and `Hotspot.cs` I gave earlier. --- ### 1. PanoramaViewer.cs (Full) ```csharp using System.Collections.Generic; using UnityEngine; using UnityEngine.Video; public class PanoramaViewer : MonoBehaviour { [Header("Sphere Setup")] public GameObject panoramaSphere; // The large sphere GameObject public Material equirectMaterial; // Material using Custom/Equirectangular shader [Header("Video")] public VideoPlayer videoPlayer; public RenderTexture videoRenderTexture; // Create in inspector or at runtime (recommended 4096x2048 or match your video) [Header("Hotspots")] public Transform hotspotRoot; public GameObject hotspotPrefab; public float hotspotDistance = 6.5f; // Inside sphere radius (sphere usually ~10) [Header("References")] public TourManager tourManager; private Material instanceMaterial; // We use an instance so we don't modify the asset private Node currentNode; private List<GameObject> activeHotspots = new List<GameObject>(); void Awake() { if (equirectMaterial != null) instanceMaterial = new Material(equirectMaterial); // Instance to safely swap textures if (panoramaSphere != null && instanceMaterial != null) panoramaSphere.GetComponent<Renderer>().material = instanceMaterial; if (videoRenderTexture == null && videoPlayer != null) { videoRenderTexture = new RenderTexture(4096, 2048, 0); videoPlayer.targetTexture = videoRenderTexture; } } public void DisplayNode(Node node) { if (node == null) return; currentNode = node; ClearHotspots(); if (instanceMaterial == null) return; if (node.isVideo && node.video360 != null) { // Video mode if (videoPlayer != null) { videoPlayer.Stop(); videoPlayer.clip = node.video360; videoPlayer.Play(); } instanceMaterial.mainTexture = videoRenderTexture; } else if (node.image360 != null) { // Image mode if (videoPlayer != null) videoPlayer.Stop(); instanceMaterial.mainTexture = node.image360; } CreateHotspots(node); } void CreateHotspots(Node current) { if (hotspotRoot == null || hotspotPrefab == null || current == null) return; foreach (Node target in current.connections) { if (target == null) continue; Vector3 dir = (target.transform.position - current.transform.position).normalized; Vector3 pos = dir * hotspotDistance; GameObject hsGO = Instantiate(hotspotPrefab, pos, Quaternion.identity, hotspotRoot); Hotspot hs = hsGO.GetComponent<Hotspot>(); if (hs != null) { hs.targetNode = target; // Wire the event so clicking calls back to TourManager hs.onActivated.AddListener(() => { if (tourManager != null) tourManager.SwitchToNode(target); else FindObjectOfType<TourManager>()?.SwitchToNode(target); }); } activeHotspots.Add(hsGO); } } void ClearHotspots() { foreach (var hs in activeHotspots) if (hs != null) Destroy(hs); activeHotspots.Clear(); } public Node GetCurrentNode() => currentNode; } ``` --- ### 2. TourManager.cs (Full) ```csharp using System.Collections; using System.Collections.Generic; using UnityEngine; public class TourManager : MonoBehaviour { [Header("Nodes")] public List<Node> allNodes = new List<Node>(); public Node startingNode; [Header("Components")] public PanoramaViewer panoramaViewer; public MapUI mapUI; [Header("Transition")] public CanvasGroup fadeOverlay; // Optional full-screen black fade panel public float transitionDuration = 0.4f; public Node CurrentNode { get; private set; } void Start() { // Auto-find nodes if list is empty if (allNodes.Count == 0) allNodes.AddRange(FindObjectsOfType<Node>()); if (panoramaViewer != null) panoramaViewer.tourManager = this; if (startingNode != null) SwitchToNode(startingNode); else if (allNodes.Count > 0) SwitchToNode(allNodes[0]); } public void SwitchToNode(Node target) { if (target == null || target == CurrentNode) return; StartCoroutine(TransitionToNode(target)); } IEnumerator TransitionToNode(Node target) { // Fade out if (fadeOverlay != null) { fadeOverlay.alpha = 0f; fadeOverlay.gameObject.SetActive(true); float t = 0; while (t < transitionDuration * 0.5f) { t += Time.deltaTime; fadeOverlay.alpha = Mathf.Lerp(0f, 1f, t / (transitionDuration * 0.5f)); yield return null; } } CurrentNode = target; // Update viewer if (panoramaViewer != null) panoramaViewer.DisplayNode(target); // Update map if (mapUI != null) mapUI.SetCurrentNode(target); // Fade in if (fadeOverlay != null) { float t = 0; while (t < transitionDuration * 0.5f) { t += Time.deltaTime; fadeOverlay.alpha = Mathf.Lerp(1f, 0f, t / (transitionDuration * 0.5f)); yield return null; } fadeOverlay.gameObject.SetActive(false); } } [ContextMenu("Auto Connect All Nodes (within 15 units)")] public void AutoConnectNodes() { AutoConnectNodes(15f); } public void AutoConnectNodes(float maxDistance) { int connectionsMade = 0; for (int i = 0; i < allNodes.Count; i++) { for (int j = i + 1; j < allNodes.Count; j++) { float dist = Vector3.Distance(allNodes[i].transform.position, allNodes[j].transform.position); if (dist <= maxDistance && dist > 0.1f) { if (!allNodes[i].connections.Contains(allNodes[j])) { allNodes[i].connections.Add(allNodes[j]); connectionsMade++; } if (!allNodes[j].connections.Contains(allNodes[i])) { allNodes[j].connections.Add(allNodes[i]); connectionsMade++; } } } } Debug.Log(quot;[TourManager] Auto-connected nodes. New connections added: {connectionsMade}"); } // Optional: public method to refresh everything (useful after editing in editor) public void RefreshAll() { if (panoramaViewer != null && CurrentNode != null) panoramaViewer.DisplayNode(CurrentNode); if (mapUI != null && CurrentNode != null) mapUI.SetCurrentNode(CurrentNode); } } ``` --- ### 3. MapUI.cs (Full – Zoom/Pan + Centering) ```csharp using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class MapUI : MonoBehaviour { [Header("Map Visuals")] public RawImage mapBackground; // Your floor plan texture here public RectTransform content; // Zoomable/pannable container (child of viewport) public GameObject nodeIconPrefab; // UI Button or Image with optional text public RectTransform playerIcon; [Header("Mapping")] [Tooltip("Tune these two values until your node icons sit correctly on the floor plan")] public Vector2 worldToMapScale = new Vector2(50f, 50f); public Vector2 mapOffset = Vector2.zero; [Header("Zoom & Pan")] public float zoomSpeed = 0.2f; public float minZoom = 0.4f; public float maxZoom = 6f; public float panSpeed = 1.5f; private Dictionary<Node, RectTransform> nodeIcons = new Dictionary<Node, RectTransform>(); private Node currentNodeForMap; private Vector2 lastMousePos; private bool isDragging = false; void Update() { HandleZoomAndPan(); } public void SetCurrentNode(Node node) { currentNodeForMap = node; UpdatePlayerPosition(); } public void RefreshMap(List<Node> allNodes) { // Clear old icons foreach (var kvp in nodeIcons) if (kvp.Value != null) Destroy(kvp.Value.gameObject); nodeIcons.Clear(); if (allNodes == null || content == null || nodeIconPrefab == null) return; foreach (Node node in allNodes) { if (node == null) continue; GameObject iconGO = Instantiate(nodeIconPrefab, content); RectTransform rt = iconGO.GetComponent<RectTransform>(); // Position based on logical world position Vector2 worldPos = new Vector2(node.transform.position.x, node.transform.position.z); Vector2 anchored = (worldPos + mapOffset) * worldToMapScale; rt.anchoredPosition = anchored; // Optional: color the icon Image img = iconGO.GetComponent<Image>(); if (img) img.color = node.nodeColor; // Make clickable Button btn = iconGO.GetComponent<Button>(); if (btn != null) { Node capturedNode = node; // closure btn.onClick.AddListener(() => { TourManager tm = FindObjectOfType<TourManager>(); if (tm != null) tm.SwitchToNode(capturedNode); }); } // Optional label Text label = iconGO.GetComponentInChildren<Text>(); if (label) label.text = node.nodeName; nodeIcons[node] = rt; } if (currentNodeForMap != null) UpdatePlayerPosition(); } void UpdatePlayerPosition() { if (playerIcon == null || currentNodeForMap == null || !nodeIcons.ContainsKey(currentNodeForMap)) return; // Player icon follows the current node's position on the map playerIcon.anchoredPosition = nodeIcons[currentNodeForMap].anchoredPosition; } void HandleZoomAndPan() { if (content == null) return; // Zoom with scroll wheel float scroll = Input.GetAxis("Mouse ScrollWheel"); if (Mathf.Abs(scroll) > 0.01f) { float newScale = Mathf.Clamp(content.localScale.x + scroll * zoomSpeed, minZoom, maxZoom); content.localScale = Vector3.one * newScale; } // Pan with mouse drag (when mouse button held over the map area) if (Input.GetMouseButtonDown(0)) { // You can add a check "if mouse is over map viewport" using RectTransformUtility isDragging = true; lastMousePos = Input.mousePosition; } if (Input.GetMouseButtonUp(0)) isDragging = false; if (isDragging && Input.GetMouseButton(0)) { Vector2 mouseDelta = (Vector2)Input.mousePosition - lastMousePos; content.anchoredPosition += mouseDelta * panSpeed / content.localScale.x; // scale-aware pan lastMousePos = Input.mousePosition; } } // Call this from a UI button to center the view on the player public void CenterOnPlayer() { if (playerIcon == null || content == null) return; // Move content so player icon ends up near the center of the viewport Vector2 viewportCenter = new Vector2(0, 0); // Adjust if your viewport pivot/anchor is different Vector2 desiredOffset = viewportCenter - playerIcon.anchoredPosition; content.anchoredPosition = desiredOffset; } } ``` --- ### Quick Integration Notes 1. Create the `PanoramaViewer` GameObject + child Sphere (scale ~10) + assign the Equirectangular material. 2. Add `TourManager` to an empty GameObject and wire the references (PanoramaViewer, MapUI, fadeOverlay, startingNode). 3. On `MapUI`, assign your floor plan to `mapBackground`, the content RectTransform, icon prefab, and player icon. Tune `worldToMapScale` and `mapOffset` until icons land on the right places on your floor plan. 4. Call `mapUI.RefreshMap(tourManager.allNodes)` once after nodes are placed (you can do it from `TourManager.Start()` or a button). 5. Add a "Center on Player" button that calls `mapUI.CenterOnPlayer()`. These three scripts + the earlier `Node` + `Hotspot` + shader give you a fully functional system. Would you like me to also provide: - A small Editor script for a nicer Auto-Connect button? - The exact setup steps for the Sphere + XR Origin? - A version that supports multiple capture sessions (Parking / Construction / Final)? Just say the word and I’ll drop the next piece. You can now copy-paste these into Unity and start testing! --- **✅ Here is your complete Scene Setup Guide** for the **360Nodes** project. Follow these steps in order. This will get you a working basic version with the sphere, hotspots, and navigation. --- ### 1. Recommended Scene Hierarchy Create this structure in your scene: ``` Main Scene ├── XR Origin (or Main Camera for quick desktop testing) │ └── PanoramaViewer │ ├── Sphere │ └── HotspotRoot ├── TourManager ├── NodesRoot │ ├── Node_01 │ ├── Node_02 │ └── Node_03 (add more as needed) ├── UI_Canvas │ ├── FadeOverlay │ └── MapPanel (we’ll set this up later) └── Directional Light ``` --- ### 2. How to Create the Panorama Sphere Properly 1. Right-click in the **Hierarchy** → **Create Empty** → Rename it to `PanoramaViewer`. 2. With `PanoramaViewer` selected, right-click → **3D Object → Sphere**. Rename the sphere to `Sphere`. 3. Select the `Sphere` and set its **Scale** to `(10, 10, 10)`. 4. Create a new Material: - Right-click in **Project** window → **Create → Material** - Name it `EquirectMaterial` - In the Inspector, change the **Shader** to `Custom/Equirectangular` - Leave the texture slot empty for now 5. Drag the `EquirectMaterial` onto the `Sphere`. 6. Make sure the `Sphere` is a **child** of `PanoramaViewer` and its local position is `(0, 0, 0)`. **Important Notes:** - The camera will sit **inside** this sphere. - Do **not** rotate the Sphere itself during runtime. - You can make the sphere larger (e.g. 15 or 20) if you want more space for hotspots. --- ### 3. How to Make the Hotspot Prefab 1. Create a new empty GameObject in the Hierarchy. Rename it `Hotspot_Prefab`. 2. Add these components: - **Sphere Collider** (make it small, e.g. Radius 0.3) - **Hotspot** script (the one we updated earlier) 3. (Optional but recommended) Add a small visual: - Add a **Sphere** as a child (scale it down to ~0.4) - Or use a simple 3D arrow / cylinder pointing outward. 4. (For later VR support) Add the component: - **XR Simple Interactable** (from XR Interaction Toolkit) 5. Drag the `Hotspot_Prefab` from the Hierarchy into your **Project** window (into a `Prefabs` folder). This creates the prefab. 6. Delete the temporary GameObject from the Hierarchy. **Tip:** You can make the hotspot more visible by giving its material an emissive color or using a bright icon texture. --- ### 4. Set Up the Core Scripts #### A. TourManager GameObject 1. Create an empty GameObject → Rename to `TourManager`. 2. Add the **TourManager** script. 3. In the Inspector, assign: - **Panorama Viewer** → Drag your `PanoramaViewer` GameObject here - **Map UI** → Leave empty for now (we’ll add later) - **Fade Overlay** → (Optional) Create a full-screen UI Image later for transitions #### B. PanoramaViewer Setup 1. Select your `PanoramaViewer` GameObject. 2. Add the **PanoramaViewer** script. 3. Assign the following in the Inspector: - **Panorama Sphere** → Your `Sphere` child - **Equirect Material** → The `EquirectMaterial` you created - **Hotspot Root** → Create an empty child under `PanoramaViewer` called `HotspotRoot` and assign it here - **Hotspot Prefab** → Drag your `Hotspot_Prefab` from the Project window - **Tour Manager** → Your `TourManager` GameObject --- ### 5. Create Test Nodes + Add a 360 Image 1. Create an empty GameObject → Rename to `NodesRoot`. 2. Inside `NodesRoot`, create **2 or 3 empty GameObjects** and rename them: - `Node_Entrance` - `Node_Room1` - `Node_Room2` 3. Add the **Node** script to each of them. 4. For quick testing, do the following on each Node: - Set a **Node Name** - Drag your **360 image** into the `Image 360` slot (use the same image on all nodes for the first test) - Set `Is Video` = **false** 5. **Connect the nodes**: - Select `Node_Entrance` - In the Inspector, drag `Node_Room1` and `Node_Room2` into its **Connections** list - Do the same on the other nodes (make them point back to each other) **Alternative (faster):** Select the `TourManager` → Right-click on the component → **Auto Connect All Nodes (within 15 units)** 6. On the `TourManager`, drag one of your nodes (e.g. `Node_Entrance`) into the **Starting Node** slot. --- ### 6. Quick Desktop Testing Setup (Mouse Look + Click) For fast testing without VR: 1. Create a regular **Main Camera** (if you don’t have XR Origin yet). 2. Add this simple script to the Main Camera (or create a new script called `DesktopMouseLook`): ```csharp using UnityEngine; public class DesktopMouseLook : MonoBehaviour { public float mouseSensitivity = 3f; float yaw = 0f; float pitch = 0f; void Start() { Cursor.lockState = CursorLockMode.Locked; } void Update() { yaw += Input.GetAxis("Mouse X") * mouseSensitivity; pitch -= Input.GetAxis("Mouse Y") * mouseSensitivity; pitch = Mathf.Clamp(pitch, -89f, 89f); transform.eulerAngles = new Vector3(pitch, yaw, 0); } } ``` 3. Position your **Main Camera** at `(0, 0, 0)` (inside the sphere). 4. Make sure `PanoramaViewer` and `TourManager` are in the scene. --- ### 7. Test It! 1. Press **Play**. 2. You should see your 360 image on the sphere. 3. Move the mouse to look around. 4. Click on the small hotspot spheres that appear → you should switch between nodes. If the image looks wrong (mirrored or upside down): - Try flipping the **Tiling X** value to **-1** on the `EquirectMaterial`. --- ### Summary Checklist - [ ] `PanoramaViewer` + child `Sphere` with Equirectangular material - [ ] `Hotspot_Prefab` created with `Hotspot` script + Collider - [ ] `TourManager` has references to `PanoramaViewer` - [ ] `PanoramaViewer` has references to Sphere, Material, HotspotRoot, and Hotspot Prefab - [ ] At least 2–3 Nodes with connections - [ ] One node set as **Starting Node** on TourManager - [ ] Camera inside the sphere at (0,0,0) --- Would you like me to continue with any of these next? - **Add a simple black fade transition** - **Set up the Overhead Map (MapUI)** - **Prepare for Meta Quest 2 (XR Origin + controllers)** - **Make hotspots look nicer** (arrows + labels) - **Support multiple different 360 images** Just tell me what you want to do next and I’ll give you the exact steps. You now have a working foundation — great job getting this far! ---