Megascans Vegetation Scattering in Solaris
2026-10-27
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_andPROTO_setup inside its own subnet. Node names must be unique per network, andVarA...VarHwill be reused by every asset. Inside a subnet the names are local, so nothing gets auto-renamed toVarA1.
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
- Add a Transform after each File SOP if needed. Megascans FBX often comes in centimetres (Uniform Scale 0.01) and sometimes Z-up (Rotate X -90). The plant should be about 0.3-1.5 m and stand along Y.
- Leave
simproxyandalternativeempty. If outputs are missing, press Fix SOP Outputs on the node. - Back at subnet level: duplicate VarA seven times and rename the copies
VarB ... VarH. The paths update automatically.
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:
- Thin Walled: on (Geometry group). Leaves and blades are single cards.
- Subsurface: 0.4-0.6 for grass, ~0.3 for dry shrubs. In thin-walled mode this acts as see-through translucency; Radius is ignored. Keep Scale at the default 1 rather than 0.
- Specular: 0.2-0.4. Vegetation is not shiny.
- Unused maps: Specular, Gloss, Cavity, Bump, Displacement, AO.
Billboard_*maps are only for the billboard LOD.
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
- Component Material: Primitives
*, Material Path -><asset>_mtl. - On the Component Output node press Save to Disk. Result:
<asset>.usd,geo.usdc,mtl.usdc,payload.usdcand a textures folder. - Check: select the output node; in Scene Graph Tree the asset prim must have a variant set
geowithVarA...VarH. - After any change to geometry or material: Save to Disk again, then Reload Files on the Reference nodes downstream.
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)`
- Duplicate the pair seven times and rename only the Reference nodes to
VarB ... VarH. - Merge all eight Set Variant nodes into a Merge named
PROTOTYPES. - Wire PROTOTYPES into the subnet's output0. After a Shift+C collapse, output0 may be attached to a random inner node: check it shows
/prototypes/VarA... (8). - This branch is never merged into the scene. It only feeds input 2 of its instancer, so
/prototypes/VarAin several PROTO_ subnets does not conflict.
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
- Dark vegetation against the light: check Thin Walled, Subsurface 0.4-0.6, Translucency in sRGB - Texture. Then raise Diffuse Limit to 2-3 in Karma Render Settings so plants pick up bounce from bright ground.
- Backlit plants will still be darker than sunlit sand; that part is physical. A weak fill Distant Light from the camera side helps if needed.
- Judge opacity and translucency only after the render converges; the first Karma XPU samples always look ghostly on thin geometry.
- Compare variants in Houdini GL for instant feedback, then check the final look in Karma.
- The viewport shows the node with the display flag, not the selected one (label in the top-left corner). Scene Graph Tree follows selection.
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
- Download Megascans set to
$HIP/models/plants/<src_dir>/. BUILD_<asset>: Component Builder, VarA with opname paths, check scale/axis, duplicate to VarH, Component Geometry Variants.- Material: 5 MtlX Images with explicit colour spaces, Opacity as Float, Thin Walled, Subsurface.
- Rename output to
<asset>, Save to Disk, verify the geo variant set. PROTO_<asset>: Reference + Set Variant x8, Merge, wire to output0.- SOP: mask (optional invert/floor), scatter (Relax off, Keep Terrain off), patches wrangle with a new offset, attributes wrangle,
POINTS_<asset>. - PointInstancer
<asset>chained after the previous one, String Attributevariant. - Render check in Karma; tune density, patches and translucency.