UE5 Optimizacija — Kompletan Vodič

UE5 Optimizacija — Kompletan Vodič

Ovaj dokument pokriva sve optimizacione tehnike koje smo primenili na archviz UE5 projektu (Simulore_Tirsova), uključujući Python skripte, konzolne komande, i workflow savete za buduće projekte.


Sadržaj

  1. Dijagnostika — kako profilisati scenu
  2. Python skripte za analizu i bulk operacije
  3. Konzolne komande za rendering optimizaciju
  4. Scalability i INI konfiguracija
  5. Packaging fix za kopirane projekte
  6. Mega Audit skripta — sve na jednom mestu
  7. Saveti i best practices

1. Dijagnostika — kako profilisati scenu

Osnovne stat komande (u konzoli ~)

stat fps                    — prikazuje FPS
stat unit                   — Game thread, Render thread, GPU, Frame time
stat gpu                    — detaljan GPU breakdown po pass-u (NAJKORISNIJI)
stat SceneRendering         — draw calls, tri count, light count
stat Nanite                 — Nanite specifične statistike
stat quad                   — quad overdraw vizualizacija
ProfileGPU                  — ili Ctrl+Shift+, za najdetaljniji GPU breakdown

Viewport vizualizacije

U viewportu klikni Lit dropdown → Optimization Viewmodes:

Kako čitati stat gpu output

Pogledaj Avg kolonu. Najveći brojevi su tvoji bottleneck-ovi. Tipični krivci:

Workflow

  1. Pokreni stat gpu
  2. Identifikuj najskuplji pass
  3. Pokreni odgovarajuću Python skriptu za analizu
  4. Primeni fix (skripta ili ručno)
  5. Ponovo stat gpu sa istog mesta
  6. Ponovi dok nisi zadovoljan

2. Python skripte za analizu i bulk operacije

VAŽNO: Pre pokretanja skripti, proveri da je Python Editor Script Plugin uključen (Edit → Plugins → Python). U Output Log-u prebaci dropdown sa "Cmd" na "Python". Za duže skripte, sačuvaj kao .py fajl i pokreni sa File → Execute Python Script.

2.1 Pronađi sve meshove bez Nanite-a iznad X tria

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

for actor in actors:
    comp = actor.get_component_by_class(unreal.StaticMeshComponent)
    if comp and comp.static_mesh:
        mesh = comp.static_mesh
        tris = mesh.get_num_triangles(0)
        nanite = mesh.get_editor_property('nanite_settings').enabled
        if tris > 5000 and not nanite:
            print(f"{actor.get_actor_label()} | {mesh.get_name()} | {tris} tris | Nanite: OFF")

Šta radiš sa outputom: Sve što ima >5k tria bez Nanite-a je kandidat za uključivanje. Izuzeci: translucent meshovi (staklo), animirani meshovi.

2.2 Bulk uključi Nanite na sve meshove >5k tria

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

processed = set()
count = 0

for actor in actors:
    comp = actor.get_component_by_class(unreal.StaticMeshComponent)
    if comp and comp.static_mesh:
        mesh = comp.static_mesh
        path = mesh.get_path_name()
        if path in processed:
            continue
        tris = mesh.get_num_triangles(0)
        settings = mesh.get_editor_property('nanite_settings')
        if tris > 5000 and not settings.enabled:
            settings.enabled = True
            mesh.set_editor_property('nanite_settings', settings)
            processed.add(path)
            count += 1
            print(f"Enabled Nanite: {mesh.get_name()} ({tris} tris)")

print(f"\nDone. Enabled Nanite on {count} unique meshes.")

Posle pokretanja: Vizuelno proveri staklo i lišće drveća — ako ima artefakata, isključi Nanite samo na tim meshovima.

2.3 Isključi Nanite na translucent meshovima

import unreal

meshes_to_fix = ["platforme_Staklo", "fasade_pt2"]  # dodaj svoja imena

for asset_path in unreal.EditorAssetLibrary.list_assets("/Game/", recursive=True):
    asset_name = asset_path.split("/")[-1].split(".")[0]
    if asset_name in meshes_to_fix:
        mesh = unreal.EditorAssetLibrary.load_asset(asset_path)
        if isinstance(mesh, unreal.StaticMesh):
            settings = mesh.get_editor_property('nanite_settings')
            if settings.enabled:
                settings.enabled = False
                mesh.set_editor_property('nanite_settings', settings)
                unreal.EditorAssetLibrary.save_loaded_asset(mesh)
                print(f"Nanite OFF + saved: {asset_path}")

2.4 Smanji Keep Triangle Percent na velikim Nanite meshovima

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

processed = set()
count = 0

