SuraFix Runtime Optimizer

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.

cullingDistance frustum
Distanz-Cutoff Frustum / Schatten-Cutoff Visual-Target

Ü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

  1. Beide Skripte (SuraFixVR_CameraCullingManager.cs, SuraFixVR_VisualCullingTarget.cs) irgendwo unter Assets/ ablegen.
  2. Ein leeres GameObject _CullingManager in deine Bootstrap- bzw. Persistent-Szene legen und SuraFixVR_CameraCullingManager draufziehen.
  3. Kamera und Spieler-Transform im Inspector zuweisen, oder leer lassen — der Manager findet sie automatisch über cameraTag bzw. SuraFix_PlayerTracker.
  4. SuraFixVR_VisualCullingTarget auf 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.

Richtwert: Bei VR mit 90 Hz und wenigen hundert Targets sind 30–60 Targets/Frame ein guter Startpunkt. Bei mehreren tausend Targets eher niedriger ansetzen und 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.

Zu beachten: Wenn ein Licht innerhalb eines 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.

Culling und Addressable-Streaming ergänzen sich: Ein Raum kann komplett entladen sein (kein Speicher belegt), während ein näherer, aber noch nicht sichtbarer Raum bereits geladen, aber per 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

  1. Distanzprüfung erkennt: Spieler ist innerhalb von loadRadius.
  2. Addressables.InstantiateAsync(structureAddressKey, transform) wird gestartet.
  3. 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 OnEnable auf 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.

Wichtig: Ohne aktiviertes 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.
Nur ein 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.

  1. Addressables-Package über den Package Manager installieren.
  2. Project Settings → Player → Scripting Define Symbols öffnen.
  3. SURA_FIX_ADDRESSABLES hinzufügen.
  4. Für jedes Raum-Prefab, das per structureAddressKey geladen werden soll, eine Addressable-Gruppe im Addressables-Fenster anlegen.
Ohne das Define bleibt der Loader inaktiv (siehe oben) — das ist kein Fehler, sondern Absicht, damit das Paket auch in Projekten ohne Addressables-Abhängigkeit importiert werden kann.

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.

Das Tool existiert in zwei Varianten je nach Define-Status: Mit 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.

Das lässt sich pro Licht im Inspector übersteuern, falls du z.B. eine sekundäre Directional Light nur in bestimmten Räumen aktiv haben willst.

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, falls refreshInterval auf 0 steht.

Empfohlenes Setup für mehrere Szenen

  1. Eine Persistent-Szene anlegen, die nie entladen wird und ausschließlich den _CullingManager enthält.
  2. Alle Gameplay-Szenen additiv dazu laden (SceneManager.LoadSceneAsync(..., LoadSceneMode.Additive)).
  3. 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.

SuraFix VR Performance Tools — interne Dokumentation