From dd3d247539822f03012f2afbd391f3c91b5e7efd Mon Sep 17 00:00:00 2001 From: Nathan Slaven Date: Thu, 21 May 2026 15:43:14 +0000 Subject: [PATCH] Upgrade Ground Zero vegetation assets --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 2 +- .../SM_AGR_GZ_CoastalOak_Proxy.uasset | 3 + .../SM_AGR_GZ_CoyoteBrush_Proxy.uasset | 3 + .../SM_AGR_GZ_DryGrassClump_Proxy.uasset | 3 + .../Maps/L_GroundZeroTerrain_Test.umap | 4 +- .../M_AGR_GZ_EdiblePlant_Resource.uasset | 2 +- .../Materials/M_AGR_GZ_Fiber_Resource.uasset | 2 +- .../Materials/M_AGR_GZ_FreshWater.uasset | 2 +- .../M_AGR_GZ_Grass_DryCoastal.uasset | 4 +- .../M_AGR_GZ_Shrub_CoyoteBrush.uasset | 4 +- .../Materials/M_AGR_GZ_Stone_Sandstone.uasset | 2 +- .../M_AGR_GZ_Terrain_CoastalScrub.uasset | 4 +- .../Materials/M_AGR_GZ_Tree_CoastalOak.uasset | 4 +- .../Materials/M_AGR_GZ_Wood_Resource.uasset | 2 +- .../GroundZeroNaturalEnvironmentPass.md | 12 +- Scripts/setup_ground_zero_demo_map.py | 214 +++++++++++++++++- .../verify_ground_zero_density_sightlines.py | 6 +- ...fy_ground_zero_natural_environment_pass.py | 76 +++++++ Scripts/verify_native_placeholder_meshes.py | 12 +- Source/AgrarianGame/AgrarianFoliagePatch.cpp | 21 +- 20 files changed, 341 insertions(+), 41 deletions(-) create mode 100644 Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoastalOak_Proxy.uasset create mode 100644 Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoyoteBrush_Proxy.uasset create mode 100644 Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_DryGrassClump_Proxy.uasset diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 1d9edc7..5bd826e 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -947,7 +947,7 @@ looks intentional, grounded, and investor-readable. 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. -- [ ] 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 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. diff --git a/Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoastalOak_Proxy.uasset b/Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoastalOak_Proxy.uasset new file mode 100644 index 0000000..1c92341 --- /dev/null +++ b/Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoastalOak_Proxy.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e877e10c36033c95a37690c722428f3bbcbbe9fe4f3fcd2b41a821776de1b4c6 +size 71776 diff --git a/Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoyoteBrush_Proxy.uasset b/Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoyoteBrush_Proxy.uasset new file mode 100644 index 0000000..16e3826 --- /dev/null +++ b/Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_CoyoteBrush_Proxy.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b6eb23c08fb6b5f8fb378669378e8e2d3c143031e719063e8e31a3a07e8c50b +size 71226 diff --git a/Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_DryGrassClump_Proxy.uasset b/Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_DryGrassClump_Proxy.uasset new file mode 100644 index 0000000..697d74a --- /dev/null +++ b/Content/Agrarian/Environment/Vegetation/SM_AGR_GZ_DryGrassClump_Proxy.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a20fbd4474b59bbccc04178c77843a5f8eae67b1b9aaaa1c51a3d1fa81b9553 +size 69466 diff --git a/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap b/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap index c9a5f86..e466598 100644 --- a/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap +++ b/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e038563e58391a3914efffb19cb1f19f23a1a7a17c52ff9138780db08da786b9 -size 7603560 +oid sha256:67515c3433eec0ed8ca6687f1e47d7f4ccda7cdfe796691a24bce47794bb7e19 +size 7603792 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_EdiblePlant_Resource.uasset b/Content/Agrarian/Materials/M_AGR_GZ_EdiblePlant_Resource.uasset index 99f47ca..263a682 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_EdiblePlant_Resource.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_EdiblePlant_Resource.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c4c52ab4ed8724ac61ced86167af7fb609b46a49767d3ee72c51aa6af6df17c +oid sha256:6f2773ae85b6cca1725ef7849e1e9a1257fe3917032e2ae00ad676761c40ed68 size 5453 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.uasset index 482b070..918aaec 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85b3740341fd56ec55d5c6bc680dcfac015e8edd47ccdaeaf0bf4b8327c05f8a +oid sha256:8a613178a328291958b06ed286951048a006e6e4e285f5989e59cd6be8ce7283 size 5417 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_FreshWater.uasset b/Content/Agrarian/Materials/M_AGR_GZ_FreshWater.uasset index 71da3b8..510c574 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_FreshWater.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_FreshWater.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:432bb4413bb043fa51cecf81c257a613710333765bf291651fde2ddba3e7cdbd +oid sha256:e95eb71e9563507319db68256d9e5910ea05824f35d124a6033ca8eafa0c5b7c size 5393 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset index b829487..fa6970f 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68321c2f88568097df87b3f75ac6caac755169f4251c7ad3561afcb9cb3462fa -size 5493 +oid sha256:047f67d751c004620548dae3b555d98b932b61998b761ddd84f59fd1d213d2ec +size 7622 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset index 1d82d40..1c0b197 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9c5a5da61a42ec63a308ca573bd297a09cddda92481331f340c6aa2e7b72e00 -size 5499 +oid sha256:6d93bba5d931414df60e5a2074110262052de838675b96430e32e9c12a56d445 +size 7628 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.uasset index dd0bd19..e528c5d 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:273e76837d7f5f1d614abeccce29305828a19424dd74ded3a510853008603dcd +oid sha256:2809e88b1eb9e1ae38e9b1bb7e267f87c60e848a833f0c29d2706a676ee931ff size 5423 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset index addf839..c664127 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cb1ca8d8b87db6a87ec88784cfb7222857c6291aa17a7dc7f00aa860cdb25c1 -size 8740 +oid sha256:2853e81d3ace2c5d5f57ea749c288fd8aa3183af21ea5601b0f08ef42c0f5ecd +size 11058 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset index 99d3856..06e5a62 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1593f87873971cd89b24a48482d7dc1af36647ff593fbe17a8b0c858ec8c2d1b -size 5487 +oid sha256:da2e021f26aa800d6160ba77af0c8a7df37a503c6c39a52fa804f16e66d436c5 +size 7574 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Wood_Resource.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Wood_Resource.uasset index 08f5bf1..373eb2f 100644 --- a/Content/Agrarian/Materials/M_AGR_GZ_Wood_Resource.uasset +++ b/Content/Agrarian/Materials/M_AGR_GZ_Wood_Resource.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd06837632542011d459df37e39c9e021ed5e0f496417bac03ef54298a103cea +oid sha256:741c82c4562ba17b612f2149fb530612830c439d298d81e2bedcc84e7390a396 size 5411 diff --git a/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md b/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md index 24c75ae..9dba718 100644 --- a/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md +++ b/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md @@ -9,8 +9,11 @@ space. - Terrain receives a procedural coastal scrub terrain material that blends 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. -- Foliage patch instances keep the current prototype meshes but use distinct - tree, shrub, and dry grass materials. +- Foliage patch instances use native low-poly coastal scrub vegetation meshes + 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 materials. - 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 materials exist, the landscape uses the terrain material, the foliage actor has 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: noise, blend, and coastal-scrub color-vector expression families must be present in the saved material package. It also checks the asset variation diff --git a/Scripts/setup_ground_zero_demo_map.py b/Scripts/setup_ground_zero_demo_map.py index e162e23..3ed0575 100644 --- a/Scripts/setup_ground_zero_demo_map.py +++ b/Scripts/setup_ground_zero_demo_map.py @@ -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) diff --git a/Scripts/verify_ground_zero_density_sightlines.py b/Scripts/verify_ground_zero_density_sightlines.py index a9dc54e..e97e2bd 100644 --- a/Scripts/verify_ground_zero_density_sightlines.py +++ b/Scripts/verify_ground_zero_density_sightlines.py @@ -7,9 +7,9 @@ import unreal MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass" EXPECTED_FOLIAGE_COUNTS = { - "trees": 64, - "shrubs": 148, - "grass": 260, + "trees": 96, + "shrubs": 220, + "grass": 420, } CRITICAL_CLEARANCE_CM = { "trees": 5200.0, diff --git a/Scripts/verify_ground_zero_natural_environment_pass.py b/Scripts/verify_ground_zero_natural_environment_pass.py index 4eb3373..46531a6 100644 --- a/Scripts/verify_ground_zero_natural_environment_pass.py +++ b/Scripts/verify_ground_zero_natural_environment_pass.py @@ -19,6 +19,20 @@ EXPECTED_FOLIAGE_COUNTS = { "shrubs": 220, "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 = { "AGR_GZ_Wood": "wood_resource", "AGR_GZ_Fiber": "fiber_resource", @@ -61,6 +75,29 @@ def material_path(material): 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): for prefix, material_key in RESOURCE_MATERIALS.items(): 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") +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(): if not unreal.EditorLevelLibrary.load_level(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(): 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)) expected = MATERIALS[material_key] if assigned != expected: 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 for actor in actors: @@ -211,9 +285,11 @@ def main(): for path, snippet in [ (docs, "asset variation"), (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] 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 grasses, shrubs, and trees with believable coastal-scrub vegetation assets"), ]: with open(path, "r", encoding="utf-8") as handle: text = handle.read() diff --git a/Scripts/verify_native_placeholder_meshes.py b/Scripts/verify_native_placeholder_meshes.py index a6b3e96..a5371db 100644 --- a/Scripts/verify_native_placeholder_meshes.py +++ b/Scripts/verify_native_placeholder_meshes.py @@ -6,6 +6,7 @@ import unreal MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" PROJECT_ROOT = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir())) PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes" +VEGETATION_MESH_FOLDER = "/Game/Agrarian/Environment/Vegetation" PLACEHOLDER_MESHES = { "SM_AGR_Placeholder_Cube", "SM_AGR_Placeholder_ChamferCube", @@ -47,6 +48,13 @@ def assert_native_mesh(path, failures): 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(): failures = [] @@ -79,7 +87,7 @@ def main(): else: for property_name in ("tree_instances", "shrub_instances", "grass_instances"): 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: label = get_actor_label(actor) @@ -90,7 +98,7 @@ def main(): if not mesh_components: failures.append(f"{label} has no static mesh component") 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: text = path.read_text(encoding="utf-8") diff --git a/Source/AgrarianGame/AgrarianFoliagePatch.cpp b/Source/AgrarianGame/AgrarianFoliagePatch.cpp index e592d9a..6b8db5d 100644 --- a/Source/AgrarianGame/AgrarianFoliagePatch.cpp +++ b/Source/AgrarianGame/AgrarianFoliagePatch.cpp @@ -9,7 +9,12 @@ 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) { @@ -19,10 +24,10 @@ void ConfigureFoliageComponent(UHierarchicalInstancedStaticMeshComponent* Compon Component->SetMobility(EComponentMobility::Static); Component->SetCollisionProfileName(CollisionProfileName); Component->SetGenerateOverlapEvents(false); - Component->bCastDynamicShadow = true; - Component->bCastStaticShadow = true; - Component->InstanceStartCullDistance = 120000; - Component->InstanceEndCullDistance = 180000; + Component->bCastDynamicShadow = bCastShadows; + Component->bCastStaticShadow = bCastShadows; + Component->InstanceStartCullDistance = StartCullDistance; + Component->InstanceEndCullDistance = EndCullDistance; } } @@ -36,15 +41,15 @@ AAgrarianFoliagePatch::AAgrarianFoliagePatch() TreeInstances = CreateDefaultSubobject(TEXT("TreeInstances")); TreeInstances->SetupAttachment(SceneRoot); - ConfigureFoliageComponent(TreeInstances, TEXT("BlockAll")); + ConfigureFoliageComponent(TreeInstances, TEXT("BlockAll"), 65000, 95000, true); ShrubInstances = CreateDefaultSubobject(TEXT("ShrubInstances")); ShrubInstances->SetupAttachment(SceneRoot); - ConfigureFoliageComponent(ShrubInstances, TEXT("NoCollision")); + ConfigureFoliageComponent(ShrubInstances, TEXT("NoCollision"), 28000, 52000, true); GrassInstances = CreateDefaultSubobject(TEXT("GrassInstances")); GrassInstances->SetupAttachment(SceneRoot); - ConfigureFoliageComponent(GrassInstances, TEXT("NoCollision")); + ConfigureFoliageComponent(GrassInstances, TEXT("NoCollision"), 9000, 22000, false); } void AAgrarianFoliagePatch::ClearFoliage()