for actor in actors:
    comp = actor.get_component_by_class(unreal.StaticMeshComponent)
    if comp and comp.static_mesh:
        mesh = comp.static_mesh
        path = mesh.get_path_name()
        if path in processed:
            continue
        tris = mesh.get_num_triangles(0)
        settings = mesh.get_editor_property('nanite_settings')
        if settings.enabled and tris > 50000:
            settings.keep_percent_triangles = 50.0
            settings.trim_relative_error = 0.03
            mesh.set_editor_property('nanite_settings', settings)
            processed.add(path)
            count += 1
            print(f"Reduced: {mesh.get_name()} ({tris} tris) -> 50% keep, 0.03 trim")

print(f"\nAdjusted {count} meshes")

Ako vidiš artefakte: Vrati na 100% samo taj mesh. Za agresivniju redukciju: 30% keep, 0.05 trim.

2.5 Prebaci sve lightove na Stationary

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

count = 0
for actor in actors:
    if isinstance(actor, (unreal.PointLight, unreal.SpotLight, unreal.RectLight)):
        actor.root_component.set_mobility(unreal.ComponentMobility.STATIONARY)
        count += 1

print(f"Set {count} lights to Stationary")

Zašto: Stationary lightovi su mnogo jeftiniji od Movable jer koriste baked shadowmape za statičnu geometriju. Posle pokretanja, uradi Build Lighting.

2.6 Analiza overlapping lightova

import unreal
import math

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

lights = []
for actor in actors:
    if isinstance(actor, (unreal.PointLight, unreal.SpotLight, unreal.RectLight)):
        loc = actor.get_actor_location()
        comp = actor.get_component_by_class(unreal.LocalLightComponent)
        if comp:
            radius = comp.attenuation_radius
            lights.append((actor.get_actor_label(), loc, radius))

print(f"Total local lights: {len(lights)}")
for i, (name, loc, rad) in enumerate(lights):
    overlaps = 0
    for j, (name2, loc2, rad2) in enumerate(lights):
        if i != j:
            dist = math.sqrt((loc.x - loc2.x) ** 2 + (loc.y - loc2.y) ** 2 + (loc.z - loc2.z) ** 2)
            if dist < rad + rad2:
                overlaps += 1
    if overlaps > 3:
        print(f"WARNING: {name} overlaps with {overlaps} other lights (radius: {rad:.0f})")

Šta radiš sa outputom: Lightovi sa >10 overlapa su problem. Smanji im attenuation radius (za interijer 300-500 je dovoljno umesto 1000+).

2.7 Bulk smanji light radius

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

count = 0
for actor in actors:
    if isinstance(actor, unreal.SpotLight):
        comp = actor.get_component_by_class(unreal.SpotLightComponent)
        if comp and comp.attenuation_radius > 500:
            comp.set_editor_property('attenuation_radius', 400.0)
            count += 1
    elif isinstance(actor, unreal.RectLight):
        comp = actor.get_component_by_class(unreal.RectLightComponent)
        if comp and comp.attenuation_radius > 2000:
            comp.set_editor_property('attenuation_radius', 2000.0)
            count += 1

print(f"Adjusted {count} lights")

Ako je pretamno: Povećaj intensity umesto radiusa — manji radius = manje overlapa = drastično jeftiniji rendering.

2.8 Isključi senke na nepotrebnim meshovima

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

no_shadow = [
    "klime", "HIDEN_GRP", "NATPIS", "SUPER_SANITARY_WC",
    "SUPER_SANITARY_SINKS", "SUPER_SANITARY_SHOWERS",
    "SUPER_SANITARY_ACCESSORIES", "SUPER_OPENINGS_PLUMBING",
    "SUPER_OPENINGS_HVAC", "SUPER_RAILINGS", "SUPER_DOORS",
    "solar_coll", "Background", "playground",
    "SUPER_CEILINGS_ARMSTRONG"
]

count = 0
for actor in actors:
    name = actor.get_actor_label()
    if name in no_shadow:
        comp = actor.get_component_by_class(unreal.StaticMeshComponent)
        if comp:
            comp.set_editor_property('cast_shadow', False)
            count += 1
            print(f"Shadows OFF: {name}")

print(f"\nDisabled shadows on {count} actors")

Prilagodi listu no_shadow svom projektu. Generalno: sanitarija, plumbing, HVAC, sitni detalji koji se ne vide iz daljine.

2.9 Isključi senke na svim tiny meshovima (<100 tria)

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

count = 0
for actor in actors:
    comp = actor.get_component_by_class(unreal.StaticMeshComponent)
    if comp and comp.static_mesh and comp.cast_shadow:
        tris = comp.static_mesh.get_num_triangles(0)
        if tris < 100:
            comp.set_editor_property('cast_shadow', False)
            count += 1

print(f"Disabled shadows on {count} tiny meshes")

