Content/Skills/materials/SKILL.md
Create and edit materials and material instances using MaterialService and MaterialNodeService
npx skillsauth add kevinpbuckley/vibeue materialsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
get_expression_pins(mat, expr_id) returns input pins with display labels including type suffixes like "TextureObject (T2d)", "TextureSize (V3)", "Use High Quality Normals (SB)". Never pass these display labels to connect_expressions — UE's connect API matches against the bare input name only:
# ❌ WRONG — display label with type suffix; connection fails silently or gets ignored
unreal.MaterialNodeService.connect_expressions(mp, src_id, "", fn_id, "TextureObject (T2d)")
# ✅ CORRECT — bare name
unreal.MaterialNodeService.connect_expressions(mp, src_id, "", fn_id, "TextureObject")
The authoritative source for input names is get_function_info(function_path).inputs — those are the bare names (TextureObject, TextureSize, WorldPosition, etc.). For built-in expressions, use get_inputs_for_material_expression from unreal.MaterialEditingLibrary.
⚠️ Why this is easy to miss:
get_expression_pinsreturnsis_connected=Truefor both real and phantom connections, and our service used to silently produce phantom wires when given display names. The wires would appear in the graph export but the shader compiler would never see the texture flow through. Always verify withunreal.MaterialEditingLibrary.get_used_textures(mat)after wiring — if it returns 0 on a material that samples textures, your connections are phantom.
get_material_diagnosticsMaterialNodeService.get_material_diagnostics(path) is the canonical way to verify a material is sampling textures and compiling cleanly. It returns the real compile errors, the textures the shader actually references, and node-type counts. Always run it after non-trivial graph rewiring:
diag = unreal.MaterialNodeService.get_material_diagnostics("/Game/Materials/M_Foo")
assert diag.success
print(f"compiled ok: {diag.is_compiled_ok}")
print(f"compile errors ({len(diag.compile_errors)}):")
for e in diag.compile_errors: print(f" {e}")
print(f"referenced textures ({len(diag.referenced_texture_paths)}):")
for t in diag.referenced_texture_paths: print(f" {t}")
print(f"expression count: {diag.expression_count}")
print(f"texture sample count: {diag.texture_sample_count}")
print(f"texture object parameter count: {diag.texture_object_parameter_count}")
print(f"function call count: {diag.function_call_count}")
Sample compile errors caught by this:
"(Function WorldAlignedTexture) (Node TextureSample) Sampler type is Color, should be Masks for /Game/.../T_xevtfjz_2K_ORM" — ORM/data textures need SAMPLERTYPE_Masks or SAMPLERTYPE_LinearColor, not the default SAMPLERTYPE_Color.Why prefer this over the older signals:
unreal.MaterialEditingLibrary.get_used_textures(mat) is unreliable for multi-branch graphs (BC + Normal + ORM, or anything with ComponentMask after a function-call output). It returns 0 even when the material is sampling textures correctly. Don't use it as proof of broken wiring.
Visual confirmation via the material editor preview is also useful but harder — see the next section.
# One-shot: opens the asset editor, focuses it, captures.
res = unreal.ScreenshotService.capture_asset_editor(
"/Game/Materials/M_Foo", "mat_preview")
# Then attach_image(file_path=res.file_path) to inspect.
ScreenshotService.capture_asset_editor handles the open + focus + capture pipeline in one call.
⚠️ Best-effort focus: when many asset editors are already open as tabs, the editor tab may not switch reliably. Close other asset editors first if the screenshot keeps catching the wrong tab:
ed = unreal.get_editor_subsystem(unreal.AssetEditorSubsystem) for path in [other_open_paths]: ed.close_all_editors_for_asset(unreal.load_asset(path))
Tiling OverrideFor Megascans M_MS_Srf and similar surface materials with a Tiling scalar parameter, the cleanest per-actor tiling fix is a child material instance with Tiling overridden — not a UV transform on the mesh. UV transforms apply to all faces uniformly and break orientation; the parent material's UV mapping is already correct per face.
# ✅ CORRECT pattern for "this disc shows brick too sparse / too dense"
r = unreal.MaterialService.create_instance(
"/Game/Fab/Megascans/.../MI_xevtfjz",
"MI_xevtfjz_Disc", "/Game/Materials/")
unreal.MaterialService.set_instance_scalar_parameter(r.asset_path, "Tiling", 4.0)
unreal.MaterialService.save_instance(r.asset_path)
# assign r.asset_path to the actor
For triplanar / world-aligned (where the same material works on cube, sphere, cylinder with no UV concerns), build a master material wrapping WorldAlignedTexture / WorldAlignedNormal (functions live under /Engine/Functions/Engine_MaterialFunctions01/Texturing/). Use TextureObjectParameter (not TextureSampleParameter2D) to feed the TextureObject input on those functions — the function brings its own sampler. Feed TextureSize from a Constant3Vector or Multiply(Scalar, Constant3Vector(1,1,1)); a bare scalar will broadcast incorrectly to V3 in this input slot.
Before adding, removing, or reconnecting nodes in an existing material, you MUST export and review the current graph first:
import unreal, json
path = "/Game/Materials/M_Existing"
# Step 1: Get high-level info (blend mode, shading model)
info = unreal.MaterialService.get_material_info(path)
print(f"Blend: {info.blend_mode}, Shading: {info.shading_model}")
# Step 2: Export the full node graph
graph = json.loads(unreal.MaterialNodeService.export_material_graph(path))
print(f"Expressions: {len(graph['expressions'])}")
print(f"Connections: {len(graph['connections'])}")
print(f"Output connections: {len(graph['output_connections'])}")
# Step 3: Review what already exists
for expr in graph['expressions']:
name = expr.get('parameter_name') or expr.get('class')
print(f" [{expr['id']}] {expr['class']} - {name}")
for oc in graph['output_connections']:
print(f" Output '{oc['property']}' ← expr {oc['expression_id']}")
Why this matters:
export_material_graph returns the ground truth — use it to plan your editsWorkflow for modifying existing materials:
.asset_path and .id, NOT the Raw Return ValueAll create methods return result objects, not raw strings. Always extract the field you need:
# create_material → MaterialCreateResult
result = unreal.MaterialService.create_material("M_MyMat", "/Game/Materials/")
if not result.success:
print(f"FAILED: {result.error_message}")
path = result.asset_path # ← use this, NOT result itself
# create_instance → MaterialCreateResult (same pattern)
result = unreal.MaterialService.create_instance("/Game/Materials/M_Base", "MI_Red", "/Game/Materials/")
instance_path = result.asset_path
# create_parameter / create_expression / create_function_call → MaterialExpressionInfo
expr = unreal.MaterialNodeService.create_parameter(path, "Vector", "BaseColor", "Surface", "1,0,0,1", -400, 0)
node_id = expr.id # ← use .id, NOT expr itself
expr2 = unreal.MaterialNodeService.create_expression(path, "Multiply", -200, 0)
mult_id = expr2.id
⚠️ Passing a result object where a string is expected gives:
TypeError: Nativize: Cannot nativize 'MaterialCreateResult' as 'String'TypeError: Nativize: Cannot nativize 'MaterialExpressionInfo' as 'String'
expr = unreal.MaterialNodeService.create_parameter(path, "Vector", "BaseColor", ...)
unreal.MaterialNodeService.connect_to_output(path, expr.id, "", "BaseColor") # use expr.id
unreal.MaterialService.compile_material(path) # REQUIRED
unreal.EditorAssetLibrary.save_asset(path)
| Type | Use | Example Value |
|------|-----|---------------|
| Scalar | Single float | 0.5 |
| Vector | Color/vector | (R=1.0,G=0.0,B=0.0,A=1.0) |
| Texture | Texture parameter | /Game/T_Diffuse.T_Diffuse |
| TextureObject | Texture object (no sampler) | /Game/T_Diffuse.T_Diffuse |
| StaticSwitch | Static bool switch | true or false |
BaseColor, Metallic, Specular, RoughnessEmissiveColor, Normal, Opacity, OpacityMaskWorldPositionOffset, AmbientOcclusion# WRONG - crashes if not found
add_node = next((n for n in nodes if "Add" in n.display_name))
# CORRECT
add_node = next((n for n in nodes if "Add" in n.display_name), None)
if add_node:
pins = unreal.MaterialNodeService.get_expression_pins(mat_path, add_node.id)
Use discover_python_class() first:
MaterialExpressionTypeInfo uses display_name, NOT nameMaterialOutputConnectionInfo uses connected_expression_id, NOT expression_idimport unreal
# create_material returns MaterialCreateResult — extract .asset_path
result = unreal.MaterialService.create_material("M_Character", "/Game/Materials/")
if not result.success:
print(f"FAILED: {result.error_message}")
path = result.asset_path
# create_parameter returns MaterialExpressionInfo — extract .id
# default_value formats: "R,G,B" or "R,G,B,A" or "(R=1.0,G=0.0,B=0.0,A=1.0)"
color_expr = unreal.MaterialNodeService.create_parameter(path, "Vector", "BaseColor", "Surface", "0.8,0.2,0.2,1.0", -500, 0)
unreal.MaterialNodeService.connect_to_output(path, color_expr.id, "", "BaseColor")
# Add roughness
rough_expr = unreal.MaterialNodeService.create_parameter(path, "Scalar", "Roughness", "Surface", "0.5", -500, 100)
unreal.MaterialNodeService.connect_to_output(path, rough_expr.id, "", "Roughness")
unreal.MaterialService.compile_material(path)
unreal.EditorAssetLibrary.save_asset(path)
# Auto-layout: arrange nodes in clean left-to-right columns
unreal.MaterialNodeService.layout_expressions(path)
import unreal
# create_instance returns MaterialCreateResult — extract .asset_path
result = unreal.MaterialService.create_instance("/Game/Materials/M_Character", "MI_PlayerRed", "/Game/Materials/")
if not result.success:
print(f"FAILED: {result.error_message}")
instance_path = result.asset_path
unreal.MaterialService.set_instance_vector_parameter(instance_path, "BaseColor", 1.0, 0.0, 0.0, 1.0)
unreal.MaterialService.set_instance_scalar_parameter(instance_path, "Roughness", 0.3)
unreal.EditorAssetLibrary.save_asset(instance_path)
import unreal
tex_expr = unreal.MaterialNodeService.create_parameter(path, "Texture", "DiffuseMap", "Textures", "", -500, 0)
unreal.MaterialNodeService.connect_to_output(path, tex_expr.id, "", "BaseColor")
unreal.MaterialService.compile_material(path)
import unreal
path = "/Game/Materials/M_Tint"
# All create_* calls return expression info objects — use .id
color_expr = unreal.MaterialNodeService.create_parameter(path, "Vector", "TintColor", "Surface", "", -600, 0)
mult_expr = unreal.MaterialNodeService.create_expression(path, "Multiply", -300, 0)
intensity_expr = unreal.MaterialNodeService.create_parameter(path, "Scalar", "Intensity", "Surface", "1.0", -600, 100)
unreal.MaterialNodeService.connect_expressions(path, color_expr.id, "", mult_expr.id, "A")
unreal.MaterialNodeService.connect_expressions(path, intensity_expr.id, "", mult_expr.id, "B")
unreal.MaterialNodeService.connect_to_output(path, mult_expr.id, "", "BaseColor")
unreal.MaterialService.compile_material(path)
import unreal
unreal.MaterialService.set_blend_mode(path, "Translucent") # For transparency
unreal.MaterialService.set_shading_model(path, "DefaultLit")
unreal.MaterialService.set_two_sided(path, True)
unreal.MaterialService.compile_material(path)
import unreal
info = unreal.MaterialService.get_material_info("/Game/Materials/M_Character")
if info:
print(f"Material: {info.name}, Blend Mode: {info.blend_mode}")
Use for MaterialFunction references (e.g., /Engine/Functions/...):
import unreal
path = "/Game/Materials/M_Complex"
# create_function_call returns MaterialExpressionInfo — use .id
func_expr = unreal.MaterialNodeService.create_function_call(
path,
"/Engine/Functions/Engine_MaterialFunctions02/Utility/BlendAngleCorrectedNormals",
-600, 0
)
# Connect like any other node
unreal.MaterialNodeService.connect_to_output(path, func_expr.id, "", "Normal")
unreal.MaterialService.compile_material(path)
import unreal
path = "/Game/Materials/M_Custom"
# create_custom_expression returns MaterialExpressionInfo — use .id
custom_expr = unreal.MaterialNodeService.create_custom_expression(
path,
"return sin(Time * Speed);", # HLSL code
"SineWave", # Description
"Float1", # OutputType: Float1, Float2, Float3, Float4, MaterialAttributes
"Time,Speed", # Comma-separated input names
-500, 0
)
# Connect inputs and outputs normally
unreal.MaterialNodeService.connect_to_output(path, custom_expr.id, "", "EmissiveColor")
unreal.MaterialService.compile_material(path)
Reference a parameter from a MaterialParameterCollection:
import unreal
path = "/Game/Materials/M_Global"
# create_collection_parameter returns MaterialExpressionInfo — use .id
coll_expr = unreal.MaterialNodeService.create_collection_parameter(
path,
"/Game/Materials/MPC_GlobalParams", # collection asset path
"WindStrength", # parameter name in collection
-500, 0
)
unreal.MaterialNodeService.connect_expressions(path, coll_expr.id, "", some_mult_id, "B")
unreal.MaterialService.compile_material(path)
import unreal
path = "/Game/Materials/M_Switchable"
# Create a static switch (compile-time boolean branch)
switch_id = unreal.MaterialNodeService.create_parameter(
path, "StaticSwitch", "UseDetailTexture", "Features", "true", -600, 0
)
# StaticSwitch has True/False/Value inputs — connect other nodes to those
import unreal
path = "/Game/Materials/M_Objects"
# TextureObject exposes texture without a sampler (for custom sampling)
tex_obj_id = unreal.MaterialNodeService.create_parameter(
path, "TextureObject", "DetailMap", "Textures",
"/Game/Textures/T_Detail.T_Detail", -500, 0
)
Create many nodes in a single transaction (much faster than individual calls):
import unreal
path = "/Game/Materials/M_Complex"
# Arrays must be same length — one entry per node
types = ["Multiply", "Add", "Lerp", "OneMinus"]
names = ["Mult1", "Add1", "Lerp1", "Invert1"] # optional display names (use "" to skip)
x_positions = [-400, -400, -200, -600]
y_positions = [0, 200, 100, 0]
result = unreal.MaterialNodeService.batch_create_expressions(
path, types, names, x_positions, y_positions
)
# result contains all created node IDs
Wire up many connections in a single transaction:
import unreal
path = "/Game/Materials/M_Complex"
# Each array entry defines one connection
source_ids = [color_id, rough_id, mult_id]
output_names = ["", "", ""] # "" = first output
target_ids = [mult_id, mult_id, ""] # "" = material output
input_names = ["A", "B", "BaseColor"]
result = unreal.MaterialNodeService.batch_connect_expressions(
path, source_ids, output_names, target_ids, input_names
)
Set many properties across multiple nodes in one call:
import unreal
path = "/Game/Materials/M_Complex"
node_ids = [tex_id, tex_id, const_id]
property_names = ["Texture", "SamplerType", "R"]
property_values = ["/Game/Textures/T_Diffuse", "SAMPLERTYPE_Color", "0.5"]
result = unreal.MaterialNodeService.batch_set_properties(
path, node_ids, property_names, property_values
)
Export the entire material graph for analysis or recreation:
import unreal
path = "/Game/Materials/M_Landscape"
# Get full JSON representation of the material graph
json_str = unreal.MaterialNodeService.export_material_graph(path)
# Parse and inspect
import json
graph = json.loads(json_str)
print(f"Expressions: {len(graph['expressions'])}")
print(f"Connections: {len(graph['connections'])}")
# Each expression includes: id, class, class_full, pos_x, pos_y,
# properties (dict), inputs (list), outputs (list)
# Parameter expressions also have: is_parameter, parameter_name, group
# Function calls have: function_path
# Custom expressions have: hlsl_code, output_type, custom_input_names
for expr in graph['expressions']:
print(f" {expr['class']} at ({expr['pos_x']}, {expr['pos_y']})")
if expr.get('is_parameter'):
print(f" Parameter: {expr['parameter_name']} (group: {expr['group']})")
Top-level keys:
material — {blend_mode, shading_model, two_sided}expressions — array of expression objectsconnections — array of connection objectsoutput_connections — array of material output connection objectsExpression object keys:
id — unique expression identifier (use for connections)class — short class name (e.g. "Add", "Multiply", "ScalarParameter")class_full — full UE class name (e.g. "MaterialExpressionAdd")pos_x, pos_y — editor position (NOT x/y)properties — dict of editable property name→value (excludes ParameterName, Group)inputs — list of input pin namesoutputs — list of output pin namesis_parameter — true for parameter expressionsparameter_name — the parameter's display name (only on parameters)group — parameter group name (only on parameters)function_path — material function asset path (only on function calls)hlsl_code — HLSL code string (only on Custom expressions)custom_input_names — comma-separated input names (only on Custom expressions)landscape_layers — array of layer configs (only on LandscapeLayerBlend)Connection object keys:
source_id — expression ID of the source nodesource_output_index — integer index of the source output pinsource_output_name — name of the source output pin (empty string for default pin)target_id — expression ID of the target nodetarget_input — name of the target input pin (NOT target_input_name)Output connection object keys (material property connections):
property — material property name (e.g. "BaseColor", "Normal")expression_id — expression ID connected to this outputoutput_index — which output pin index of the expressionoutput_name — the output pin name (empty string for default)When setting material properties via set_property, enum values accept:
"BLEND_Masked", "MSM_DefaultLit", "MD_Surface""Masked", "DefaultLit", "Surface"Use export_material_graph + batch operations for material recreation:
import unreal, json
# 1. Export source material
source_json = unreal.MaterialNodeService.export_material_graph("/Game/Materials/M_Source")
graph = json.loads(source_json)
# 2. Create new material — extract .asset_path from result
result = unreal.MaterialService.create_material("M_Source_Copy", "/Game/Materials/")
new_path = result.asset_path
# 3. Set material properties
mat = graph['material']
unreal.MaterialService.set_blend_mode(new_path, mat['blend_mode'])
unreal.MaterialService.set_shading_model(new_path, mat['shading_model'])
# 4. Batch create all expressions
# Use class_full or class (both accepted by create APIs)
types = [e['class'] for e in graph['expressions']]
x_positions = [e['pos_x'] for e in graph['expressions']]
y_positions = [e['pos_y'] for e in graph['expressions']]
# For function calls and specialized types, use batch_create_specialized
# For generic types, use batch_create_expressions
# 5. Set parameter names FIRST (before batch_set_properties)
# The export's parameter_name field has the correct name
for expr in graph['expressions']:
if expr.get('is_parameter'):
unreal.MaterialNodeService.set_expression_property(
new_path, new_id_map[expr['id']], "ParameterName", expr['parameter_name'])
if expr.get('group'):
unreal.MaterialNodeService.set_expression_property(
new_path, new_id_map[expr['id']], "Group", expr['group'])
# 6. Batch set all other properties (ParameterName/Group excluded from export)
# 7. Batch connect using connections array
source_ids = [new_id_map[c['source_id']] for c in graph['connections']]
output_names = [c['source_output_name'] for c in graph['connections']]
target_ids = [new_id_map[c['target_id']] for c in graph['connections']]
input_names = [c['target_input'] for c in graph['connections']]
unreal.MaterialNodeService.batch_connect_expressions(
new_path, source_ids, output_names, target_ids, input_names)
# 8. Connect output_connections (material property outputs)
for oc in graph['output_connections']:
unreal.MaterialNodeService.connect_to_output(
new_path, new_id_map[oc['expression_id']],
oc['output_name'], oc['property'])
# 9. Compile
unreal.MaterialService.compile_material(new_path)
For creating and editing Material Functions (reusable node subgraphs):
MaterialNodeService.get_function_info(function_path) to inspect a function's inputs/outputsMaterialNodeService.export_function_graph(function_path) to export a function's complete node graph as JSONMaterialNodeService.create_material_function(name, dir) to create new functionsMaterialNodeService.add_function_input/output(...) to add I/O pinsMaterialNodeService.create_function_call(mat_path, func_path) to reference functions in material graphslandscape-auto-material skill for landscape-specific material function patternsLandscapeLayerBlend nodestools
Create and manage Niagara particle systems - system lifecycle, adding/copying emitters, user parameters, system script settings, scratch-pad authoring
development
Configure Niagara emitter internals - modules, renderers, rapid iteration parameters, graph positioning, and scratch-pad authoring (Custom HLSL + node graph)
tools
Create and modify Blueprint assets, variables, functions, and components
tools
Search, find, open, move, duplicate, save, delete, import, and export assets