Materials logo

Materials

Community
kevinpbuckley
materials

Create and edit materials and material instances — graph nodes, parameters, functions, custom HLSL, and instance overrides (MaterialService + MaterialNodeService). Use when the user asks to create or edit a material/material instance, wire material nodes, add material parameters, set blend/shading modes, or recreate a material graph. For landscape materials load landscape-materials. Also use for generated organic PBR detail, texture seams, projection coordinates, and surface-normal diagnosis.

Overview

Publisherkevinpbuckley
RepositoryVibeUE
Skill namematerials
Stars
735
Forks
161
Bundled files
7
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 7 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by kevinpbuckley on GitHub. Read the source before you install it.

Installation

Install the Materials AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/kevinpbuckley/VibeUE/tree/master/Content/Skills/materials
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/kevinpbuckley/VibeUE.git /tmp/VibeUE
mkdir -p .claude/skills
cp -r /tmp/VibeUE/Content/Skills/materials .claude/skills/materials
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Materials in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Materials on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Materials is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

🧠 Brains complement: IF an unreal-engine-skills-manager tool (external MCP) exists in this session, call it with {action: "load", skill: "materials-and-shaders"} for UE domain knowledge on this topic — correct APIs, architecture, best practices — and treat it as the rubric for any review / "best practices" question. If no such tool is available (e.g. running under Claude Code or Codex without that MCP), skip this line entirely and proceed with this skill alone — do NOT attempt the call.

Material System Skill

For generated textures on large organic surfaces, seams after triplanar projection, or overly strong tissue relief, read generated organic surfaces. For gray/default surfaces after mesh reimport, use the asset-management skill's LOD-section mapping helper before changing the shader or buying new textures.

Critical Rules

🚨 Connecting Function Call Inputs: Use Bare Names, Not Display Names

get_expression_pins(mat, expr_id) (MaterialNodeService) returns input pins with display labels including type suffixes like "TextureObject (T2d)", "TextureSize (V3)", "Use High Quality Normals (SB)". Connections are made through the engine MaterialTools.connect_expressions call, and UE's connect API matches against the bare input name only — never pass these display labels as to_input_name:

python
MT = "editor_toolset.toolsets.material.MaterialTools"
# ❌ WRONG — display label with type suffix; connection fails silently or gets ignored
call_tool(tool_name="connect_expressions", toolset_name=MT,
          arguments={"from_expression": src_id, "from_output_name": "",
                     "to_expression": fn_id, "to_input_name": "TextureObject (T2d)"})

