Asset streaming sample overview
Updated: May 11, 2026
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.
- 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
- 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.
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.
| File / Scene | What it demonstrates | Key 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 |
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.
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.
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.
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.
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.
- 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.