2.10 Sakrij nevidljive/nepotrebne aktore

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

for actor in actors:
    name = actor.get_actor_label()
    if name in ("HIDEN_GRP", "klime"):  # prilagodi
        actor.set_is_temporarily_hidden_in_editor(True)
        actor.set_actor_hidden_in_game(True)
        print(f"Hidden: {name}")

2.11 Pronađi translucent materijale (overdraw krivci)

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

for actor in actors:
    comp = actor.get_component_by_class(unreal.StaticMeshComponent)
    if comp and comp.static_mesh:
        for i in range(comp.get_num_materials()):
            mat = comp.get_material(i)
            if mat:
                try:
                    blend = mat.get_editor_property('blend_mode')
                    if blend != unreal.BlendMode.BLEND_OPAQUE:
                        print(f"{actor.get_actor_label()} | slot {i} | {mat.get_name()} | BlendMode: {blend}")
                except:
                    pass

2.12 Tri budget po mesh tipu — top potrošači

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

mesh_stats = {}
for actor in actors:
    if actor.is_hidden_ed() or actor.is_temporarily_hidden_in_editor():
        continue
    comp = actor.get_component_by_class(unreal.StaticMeshComponent)
    if comp and comp.static_mesh:
        name = comp.static_mesh.get_name()
        tris = comp.static_mesh.get_num_triangles(0)
        if name not in mesh_stats:
            mesh_stats[name] = {"tris": tris, "count": 0}
        mesh_stats[name]["count"] += 1

sorted_stats = sorted(mesh_stats.items(), key=lambda x: x[1]["tris"] * x[1]["count"], reverse=True)
print(f"{'Mesh':<35} {'Tris':>10} {'Count':>6} {'Total':>12}")
print("-" * 65)
for name, s in sorted_stats[:25]:
    total = s['tris'] * s['count']
    print(f"{name:<35} {s['tris']:>10} {s['count']:>6} {total:>12}")

2.13 Proveri mobility svih meshova

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

count = 0
for actor in actors:
    comp = actor.get_component_by_class(unreal.StaticMeshComponent)
    if comp and comp.static_mesh:
        mob = comp.get_editor_property('mobility')
        if mob != unreal.ComponentMobility.STATIC:
            print(f"{actor.get_actor_label()} | {comp.static_mesh.get_name()} | Mobility: {mob}")
            count += 1

print(f"\n{count} non-static meshes found")

Pravilo: Sve što se ne pomera runtime treba da bude Static. Sky sphere, kamere, fog, day/night sistemi ostaju Movable.

2.14 Draw call estimator

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

total_draws = 0
for actor in actors:
    if actor.is_hidden_ed():
        continue
    comp = actor.get_component_by_class(unreal.StaticMeshComponent)
    if comp and comp.static_mesh:
        total_draws += comp.get_num_materials()

print(f"Estimated draw calls from static meshes: {total_draws}")

2.15 Skupi collision meshovi

import unreal

editor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = editor.get_all_level_actors()

for actor in actors:
    comp = actor.get_component_by_class(unreal.StaticMeshComponent)
    if comp and comp.static_mesh:
        try:
            col = comp.get_editor_property('collision_enabled')
            tris = comp.static_mesh.get_num_triangles(0)
            if col != unreal.CollisionEnabled.NO_COLLISION and tris > 50000:
                print(f"{actor.get_actor_label()} | {tris} tris | Collision: {col}")
        except:
            pass

3. Konzolne komande za rendering optimizaciju

Shadow optimizacija

r.Shadow.MaxResolution 512
r.Shadow.MaxCSMResolution 1024
r.Shadow.CSM.MaxCascades 1
r.Shadow.RadiusThreshold 0.03
r.Shadow.Virtual.SMRT.RayCountLocal 4
r.Shadow.Virtual.SMRT.SamplesPerRayLocal 4
r.Shadow.Virtual.SMRT.RayCountDirectional 4
r.Shadow.Virtual.SMRT.SamplesPerRayDirectional 4
r.Shadow.Virtual.AllowScreenOverflowMessages 0

Lumen GI i refleksije

r.Lumen.ScreenProbeGather.DownsampleFactor 8
r.Lumen.ScreenProbeGather.TracingOctahedronResolution 16
r.Lumen.ScreenProbeGather.FullResolutionJitterWidth 0.5
r.Lumen.ScreenProbeGather.ScreenSpacePass.HalfRes 1
r.Lumen.Reflections.ScreenSpacePass.HalfRes 1
r.Lumen.Reflections.MaxRoughnessToTrace 0.4
r.Lumen.Reflections.ScreenSpacePass 1
r.LumenScene.Radiosity.HalfRes 1

DLSS

r.NGX.DLSS.Enable 1
r.ScreenPercentage 58          — Balanced mode