# ✅ CORRECT — bare name
call_tool(tool_name="connect_expressions", toolset_name=MT,
          arguments={"from_expression": src_id, "from_output_name": "",
                     "to_expression": fn_id, "to_input_name": "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_pins returns is_connected=True for 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 with unreal.MaterialEditingLibrary.get_used_textures(mat) after wiring — if it returns 0 on a material that samples textures, your connections are phantom.

🚨 Verifying Wiring: Use get_material_diagnostics

MaterialNodeService.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:

python
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.
  • Type mismatch errors when wiring scalars to vector inputs without proper conversion.
  • Missing required inputs on function calls.

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. Asset-editor capture now goes through the engine EditorAppToolset (toolset_name EditorToolset.EditorAppToolset via call_tool) — it opens, focuses, and captures the asset editor. (The old unreal.ScreenshotService and its capture_asset_editor are gone.) After the capture, attach_image(...) the returned file to inspect it.

⚠️ 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:

python
ed = unreal.get_editor_subsystem(unreal.AssetEditorSubsystem)
for path in [other_open_paths]:
    ed.close_all_editors_for_asset(unreal.load_asset(path))

🚨 Tiling on Basic Shapes: Prefer a Child MI with Tiling Override

For 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.

python
# ✅ CORRECT pattern for "this disc shows brick too sparse / too dense"
# Material instances are created and overridden through the engine MaterialInstanceTools toolset.
MIT = "editor_toolset.toolsets.material_instance.MaterialInstanceTools"
call_tool(tool_name="create_material_instance", toolset_name=MIT,
          arguments={"parent_material": "/Game/Fab/Megascans/.../MI_xevtfjz",
                     "asset_name": "MI_xevtfjz_Disc", "folder_path": "/Game/Materials/"})
ip = "/Game/Materials/MI_xevtfjz_Disc"
call_tool(tool_name="set_scalar_parameter", toolset_name=MIT,
          arguments={"instance_path": ip, "parameter_name": "Tiling", "value": 4.0})
unreal.EditorAssetLibrary.save_asset(ip)
# assign ip 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.

🚨 Inspect Before Modifying Existing Materials

Before adding, removing, or reconnecting nodes in an existing material, you MUST export and review the current graph first:

python
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:

  • The material may already have nodes connected to the output you want to modify
  • Blindly adding nodes creates duplicates and orphaned connections
  • You need to know what IDs exist so you can reconnect or replace, not just append
  • export_material_graph returns the ground truth — use it to plan your edits

Workflow for modifying existing materials:

  1. Export the graph JSON and review expressions + connections
  2. Plan your changes — identify which nodes to keep, replace, or add
  3. Disconnect existing connections if needed before reconnecting
  4. Add only the new nodes that don't already exist
  5. Reconnect outputs to their correct sources
  6. Compile and save

🚨 Return Types — create_parameter/create_function_call Return .id, NOT the Raw Object

Material/instance creation now goes through the engine toolsets (MaterialTools.create_material returns the Material object itself — not a result-with-.asset_path; MaterialInstanceTools creates the instance), so build the asset path yourself from folder_path + asset_name:

python
# Engine: create_material returns the Material object (no .asset_path) — reference by path you chose.
call_tool(tool_name="create_material",
          toolset_name="editor_toolset.toolsets.material.MaterialTools",
          arguments={"folder_path": "/Game/Materials/", "asset_name": "M_MyMat"})
path = "/Game/Materials/M_MyMat"

# Engine: create the instance via MaterialInstanceTools, then reference the path you chose.
call_tool(tool_name="create_material_instance",
          toolset_name="editor_toolset.toolsets.material_instance.MaterialInstanceTools",
          arguments={"parent_material": "/Game/Materials/M_Base",
                     "asset_name": "MI_Red", "folder_path": "/Game/Materials/"})
instance_path = "/Game/Materials/MI_Red"

The surviving VibeUE graph-authoring calls still return result objects — extract the field:

python
# create_parameter / 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

⚠️ Passing a MaterialExpressionInfo where a string is expected gives: TypeError: Nativize: Cannot nativize 'MaterialExpressionInfo' as 'String'

⚠️ Two Services

  • MaterialService - Create materials, instances, manage properties
  • MaterialNodeService - Build material graphs, expressions, parameters

⚠️ Compile After Graph Changes

Full graph workflow: create nodes → connect nodes → connect to material output → compile → save.

python
mns = unreal.MaterialNodeService
expr = mns.create_parameter(path, "Vector", "BaseColor", ...)

# Wire an expression output straight into a material output (BaseColor, Normal, Roughness, ...).
# output_name="" selects output 0; property accepts "BaseColor" or "MP_BaseColor" (case-insensitive).
mns.connect_expression_to_output(path, expr.id, "", "BaseColor")   # returns True only after a read-back
# ...and to clear it again:  mns.disconnect_output(path, "BaseColor")

unreal.MaterialService.compile_material(path)  # REQUIRED after graph changes
unreal.EditorAssetLibrary.save_asset(path)

⚠️ expr.id is session-specific ("<Class>_<pointer>") — it changes on editor restart. For a durable reference use expr.object_path, which every id-based call (connect_expression_to_output, get_expression_details, set_expression_property, ...) also accepts. Empty / bogus / foreign ids are now rejected (they used to silently resolve to expression index 0).

The engine's own MaterialTools.connect_to_output (call_tool, material_property="MP_BaseColor") still works and is interchangeable — use whichever fits the surrounding code.

⚠️ Parameter Types

TypeUseExample Value
ScalarSingle float0.5
VectorColor/vector(R=1.0,G=0.0,B=0.0,A=1.0)
TextureTexture parameter/Game/T_Diffuse.T_Diffuse
TextureObjectTexture object (no sampler)/Game/T_Diffuse.T_Diffuse
StaticSwitchStatic bool switchtrue or false

⚠️ Material Output Names

  • BaseColor, Metallic, Specular, Roughness
  • EmissiveColor, Normal, Opacity, OpacityMask
  • WorldPositionOffset, AmbientOcclusion

⚠️ Discovering Node Types & Categories

To answer "what nodes / categories are available", use the MaterialNodeService discovery calls — do not hunt through discover_python_module or a material-editor palette API (there isn't a Python one):

python
# All material-expression categories (Math, Color, Constants, Coordinates, Parameters, ...)
cats = unreal.MaterialNodeService.get_categories()

# Search node types by name/category (returns MaterialExpressionTypeInfo:
#   class_name, display_name, category, description, is_parameter)
types = unreal.MaterialNodeService.discover_types(category="", search_term="Add", max_results=100)

To create a node, pass the bare class name minus the MaterialExpression prefix ("Add", "Multiply", "Constant3Vector", "OneMinus", "LinearInterpolate"/"Lerp") as expression_class to the engine MaterialTools.add_expression call (single node), or to the surviving MaterialNodeService.batch_create_expressions (many nodes in one transaction).

⚠️ Check Node Existence

python
# 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)

⚠️ Property Names

Use discover_python_class() first (batch: class_name='unreal.A, unreal.B'):

  • MaterialExpressionTypeInfo uses display_name, NOT name
  • MaterialOutputConnectionInfo uses connected_expression_id, NOT expression_id

Complete field lists (don't guess):

StructFields
MaterialExpressionInfo (from list_expressions / get_expression_info)id, class_name, display_name, category, description, pos_x, pos_y, is_parameter, parameter_name, input_names, output_names — the class is class_name, not expression_class
MaterialNodePropertyInfo (from expression property lists)name, value, property_type, is_editable — it's name here, not property_name
MaterialSummary (from MaterialService.summarize)material_name, material_path, blend_mode, shading_model, material_domain, two_sided, expression_count, parameter_count, parameter_names, key_properties, editable_properties — it's material_name/material_path/parameter_count, not name/path/num_parameters
MaterialDetailedInfo (from MaterialService.get_material_info)material_name, material_path, blend_mode, shading_model, material_domain, two_sided, expression_count, texture_sample_count, is_material_instance, parent_material, parameters — it's two_sided, not is_two_sided
MaterialPropertyInfo_Custom (from MaterialService.list_properties / get_property_info)property_name, display_name, property_type, current_value, allowed_values, category, is_editable, is_advanced — it's property_name/current_value, not name/value (differs from MaterialNodePropertyInfo above)

🚨 Setting Material Properties: set_property Takes Display OR Internal Name

MaterialService.set_property(path, name, value) / get_property accept either the editor display name ("Two Sided", "Blend Mode", "Opacity Mask Clip Value") or the internal property name ("TwoSided", "BlendMode", "OpacityMaskClipValue") — matching is case-insensitive and ignores spaces. Both of these work:

python
unreal.MaterialService.set_property(path, "Two Sided", "true")   # display name
unreal.MaterialService.set_property(path, "BlendMode", "BLEND_Masked")  # internal name
unreal.MaterialService.set_property(path, "OpacityMaskClipValue", "0.33")
unreal.MaterialService.compile_material(path)   # enum/blend changes need a recompile
unreal.MaterialService.save_material(path)

⚠️ set_property returns a bool (True/False), it does not raise on an unknown property — a False means the name didn't resolve or the value didn't parse. Don't assume success: check the return, or verify with get_property / summarize afterward. Enum values are the BLEND_* / MSM_* / MD_* identifiers (see below), not the friendly labels.

Discover legal enum values, don't guess: list_properties / get_property_info populate allowed_values for enum properties — including the classic TEnumAsByte ones (BlendMode, ShadingModel, MaterialDomain). Read them instead of probing unreal for the enum class:

python
for p in unreal.MaterialService.list_properties(path):
    if p.property_name == "BlendMode":
        print(p.allowed_values)
        # ['BLEND_Opaque','BLEND_Masked','BLEND_Translucent','BLEND_Additive', ...]

Common material property internal names: TwoSided, BlendMode, ShadingModel, MaterialDomain, OpacityMaskClipValue, bUsedWithStaticLighting, DitheredLODTransition.


Task Index

TaskWorkflowSample script
Create a material (+ params)workflows.md → Create a basic materialscripts/create_material.txt
Create a material instanceworkflows.md → Create a material instancescripts/create_instance.txt
Add texture / math / function / HLSL nodesworkflows.md—
Build a graph with batch opsworkflows.md → Batch create/connect/setscripts/material_graph_batch.txt
Inspect/export an existing graphreference.md → Export JSON schemascripts/export_graph.txt
Recreate a material from exportreference.md → Recreate from export—
Verify wiring / compileCritical Rules → get_material_diagnostics—

Sub-docs

  • workflows.md — create material/instance, texture/math/function/HLSL/collection/switch nodes, set properties, and batch create/connect/set.
  • reference.md — parameter types, output names, enum formats, the export_material_graph JSON schema, recreate-from-export steps, and Material Function authoring.

Verification

After non-trivial wiring run MaterialNodeService.get_material_diagnostics(path) and confirm is_compiled_ok and the expected referenced_texture_paths — do not rely on get_used_textures (unreliable for multi-branch graphs). compile_material + save_asset to persist.

Related skills

  • landscape-materials — LandscapeLayerBlend nodes.
  • landscape-auto-material — production landscape materials (functions, RVT, instances).

Additional gotchas

  • StaticMesh.get_editor_property("static_materials") returns struct COPIES, so writing the array back is a silent no-op; use set_material(index, mat) and confirm with get_material(index).
  • MEL.set_material_instance_scalar_parameter_value returns False on success, and the static-switch setter returns False while the write still lands — verify by readback; MI reads return 0.0 and writes no-op while PIE runs.
  • Changing body_setup.collision_trace_flag does not rebuild the physics mesh; toggle StaticMeshEditorSubsystem.enable_section_collision off then on.
  • Enumerate expressions with ObjectIterator(unreal.MaterialExpression) filtered on path.startswith(mat_path + ":") (class-specific iterators miss nodes); delete_material_expression enables in-place rebuilds that keep instance overrides when parameter names match.
  • Pin names: StaticSwitchParameter inputs are True/False; LandscapeLayerBlend inputs are "Layer <name>"/"Height <name>"; LayerBlendInput fields are invisible to dir() but set_editor_property("layer_name"|"blend_type"|"preview_weight") works.
  • Gate optional master-material features behind STATIC switches (a scalar "off" still samples); measure with MEL.get_statistics. A material used on a Niagara sprite renderer needs bUsedWithNiagaraSprites.
  • Delete a material with AssetDiscoveryService.delete_asset_unattended; it refuses (and returns a result struct naming the holder) when the material is still referenced or held by a Python global, instead of wedging on a modal dialog.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Materials AI skill do?

Create and edit materials and material instances — graph nodes, parameters, functions, custom HLSL, and instance overrides (MaterialService + MaterialNodeService). Use when the user asks to create or edit a material/material instance, wire material nodes, add material parameters, set blend/shading modes, or recreate a material graph. For landscape materials load landscape-materials. Also use for generated organic PBR detail, texture seams, projection coordinates, and surface-normal diagnosis.

Why use Materials on TypingMind?

Because you install it once and use it with any model. Materials is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Materials in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/kevinpbuckley/VibeUE/tree/master/Content/Skills/materials. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Materials?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Materials?

As many as you like. As long as a model supports skills, you can use Materials with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Materials AI skill free?

Yes. It is published on GitHub by kevinpbuckley under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