Develop

Asset streaming sample overview

Updated: May 11, 2026

Overview

This sample demonstrates how to implement quadtree-based level-of-detail (LOD) streaming for open-world VR environments using Unity Addressables, teaching you memory-efficient scene management patterns essential for Quest development. Four biomes (alpine, canyon, fort, townhub) are divided into grid-based subscenes across three LOD levels, with custom lightmap batching and mesh decimation via edge collapse algorithms. The LODGenerator script has partially removed code that requires the commercial Mesh Baker plugin, but the sample ships with pre-generated LOD scenes — Mesh Baker is only needed if you want to re-generate the LOD hierarchy from source assets.

What you will learn

  • Implement quadtree-based spatial partitioning for progressive scene streaming based on camera distance
  • Use Unity Addressables to load and unload grid-based subscenes asynchronously without performance hitches
  • Batch per-scene lightmaps into a Texture2DArray to avoid per-scene lightmap loading overhead
  • Manage a three-state asset lifecycle (Unloaded → Loaded/Hidden → Enabled) to preload nearby areas and prevent pop-in
  • Throttle async operations and defer mesh unloading to maintain stable frame rates during streaming

Requirements

  • A Meta Quest headset
  • A Unity development environment configured for Meta Quest
For specific Unity and SDK version requirements, see the sample’s README. For development environment setup, see the platform setup documentation.

Get started

Clone the sample repository from GitHub using Git LFS (required for large asset files). Open the project in Unity, then build and deploy to your Quest device using the custom build menu items under AssetStreaming > Build Addressables and APK. For specific Unity version requirements, detailed build instructions, and troubleshooting, see the sample’s README.

Explore the sample

File / SceneWhat it demonstratesKey concepts
Startup.unity
Entry point scene that loads the main world via Addressables
Addressables scene loading, async scene references
combined.unity
Main world scene containing LODManager instances for each biome
Spatial partitioning across multiple managers
combined/scenes/LOD0/, LOD1/, LOD2/
264 pre-generated subscenes organized by detail level and grid position
Grid-based scene streaming, naming convention M{index}_LOD{level}{biome}_merged{x}_{y}
LODManager.cs
Central controller for quadtree-based streaming
Camera-to-grid mapping, async operation throttling, mesh unload deferral
LODTreeNode.cs
Individual quadtree node with three-state lifecycle
Double-buffered operation queue, parent-stays-visible pattern
SublevelCombiner.cs + SublevelCombinerEditor.cs
Runtime and editor components for lightmap batching
Texture2DArray creation, UV2.z channel packing, global shader texture
LODGenerator.cs + LODGeneratorEditor.cs
Configuration and pipeline for LOD generation
Edge collapse decimation, grid cell sizing, LOD radius thresholds
UserInput.cs
VR input handling and locomotion mode switching
Teleport/Walk/Free modes, debug UI integration
Benchmark.cs + BenchmarkWalker.cs
Automated waypoint traversal for performance testing
Looping path interpolation, repeatable benchmark routes
BuildUtils.cs
Custom menu items for Addressables + APK builds
Android build automation, addressables build pipeline

Runtime behavior

The Startup scene loads the main combined world scene via Addressables and displays an info overlay that fades automatically. The LOD system begins streaming immediately: distant areas appear at LOD2 (lowest detail) while areas within one grid cell of your camera load at LOD0 (highest detail). The sample defaults to teleport locomotion — point and click with the right controller to move, or switch modes via the debug panel.
As you move through the world, nearby grid cells progressively load higher-detail subscenes while distant cells unload or remain at lower detail. Parent LOD nodes stay visible until all children finish loading, preventing visual pop-in. Press Y on the left controller to open the debug panel, which displays LOD visualization overlays, forced LOD controls, a freeze LOD toggle, and benchmark walker controls for automated performance testing routes. If you fall below the killZ boundary, the sample teleports you back to the spawn position.

Key concepts

Quadtree LOD system

The sample uses quadtree spatial partitioning where each parent node has four children representing the same area at higher detail. LODManager.cs maps the camera position to grid coordinates (FloorToInt(cameraPos / gridCellSize)), then LODTreeNode.cs recursively updates visibility: nodes within one cell-width of the camera show their children (higher detail), while farther nodes show themselves (lower detail) and disable children. A dead zone of 1.0 unit prevents thrashing when the camera position oscillates near a cell boundary.
For the complete implementation, see LODManager.cs and LODTreeNode.cs.

Addressables integration

The sample loads each grid cell as a separate addressable scene using sceneRef.LoadSceneAsync(LoadSceneMode.Additive, priority:lodLevel), where the LOD level determines load priority (LOD2 loads before LOD0). Scenes are grouped by biome (alpine, canyon, fort, townhub) in separate Addressables groups, allowing independent build iteration per biome. The three-state lifecycle (Unloaded → Loaded/Hidden → Enabled) acts as a preload buffer: nearby cells load into memory but remain hidden until needed, reducing visible pop-in.
See LODTreeNode.cs for the load/enable state machine and AddressableAssetsData/ for group configuration.

Memory management

The sample throttles to a maximum of four async load/unload operations per frame to avoid hitches. The system unloads mesh assets one per frame via Resources.UnloadAsset() to spread the CPU cost over multiple frames. A double-buffered operation queue allows the system to cancel pending operations when the player reverses direction, preventing wasted work loading scenes the player has already left.
See the throttling logic in LODManager.cs and the operation queue in LODTreeNode.cs.

Lightmap batching

The editor tool SublevelCombinerEditor.cs merges per-scene lightmaps into a single Texture2DArray, then bakes the lightmap array index into each mesh’s UV2.z channel. At runtime, SublevelCombiner.cs sets this batched lightmap as a global shader texture via Shader.SetGlobalTexture("BatchedLightmap", batchedLightmap), enabling the BATCHED_LIGHTMAP shader keyword. This eliminates per-scene lightmap loading overhead and reduces draw calls.
See SublevelCombinerEditor.cs for the UV modification and Texture2DArray creation logic.

Extend the sample

  • Add a fifth biome with custom LOD settings by creating a new LODManager and configuring its grid cell size and LOD radius thresholds in the inspector
  • Implement dynamic time-of-day lighting by modifying TimeOfDay.cs to interpolate _TimeOfDayWorldTint and _TimeOfDayTerrainTint shader properties based on in-game time
  • Extend the benchmark walker system in BenchmarkWalker.cs to record frame time, draw calls, and memory usage at each waypoint for automated performance regression testing
Note: To regenerate the LOD hierarchy from source assets (rather than using the pre-generated scenes), you need the commercial Mesh Baker plugin or an equivalent mesh combining solution.