DLSS modovi: Quality = 67, Balanced = 58, Performance = 50, Ultra Performance = 33

Post processing

r.SSR.Quality 2
r.BloomQuality 4
r.MotionBlurQuality 0
r.AmbientOcclusionLevels 1
r.Tonemapper.Quality 0

Foliage

foliage.MinimumScreenSize 0.001
foliage.LODDistanceScale 0.8

Ostalo

r.MaxAnisotropy 16             — besplatno na RTX kartama

Scalability grupe (3 = High)

sg.ShadowQuality 3
sg.FoliageQuality 3
sg.EffectsQuality 3
sg.PostProcessQuality 3

4. Scalability i INI konfiguracija

Gde stavljati šta u DefaultEngine.ini

[ConsoleVariables]
r.Shadow.MaxResolution=512
r.Shadow.MaxCSMResolution=1024
r.Shadow.CSM.MaxCascades=1
r.Shadow.RadiusThreshold=0.03
r.Shadow.Virtual.SMRT.RayCountDirectional=4
r.Shadow.Virtual.AllowScreenOverflowMessages=0
r.Lumen.ScreenProbeGather.DownsampleFactor=8
r.Lumen.ScreenProbeGather.TracingOctahedronResolution=16
r.Lumen.ScreenProbeGather.FullResolutionJitterWidth=0.5
r.Lumen.ScreenProbeGather.ScreenSpacePass.HalfRes=1
r.Lumen.Reflections.ScreenSpacePass.HalfRes=1
r.Lumen.Reflections.MaxRoughnessToTrace=0.4
r.Lumen.Reflections.ScreenSpacePass=1
r.LumenScene.Radiosity.HalfRes=1
r.ScreenPercentage=58
r.NGX.DLSS.Enable=1
r.MaxAnisotropy=16
r.SSR.Quality=2
r.BloomQuality=4
r.MotionBlurQuality=0
r.AmbientOcclusionLevels=1
r.Tonemapper.Quality=0
foliage.MinimumScreenSize=0.001
foliage.LODDistanceScale=0.8

DefaultGameUserSettings.ini — scalability defaults

[ScalabilityGroups]
sg.ShadowQuality=3
sg.FoliageQuality=3
sg.EffectsQuality=3
sg.PostProcessQuality=3

VAŽNO — Config priority

Device Profiles (prioritet 0x05) mogu da pregaze [ConsoleVariables] (prioritet 0x04). Ako vidiš da se tvoji cvari resetuju, koristi [SystemSettings] umesto [ConsoleVariables] — ima isti prioritet ali se čita posle.


5. Packaging fix za kopirane projekte

Kad kopiraš UE5 projekat na drugi komp:

  1. Obriši Intermediate/, Binaries/, DerivedDataCache/, Saved/StagedBuilds/, Saved/Cooked/
  2. Desni klik na .uproject → Switch Unreal Engine Version → izaberi svoj UE5
  3. Proveri DefaultGame.iniStagingDirectory ne sme da ima putanju sa starog kompa
  4. Proveri Saved/Config/WindowsEditor/Game.iniPackageBuildTarget mora da bude postavljeno
  5. Grep sve .ini fajlove za putanje sa starog kompa: grep -rn "C:/Users/stariuser" Config/ Saved/Config/
  6. Ako je BP-only projekat, BuildTarget u DefaultGame.ini treba da bude prazan ili obrisan
  7. Ako dodaješ C++ Source, moraš prvo da bilduješ i Editor i Game target iz komandne linije pre nego što otvoris editor

6. Mega Audit skripta

Sačuvaj kao .py fajl i pokreni sa File → Execute Python Script. Daje kompletan pregled scene: overview, warnings, light overlap analizu, tri budget, i optimization score.

Fajl: audit.py (već ti je generisan)


7. Saveti i best practices

Nanite

Lightovi

Senke

Mobility

Materijali

Build vs Editor

DLSS

Lumen noise

Workflow sa Claude-om

  1. Pokreni stat gpu i pošalji screenshot
  2. Claude identifikuje bottleneck
  3. Pokreni odgovarajuću analitičku skriptu i pošalji output
  4. Claude napravi fix skriptu
  5. Pokreni fix, ponovo stat gpu, ponovi

Za nove projekte, pokreni Mega Audit skriptu na početku da vidiš gde si i šta treba da se radi.


Rezultati naše optimizacije

Metrika Pre Posle Poboljšanje
GPU Total 11.00ms ~7.00ms ~36%
RenderDeferredLighting 2.74ms 0.57ms 79%
Lights 2.39ms 0.37ms 85%
Basepass 1.55ms 0.97ms 37%
Shadow Depths 1.39ms 1.22ms 12%
VSM Non-Nanite Warning DA NE Rešeno

Šta smo uradili: