How to Create a 360° Panorama in Unity with a Free Script

The Panorama360Capture script allows you to capture a 360° panorama directly from a Unity scene and save it as a PNG image in equirectangular 2:1 format.

You can then test the generated panorama using an online 360° Viewer.



1. Add the Script to Your Camera

Select a Camera in your Unity scene and attach the Panorama360Capture script to it.

The script uses:

[RequireComponent(typeof(Camera))]

so Unity will automatically add a Camera component if one is not already present.

using UnityEngine; using System.IO; #if UNITY_EDITOR using UnityEditor; #endif // Cara pakai: // 1. Attach script ini ke GameObject yang punya komponen Camera // (atau GameObject kosong, Camera akan otomatis ditambahkan // karena ada [RequireComponent]). // 2. Atur posisi kamera di titik tempat kamu ingin mengambil panorama. // 3. Klik kanan komponen ini di Inspector -> pilih "Capture 360 Panorama" // (bisa saat Play Mode maupun Edit Mode), ATAU panggil // method CapturePanorama() dari script lain / UI Button. // 4. Akan muncul Save File dialog untuk memilih lokasi penyimpanan .png. [RequireComponent(typeof(Camera))] public class Panorama360Capture : MonoBehaviour { [Header("Pengaturan Cubemap")] [Tooltip("Resolusi tiap sisi cubemap internal. Semakin tinggi = semakin detail tapi semakin lambat.")] [Min(16)] public int cubemapFaceSize = 1024; [Header("Pengaturan Output Equirectangular")] [Tooltip("Lebar gambar panorama output (disarankan kelipatan 2)")] [Min(16)] public int outputWidth = 4096; [Tooltip("Tinggi gambar panorama output (idealnya = setengah dari lebar, rasio 2:1)")] [Min(16)] public int outputHeight = 2048; [Header("Output File")] [Tooltip("Nama file default yang muncul di save dialog (tanpa ekstensi)")] public string defaultFileName = "panorama360"; [Tooltip("Folder awal saat save dialog dibuka. Kosongkan = folder default (persistentDataPath)")] public string defaultFolderPath = ""; private Camera cam; // Urutan face cubemap yang dipakai konsisten di seluruh script private static readonly CubemapFace[] FaceOrder = { CubemapFace.PositiveX, CubemapFace.NegativeX, CubemapFace.PositiveY, CubemapFace.NegativeY, CubemapFace.PositiveZ, CubemapFace.NegativeZ }; private void Awake() { cam = GetComponent<Camera>(); } [ContextMenu("Capture 360 Panorama")] public void CapturePanorama() { if (cam == null) cam = GetComponent<Camera>(); // === 1. Tentukan lokasi save lewat dialog === string fullPath = GetSavePath(); if (string.IsNullOrEmpty(fullPath)) { Debug.Log("[Panorama360Capture] Capture dibatalkan oleh user."); return; } if (!fullPath.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase)) fullPath += ".png"; // Validasi sederhana supaya tidak ada nilai aneh dari Inspector int faceSize = Mathf.Max(16, cubemapFaceSize); int width = Mathf.Max(16, outputWidth); int height = Mathf.Max(16, outputHeight); Cubemap cubemap = null; Texture2D panorama = null; try { // === 2. Render cubemap === cubemap = new Cubemap(faceSize, TextureFormat.RGB24, false); if (!cam.RenderToCubemap(cubemap)) { Debug.LogError("[Panorama360Capture] Gagal merender cubemap. " + "Pastikan platform/device mendukung RenderToCubemap."); return; } // === 3. Ambil pixel tiap sisi cubemap === Color[][] faces = new Color[FaceOrder.Length][]; for (int i = 0; i < FaceOrder.Length; i++) faces[i] = cubemap.GetPixels(FaceOrder[i]); // === 4. Bangun equirectangular === panorama = BuildEquirectangular(faces, faceSize, width, height); // === 5. Simpan ke path yang dipilih user === SaveTextureAsPng(panorama, fullPath); Debug.Log("[Panorama360Capture] Panorama 360 disimpan di: " + fullPath); } catch (System.Exception e) { Debug.LogError("[Panorama360Capture] Terjadi error saat capture: " + e); } finally { // === 6. Bersihkan resource sementara (aman di Edit & Play Mode) === DestroySafe(cubemap); DestroySafe(panorama); } } // --------------------------------------------------------------------- // Save dialog // --------------------------------------------------------------------- private string GetSavePath() { string folder = string.IsNullOrEmpty(defaultFolderPath) ? Application.persistentDataPath : defaultFolderPath; #if UNITY_EDITOR // Save dialog native di Editor (jalan di Edit Mode maupun Play Mode) return EditorUtility.SaveFilePanel( "Simpan Panorama 360", folder, defaultFileName, "png" ); #else // Fallback di build: simpan otomatis ke persistentDataPath Debug.LogWarning("[Panorama360Capture] Save dialog native tidak tersedia di build. " + "Menyimpan otomatis ke persistentDataPath."); if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); return Path.Combine(folder, defaultFileName + ".png"); #endif } // --------------------------------------------------------------------- // Equirectangular builder // --------------------------------------------------------------------- private Texture2D BuildEquirectangular(Color[][] faces, int faceSize, int width, int height) { var panorama = new Texture2D(width, height, TextureFormat.RGB24, false); var outputPixels = new Color[width * height]; for (int y = 0; y < height; y++) { float v = (float)y / (height - 1); float theta = (v - 0.5f) * Mathf.PI; // latitude: -pi/2 .. pi/2 for (int x = 0; x < width; x++) { float u = (float)x / (width - 1); float phi = (u - 0.5f) * 2f * Mathf.PI; // longitude: -pi .. pi Vector3 dir = new Vector3( Mathf.Cos(theta) * Mathf.Sin(phi), Mathf.Sin(theta), Mathf.Cos(theta) * Mathf.Cos(phi) ); outputPixels[y * width + x] = SampleCubemap(dir, faces, faceSize); } } panorama.SetPixels(outputPixels); panorama.Apply(); return panorama; } // --------------------------------------------------------------------- // Simpan PNG // --------------------------------------------------------------------- private void SaveTextureAsPng(Texture2D texture, string fullPath) { byte[] pngData = texture.EncodeToPNG(); string folder = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(folder) && !Directory.Exists(folder)) Directory.CreateDirectory(folder); File.WriteAllBytes(fullPath, pngData); } // --------------------------------------------------------------------- // Cubemap sampling // --------------------------------------------------------------------- // Menentukan sisi cubemap mana yang harus disample berdasarkan arah 3D, // lalu mengambil warnanya dengan interpolasi bilinear. private Color SampleCubemap(Vector3 dir, Color[][] faces, int faceSize) { float absX = Mathf.Abs(dir.x); float absY = Mathf.Abs(dir.y); float absZ = Mathf.Abs(dir.z); int faceIndex; float u, v, ma; if (absX >= absY && absX >= absZ) { ma = absX; if (dir.x > 0) { faceIndex = 0; u = -dir.z; v = -dir.y; } // +X else { faceIndex = 1; u = dir.z; v = -dir.y; } // -X } else if (absY >= absX && absY >= absZ) { ma = absY; if (dir.y > 0) { faceIndex = 2; u = dir.x; v = dir.z; } // +Y else { faceIndex = 3; u = dir.x; v = -dir.z; } // -Y } else { ma = absZ; if (dir.z > 0) { faceIndex = 4; u = dir.x; v = -dir.y; } // +Z else { faceIndex = 5; u = -dir.x; v = -dir.y; } // -Z } u = (u / ma + 1f) * 0.5f; v = (v / ma + 1f) * 0.5f; return SampleBilinear(faces[faceIndex], faceSize, u, v); } private Color SampleBilinear(Color[] face, int size, float u, float v) { u = Mathf.Clamp01(u); v = Mathf.Clamp01(v); float fx = u * (size - 1); float fy = v * (size - 1); int x0 = Mathf.FloorToInt(fx); int y0 = Mathf.FloorToInt(fy); int x1 = Mathf.Min(x0 + 1, size - 1); int y1 = Mathf.Min(y0 + 1, size - 1); float tx = fx - x0; float ty = fy - y0; Color c00 = face[y0 * size + x0]; Color c10 = face[y0 * size + x1]; Color c01 = face[y1 * size + x0]; Color c11 = face[y1 * size + x1]; Color cx0 = Color.Lerp(c00, c10, tx); Color cx1 = Color.Lerp(c01, c11, tx); return Color.Lerp(cx0, cx1, ty); } // --------------------------------------------------------------------- // Utility: Destroy aman di Edit Mode & Play Mode // --------------------------------------------------------------------- private void DestroySafe(Object obj) { if (obj == null) return; if (Application.isPlaying) { // Runtime / Play Mode Destroy(obj); } else { // Edit Mode (mis. dipanggil dari ContextMenu saat tidak Play) #if UNITY_EDITOR DestroyImmediate(obj); #else Destroy(obj); #endif } } }

