Added: Initial world generation tool
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
"""Dumps the actors of one or more levels to JSON, so a reference level (a pack's demo map) can be read without
|
||||
opening it in the editor: class, label, transform, and the properties that matter for lighting, sky, fog,
|
||||
post-processing and landscape. Written for reading Elite_RockyMeadows' demo maps while building L_World.
|
||||
|
||||
UnrealEditor-Cmd.exe Salty.uproject -run=pythonscript -script="Scripts/Authoring/dump_level.py /Game/Elite_RockyMeadows/Maps/Rocky_Meadows_01"
|
||||
|
||||
Levels come from the script arguments, else from the SALTY_DUMP_LEVELS environment variable (semicolon
|
||||
separated), else the pack's first demo map. Output: Saved/Authoring/<LevelName>.json, one per level.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import unreal
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT_DIR = os.path.normpath(os.path.join(HERE, "..", "..", "Saved", "Authoring"))
|
||||
DEFAULT_LEVELS = ["/Game/Elite_RockyMeadows/Maps/Rocky_Meadows_01"]
|
||||
|
||||
# Component class name -> editor properties worth reading. Missing ones are skipped, so the list can be generous.
|
||||
COMPONENT_PROPERTIES = {
|
||||
"DirectionalLightComponent": [
|
||||
"intensity", "light_color", "use_temperature", "temperature", "light_function_material", "light_function_scale",
|
||||
"light_function_fade_distance", "atmosphere_sun_light", "cast_shadows", "dynamic_shadow_distance_movable_light",
|
||||
"dynamic_shadow_distance_stationary_light", "num_dynamic_shadow_cascades", "cascade_distribution_exponent",
|
||||
"cascade_transition_fraction", "shadow_distance_fadeout_fraction", "light_source_angle", "shadow_bias",
|
||||
"volumetric_scattering_intensity", "cast_cloud_shadows", "cast_volumetric_shadow", "indirect_lighting_intensity",
|
||||
"mobility", "specular_scale",
|
||||
],
|
||||
"SkyLightComponent": [
|
||||
"intensity", "light_color", "source_type", "cubemap", "cubemap_resolution", "source_cubemap_angle", "real_time_capture",
|
||||
"lower_hemisphere_color", "sky_distance_threshold", "cast_shadows", "volumetric_scattering_intensity",
|
||||
"indirect_lighting_intensity", "occlusion_max_distance", "mobility",
|
||||
],
|
||||
"ExponentialHeightFogComponent": [
|
||||
"fog_density", "fog_height_falloff", "second_fog_data", "fog_inscattering_luminance", "skybox_inscattering_color_cubemap",
|
||||
"fog_inscattering_luminance_scale", "directional_inscattering_luminance", "directional_inscattering_exponent",
|
||||
"directional_inscattering_start_distance", "fog_max_opacity", "start_distance", "end_distance", "fog_cutoff_distance",
|
||||
"volumetric_fog", "volumetric_fog_scattering_distribution", "volumetric_fog_albedo", "volumetric_fog_emissive",
|
||||
"volumetric_fog_extinction_scale", "volumetric_fog_distance", "volumetric_fog_start_distance",
|
||||
"volumetric_fog_near_fade_in_distance", "volumetric_fog_static_lighting_scattering_intensity",
|
||||
],
|
||||
"StaticMeshComponent": ["static_mesh", "cast_shadow", "visible"],
|
||||
"SkyAtmosphereComponent": ["rayleigh_scattering_scale", "mie_scattering_scale", "aerial_pespective_view_distance_scale"],
|
||||
"VolumetricCloudComponent": ["layer_bottom_altitude", "layer_height", "material"],
|
||||
}
|
||||
ACTOR_PROPERTIES = {
|
||||
"Landscape": ["landscape_material", "landscape_hole_material", "component_size_quads", "subsection_size_quads", "num_subsections",
|
||||
"static_lighting_lod", "lod_distribution_setting", "lod0_distribution_setting", "lod0_screen_size",
|
||||
"streaming_distance_multiplier", "collision_mip_level", "nanite_lod_index", "enable_nanite", "target_display_order"],
|
||||
"LandscapeStreamingProxy": ["landscape_material", "component_size_quads", "subsection_size_quads", "num_subsections"],
|
||||
"PostProcessVolume": ["unbound", "enabled", "priority", "blend_weight"],
|
||||
"Actor": ["hidden"],
|
||||
}
|
||||
|
||||
|
||||
def encode(value):
|
||||
"""A JSON-able rendering of a Python-wrapped Unreal value."""
|
||||
if value is None or isinstance(value, (bool, int, float, str)):
|
||||
return value
|
||||
if isinstance(value, unreal.Object):
|
||||
return value.get_path_name()
|
||||
if isinstance(value, (unreal.Name, unreal.Text)):
|
||||
return str(value)
|
||||
if isinstance(value, (unreal.Vector, unreal.Rotator, unreal.LinearColor, unreal.Color, unreal.Vector2D, unreal.IntPoint)):
|
||||
return list(value.to_tuple())
|
||||
if isinstance(value, unreal.Array):
|
||||
return [encode(v) for v in value]
|
||||
return str(value)
|
||||
|
||||
|
||||
def read_properties(obj, names):
|
||||
out = {}
|
||||
for name in names:
|
||||
try:
|
||||
out[name] = encode(obj.get_editor_property(name))
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def overridden_post_process(settings):
|
||||
"""The FPostProcessSettings fields whose override flag is set, with their values."""
|
||||
out = {}
|
||||
for name in dir(settings):
|
||||
if not name.startswith("override_"):
|
||||
continue
|
||||
try:
|
||||
if not settings.get_editor_property(name):
|
||||
continue
|
||||
field = name[len("override_"):]
|
||||
out[field] = encode(settings.get_editor_property(field))
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def dump_actor(actor):
|
||||
class_name = actor.get_class().get_name()
|
||||
record = {
|
||||
"class": class_name,
|
||||
"label": actor.get_actor_label(),
|
||||
"location": list(actor.get_actor_location().to_tuple()),
|
||||
"rotation": list(actor.get_actor_rotation().to_tuple()), # the tuple order is (roll, pitch, yaw)
|
||||
"scale": list(actor.get_actor_scale3d().to_tuple()),
|
||||
}
|
||||
for key, names in ACTOR_PROPERTIES.items():
|
||||
if key == "Actor" or class_name.startswith(key):
|
||||
record.update(read_properties(actor, names))
|
||||
if class_name.startswith("PostProcessVolume"):
|
||||
try:
|
||||
record["settings"] = overridden_post_process(actor.get_editor_property("settings"))
|
||||
except Exception as error:
|
||||
record["settings_error"] = str(error)
|
||||
components = {}
|
||||
for component in actor.get_components_by_class(unreal.ActorComponent):
|
||||
component_class = component.get_class().get_name()
|
||||
for key, names in COMPONENT_PROPERTIES.items():
|
||||
if component_class == key or component_class.startswith(key):
|
||||
entry = read_properties(component, names)
|
||||
if isinstance(component, unreal.SceneComponent):
|
||||
entry["relative_location"] = list(component.get_editor_property("relative_location").to_tuple())
|
||||
entry["relative_rotation"] = list(component.get_editor_property("relative_rotation").to_tuple())
|
||||
entry["relative_scale3d"] = list(component.get_editor_property("relative_scale3d").to_tuple())
|
||||
if isinstance(component, unreal.MeshComponent):
|
||||
entry["materials"] = [encode(m) for m in component.get_materials()]
|
||||
components[f"{component_class}:{component.get_name()}"] = entry
|
||||
if class_name.startswith("Landscape"):
|
||||
try:
|
||||
components["landscape_component_count"] = len(actor.get_editor_property("landscape_components"))
|
||||
except Exception:
|
||||
pass
|
||||
record["components"] = components
|
||||
return record
|
||||
|
||||
|
||||
def dump_level(level_path):
|
||||
unreal.log(f"dump_level: loading {level_path}")
|
||||
level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
||||
if not level_subsystem.load_level(level_path):
|
||||
unreal.log_error(f"dump_level: could not load {level_path}")
|
||||
return
|
||||
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
||||
actors = [dump_actor(actor) for actor in actor_subsystem.get_all_level_actors()]
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
out_path = os.path.join(OUT_DIR, level_path.rsplit("/", 1)[-1] + ".json")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump({"level": level_path, "actors": actors}, f, indent=2)
|
||||
unreal.log(f"dump_level: {len(actors)} actors of {level_path} written to {out_path}")
|
||||
|
||||
|
||||
def main():
|
||||
levels = [a for a in sys.argv[1:] if a.startswith("/")]
|
||||
if not levels and os.environ.get("SALTY_DUMP_LEVELS"):
|
||||
levels = [p for p in os.environ["SALTY_DUMP_LEVELS"].split(";") if p]
|
||||
for level in levels or DEFAULT_LEVELS:
|
||||
dump_level(level)
|
||||
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user