Upgrade Ground Zero vegetation assets

This commit is contained in:
2026-05-21 15:43:14 +00:00
parent 98ab61a7a4
commit dd3d247539
20 changed files with 341 additions and 41 deletions
+205 -9
View File
@@ -18,6 +18,8 @@ LANDSCAPE_MIN_XY = -50000.0
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
FOLIAGE_RANDOM_SEED = 4160544
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes"
VEGETATION_MESH_FOLDER = "/Game/Agrarian/Environment/Vegetation"
VEGETATION_SOURCE_FOLDER = PROJECT_ROOT / "Saved" / "CodexGenerated" / "Vegetation"
PLACEHOLDER_MESH_SOURCES = {
"SM_AGR_Placeholder_Cube": "/Game/LevelPrototyping/Meshes/SM_Cube",
"SM_AGR_Placeholder_ChamferCube": "/Game/LevelPrototyping/Meshes/SM_ChamferCube",
@@ -30,9 +32,9 @@ PLACEHOLDER_MESHES = {
for name in PLACEHOLDER_MESH_SOURCES
}
FOLIAGE_MESHES = {
"tree": "/Engine/BasicShapes/Cone",
"shrub": "/Engine/BasicShapes/Sphere",
"grass": "/Engine/BasicShapes/Plane",
"tree": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoastalOak_Proxy",
"shrub": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoyoteBrush_Proxy",
"grass": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_DryGrassClump_Proxy",
}
VARIATION_MESHES = {
"cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"],
@@ -40,8 +42,9 @@ VARIATION_MESHES = {
"cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
"quarter_cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_QuarterCylinder"],
"plane": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Plane"],
"sphere": "/Engine/BasicShapes/Sphere",
"cone": "/Engine/BasicShapes/Cone",
"coastal_oak": FOLIAGE_MESHES["tree"],
"coyote_brush": FOLIAGE_MESHES["shrub"],
"dry_grass_clump": FOLIAGE_MESHES["grass"],
}
MATERIAL_FOLDER = "/Game/Agrarian/Materials"
ENVIRONMENT_MATERIALS = {
@@ -57,20 +60,25 @@ ENVIRONMENT_MATERIALS = {
"tree": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak",
"color": unreal.LinearColor(0.07, 0.18, 0.06, 1.0),
"variation_color": unreal.LinearColor(0.14, 0.24, 0.09, 1.0),
"roughness": 0.88,
"used_with_instanced_static_meshes": True,
},
"shrub": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush",
"color": unreal.LinearColor(0.15, 0.28, 0.10, 1.0),
"variation_color": unreal.LinearColor(0.24, 0.34, 0.15, 1.0),
"roughness": 0.9,
"used_with_instanced_static_meshes": True,
"two_sided": True,
},
"grass": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal",
"color": unreal.LinearColor(0.32, 0.34, 0.13, 1.0),
"variation_color": unreal.LinearColor(0.52, 0.45, 0.22, 1.0),
"roughness": 0.95,
"used_with_instanced_static_meshes": True,
"two_sided": True,
},
"wood_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource",
@@ -355,7 +363,7 @@ WEATHER_EXPOSURE_ZONES = [
ENVIRONMENT_VARIATION_ACTORS = [
{
"label": "AGR_GZ_EnvVar_Tree_Canopy_01",
"mesh_key": "sphere",
"mesh_key": "coastal_oak",
"material_key": "tree",
"location_xy": unreal.Vector(-27500.0, 6900.0, 0.0),
"z_offset": 390.0,
@@ -373,7 +381,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
},
{
"label": "AGR_GZ_EnvVar_Tree_Canopy_02",
"mesh_key": "sphere",
"mesh_key": "coastal_oak",
"material_key": "tree",
"location_xy": unreal.Vector(17600.0, 31800.0, 0.0),
"z_offset": 430.0,
@@ -391,7 +399,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
},
{
"label": "AGR_GZ_EnvVar_Bush_Rounded_01",
"mesh_key": "sphere",
"mesh_key": "coyote_brush",
"material_key": "shrub",
"location_xy": unreal.Vector(-33400.0, -15200.0, 0.0),
"z_offset": 70.0,
@@ -400,7 +408,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
},
{
"label": "AGR_GZ_EnvVar_Bush_Rounded_02",
"mesh_key": "sphere",
"mesh_key": "coyote_brush",
"material_key": "shrub",
"location_xy": unreal.Vector(30400.0, -3900.0, 0.0),
"z_offset": 75.0,
@@ -783,6 +791,154 @@ def ensure_native_placeholder_meshes():
unreal.log(f"Created native placeholder mesh: {destination_path}")
def write_obj_mesh(path, vertices, faces):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
handle.write("# Agrarian generated coastal scrub vegetation proxy\n")
for vertex in vertices:
handle.write(f"v {vertex[0]:.3f} {vertex[1]:.3f} {vertex[2]:.3f}\n")
for face in faces:
handle.write("f " + " ".join(str(index + 1) for index in face) + "\n")
def add_box(vertices, faces, center, size):
cx, cy, cz = center
sx, sy, sz = size[0] * 0.5, size[1] * 0.5, size[2] * 0.5
base = len(vertices)
vertices.extend(
[
(cx - sx, cy - sy, cz - sz),
(cx + sx, cy - sy, cz - sz),
(cx + sx, cy + sy, cz - sz),
(cx - sx, cy + sy, cz - sz),
(cx - sx, cy - sy, cz + sz),
(cx + sx, cy - sy, cz + sz),
(cx + sx, cy + sy, cz + sz),
(cx - sx, cy + sy, cz + sz),
]
)
faces.extend(
[
(base + 0, base + 1, base + 2, base + 3),
(base + 4, base + 7, base + 6, base + 5),
(base + 0, base + 4, base + 5, base + 1),
(base + 1, base + 5, base + 6, base + 2),
(base + 2, base + 6, base + 7, base + 3),
(base + 3, base + 7, base + 4, base + 0),
]
)
def add_leaf_card(vertices, faces, center, width, height, yaw_degrees, lean_degrees=0.0):
yaw = math.radians(yaw_degrees)
lean = math.radians(lean_degrees)
right = (math.cos(yaw) * width * 0.5, math.sin(yaw) * width * 0.5, 0.0)
up_offset = (math.sin(lean) * height * 0.35, 0.0, math.cos(lean) * height)
cx, cy, cz = center
base = len(vertices)
vertices.extend(
[
(cx - right[0], cy - right[1], cz),
(cx + right[0], cy + right[1], cz),
(cx + right[0] + up_offset[0], cy + right[1] + up_offset[1], cz + up_offset[2]),
(cx - right[0] + up_offset[0], cy - right[1] + up_offset[1], cz + up_offset[2]),
]
)
faces.append((base + 0, base + 1, base + 2, base + 3))
def add_low_poly_ellipsoid(vertices, faces, center, radius_x, radius_y, radius_z, segments=10):
cx, cy, cz = center
top_index = len(vertices)
vertices.append((cx, cy, cz + radius_z))
upper = []
lower = []
for ring_z, ring_radius_scale, target in ((0.18, 0.9, upper), (-0.25, 1.0, lower)):
for index in range(segments):
angle = (math.tau * index) / segments
target.append(len(vertices))
vertices.append(
(
cx + math.cos(angle) * radius_x * ring_radius_scale,
cy + math.sin(angle) * radius_y * ring_radius_scale,
cz + (radius_z * ring_z),
)
)
bottom_index = len(vertices)
vertices.append((cx, cy, cz - radius_z * 0.45))
for index in range(segments):
next_index = (index + 1) % segments
faces.append((top_index, upper[index], upper[next_index]))
faces.append((upper[index], lower[index], lower[next_index], upper[next_index]))
faces.append((bottom_index, lower[next_index], lower[index]))
def coastal_oak_mesh():
vertices = []
faces = []
add_box(vertices, faces, (0.0, 0.0, 140.0), (36.0, 30.0, 280.0))
add_box(vertices, faces, (-34.0, 14.0, 250.0), (18.0, 16.0, 125.0))
add_box(vertices, faces, (42.0, -12.0, 275.0), (18.0, 16.0, 115.0))
add_low_poly_ellipsoid(vertices, faces, (0.0, 0.0, 350.0), 150.0, 125.0, 105.0)
add_low_poly_ellipsoid(vertices, faces, (-95.0, 30.0, 320.0), 105.0, 82.0, 78.0)
add_low_poly_ellipsoid(vertices, faces, (95.0, -20.0, 330.0), 112.0, 88.0, 82.0)
return vertices, faces
def coyote_brush_mesh():
vertices = []
faces = []
for yaw in (0.0, 35.0, 82.0, 128.0, 171.0):
add_leaf_card(vertices, faces, (0.0, 0.0, 0.0), 190.0, 145.0, yaw, 8.0)
add_low_poly_ellipsoid(vertices, faces, (-32.0, 18.0, 78.0), 92.0, 68.0, 56.0, 8)
add_low_poly_ellipsoid(vertices, faces, (48.0, -16.0, 70.0), 86.0, 70.0, 50.0, 8)
add_low_poly_ellipsoid(vertices, faces, (0.0, 42.0, 62.0), 78.0, 52.0, 45.0, 8)
return vertices, faces
def dry_grass_clump_mesh():
vertices = []
faces = []
for index, yaw in enumerate((0.0, 22.0, 47.0, 76.0, 111.0, 146.0, 178.0)):
width = 18.0 + (index % 3) * 4.0
height = 95.0 + (index % 4) * 14.0
add_leaf_card(vertices, faces, (0.0, 0.0, 0.0), width, height, yaw, -5.0 + (index % 3) * 5.0)
return vertices, faces
def import_static_mesh_obj(obj_path, destination_folder, asset_name):
task = unreal.AssetImportTask()
task.set_editor_property("filename", str(obj_path))
task.set_editor_property("destination_path", destination_folder)
task.set_editor_property("destination_name", asset_name)
task.set_editor_property("automated", True)
task.set_editor_property("replace_existing", True)
task.set_editor_property("save", True)
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
asset_path = f"{destination_folder}/{asset_name}"
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
if not asset:
raise RuntimeError(f"Could not import vegetation mesh asset: {asset_path}")
return asset
def ensure_ground_zero_vegetation_meshes():
if not unreal.EditorAssetLibrary.does_directory_exist(VEGETATION_MESH_FOLDER):
unreal.EditorAssetLibrary.make_directory(VEGETATION_MESH_FOLDER)
mesh_builders = {
"SM_AGR_GZ_CoastalOak_Proxy": coastal_oak_mesh,
"SM_AGR_GZ_CoyoteBrush_Proxy": coyote_brush_mesh,
"SM_AGR_GZ_DryGrassClump_Proxy": dry_grass_clump_mesh,
}
for asset_name, builder in mesh_builders.items():
vertices, faces = builder()
obj_path = VEGETATION_SOURCE_FOLDER / f"{asset_name}.obj"
write_obj_mesh(obj_path, vertices, faces)
import_static_mesh_obj(obj_path, VEGETATION_MESH_FOLDER, asset_name)
unreal.log(f"Created or refreshed Ground Zero vegetation mesh: {VEGETATION_MESH_FOLDER}/{asset_name}")
def ensure_environment_materials():
if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER):
unreal.EditorAssetLibrary.make_directory(MATERIAL_FOLDER)
@@ -843,6 +999,8 @@ def ensure_environment_materials():
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
if key == "terrain":
rebuild_ground_zero_terrain_material(material, spec)
if key in {"tree", "shrub", "grass"}:
rebuild_ground_zero_foliage_material(material, spec)
created_or_loaded[key] = material
return created_or_loaded
@@ -937,6 +1095,43 @@ def rebuild_ground_zero_terrain_material(material, spec):
unreal.log("Rebuilt Ground Zero terrain material with coastal scrub color variation and procedural noise.")
def rebuild_ground_zero_foliage_material(material, spec):
"""Use per-instance color variation so repeated foliage clumps do not tile visually."""
if hasattr(unreal.MaterialEditingLibrary, "delete_all_material_expressions"):
unreal.MaterialEditingLibrary.delete_all_material_expressions(material)
color_a = create_constant_color(material, spec["color"], -720, -160)
color_b = create_constant_color(material, spec["variation_color"], -720, 20)
blend = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionLinearInterpolate, -360, -80
)
connect_expression(color_a, "", blend, "A")
connect_expression(color_b, "", blend, "B")
random_expression_class = getattr(unreal, "MaterialExpressionPerInstanceRandom", None)
if random_expression_class:
random_expression = unreal.MaterialEditingLibrary.create_material_expression(
material, random_expression_class, -600, 170
)
connect_expression(random_expression, "", blend, "Alpha")
unreal.MaterialEditingLibrary.connect_material_property(
blend, "", unreal.MaterialProperty.MP_BASE_COLOR
)
roughness = create_constant_scalar(material, spec["roughness"], -360, 140)
unreal.MaterialEditingLibrary.connect_material_property(
roughness, "", unreal.MaterialProperty.MP_ROUGHNESS
)
material.set_editor_property("use_material_attributes", False)
material.set_editor_property("used_with_instanced_static_meshes", True)
if spec.get("two_sided"):
material.set_editor_property("two_sided", True)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
def apply_material_to_actor_meshes(actor, material):
applied_count = 0
for component in actor.get_components_by_class(unreal.StaticMeshComponent):
@@ -1363,6 +1558,7 @@ def main():
raise RuntimeError(f"Could not load map: {MAP_PATH}")
ensure_native_placeholder_meshes()
ensure_ground_zero_vegetation_meshes()
labels = {spec["label"] for spec in DEMO_ACTORS}
labels.update(LEGACY_DEMO_LIGHTING_LABELS)