Houdini uses the primitive string attribute shop_materialpath to assign materials to geometry. When you import an FBX file — from a DCC tool, a game engine export, a scan pipeline, or a client asset — Houdini's FBX importer reads the material names embedded in the file and stores them as string attributes on primitives.
The problem is that FBX material names are arbitrary strings. They come from whatever naming convention the source application used. They frequently contain characters that are illegal in Houdini operator paths, spaces that break path resolution, or prefixes from the originating software that have no meaning in a Houdini scene.
When Houdini's FBX importer reads material data, it typically stores the raw material name from the FBX into a primitive string attribute — often called material_name, fbx_material, or a custom attribute depending on importer settings. This is not shop_materialpath yet. It is raw text.
| RAW FBX STRING | PROBLEM | HOUDINI RESULT |
|---|---|---|
| M_Body Paint 01 | Space in name | Path resolution fails — Houdini cannot find the material |
| Material#12 | Hash is special character | Houdini interprets # as operator flag — path corrupt |
| MI_Chassis_LOD0 | Engine-specific prefix / suffix | No matching material in Houdini scene — silent miss |
| /Game/Assets/Mat_Door | UE4-style asset path | Leading slash resolves as absolute path — wrong context |
| lambert1 | Maya default placeholder | Maps to nothing — assigned geometry has no material |
The silent failure mode is the most dangerous: Houdini accepts an invalid shop_materialpath without error. The geometry renders with no material, or with a fallback grey, and the problem only surfaces at render time — sometimes hours into a farm job.
shop_materialpath is a primitive-level string attribute that Houdini's rendering engine reads per-primitive to resolve material assignment. It must contain a valid operator path — the same format as paths in the Houdini scene graph.
| PATH FORM | EXAMPLE | RESOLVES TO |
|---|---|---|
| Absolute | /mat/body_paint | Material at exact node path in scene |
| Relative | ../mat/body_paint | Relative to current SOP context — fragile across hierarchy |
| Named (flat) | body_paint | Resolved by Houdini against the material context — less predictable |
The safest and most portable form for production is an absolute path pointing into a /mat network. This form is unambiguous regardless of where the geometry node lives in the scene hierarchy, and it survives file references, HDAs, and subnet context changes.
The path must satisfy Houdini operator naming rules:
The tool reads a specified string attribute from geometry primitives and generates a valid shop_materialpath from it. The transformation pipeline runs in order: strip, sanitize, prefix, deduplicate.
| STEP | OPERATION | EXAMPLE |
|---|---|---|
| 1. Strip prefix | Remove configurable engine prefixes: M_, MI_, mat_ | M_Body Paint → Body Paint |
| 2. Lowercase | Normalize case — prevents duplicates from case variation | Body Paint → body paint |
| 3. Replace illegal chars | Spaces → underscore. Strip # @ ! % ( ) and other special chars | body paint #01 → body_paint_01 |
| 4. Validate segment start | If segment starts with digit, prepend underscore | 3d_asset → _3d_asset |
| 5. Prepend path prefix | Add configurable root: /mat/ | body_paint_01 → /mat/body_paint_01 |
| 6. Write attribute | Set shop_materialpath on primitive | All primitives with matching source attribute updated |
The core logic can be expressed as a VEX wrangle — the tool wraps this into an HDA interface with configurable parameters for source attribute name, prefix stripping rules, and path root:
// VEX Attribute Wrangle (Primitives) — core sanitization pattern
// Source attribute: configurable — typically 'material_name' from FBX import
string srcAttr = "material_name"; // read from HDA parameter
string pathRoot = "/mat/"; // configurable root prefix
// Read raw string from source attribute
string raw = prim(0, srcAttr, @primnum);
// Step 1: strip known engine prefixes (configurable list)
string prefixes[] = { "M_", "MI_", "mat_", "M_Inst_" };
foreach (string p; prefixes) {
if (startswith(raw, p))
raw = substring(raw, len(p), len(raw) - len(p));
}
// Step 2: lowercase — prevents silent duplicates from case variation
raw = tolower(raw);
// Step 3: replace illegal characters
// Space → underscore
raw = re_replace(" ", "_", raw);
// Strip any char that is NOT alphanumeric, underscore, or slash
raw = re_replace("[^a-z0-9_/]", "", raw);
// Collapse multiple underscores from repeated replacements
raw = re_replace("_+", "_", raw);
// Step 4: validate path segment start — cannot begin with digit
if (re_match("[0-9]", substring(raw, 0, 1)))
raw = "_" + raw;
// Step 5: build final valid path
string matpath = pathRoot + raw;
// Step 6: write to shop_materialpath
s@shop_materialpath = matpath;
Idempotency: running the tool twice on the same geometry produces identical output. The sanitization is deterministic — the same input string always produces the same shop_materialpath. This makes it safe to include in automated pipelines and re-cook without side effects.
| CONTEXT | PROBLEM SOLVED |
|---|---|
| FBX import from Unity / Unreal | Engine-prefixed material names (MI_, M_) converted to valid Houdini paths automatically |
| Scan / photogrammetry assets | Auto-generated names with spaces and special chars sanitized without manual intervention |
| Client asset integration | Unpredictable third-party naming conventions normalized to a consistent internal path scheme |
| Procedural geometry pipelines | VEX-generated string attributes converted to material assignments — no separate wrangle needed |
| Batch LOD processing | Multiple LOD meshes with shared material names all receive consistent paths in one pass |
The tool is not a material creation tool — it assumes materials already exist at the target paths in your Houdini scene or are created separately. Its role is strictly path normalization and attribute assignment. It fits cleanly between the import stage and the material authoring stage in any Houdini-based pipeline.