EN RU

Megascans Vegetation Scattering in Solaris

2026-10-27

Houdini Solaris Karma MaterialX USD

Megascans Vegetation Scattering in Solaris

Step-by-step technique: multi-variant USD component assets, point-instanced over a heightfield with Karma. Written for Houdini 22 (Solaris / MaterialX / Karma XPU).

0. Overview

The technique has four independent pieces. Each vegetation type (grass, shrubs, etc.) gets its own copy of the pieces, so types can be tuned, disabled or rendered separately.

SOP  /obj/<terrain>                      LOP  /stage
-------------------                      ----------------------------------------
heightfield (layers: height, mask)       BUILD_<asset>  (subnet, writes <asset>.usd)
  -> heightfield_scatter                 PROTO_<asset>  (subnet, 8 prototypes)
  -> patches (wrangle)                        |
  -> attribs (wrangle)                        | input 2
  -> POINTS_<asset> (null) ----- SOP Path --> <asset> (PointInstancer)
                                         ^ input 1
                     scene -> ... -> previous instancer

1. Naming and paths

Placeholder Meaning Example
<asset> Short snake_case asset name wild_grass
<src_dir> Downloaded Megascans folder wild_grass_abc123_mid
<mesh_prefix> FBX name before _VarX_LODn Wild_Grass_abc123_Mid_abc123
<tex_prefix> Texture name before the map suffix Wild_Grass_abc123_Mid_2K
<terrain> SOP object with the heightfield /obj/terrain
Item Name / path
Source files $HIP/models/plants/<src_dir>/
Asset build subnet (LOP) BUILD_<asset>
Exported asset $HIP/usd/assets/<asset>/<asset>.usd
Prototype subnet (LOP) PROTO_<asset>
Scatter output (SOP) /obj/<terrain>/POINTS_<asset>
Instancer node / prim <asset> / /vegetation/<asset>
Geometry variants VarA ... VarH (must match the Megascans variant names)

Tip. Keep every BUILD_ and PROTO_ setup inside its own subnet. Node names must be unique per network, and VarA...VarH will be reused by every asset. Inside a subnet the names are local, so nothing gets auto-renamed to VarA1.

2. Build the USD asset (BUILD_)

In /stage create a Subnet named BUILD_<asset>, dive in (I) and drop a Component Builder. Rename the Component Output node to <asset> (its Root Prim is /$OS, so the prim and the file take the node name).

2.1 Geometry: one Component Geometry per variant

Rename the Component Geometry node to VarA and dive in. Use the node name in the file paths, so duplicates only need a rename. ../../.. resolves file -> geo -> sopnet -> VarA.

file1 -> output default (render geometry):

$HIP/models/plants/<src_dir>/<mesh_prefix>_`opname("../../..")`_LOD1.fbx

file2 -> output proxy (viewport only, Megascans LOD3 is usually a billboard):

$HIP/models/plants/<src_dir>/<mesh_prefix>_`opname("../../..")`_LOD3.fbx

2.2 Combine variants

Wire all eight into Component Geometry Variants and its output into input 1 of Component Material.

Watch out. Use Component Geometry Variants, not a plain Merge. A Merge layers all variants onto the same prim, so the asset gets no variant set and every prototype looks identical.

2.3 Material (MaterialX, inside the Material Library)

Wire the Material Library into input 2 of Component Material. Inside, create MtlX Standard Surface named <asset>_mtl and one MtlX Image per map. Each File field gets the full path:

$HIP/models/plants/<src_dir>/<tex_prefix>_<Map>.jpg
Map Signature File Color Space Connect to
BaseColor Color sRGB - Texture base_color
Roughness Float Raw specular_roughness
Normal Vector Raw MtlX Normal Map -> normal
Opacity Float Raw opacity
Translucency Color sRGB - Texture subsurface_color

Standard Surface settings:

Tip. Always set File Color Space explicitly. An empty field makes Houdini guess, which can darken colours. Never pick a Display or Output transform for textures.

2.4 Assign and export

3. Prototypes (PROTO_)

Create a Subnet PROTO_<asset>. Inside, build one pair per variant:

Reference LOP, node named VarA:

Reference File : $HIP/usd/assets/<asset>/<asset>.usd
Primitive Path : /prototypes/$OS
Reference Prim : default (Component Output already set defaultPrim)

Set Variant LOP right after it (paste exactly, the expression reads the input node name):

Primitives  : /prototypes/`opinput(".", 0)`
Variant Set : geo
Variant Name: `opinput(".", 0)`

4. Scatter points (SOP, /obj/)

4.1 Mask (optional): invert and keep only low ground

To place a second plant type where the first one is not, and only on the floor, branch off the blurred mask with a Volume Wrangle named mask_floor_inv and use it as the mask input of the new scatter. @mask and @height bind to the heightfield layers by name.

// invert the existing mask
float inv = 1.0 - @mask;
// 1 on the floor, fades to 0 between h_min and h_max
float floor_mask = 1.0 - smooth(chf("h_min"), chf("h_max"), @height);
@mask = clamp(inv * floor_mask, 0, 1);

Tune h_min (top of the floor) and h_max (where the mask reaches 0) while displaying the node: the mask draws red in the viewport. A wider gap gives a softer transition onto slopes. Remove the 1.0 - in the first line to keep the original mask instead of inverting it.

