Foliage Pivot Baking¶
Summary: How per-blade wind/bend data is encoded into foliage meshes so
M_Grasscan animate each blade around its own base. The convention: UV2 = per-blade local-space pivot XY, VertexColor.R = root→tip ramp. Data is baked withTools/Python/bake_grass_pivots.py. Critical footgun: static-mesh lightmap-UV regeneration silently overwrites the baked UV2 channel on every build — disable it after the bake.
Table of Contents¶
- Encoding Convention
- How M_Grass Reads It
- Baking Workflow
- The Lightmap UV Footgun
- Applying to a New Foliage Mesh
- Verification
- Related Systems
- Recent Changes
Encoding Convention¶
Each blade of a grass mesh carries per-blade animation data in its vertex channels:
| Channel | Meaning |
|---|---|
| UV2.xy | Local-space pivot point of the blade — one shared (X, Y) per blade; every vertex of the same blade carries identical UV2. Z is appended as a constant at runtime. |
| UV1 | Unread by the bend chain — leave it free, or mirror UV2 as a safety placeholder. |
| VertexColor.R | Root→tip mask (z - z_min_blade) / (z_max_blade - z_min_blade) per blade. Drives the Power(R, n) * BendWPO path and the wiggler root-tip weight. |
| VertexColor.G | Wind weight constant (≈ 0.5 in the reference asset). |
| VertexColor.B | Mirrors R. |
| VertexColor.A | 1 in the reference asset. |
How M_Grass Reads It¶
M_Grass (/Game/MaterialLibrary/Grass/M_Grass) drives per-blade wind/bend through the Prismatiscape_FoliageBender + Prismatiscape_FoliageWiggler material functions. Both take a PivotPoint input fed by a single MaterialExpressionNamedRerouteDeclaration labeled PivotPointPerBladeOfGrass, which evaluates to:
i.e. UV2.xy → local pivot, Z appended, transformed to world space.
Discovery note: this reroute lives in a "dead branch" of the graph that Python's MaterialEditingLibrary output-walk cannot reach (a NamedRerouteDeclaration is referenced by GUID, not through input pins). It was found via unreal.find_object(mat, "MaterialExpressionNamedRerouteDeclaration_N") enumeration — see UE Python Material API Reference.
Baking Workflow¶
Tools/Python/bake_grass_pivots.py runs inside the editor (via the unreal-engine MCP system_control execute_command: py "..."). Algorithm:
- Copy LOD0 into a DynamicMesh via
GeometryScript_AssetUtils.copy_mesh_from_static_mesh. - BFS connected components on triangle adjacency to identify individual blades.
- Take the centroid of each blade's bottom-25% Z-vertices as its pivot.
- Write UV1 + UV2 + vertex color per the convention above.
copy_mesh_to_static_meshback onto the asset.- Disable lightmap UV regen (see next section) — the copy resets build settings, so this must happen last.
Reference asset /Game/Art/Levels/Shared/Foliage/Grass/SM_Grass matches this convention; SM_Grass_01b_Pivot was authored from SM_Grass_01b with this script.
The Lightmap UV Footgun¶
Every LOD's FMeshBuildSettings carries bGenerateLightmapUVs = true plus SrcLightmapIndex / DstLightmapIndex. On every static-mesh build, the engine auto-unwraps from src and writes to dst — silently clobbering whatever custom data you stored in that channel. Because copy_mesh_to_static_mesh itself resets build settings to defaults, disabling regen must happen after the copy, not before.
Insidious symptom: the source-asset MeshDescription still shows your baked data via DynamicMesh extraction (so verification scripts pass), while the actual rendered render-data uses the regenerated lightmap. On M_Grass this meant UV2 was both the pivot channel and the auto-lightmap dst index — the whole grass mesh tilted as one rigid body around a meaningless world-space pivot instead of bending per-blade.
Fix (Python), immediately after copy_mesh_to_static_mesh:
unreal.StaticMeshUtilitiesLibrary.set_generate_lightmap_uv(mesh, False) # covers all LODs
mesh.set_editor_property("light_map_coordinate_index", 0) # point nothing at the reused channel
mesh.modify()
unreal.EditorAssetLibrary.save_asset(path)
This applies to any bake into UV1/UV2/UV3 of an existing static mesh — pivot painter, wind data, custom masks, AO bakes, etc.
Applying to a New Foliage Mesh¶
To bake pivots into another mesh that should accept M_Grass:
1. Write per-blade local-space pivot XY into UV2, and the root→tip ramp into VertexColor.R. UV1 is optional.
2. Disable the static mesh's lightmap UV regen (above) — do this after copy_mesh_to_static_mesh, and reset light_map_coordinate_index off channel 2.
Verification¶
Spawn the asset in a level and view UV2 with a debug material (TextureCoordinate(2) → BaseColor). Correct data shows clustered flat-color zones (one color per blade), not a unique-per-vertex unwrap. A rigid whole-mesh tilt in wind = the lightmap regen clobbered UV2.
Related Systems¶
- UE Python Material API Reference —
find_objectnode enumeration, protected-property workarounds - Unreal MCP + Python Automation Guide — running editor Python via MCP
- Blender Room Workflow — related mesh authoring/export tooling
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-07-03 | Documented the pivot convention + lightmap-UV footgun together | Single reference for foliage pivot bakes |