Tune Ground Zero foliage sightlines
This commit is contained in:
@@ -629,6 +629,37 @@ FOLIAGE_ZONES = {
|
||||
},
|
||||
}
|
||||
|
||||
SIGHTLINE_TUNING_CONFIG = {
|
||||
"critical_clearance_cm": {
|
||||
"trees": 5200.0,
|
||||
"shrubs": 3400.0,
|
||||
"grass": 1250.0,
|
||||
},
|
||||
"corridor_clearance_cm": {
|
||||
"trees": 3600.0,
|
||||
"shrubs": 2100.0,
|
||||
"grass": 850.0,
|
||||
},
|
||||
"corridor_sample_spacing_cm": 1600.0,
|
||||
"critical_actor_labels": {
|
||||
"AGR_DemoPlayerStart",
|
||||
"AGR_DemoWoodResource_01",
|
||||
"AGR_DemoFiberResource_01",
|
||||
"AGR_DemoCampfire_01",
|
||||
"AGR_DemoPrimitiveShelter_01",
|
||||
"AGR_DemoRabbitWildlife_01",
|
||||
"AGR_GZ_FreshWaterSource_01",
|
||||
},
|
||||
"sightline_pairs": [
|
||||
("AGR_DemoPlayerStart", "AGR_DemoWoodResource_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_DemoFiberResource_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_DemoCampfire_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_DemoPrimitiveShelter_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_DemoRabbitWildlife_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_GZ_FreshWaterSource_01"),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def get_actor_label(actor):
|
||||
try:
|
||||
@@ -796,6 +827,42 @@ def distance_2d(a, b):
|
||||
return math.hypot(a.x - b.x, a.y - b.y)
|
||||
|
||||
|
||||
def reserved_point(point, source_label, clearances):
|
||||
return {
|
||||
"point": point,
|
||||
"source_label": source_label,
|
||||
"clearances": clearances,
|
||||
}
|
||||
|
||||
|
||||
def reserved_point_clearance(reservation, foliage_family):
|
||||
return reservation["clearances"].get(foliage_family, 0.0)
|
||||
|
||||
|
||||
def add_sightline_corridor_reservations(reservations, label_points):
|
||||
spacing_cm = SIGHTLINE_TUNING_CONFIG["corridor_sample_spacing_cm"]
|
||||
corridor_clearances = SIGHTLINE_TUNING_CONFIG["corridor_clearance_cm"]
|
||||
|
||||
for start_label, end_label in SIGHTLINE_TUNING_CONFIG["sightline_pairs"]:
|
||||
start = label_points.get(start_label)
|
||||
end = label_points.get(end_label)
|
||||
if not start or not end:
|
||||
continue
|
||||
|
||||
length_cm = distance_2d(start, end)
|
||||
sample_count = max(1, int(length_cm / spacing_cm))
|
||||
for index in range(1, sample_count):
|
||||
alpha = index / sample_count
|
||||
point = unreal.Vector(
|
||||
start.x + ((end.x - start.x) * alpha),
|
||||
start.y + ((end.y - start.y) * alpha),
|
||||
0.0,
|
||||
)
|
||||
reservations.append(
|
||||
reserved_point(point, f"{start_label}->{end_label}", corridor_clearances)
|
||||
)
|
||||
|
||||
|
||||
def validate_safe_spawn_location(height_values, location_xy, resource_points, water_points):
|
||||
failures = []
|
||||
elevation_m = terrain_elevation_m(height_values, location_xy.x, location_xy.y)
|
||||
@@ -882,7 +949,7 @@ def apply_foliage_materials(foliage_actor, materials):
|
||||
component.set_material(0, material)
|
||||
|
||||
|
||||
def choose_foliage_points(height_values, zone, reserved_points, existing_points):
|
||||
def choose_foliage_points(height_values, family_name, zone, reservations, existing_points):
|
||||
rng = random.Random(FOLIAGE_RANDOM_SEED + len(existing_points) + int(zone["count"]))
|
||||
chosen = []
|
||||
attempts = 0
|
||||
@@ -897,7 +964,10 @@ def choose_foliage_points(height_values, zone, reserved_points, existing_points)
|
||||
if terrain_elevation_m(height_values, x, y) < zone["min_elevation_m"]:
|
||||
continue
|
||||
|
||||
if any(distance_2d(point, reserved) < zone["avoid_radius_cm"] for reserved in reserved_points):
|
||||
if any(
|
||||
distance_2d(point, reservation["point"]) < reserved_point_clearance(reservation, family_name)
|
||||
for reservation in reservations
|
||||
):
|
||||
continue
|
||||
|
||||
if any(distance_2d(point, existing) < zone["avoid_radius_cm"] * 0.55 for existing in existing_points):
|
||||
@@ -912,12 +982,33 @@ def choose_foliage_points(height_values, zone, reserved_points, existing_points)
|
||||
return chosen
|
||||
|
||||
|
||||
def spawn_foliage_actor(height_values, materials):
|
||||
reserved_points = [
|
||||
spec["location_xy"]
|
||||
for spec in DEMO_ACTORS
|
||||
if spec["label"] not in {"AGR_DemoSkyLightingController", "AGR_DemoWeatherAudioController", "AGR_DemoNoticeActor"}
|
||||
]
|
||||
def build_foliage_reservations(safe_spawn_location_xy):
|
||||
label_points = {}
|
||||
specs = DEMO_ACTORS + BIOME_RESOURCE_ACTORS + WATER_SOURCE_ACTORS
|
||||
for spec in specs:
|
||||
label = spec["label"]
|
||||
location_xy = spec["location_xy"]
|
||||
if label == SAFE_SPAWN_CONFIG["player_start_label"] and safe_spawn_location_xy is not None:
|
||||
location_xy = safe_spawn_location_xy
|
||||
label_points[label] = location_xy
|
||||
|
||||
reservations = []
|
||||
critical_clearances = SIGHTLINE_TUNING_CONFIG["critical_clearance_cm"]
|
||||
for label in SIGHTLINE_TUNING_CONFIG["critical_actor_labels"]:
|
||||
point = label_points.get(label)
|
||||
if point:
|
||||
reservations.append(reserved_point(point, label, critical_clearances))
|
||||
|
||||
for spec in BIOME_RESOURCE_ACTORS:
|
||||
reservations.append(reserved_point(spec["location_xy"], spec["label"], critical_clearances))
|
||||
|
||||
add_sightline_corridor_reservations(reservations, label_points)
|
||||
unreal.log(f"Prepared {len(reservations)} Ground Zero foliage sightline reservation point(s).")
|
||||
return reservations
|
||||
|
||||
|
||||
def spawn_foliage_actor(height_values, materials, safe_spawn_location_xy):
|
||||
reservations = build_foliage_reservations(safe_spawn_location_xy)
|
||||
|
||||
foliage_actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
|
||||
unreal.AgrarianFoliagePatch,
|
||||
@@ -936,19 +1027,19 @@ def spawn_foliage_actor(height_values, materials):
|
||||
existing_points = []
|
||||
rng = random.Random(FOLIAGE_RANDOM_SEED)
|
||||
|
||||
for point in choose_foliage_points(height_values, FOLIAGE_ZONES["trees"], reserved_points, existing_points):
|
||||
for point in choose_foliage_points(height_values, "trees", FOLIAGE_ZONES["trees"], reservations, existing_points):
|
||||
scale = rng.uniform(*FOLIAGE_ZONES["trees"]["scale_range"])
|
||||
foliage_actor.add_tree_instance(
|
||||
make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 5.0, scale, scale * 5.5)
|
||||
)
|
||||
|
||||
for point in choose_foliage_points(height_values, FOLIAGE_ZONES["shrubs"], reserved_points, existing_points):
|
||||
for point in choose_foliage_points(height_values, "shrubs", FOLIAGE_ZONES["shrubs"], reservations, existing_points):
|
||||
scale = rng.uniform(*FOLIAGE_ZONES["shrubs"]["scale_range"])
|
||||
foliage_actor.add_shrub_instance(
|
||||
make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 4.0, scale * 2.2, scale * 0.85)
|
||||
)
|
||||
|
||||
for point in choose_foliage_points(height_values, FOLIAGE_ZONES["grass"], reserved_points, existing_points):
|
||||
for point in choose_foliage_points(height_values, "grass", FOLIAGE_ZONES["grass"], reservations, existing_points):
|
||||
scale = rng.uniform(*FOLIAGE_ZONES["grass"]["scale_range"])
|
||||
foliage_actor.add_grass_instance(
|
||||
make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 2.0, scale * 0.28, scale * 1.6)
|
||||
@@ -1091,7 +1182,7 @@ def main():
|
||||
apply_landscape_material(materials["terrain"])
|
||||
height_values = load_heightmap()
|
||||
safe_spawn_location_xy = select_safe_spawn_location(height_values)
|
||||
spawn_foliage_actor(height_values, materials)
|
||||
spawn_foliage_actor(height_values, materials, safe_spawn_location_xy)
|
||||
for spec in BIOME_RESOURCE_ACTORS:
|
||||
spawn_demo_actor(spec, height_values, materials)
|
||||
for spec in WATER_SOURCE_ACTORS:
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
|
||||
|
||||
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
||||
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
|
||||
EXPECTED_FOLIAGE_COUNTS = {
|
||||
"trees": 64,
|
||||
"shrubs": 148,
|
||||
"grass": 260,
|
||||
}
|
||||
CRITICAL_CLEARANCE_CM = {
|
||||
"trees": 5200.0,
|
||||
"shrubs": 3400.0,
|
||||
"grass": 1250.0,
|
||||
}
|
||||
CORRIDOR_CLEARANCE_CM = {
|
||||
"trees": 3600.0,
|
||||
"shrubs": 2100.0,
|
||||
"grass": 850.0,
|
||||
}
|
||||
CORRIDOR_SAMPLE_SPACING_CM = 1600.0
|
||||
CRITICAL_ACTOR_LABELS = {
|
||||
"AGR_DemoPlayerStart",
|
||||
"AGR_DemoWoodResource_01",
|
||||
"AGR_DemoFiberResource_01",
|
||||
"AGR_DemoCampfire_01",
|
||||
"AGR_DemoPrimitiveShelter_01",
|
||||
"AGR_DemoRabbitWildlife_01",
|
||||
"AGR_GZ_FreshWaterSource_01",
|
||||
"AGR_GZ_Wood_CoastalScrub_01",
|
||||
"AGR_GZ_Wood_CoastalScrub_02",
|
||||
"AGR_GZ_Wood_Hillside_03",
|
||||
"AGR_GZ_Fiber_Grassland_01",
|
||||
"AGR_GZ_Fiber_Grassland_02",
|
||||
"AGR_GZ_Fiber_Scrub_03",
|
||||
"AGR_GZ_Fiber_DrainageCandidate_04",
|
||||
"AGR_GZ_EdiblePlant_CoastalScrub_01",
|
||||
"AGR_GZ_EdiblePlant_Grassland_02",
|
||||
"AGR_GZ_EdiblePlant_DrainageCandidate_03",
|
||||
"AGR_GZ_Stone_Slope_01",
|
||||
"AGR_GZ_Stone_Slope_02",
|
||||
"AGR_GZ_Stone_ExposedTerrain_03",
|
||||
"AGR_GZ_Stone_ValleyEdge_04",
|
||||
}
|
||||
SIGHTLINE_PAIRS = [
|
||||
("AGR_DemoPlayerStart", "AGR_DemoWoodResource_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_DemoFiberResource_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_DemoCampfire_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_DemoPrimitiveShelter_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_DemoRabbitWildlife_01"),
|
||||
("AGR_DemoPlayerStart", "AGR_GZ_FreshWaterSource_01"),
|
||||
]
|
||||
|
||||
|
||||
def get_actor_label(actor):
|
||||
try:
|
||||
return actor.get_actor_label()
|
||||
except Exception:
|
||||
return actor.get_name()
|
||||
|
||||
|
||||
def distance_2d(a, b):
|
||||
return math.hypot(a.x - b.x, a.y - b.y)
|
||||
|
||||
|
||||
def transform_location(transform_result):
|
||||
transform = transform_result[0] if isinstance(transform_result, tuple) else transform_result
|
||||
if hasattr(transform, "translation"):
|
||||
return transform.translation
|
||||
try:
|
||||
return transform.get_editor_property("translation")
|
||||
except Exception:
|
||||
return transform.get_location()
|
||||
|
||||
|
||||
def instance_locations(component):
|
||||
locations = []
|
||||
for index in range(component.get_instance_count()):
|
||||
locations.append(transform_location(component.get_instance_transform(index, True)))
|
||||
return locations
|
||||
|
||||
|
||||
def sampled_corridor_points(start, end):
|
||||
length_cm = distance_2d(start, end)
|
||||
sample_count = max(1, int(length_cm / CORRIDOR_SAMPLE_SPACING_CM))
|
||||
points = []
|
||||
for index in range(1, sample_count):
|
||||
alpha = index / sample_count
|
||||
points.append(
|
||||
unreal.Vector(
|
||||
start.x + ((end.x - start.x) * alpha),
|
||||
start.y + ((end.y - start.y) * alpha),
|
||||
0.0,
|
||||
)
|
||||
)
|
||||
return points
|
||||
|
||||
|
||||
def main():
|
||||
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
||||
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||
|
||||
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
actors_by_label = {get_actor_label(actor): actor for actor in actors}
|
||||
failures = []
|
||||
|
||||
missing_labels = sorted(label for label in CRITICAL_ACTOR_LABELS if label not in actors_by_label)
|
||||
if missing_labels:
|
||||
failures.append("missing critical actor label(s): " + ", ".join(missing_labels))
|
||||
|
||||
foliage = actors_by_label.get(FOLIAGE_LABEL)
|
||||
if not foliage:
|
||||
failures.append(f"missing {FOLIAGE_LABEL}")
|
||||
else:
|
||||
foliage_components = {
|
||||
"trees": foliage.get_editor_property("tree_instances"),
|
||||
"shrubs": foliage.get_editor_property("shrub_instances"),
|
||||
"grass": foliage.get_editor_property("grass_instances"),
|
||||
}
|
||||
foliage_locations = {
|
||||
family: instance_locations(component)
|
||||
for family, component in foliage_components.items()
|
||||
}
|
||||
|
||||
for family, expected_count in EXPECTED_FOLIAGE_COUNTS.items():
|
||||
actual_count = len(foliage_locations[family])
|
||||
if actual_count != expected_count:
|
||||
failures.append(f"{family} expected {expected_count}, got {actual_count}")
|
||||
|
||||
critical_points = {
|
||||
label: actors_by_label[label].get_actor_location()
|
||||
for label in CRITICAL_ACTOR_LABELS
|
||||
if label in actors_by_label
|
||||
}
|
||||
for family, locations in foliage_locations.items():
|
||||
clearance = CRITICAL_CLEARANCE_CM[family]
|
||||
for label, point in critical_points.items():
|
||||
nearest = min((distance_2d(location, point) for location in locations), default=999999.0)
|
||||
if nearest < clearance:
|
||||
failures.append(
|
||||
f"{family} nearest to {label} is {nearest:.0f}cm, below {clearance:.0f}cm clearance"
|
||||
)
|
||||
|
||||
for start_label, end_label in SIGHTLINE_PAIRS:
|
||||
start_actor = actors_by_label.get(start_label)
|
||||
end_actor = actors_by_label.get(end_label)
|
||||
if not start_actor or not end_actor:
|
||||
continue
|
||||
|
||||
corridor_points = sampled_corridor_points(
|
||||
start_actor.get_actor_location(),
|
||||
end_actor.get_actor_location(),
|
||||
)
|
||||
for family, locations in foliage_locations.items():
|
||||
clearance = CORRIDOR_CLEARANCE_CM[family]
|
||||
for point in corridor_points:
|
||||
nearest = min((distance_2d(location, point) for location in locations), default=999999.0)
|
||||
if nearest < clearance:
|
||||
failures.append(
|
||||
f"{family} nearest to {start_label}->{end_label} corridor is "
|
||||
f"{nearest:.0f}cm, below {clearance:.0f}cm clearance"
|
||||
)
|
||||
break
|
||||
|
||||
project_root = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()))
|
||||
docs = project_root / "Docs" / "Terrain" / "GroundZeroDensitySightlineTuning.md"
|
||||
roadmap = project_root / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
||||
for path, snippets in {
|
||||
docs: ["Ground Zero Density And Sightline Tuning", "First-Look Sightlines"],
|
||||
roadmap: ["[x] Add density and sightline tuning"],
|
||||
}.items():
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for snippet in snippets:
|
||||
if snippet not in text:
|
||||
failures.append(f"{path} missing snippet: {snippet}")
|
||||
|
||||
if failures:
|
||||
raise RuntimeError("Ground Zero density/sightline verification failed: " + "; ".join(failures))
|
||||
|
||||
unreal.log(
|
||||
"Ground Zero density/sightline verification complete: "
|
||||
f"{len(CRITICAL_ACTOR_LABELS)} critical labels, "
|
||||
f"{len(SIGHTLINE_PAIRS)} sightline corridor(s), "
|
||||
f"{EXPECTED_FOLIAGE_COUNTS['trees']} trees, "
|
||||
f"{EXPECTED_FOLIAGE_COUNTS['shrubs']} shrubs, "
|
||||
f"{EXPECTED_FOLIAGE_COUNTS['grass']} grass clumps."
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user