2. Position the Camera

Place the Camera at the location where you want to capture the panorama.

For example:

  • Inside a room
  • In an outdoor environment
  • Inside a vehicle
  • At a specific location in your game scene

The Camera position becomes the center point of the 360° panorama.

3. Configure the Resolution

The script provides several settings in the Inspector.

Cubemap Face Size

1024

This controls the resolution of each internal cubemap face.

For testing, 1024 is a good starting point.

Output Width and Height

Output Width  : 4096
Output Height : 2048

The recommended panorama aspect ratio is 2:1, for example:

2048 × 1024
4096 × 2048
8192 × 4096

For your first test, you can use:

Cubemap Face Size : 1024
Output Width      : 4096
Output Height     : 2048

4. Capture the 360° Panorama

Once the Camera and settings are ready, open the Panorama360Capture component in the Inspector.

Choose:

Capture 360 Panorama

The script will:

  1. Render the scene into a cubemap
  2. Read the six cubemap faces
  3. Convert them into an equirectangular panorama
  4. Open a save dialog
  5. Save the result as a PNG file

For example:

panorama360.png

The capture can be performed in both Edit Mode and Play Mode.



5. Check the Generated Image

The output will be an equirectangular panorama such as:

4096 × 2048

When opened as a normal image, it may look stretched or unusual. This is normal because the image is designed to be displayed by a 360° panorama viewer.

6. Test the Panorama in the 360° Viewer

After generating the PNG, open:

360° Viewer

Upload the panorama360.png file you generated from Unity.

Once loaded, you can interactively explore the scene:

  • Look left and right
  • Look up and down
  • Rotate around the entire environment
  • Check the quality of your Unity panorama

Quick Workflow

Unity Scene
    ↓
Attach Panorama360Capture to Camera
    ↓
Position the Camera
    ↓
Configure Resolution
    ↓
Capture 360 Panorama
    ↓
Save PNG
    ↓
Upload to 360° Viewer
    ↓
Explore the Panorama

That's it! With this simple script, you can turn any suitable Unity scene into a 360° equirectangular panorama and preview the result directly in your browser.

Comments

Popular posts from this blog

Smart FBX Exporter: The Best Way to Export Unity Models Cleanly

Power Pole Wiring System – Procedural Cable & Wire Generator for Unity (Free Tool)

Stop Manual Scene Switching: A Unity Editor Tool to Play from Start Scene