Files
DrownedCityVR/DrownedCityVR/Content/Python/FixTerraceLighting.py
T
2026-05-26 19:53:02 +03:00

187 lines
6.0 KiB
Python

"""
FixTerraceLighting.py
=====================
For the Terrace level:
1. Positions the RuinedCrypt_01_P Level Instance relative to PlayerStart
2. Sets lighting to match RuinedCrypt's cool atmospheric tone
3. Disables shadow casting on excess movable lights (fixes overlap warning)
How to run:
File -> Execute Python Script -> select this file
Terrace.umap must be open (tab should show "Terrace")
"""
import unreal
USE_OPEN_LEVEL = True # Run against the currently open map
# How many units BELOW PlayerStart to place the RuinedCrypt ground
# (keeps terrain hidden beneath the tower platform)
RUIN_Z_OFFSET = -350.0
# RuinedCrypt scale — 0.7 = 70% of original size
RUIN_SCALE = 0.7
# Atmospheric light color: cool blue-green to match RuinedCrypt's soft tone
SUN_COLOR = unreal.LinearColor(0.65, 0.80, 0.90, 1.0) # cool / blue-white
SUN_LUX = 8000.0 # low intensity for dusk/evening mood
SKY_INTENSITY = 1.2
FOG_DENSITY = 0.04 # subtle atmospheric fog
FOG_FALLOFF = 0.25
# ------------------------------------------------------------------ helpers --
def _log(msg):
unreal.log(f"[FixTerrace] {msg}")
def _warn(msg):
unreal.log_warning(f"[FixTerrace] {msg}")
def _actors():
sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
return list(sub.get_all_level_actors())
def _of_class(cls):
return [a for a in _actors() if a.get_class() == cls]
# --------------------------------------------------------------- functions ---
def fix_ruinedcrypt_position():
"""Aligns the RuinedCrypt Level Instance to the PlayerStart location."""
all_actors = _actors()
# Find PlayerStart
ps = next((a for a in all_actors if a.get_class() == unreal.PlayerStart), None)
if ps is None:
_warn("PlayerStart not found — skipping RuinedCrypt positioning.")
return
ps_loc = ps.get_actor_location()
_log(f"PlayerStart location: X={ps_loc.x:.0f} Y={ps_loc.y:.0f} Z={ps_loc.z:.0f}")
# Find RuinedCrypt Level Instance by label or class name
ruin = None
for a in all_actors:
label = a.get_actor_label().lower()
cls = a.get_class().get_name().lower()
if "ruinedcrypt" in label or "ruined_crypt" in label or \
("levelinstance" in cls and "ruined" in label):
ruin = a
break
if ruin is None:
_warn("RuinedCrypt_01_P Level Instance not found. "
"Check the actor label in the Outliner.")
return
# New position: same X/Y as PlayerStart, Z offset downward
new_loc = unreal.Vector(ps_loc.x, ps_loc.y, ps_loc.z + RUIN_Z_OFFSET)
ruin.set_actor_location(new_loc, sweep=False, teleport=True)
ruin.set_actor_scale3d(unreal.Vector(RUIN_SCALE, RUIN_SCALE, RUIN_SCALE))
ruin.set_actor_rotation(unreal.Rotator(0.0, 0.0, 0.0), teleport_physics=True)
_log(f"RuinedCrypt positioned at Z={new_loc.z:.0f} Scale={RUIN_SCALE}")
def fix_directional_light():
"""Sets the DirectionalLight to a cool atmospheric color."""
lights = _of_class(unreal.DirectionalLight)
if not lights:
_warn("DirectionalLight not found.")
return
# Keep only the first one, remove duplicates
keep = lights[0]
for extra in lights[1:]:
unreal.get_editor_subsystem(unreal.EditorActorSubsystem).destroy_actor(extra)
_log(f"Removed {len(lights)-1} duplicate DirectionalLight(s).")
comp = keep.get_component_by_class(unreal.DirectionalLightComponent)
if comp:
comp.set_intensity(SUN_LUX)
comp.set_light_color(SUN_COLOR)
comp.set_atmosphere_sun_light(True)
comp.set_cast_shadows(True)
comp.set_mobility(unreal.ComponentMobility.STATIONARY)
_log("DirectionalLight updated.")
def fix_sky_light():
"""Sets SkyLight intensity and removes duplicates."""
skies = _of_class(unreal.SkyLight)
if not skies:
return
keep = skies[0]
for extra in skies[1:]:
unreal.get_editor_subsystem(unreal.EditorActorSubsystem).destroy_actor(extra)
comp = keep.get_component_by_class(unreal.SkyLightComponent)
if comp:
comp.set_intensity(SKY_INTENSITY)
comp.set_mobility(unreal.ComponentMobility.STATIONARY)
_log("SkyLight updated.")
def fix_fog():
"""Configures subtle atmospheric height fog."""
fogs = _of_class(unreal.ExponentialHeightFog)
if not fogs:
_warn("ExponentialHeightFog not found — skipping fog setup.")
return
comp = fogs[0].get_component_by_class(unreal.ExponentialHeightFogComponent)
if comp:
comp.set_fog_density(FOG_DENSITY)
comp.set_fog_height_falloff(FOG_FALLOFF)
comp.set_volumetric_fog(False)
_log("Fog updated.")
def disable_excess_movable_shadows():
"""Disables shadow casting on movable PointLights and SpotLights
to fix the TOO MANY OVERLAPPING SHADOWED MOVABLE LIGHTS warning."""
all_actors = _actors()
fixed = 0
for a in all_actors:
cls = a.get_class()
if cls not in (unreal.PointLight, unreal.SpotLight):
continue
for comp_cls in (unreal.PointLightComponent, unreal.SpotLightComponent):
comp = a.get_component_by_class(comp_cls)
if comp and comp.get_mobility() == unreal.ComponentMobility.MOVABLE:
comp.set_cast_shadows(False)
fixed += 1
if fixed:
_log(f"Disabled shadow casting on {fixed} movable PointLight/SpotLight(s).")
def save():
world = unreal.get_editor_subsystem(
unreal.UnrealEditorSubsystem).get_editor_world()
if world:
try:
unreal.EditorLoadingAndSavingUtils.save_dirty_packages_with_confirmation(False)
_log("Saved.")
except Exception as e:
_warn(f"Save error: {e}")
# ------------------------------------------------------------------- main --
def run():
_log("Starting...")
fix_ruinedcrypt_position()
fix_directional_light()
fix_sky_light()
fix_fog()
disable_excess_movable_shadows()
save()
_log("Done. Move the camera in the Viewport to see the changes.")
if __name__ == "__main__":
run()