Performance-Tooling für Unity VR
SuraFix Culling System

Frustum-, Distanz- und Licht-Culling mit Time-Slicing, plus Addressable-basiertes Raum-Streaming — ausgelegt für große, additiv geladene Welten mit vielen gleichzeitig aktiven Szenen.
Überblick
Das Culling System besteht aus zwei Komponenten, die zusammenarbeiten:
| Komponente | Rolle |
|---|---|
SuraFixVR_CameraCullingManager |
Ein Singleton pro aktiver Kamera-Konfiguration. Berechnet Frustum-Planes, verteilt Arbeit per Time-Slicing und entscheidet, was sichtbar ist. |
SuraFixVR_VisualCullingTarget |
Sitzt auf jedem cullbaren Objekt. Cached Renderer, Partikel, Animatoren und Lichter; wird vom Manager an- und abgeschaltet. |
SuraFix_AddressableRoomLoader |
Lädt/entlädt ganze Raum-Prefabs per Addressables, abhängig von der Spielerdistanz. Ergänzt das Renderer-Culling um echtes Speicher-Streaming. |
SuraFix_PlayerTracker |
Gemeinsame, statische Spieler-Referenz — wird sowohl vom Culling-Manager als auch vom RoomLoader genutzt. |
Targets melden sich selbst beim Manager an (OnEnable / OnDisable), nicht umgekehrt. Das ist der Grund, warum das System problemlos über mehrere additiv geladene Szenen hinweg funktioniert — dazu mehr unter Mehrere Szenen.
Installation
- Beide Skripte (
SuraFixVR_CameraCullingManager.cs,SuraFixVR_VisualCullingTarget.cs) irgendwo unterAssets/ablegen. - Ein leeres GameObject
_CullingManagerin deine Bootstrap- bzw. Persistent-Szene legen undSuraFixVR_CameraCullingManagerdraufziehen. - Kamera und Spieler-Transform im Inspector zuweisen, oder leer lassen — der Manager findet sie automatisch über
cameraTagbzw.SuraFix_PlayerTracker. SuraFixVR_VisualCullingTargetauf jedes Objekt setzen, das gecullt werden soll (Props, Räume, Licht-Rigs, Partikel-Setups).
Schnellstart
Minimal-Setup für ein einzelnes Objekt mit Distanz- und Frustum-Culling:
using UnityEngine;
// Wird automatisch beim Manager registriert, sobald aktiv.
[RequireComponent(typeof(SuraFixVR_VisualCullingTarget))]
public class ExampleProp : MonoBehaviour { }
Mehr ist nicht nötig — Renderer, Partikel und Animatoren werden beim Awake automatisch gecached.
CameraCullingManager
Läuft pro Frame über eine Teilmenge aller registrierten Targets (targetsPerFrame) und wertet für jedes Target Distanz und Frustum-Sichtbarkeit aus.
Time-Slicing
Bei vielen Objekten ist es teurer, alle pro Frame zu prüfen als die Prüfung über mehrere Frames zu verteilen. targetsPerFrame steuert genau das — höhere Werte reagieren schneller auf Bewegung, niedrigere Werte sparen CPU.
refreshInterval erhöhen.Auto-Refresh der Zielliste
refreshInterval bestimmt, wie oft die komplette Liste aktiver Targets neu eingesammelt wird (z.B. nach dem Laden neuer Räume). Auf 0 setzen, um Auto-Refresh komplett zu deaktivieren — sinnvoll, wenn Register/Unregister manuell oder über Szenen-Events gesteuert wird.
VisualCullingTarget
Jedes Target unterstützt drei Bounds-Modi:
| Modus | Verwendung |
|---|---|
Object |
Bounds werden automatisch aus den gecachten Renderern berechnet. |
ObjectWithCustomBounds |
Manuell definierte Box (objectBoundsOffset / -Size) — nützlich bei Objekten ohne eigenen Renderer. |
Room |
Größere Bounds, die einen ganzen Raum repräsentieren. Siehe Room-Modus. |
Partikelsysteme (ParticleSystemRenderer) werden seit v1.1 separat erfasst und fließen sowohl in die Sichtbarkeits-Bounds als auch ins Ein-/Ausblenden ein — relevant, wenn ein Effekt-Objekt keinen eigenen MeshRenderer besitzt.
Room-Modus
Im Room-Modus gilt ein Objekt immer als sichtbar, solange sich der Spieler innerhalb der definierten cullingBoundsSize befindet — unabhängig vom Frustum. Das verhindert Pop-in direkt im Rücken des Spielers in Innenräumen.
Room-Objekts respectFrustum aktiviert hat, wird es an den Room-Bounds gecullt, nicht an seiner eigenen Lichtreichweite. Bei großen Räumen mit kurzer Lichtreichweite kann das zu früherem Sichtbarwerden führen als erwartet.Addressable Streaming: Konzept
Während das Culling-System nur Rendering an- und ausschaltet, geht SuraFix_AddressableRoomLoader einen Schritt weiter: Es lädt und entlädt ganze Prefabs über die Addressables-API, abhängig von der Distanz zum Spieler. Damit sinkt nicht nur die Render-Last, sondern auch der tatsächliche Speicherverbrauch.
VisualCullingTarget unsichtbar geschaltet ist.Hysterese statt harter Grenze
Laden und Entladen nutzen bewusst zwei unterschiedliche Radien:
| Feld | Bedeutung |
|---|---|
loadRadius |
Innerhalb dieser Distanz wird die Struktur geladen. |
unloadBuffer |
Zusätzlicher Puffer — entladen passiert erst bei loadRadius + unloadBuffer. |
Ohne diesen Puffer würde ein Objekt exakt an der Grenze ständig laden und sofort wieder entladen, sobald sich der Spieler minimal hin- und herbewegt. Der Puffer verhindert dieses Flackern.
SuraFix_AddressableRoomLoader
Prüft per Coroutine (checkInterval, Standard 0,25s) regelmäßig die Distanz zum Spieler und steuert darüber den Ladezustand.
Ablauf beim Laden
- Distanzprüfung erkennt: Spieler ist innerhalb von
loadRadius. Addressables.InstantiateAsync(structureAddressKey, transform)wird gestartet.- Nach erfolgreichem Laden wird die Instanz unter das Loader-Objekt gehängt, lokal zentriert und zunächst deaktiviert, einen Frame gewartet, dann erst aktiviert — das vermeidet Initialisierungs-Hakler in Skripten, die in
OnEnableauf bereits gesetzte Transform-Werte angewiesen sind.
Ablauf beim Entladen
Sobald die Distanz unloadRadius überschreitet, wird Addressables.ReleaseInstance(...) aufgerufen. Eine laufende Lade-Coroutine wird dabei sauber abgebrochen, falls der Spieler den Bereich verlässt, bevor das Laden abgeschlossen ist.
SURA_FIX_ADDRESSABLES-Define (siehe Setup) führt der Loader keinerlei Lade-Logik aus und schreibt nur eine Warnung in die Konsole. Das Skript funktioniert dann als reines No-Op, ohne Compile-Fehler zu verursachen.Gizmos
Im Editor zeigt das Objekt zwei Wireframe-Kugeln: grün für loadRadius, gelb für die effektive Entlade-Distanz.
SuraFix_PlayerTracker
Eine bewusst minimale, statische Referenz auf den aktuellen Spieler-Transform — genutzt vom RoomLoader und als Fallback vom CameraCullingManager, falls dort kein Spieler-Transform manuell zugewiesen wurde.
| Member | Beschreibung |
|---|---|
PlayerTracker.Current |
Statische Property mit der aktuellen Spieler-Transform. |
PlayerTracker.Instance |
Legacy-Alias auf Current, für Rückwärtskompatibilität zu älteren Setups. |
SetCurrent(Transform) |
Setzt die Referenz manuell, z.B. beim Spawnen eines Netzwerk-Spielers. |
PlayerTracker sollte gleichzeitig aktiv sein. overrideExisting steuert, ob ein neu hinzukommender Tracker einen bestehenden ersetzt — relevant beim Szenenwechsel mit mehreren XR-Rigs.Setup & Scripting Define Symbol
Das Addressable-System ist optional und kompiliert auch ohne installiertes Addressables-Package — dafür sorgt das Define SURA_FIX_ADDRESSABLES.
- Addressables-Package über den Package Manager installieren.
- Project Settings → Player → Scripting Define Symbols öffnen.
SURA_FIX_ADDRESSABLEShinzufügen.- Für jedes Raum-Prefab, das per
structureAddressKeygeladen werden soll, eine Addressable-Gruppe im Addressables-Fenster anlegen.
Key Fixer (Editor-Tool)
Erreichbar über SuraFix → Addressables → Fix Keys im Editor-Menü. Gleicht den Addressable-address-Wert jedes Eintrags an den tatsächlichen Dateinamen des Assets an.
Sinnvoll, wenn Assets im Project-Fenster umbenannt wurden, ohne dass die Addressable-Adresse mitgepflegt wurde — structureAddressKey im RoomLoader muss exakt mit dieser Adresse übereinstimmen, sonst schlägt das Laden mit einem Fehler in der Konsole fehl.
SURA_FIX_ADDRESSABLES repariert es echte Keys, ohne das Define zeigt es nur einen Hinweis, dass Addressables fehlt — beide Varianten erscheinen unter unterschiedlichen Menüpfaden (SuraFix/... bzw. Tools/SuraFix/...), ein kleines Detail, das beim Suchen im Menü leicht übersehen wird.Licht-Culling: Konzept & Inspector
Lichter werden pro Eintrag (LightCullingEntry) individuell konfiguriert. Jeder Eintrag hat zwei Cutoff-Distanzen:
| Feld | Bedeutung |
|---|---|
shadowCullingDistance |
Ab hier werden nur die Schatten deaktiviert (LightShadows.None) — der teuerste Teil, aber das Licht selbst bleibt an. |
lightCullingDistance |
Ab hier wird das Licht komplett deaktiviert. Sollte größer oder gleich shadowCullingDistance sein. |
respectFrustum |
Wenn aktiv, muss das Objekt zusätzlich im Frustum/Room sichtbar sein, damit Licht bzw. Schatten an bleiben. |
enableCulling |
Master-Schalter pro Licht. Deaktivieren, um ein bestimmtes Licht komplett von der Culling-Logik auszunehmen. |
Empfohlene ReihenfolgeSchatten zuerst
Schatten kosten in VR überproportional viel — sie zuerst und früher abzuschalten gibt den größten Performance-Gewinn, bevor das Licht selbst optisch auffällt.
Directional Lights
Beim automatischen Caching werden LightType.Directional-Lichter standardmäßig mit enableCulling = false angelegt. Eine Sonne hat keine sinnvolle Distanz zum Spieler — sie würde sonst je nach Position des Objekts willkürlich an- und ausgehen.
Szenenübergreifender Einsatz
Das System ist bewusst so gebaut, dass es additiv geladene, mehrfach gleichzeitig aktive Szenen unterstützt:
- Der Manager ist ein einzelnes Singleton, unabhängig davon, wie viele Szenen aktuell geladen sind.
- Targets registrieren sich selbst beim Aktivieren (
OnEnable) und entfernen sich beim Deaktivieren (OnDisable) — das passiert automatisch beim Laden/Entladen einer Szene, ganz ohne Zusatzcode. RefreshTargets()kann zusätzlich manuell nach einem Szenenwechsel aufgerufen werden, fallsrefreshIntervalauf0steht.
Empfohlenes Setup für mehrere Szenen
- Eine Persistent-Szene anlegen, die nie entladen wird und ausschließlich den
_CullingManagerenthält. - Alle Gameplay-Szenen additiv dazu laden (
SceneManager.LoadSceneAsync(..., LoadSceneMode.Additive)). - Jede Szene bringt ihre eigenen
VisualCullingTarget-Objekte mit — keine manuelle Registrierung nötig.
Warum das wichtig istArchitektur
Weil Registrierung über Lifecycle-Events läuft statt über harte Referenzen, bleibt der Manager auch dann korrekt, wenn Szenen in beliebiger Reihenfolge geladen oder entladen werden — z.B. beim Streamen großer offener Welten oder beim Wechsel zwischen Innen- und Außenbereichen. Derselbe Gedanke trägt das Addressable-System: SuraFix_PlayerTracker ist eine einzige, statische Referenz, die unabhängig davon funktioniert, in welcher Szene Spieler, RoomLoader oder Culling-Manager gerade liegen.
API-Referenz: CameraCullingManager
| Member | Beschreibung |
|---|---|
Instance |
Statischer Zugriff auf den aktiven Manager. |
RefreshTargets() |
Sammelt alle aktiven Targets in der Szene neu ein. |
Register(target) |
Fügt ein Target manuell hinzu (normalerweise automatisch über OnEnable). |
Unregister(target) |
Entfernt ein Target manuell (normalerweise automatisch über OnDisable). |
cullingDistance |
Globale Distanz-Grenze für die Visual-Sichtbarkeit. |
targetsPerFrame |
Time-Slicing-Budget pro Frame. |
API-Referenz: VisualCullingTarget
| Member | Beschreibung |
|---|---|
CacheRenderers() |
Erfasst Renderer, Partikel, Animatoren und Lichter neu. Bestehende Licht-Einstellungen bleiben dabei erhalten. |
SetVisualsEnabled(bool) |
Schaltet alle gecachten Renderer/Partikel (und optional Animatoren) um. |
UpdateLightState(sqrDistance, isVisible) |
Wird vom Manager pro Frame aufgerufen, wertet alle lightEntries aus. |
GetCullingBounds() |
Liefert die für den aktuellen cullingType relevanten Welt-Bounds. |
FAQ
Meine Partikeleffekte verschwinden unerwartet.
Seit v1.1 werden ParticleSystemRenderer in die Bounds-Berechnung mit einbezogen. Stelle sicher, dass du die aktuelle Version beider Skripte verwendest — ältere Versionen kannten nur MeshRenderer und SkinnedMeshRenderer.
Ein Licht in einem Room-Objekt schaltet zu früh ab.
Prüfe, ob respectFrustum für dieses Licht aktiv ist — im Room-Modus zählen dann die Room-Bounds, nicht die individuelle Lichtreichweite. Siehe Room-Modus.
Funktioniert das System mit Unity vor 2023.1?
Ja — der Manager nutzt automatisch FindObjectsOfType als Fallback über #if UNITY_2023_1_OR_NEWER.
Der RoomLoader tut nichts, keine Fehler, keine Logs.
Prüfe, ob SURA_FIX_ADDRESSABLES unter Scripting Define Symbols gesetzt ist (siehe Setup). Ohne dieses Define kompiliert das Skript zwar, lädt aber bewusst nichts.
Mein Raum-Prefab lädt nicht, obwohl Addressables aktiv ist.
structureAddressKey muss exakt der Addressable-address entsprechen. Nach Umbenennungen im Project-Fenster hilft SuraFix → Addressables → Fix Keys, siehe Key Fixer.