4.2 HeightField Scatter

Parameter Value Why
Scatter Method By Coverage using Mask Layer Scatter only where the mask is
Coverage 1 Density is then driven by Outer Radius
Outer Radius 0.15-0.3 grass, 0.3-1 shrubs Smaller = denser
Relax Points off Relax + Self Overlap produce a visible even grid
Keep Incoming Terrain off Otherwise the heightfield volume points (at 0,0,0) pass through and get instances

4.3 Patches wrangle (Attribute Wrangle, Points)

// large vegetation patches
float n = noise(@P * chf("freq") + chf("offset"));
n = fit(n, chf("lo"), chf("hi"), 0, 1);
// the lower the noise, the more points are removed
if (rand(@ptnum + 77) > n * chf("density"))
    removepoint(0, @ptnum);
Parameter Start Effect
freq 0.005 Patch size. Lower = bigger patches (0.005 ~ 200 m, 0.02 ~ 50 m, 0.1 = fine ripple).
offset 0 Shifts the noise pattern. Use a different value per plant type so their patches do not coincide.
lo / hi 0.35 / 0.65 Patch contrast. Narrow range = hard edges; wide = soft. Both up = more bare ground; both down = denser overall.
density 1 Global survival multiplier inside patches. 0.5 thins even the densest areas by half.

4.4 Attributes wrangle (Attribute Wrangle, Points)

float seed = chf("seed");
// variant as a STRING matching the prototype names
string vars[] = {"VarA","VarB","VarC","VarD","VarE","VarF","VarG","VarH"};
s@variant = vars[int(rand(@ptnum + seed) * len(vars))];
// scale
f@pscale = fit01(rand(@ptnum + seed + 1), chf("scale_min"), chf("scale_max"));
// random yaw + slight tilt
float yaw = rand(@ptnum + seed + 2) * 2 * PI;
float tilt = radians(fit01(rand(@ptnum + seed + 3), -chf("max_tilt"), chf("max_tilt")));
p@orient = qmultiply(quaternion(yaw, {0,1,0}), quaternion(tilt, {1,0,0}));

Press the Create spare parameters button. Starting values: scale_min 0.4-0.5, scale_max 1.4-1.6, max_tilt 5 (shrubs) to 10 (grass). Optional, bigger plants inside dense patches (append after the pscale line):

f@pscale *= fit(noise(@P * chf("freq")), 0.35, 0.65, 0.6, 1.3);

End the chain with a Null named POINTS_<asset>.

Tip. If the scatter has its own Variability (scale range), the wrangle's pscale overwrites it. Use one or the other.

5. PointInstancer (LOP)

One PointInstancer per plant type, chained one after another in the main stream.

Parameter Value
Node name <asset>
Input 1 Main scene (terrain, rocks, previous instancer)
Input 2 PROTO_<asset>
Primitive Path /vegetation/<asset> (keeps all vegetation under one branch)
Prototype Source Second Input
Primitives /prototypes/* (more robust than the default lopinputprims through a subnet)
Import SOP Points > SOP Source External SOP
SOP Path /obj/<terrain>/POINTS_<asset> (no trailing slash)
Prototype Assignment > Prototype Index String Attribute, attribute variant

Orientation (orient) and scale (pscale) are picked up automatically. Warnings (!) on the copied prototypes in Scene Graph Tree are kind-hierarchy notices and do not affect rendering.

6. Lighting and render notes

7. Troubleshooting

Symptom Cause / fix
Unable to find SOP attribute: variant No s@variant on the points, or it is an int. Use the string version of the wrangle.
All instances look the same Variants combined with Merge instead of Component Geometry Variants; or prototypes not reloaded after Save to Disk.
Instance cluster at the origin Keep Incoming Terrain is on in HeightField Scatter.
Plants arranged on a grid Relax Points is on.
Too sparse / too dense Outer Radius in the scatter, then density in the patches wrangle.
Leaves semi-transparent Opacity image not set to Float, or render not converged. Optionally threshold the opacity with MtlX Range (0.45-0.55, clamp).
Plants lying down or giant Fix the Transform inside Component Geometry (scale 0.01, rotate X -90), re-save.
Only one prototype arrives PROTO_ subnet output0 is wired to a single Set Variant instead of the Merge.
Node renamed to VarA1 Name clash in the same network. Build inside BUILD_ / PROTO_ subnets.
Colours too dark / washed out File Color Space empty or wrong: colour maps sRGB - Texture, data maps Raw.

8. Checklist for a new plant type

  1. Download Megascans set to $HIP/models/plants/<src_dir>/.
  2. BUILD_<asset>: Component Builder, VarA with opname paths, check scale/axis, duplicate to VarH, Component Geometry Variants.
  3. Material: 5 MtlX Images with explicit colour spaces, Opacity as Float, Thin Walled, Subsurface.
  4. Rename output to <asset>, Save to Disk, verify the geo variant set.
  5. PROTO_<asset>: Reference + Set Variant x8, Merge, wire to output0.
  6. SOP: mask (optional invert/floor), scatter (Relax off, Keep Terrain off), patches wrangle with a new offset, attributes wrangle, POINTS_<asset>.
  7. PointInstancer <asset> chained after the previous one, String Attribute variant.
  8. Render check in Karma; tune density, patches and translucency.