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
+1 -1
View File
@@ -947,7 +947,7 @@ looks intentional, grounded, and investor-readable.
Required order: Required order:
- [x] Replace or upgrade the terrain material first so Ground Zero no longer reads as flat tan placeholder ground. Rebuilt `M_AGR_GZ_Terrain_CoastalScrub` as a procedural coastal scrub material that blends dry soil, scrub green, and sandy path color families with broad and fine noise, documented the visual baseline, and extended the natural-environment verifier so flat constant-color terrain fails. - [x] Replace or upgrade the terrain material first so Ground Zero no longer reads as flat tan placeholder ground. Rebuilt `M_AGR_GZ_Terrain_CoastalScrub` as a procedural coastal scrub material that blends dry soil, scrub green, and sandy path color families with broad and fine noise, documented the visual baseline, and extended the natural-environment verifier so flat constant-color terrain fails.
- [ ] Replace or upgrade grasses, shrubs, and trees with believable coastal-scrub vegetation assets, density, color variation, scale variation, and LOD/performance limits. - [x] Replace or upgrade grasses, shrubs, and trees with believable coastal-scrub vegetation assets, density, color variation, scale variation, and LOD/performance limits. Added native generated coastal oak, coyote brush, and dry grass clump mesh assets under `/Game/Agrarian/Environment/Vegetation`, switched the Ground Zero foliage patch off engine basic shapes, rebuilt foliage materials with per-instance color variation, preserved investor-facing density and scale variation, added explicit HISM cull/shadow performance limits, and extended verifiers so basic-shape vegetation or missing cull limits fail.
- [ ] Replace or upgrade freshwater visuals with readable water surface, edge treatment, bank dressing, reflection/roughness tuning, and collectability cues. - [ ] Replace or upgrade freshwater visuals with readable water surface, edge treatment, bank dressing, reflection/roughness tuning, and collectability cues.
- [ ] Replace or upgrade character bodies and clothing so selected characters read as realistic near-future post-collapse frontier people rather than template mannequins or proxy stacks. - [ ] Replace or upgrade character bodies and clothing so selected characters read as realistic near-future post-collapse frontier people rather than template mannequins or proxy stacks.
- [ ] Replace or upgrade resource objects so wood, stone, fiber, edible plants, pickups, and gathered items look like world objects rather than debug primitives. - [ ] Replace or upgrade resource objects so wood, stone, fiber, edible plants, pickups, and gathered items look like world objects rather than debug primitives.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,8 +9,11 @@ space.
- Terrain receives a procedural coastal scrub terrain material that blends - Terrain receives a procedural coastal scrub terrain material that blends
dry soil, scrub green, and sandy path color families with broad and fine dry soil, scrub green, and sandy path color families with broad and fine
noise so Ground Zero does not read as flat tan placeholder ground. noise so Ground Zero does not read as flat tan placeholder ground.
- Foliage patch instances keep the current prototype meshes but use distinct - Foliage patch instances use native low-poly coastal scrub vegetation meshes
tree, shrub, and dry grass materials. under `/Game/Agrarian/Environment/Vegetation`: a coastal oak proxy, coyote
brush proxy, and dry grass clump proxy. The foliage materials include
per-instance color variation so repeated instances do not read as copied
engine primitives.
- Wood, fiber, stone, and freshwater actors receive distinct first-pass - Wood, fiber, stone, and freshwater actors receive distinct first-pass
materials. materials.
- Investor-facing asset variation actors add additional tree canopies/trunks, - Investor-facing asset variation actors add additional tree canopies/trunks,
@@ -50,7 +53,10 @@ space.
`Scripts/verify_ground_zero_natural_environment_pass.py` checks that the `Scripts/verify_ground_zero_natural_environment_pass.py` checks that the
materials exist, the landscape uses the terrain material, the foliage actor has materials exist, the landscape uses the terrain material, the foliage actor has
the expected investor-facing instance counts and material assignments, and the expected investor-facing instance counts and material assignments, and
resource/water actors are visually dressed. It also checks that the terrain resource/water actors are visually dressed. It also checks that foliage no
longer points at `/Engine/BasicShapes` or template meshes, that the three
coastal-scrub vegetation assets are assigned, and that tree, shrub, and grass
components have explicit cull distances for performance. It also checks that the terrain
material contains procedural color breakup rather than a flat constant color: material contains procedural color breakup rather than a flat constant color:
noise, blend, and coastal-scrub color-vector expression families must be present noise, blend, and coastal-scrub color-vector expression families must be present
in the saved material package. It also checks the asset variation in the saved material package. It also checks the asset variation
+205 -9
View File
@@ -18,6 +18,8 @@ LANDSCAPE_MIN_XY = -50000.0
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass" FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
FOLIAGE_RANDOM_SEED = 4160544 FOLIAGE_RANDOM_SEED = 4160544
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes" 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 = { PLACEHOLDER_MESH_SOURCES = {
"SM_AGR_Placeholder_Cube": "/Game/LevelPrototyping/Meshes/SM_Cube", "SM_AGR_Placeholder_Cube": "/Game/LevelPrototyping/Meshes/SM_Cube",
"SM_AGR_Placeholder_ChamferCube": "/Game/LevelPrototyping/Meshes/SM_ChamferCube", "SM_AGR_Placeholder_ChamferCube": "/Game/LevelPrototyping/Meshes/SM_ChamferCube",
@@ -30,9 +32,9 @@ PLACEHOLDER_MESHES = {
for name in PLACEHOLDER_MESH_SOURCES for name in PLACEHOLDER_MESH_SOURCES
} }
FOLIAGE_MESHES = { FOLIAGE_MESHES = {
"tree": "/Engine/BasicShapes/Cone", "tree": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoastalOak_Proxy",
"shrub": "/Engine/BasicShapes/Sphere", "shrub": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_CoyoteBrush_Proxy",
"grass": "/Engine/BasicShapes/Plane", "grass": f"{VEGETATION_MESH_FOLDER}/SM_AGR_GZ_DryGrassClump_Proxy",
} }
VARIATION_MESHES = { VARIATION_MESHES = {
"cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"], "cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"],
@@ -40,8 +42,9 @@ VARIATION_MESHES = {
"cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"], "cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
"quarter_cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_QuarterCylinder"], "quarter_cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_QuarterCylinder"],
"plane": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Plane"], "plane": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Plane"],
"sphere": "/Engine/BasicShapes/Sphere", "coastal_oak": FOLIAGE_MESHES["tree"],
"cone": "/Engine/BasicShapes/Cone", "coyote_brush": FOLIAGE_MESHES["shrub"],
"dry_grass_clump": FOLIAGE_MESHES["grass"],
} }
MATERIAL_FOLDER = "/Game/Agrarian/Materials" MATERIAL_FOLDER = "/Game/Agrarian/Materials"
ENVIRONMENT_MATERIALS = { ENVIRONMENT_MATERIALS = {
@@ -57,20 +60,25 @@ ENVIRONMENT_MATERIALS = {
"tree": { "tree": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak", "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak",
"color": unreal.LinearColor(0.07, 0.18, 0.06, 1.0), "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, "roughness": 0.88,
"used_with_instanced_static_meshes": True, "used_with_instanced_static_meshes": True,
}, },
"shrub": { "shrub": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush", "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush",
"color": unreal.LinearColor(0.15, 0.28, 0.10, 1.0), "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, "roughness": 0.9,
"used_with_instanced_static_meshes": True, "used_with_instanced_static_meshes": True,
"two_sided": True,
}, },
"grass": { "grass": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal", "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal",
"color": unreal.LinearColor(0.32, 0.34, 0.13, 1.0), "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, "roughness": 0.95,
"used_with_instanced_static_meshes": True, "used_with_instanced_static_meshes": True,
"two_sided": True,
}, },
"wood_resource": { "wood_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource", "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource",
@@ -355,7 +363,7 @@ WEATHER_EXPOSURE_ZONES = [
ENVIRONMENT_VARIATION_ACTORS = [ ENVIRONMENT_VARIATION_ACTORS = [
{ {
"label": "AGR_GZ_EnvVar_Tree_Canopy_01", "label": "AGR_GZ_EnvVar_Tree_Canopy_01",
"mesh_key": "sphere", "mesh_key": "coastal_oak",
"material_key": "tree", "material_key": "tree",
"location_xy": unreal.Vector(-27500.0, 6900.0, 0.0), "location_xy": unreal.Vector(-27500.0, 6900.0, 0.0),
"z_offset": 390.0, "z_offset": 390.0,
@@ -373,7 +381,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
}, },
{ {
"label": "AGR_GZ_EnvVar_Tree_Canopy_02", "label": "AGR_GZ_EnvVar_Tree_Canopy_02",
"mesh_key": "sphere", "mesh_key": "coastal_oak",
"material_key": "tree", "material_key": "tree",
"location_xy": unreal.Vector(17600.0, 31800.0, 0.0), "location_xy": unreal.Vector(17600.0, 31800.0, 0.0),
"z_offset": 430.0, "z_offset": 430.0,
@@ -391,7 +399,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
}, },
{ {
"label": "AGR_GZ_EnvVar_Bush_Rounded_01", "label": "AGR_GZ_EnvVar_Bush_Rounded_01",
"mesh_key": "sphere", "mesh_key": "coyote_brush",
"material_key": "shrub", "material_key": "shrub",
"location_xy": unreal.Vector(-33400.0, -15200.0, 0.0), "location_xy": unreal.Vector(-33400.0, -15200.0, 0.0),
"z_offset": 70.0, "z_offset": 70.0,
@@ -400,7 +408,7 @@ ENVIRONMENT_VARIATION_ACTORS = [
}, },
{ {
"label": "AGR_GZ_EnvVar_Bush_Rounded_02", "label": "AGR_GZ_EnvVar_Bush_Rounded_02",
"mesh_key": "sphere", "mesh_key": "coyote_brush",
"material_key": "shrub", "material_key": "shrub",
"location_xy": unreal.Vector(30400.0, -3900.0, 0.0), "location_xy": unreal.Vector(30400.0, -3900.0, 0.0),
"z_offset": 75.0, "z_offset": 75.0,
@@ -783,6 +791,154 @@ def ensure_native_placeholder_meshes():
unreal.log(f"Created native placeholder mesh: {destination_path}") 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(): def ensure_environment_materials():
if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER): if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER):
unreal.EditorAssetLibrary.make_directory(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) unreal.EditorAssetLibrary.save_asset(spec["path"], only_if_is_dirty=False)
if key == "terrain": if key == "terrain":
rebuild_ground_zero_terrain_material(material, spec) 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 created_or_loaded[key] = material
return created_or_loaded 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.") 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): def apply_material_to_actor_meshes(actor, material):
applied_count = 0 applied_count = 0
for component in actor.get_components_by_class(unreal.StaticMeshComponent): 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}") raise RuntimeError(f"Could not load map: {MAP_PATH}")
ensure_native_placeholder_meshes() ensure_native_placeholder_meshes()
ensure_ground_zero_vegetation_meshes()
labels = {spec["label"] for spec in DEMO_ACTORS} labels = {spec["label"] for spec in DEMO_ACTORS}
labels.update(LEGACY_DEMO_LIGHTING_LABELS) labels.update(LEGACY_DEMO_LIGHTING_LABELS)
@@ -7,9 +7,9 @@ import unreal
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass" FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
EXPECTED_FOLIAGE_COUNTS = { EXPECTED_FOLIAGE_COUNTS = {
"trees": 64, "trees": 96,
"shrubs": 148, "shrubs": 220,
"grass": 260, "grass": 420,
} }
CRITICAL_CLEARANCE_CM = { CRITICAL_CLEARANCE_CM = {
"trees": 5200.0, "trees": 5200.0,
@@ -19,6 +19,20 @@ EXPECTED_FOLIAGE_COUNTS = {
"shrubs": 220, "shrubs": 220,
"grass": 420, "grass": 420,
} }
EXPECTED_FOLIAGE_MESHES = {
"tree_instances": "/Game/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoastalOak_Proxy",
"shrub_instances": "/Game/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoyoteBrush_Proxy",
"grass_instances": "/Game/Agrarian/Environment/Vegetation/SM_AGR_GZ_DryGrassClump_Proxy",
}
EXPECTED_FOLIAGE_CULL_DISTANCES = {
"tree_instances": (65000, 95000),
"shrub_instances": (28000, 52000),
"grass_instances": (9000, 22000),
}
FORBIDDEN_FOLIAGE_MESH_PREFIXES = (
"/Engine/BasicShapes/",
"/Game/LevelPrototyping/",
)
RESOURCE_MATERIALS = { RESOURCE_MATERIALS = {
"AGR_GZ_Wood": "wood_resource", "AGR_GZ_Wood": "wood_resource",
"AGR_GZ_Fiber": "fiber_resource", "AGR_GZ_Fiber": "fiber_resource",
@@ -61,6 +75,29 @@ def material_path(material):
return material.get_path_name().split(".", 1)[0] return material.get_path_name().split(".", 1)[0]
def asset_path(asset):
if not asset:
return ""
return asset.get_path_name().split(".", 1)[0]
def get_component_property(component, property_name, default=None):
try:
return component.get_editor_property(property_name)
except Exception:
return getattr(component, property_name, default)
def static_mesh_vertex_count(mesh):
library = getattr(unreal, "EditorStaticMeshLibrary", None)
if not mesh or not library:
return -1
try:
return library.get_number_verts(mesh, 0)
except Exception:
return -1
def material_key_for_label(label): def material_key_for_label(label):
for prefix, material_key in RESOURCE_MATERIALS.items(): for prefix, material_key in RESOURCE_MATERIALS.items():
if label.startswith(prefix): if label.startswith(prefix):
@@ -104,6 +141,22 @@ def verify_terrain_material_is_not_flat(material, failures):
failures.append("terrain material should include coastal scrub color vector expressions") failures.append("terrain material should include coastal scrub color vector expressions")
def verify_foliage_material_has_variation(material_path_name, failures):
project_root = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())
package_path = project_root + "Content" + material_path_name.replace("/Game", "") + ".uasset"
try:
with open(package_path, "rb") as handle:
package_bytes = handle.read()
except Exception as exc:
failures.append(f"could not inspect foliage material package {material_path_name}: {exc}")
return
if package_bytes.count(b"MaterialExpressionLinearInterpolate") < 1:
failures.append(f"{material_path_name} should blend foliage color variation")
if package_bytes.count(b"MaterialExpressionConstant3Vector") < 1:
failures.append(f"{material_path_name} should include foliage color vectors")
def main(): def main():
if not unreal.EditorLevelLibrary.load_level(MAP_PATH): if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
raise RuntimeError(f"Could not load map: {MAP_PATH}") raise RuntimeError(f"Could not load map: {MAP_PATH}")
@@ -143,10 +196,31 @@ def main():
} }
for property_name, material_key in component_expectations.items(): for property_name, material_key in component_expectations.items():
component = foliage.get_editor_property(property_name) component = foliage.get_editor_property(property_name)
static_mesh = component.get_editor_property("static_mesh")
mesh_path = asset_path(static_mesh)
expected_mesh = EXPECTED_FOLIAGE_MESHES[property_name]
if mesh_path != expected_mesh:
failures.append(f"{property_name} mesh expected {expected_mesh}, got {mesh_path}")
if mesh_path.startswith(FORBIDDEN_FOLIAGE_MESH_PREFIXES):
failures.append(f"{property_name} still uses placeholder/basic mesh {mesh_path}")
vertex_count = static_mesh_vertex_count(static_mesh)
if vertex_count <= 0:
failures.append(f"{property_name} mesh {mesh_path} has no renderable vertices")
assigned = material_path(component.get_material(0)) assigned = material_path(component.get_material(0))
expected = MATERIALS[material_key] expected = MATERIALS[material_key]
if assigned != expected: if assigned != expected:
failures.append(f"{property_name} material expected {expected}, got {assigned}") failures.append(f"{property_name} material expected {expected}, got {assigned}")
verify_foliage_material_has_variation(expected, failures)
start_cull, end_cull = EXPECTED_FOLIAGE_CULL_DISTANCES[property_name]
actual_start = int(get_component_property(component, "instance_start_cull_distance", -1))
actual_end = int(get_component_property(component, "instance_end_cull_distance", -1))
if actual_start != start_cull or actual_end != end_cull:
failures.append(
f"{property_name} cull distances expected {start_cull}/{end_cull}, "
f"got {actual_start}/{actual_end}"
)
checked_resource_actors = 0 checked_resource_actors = 0
for actor in actors: for actor in actors:
@@ -211,9 +285,11 @@ def main():
for path, snippet in [ for path, snippet in [
(docs, "asset variation"), (docs, "asset variation"),
(docs, "procedural coastal scrub terrain material"), (docs, "procedural coastal scrub terrain material"),
(docs, "native low-poly coastal scrub vegetation meshes"),
(roadmap, "[x] Replace grey-box environment presentation with an MVP natural environment pass"), (roadmap, "[x] Replace grey-box environment presentation with an MVP natural environment pass"),
(roadmap, "[x] Add first-pass environment asset variation"), (roadmap, "[x] Add first-pass environment asset variation"),
(roadmap, "[x] Replace or upgrade the terrain material first so Ground Zero no longer reads as flat tan placeholder ground."), (roadmap, "[x] Replace or upgrade the terrain material first so Ground Zero no longer reads as flat tan placeholder ground."),
(roadmap, "[x] Replace or upgrade grasses, shrubs, and trees with believable coastal-scrub vegetation assets"),
]: ]:
with open(path, "r", encoding="utf-8") as handle: with open(path, "r", encoding="utf-8") as handle:
text = handle.read() text = handle.read()
+10 -2
View File
@@ -6,6 +6,7 @@ import unreal
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
PROJECT_ROOT = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())) PROJECT_ROOT = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()))
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes" PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes"
VEGETATION_MESH_FOLDER = "/Game/Agrarian/Environment/Vegetation"
PLACEHOLDER_MESHES = { PLACEHOLDER_MESHES = {
"SM_AGR_Placeholder_Cube", "SM_AGR_Placeholder_Cube",
"SM_AGR_Placeholder_ChamferCube", "SM_AGR_Placeholder_ChamferCube",
@@ -47,6 +48,13 @@ def assert_native_mesh(path, failures):
failures.append(f"template mesh reference remains: {path}") failures.append(f"template mesh reference remains: {path}")
def assert_agrarian_environment_mesh(path, failures):
if not path.startswith((PLACEHOLDER_MESH_FOLDER, VEGETATION_MESH_FOLDER)):
failures.append(f"expected native Agrarian environment mesh, got {path}")
if "LevelPrototyping" in path or path.startswith("/Engine/BasicShapes/"):
failures.append(f"template/basic mesh reference remains: {path}")
def main(): def main():
failures = [] failures = []
@@ -79,7 +87,7 @@ def main():
else: else:
for property_name in ("tree_instances", "shrub_instances", "grass_instances"): for property_name in ("tree_instances", "shrub_instances", "grass_instances"):
component = foliage_actors[0].get_editor_property(property_name) component = foliage_actors[0].get_editor_property(property_name)
assert_native_mesh(asset_path(component.get_editor_property("static_mesh")), failures) assert_agrarian_environment_mesh(asset_path(component.get_editor_property("static_mesh")), failures)
for actor in actors: for actor in actors:
label = get_actor_label(actor) label = get_actor_label(actor)
@@ -90,7 +98,7 @@ def main():
if not mesh_components: if not mesh_components:
failures.append(f"{label} has no static mesh component") failures.append(f"{label} has no static mesh component")
continue continue
assert_native_mesh(asset_path(mesh_components[0].get_editor_property("static_mesh")), failures) assert_agrarian_environment_mesh(asset_path(mesh_components[0].get_editor_property("static_mesh")), failures)
for path, snippet in DOC_SNIPPETS: for path, snippet in DOC_SNIPPETS:
text = path.read_text(encoding="utf-8") text = path.read_text(encoding="utf-8")
+13 -8
View File
@@ -9,7 +9,12 @@
namespace namespace
{ {
void ConfigureFoliageComponent(UHierarchicalInstancedStaticMeshComponent* Component, const FName CollisionProfileName) void ConfigureFoliageComponent(
UHierarchicalInstancedStaticMeshComponent* Component,
const FName CollisionProfileName,
const int32 StartCullDistance,
const int32 EndCullDistance,
const bool bCastShadows)
{ {
if (!Component) if (!Component)
{ {
@@ -19,10 +24,10 @@ void ConfigureFoliageComponent(UHierarchicalInstancedStaticMeshComponent* Compon
Component->SetMobility(EComponentMobility::Static); Component->SetMobility(EComponentMobility::Static);
Component->SetCollisionProfileName(CollisionProfileName); Component->SetCollisionProfileName(CollisionProfileName);
Component->SetGenerateOverlapEvents(false); Component->SetGenerateOverlapEvents(false);
Component->bCastDynamicShadow = true; Component->bCastDynamicShadow = bCastShadows;
Component->bCastStaticShadow = true; Component->bCastStaticShadow = bCastShadows;
Component->InstanceStartCullDistance = 120000; Component->InstanceStartCullDistance = StartCullDistance;
Component->InstanceEndCullDistance = 180000; Component->InstanceEndCullDistance = EndCullDistance;
} }
} }
@@ -36,15 +41,15 @@ AAgrarianFoliagePatch::AAgrarianFoliagePatch()
TreeInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("TreeInstances")); TreeInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("TreeInstances"));
TreeInstances->SetupAttachment(SceneRoot); TreeInstances->SetupAttachment(SceneRoot);
ConfigureFoliageComponent(TreeInstances, TEXT("BlockAll")); ConfigureFoliageComponent(TreeInstances, TEXT("BlockAll"), 65000, 95000, true);
ShrubInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("ShrubInstances")); ShrubInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("ShrubInstances"));
ShrubInstances->SetupAttachment(SceneRoot); ShrubInstances->SetupAttachment(SceneRoot);
ConfigureFoliageComponent(ShrubInstances, TEXT("NoCollision")); ConfigureFoliageComponent(ShrubInstances, TEXT("NoCollision"), 28000, 52000, true);
GrassInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("GrassInstances")); GrassInstances = CreateDefaultSubobject<UHierarchicalInstancedStaticMeshComponent>(TEXT("GrassInstances"));
GrassInstances->SetupAttachment(SceneRoot); GrassInstances->SetupAttachment(SceneRoot);
ConfigureFoliageComponent(GrassInstances, TEXT("NoCollision")); ConfigureFoliageComponent(GrassInstances, TEXT("NoCollision"), 9000, 22000, false);
} }
void AAgrarianFoliagePatch::ClearFoliage() void AAgrarianFoliagePatch::ClearFoliage()