Create Material Instances from Selection

An Unreal Engine Editor Utility that batch-creates new MaterialInstanceConstant assets from a chosen parent material, using a selection of existing material instances as a texture source — harvesting their assigned textures and automatically mapping them across to the new instances by semantic keyword, with no manual slot assignment required.

Preview

The Idea

Creating a batch of material instances for a new parent material is a repetitive task in production — especially when you already have existing instances whose textures are correctly assigned and just need to be brought across. The standard workflow is to create each new instance by hand, then re-drag every texture into the correct slot. This tool automates the entire process: select any number of existing material instances in the Content Browser, set the desired parent material in the details panel, and run the action. New instances are created from that parent, named, and populated with textures pulled from the selected instances — all in one pass.

Editor Utility Entry Point

The function is exposed to the editor via UFUNCTION(CallInEditor), which adds a button directly to the details panel of the utility Blueprint. The parent material is passed as a parameter — also visible and assignable in the details panel — so the tool requires no code changes between uses on different projects or parent materials.

UFUNCTION(CallInEditor, Category = "Material Actions")
void CreateNewMaterialInstancesFromSelected(UMaterialInterface* ParentMaterial);

At runtime the function first validates the parent material and early-outs with a log error if it is missing, then grabs the current Content Browser selection via UEditorUtilityLibrary::GetSelectedAssets() and iterates over each asset that casts successfully to UMaterialInstanceConstant.

Texture Harvesting

The first step for each selected material instance is to read and store all of its texture parameter values into a name-keyed map. The selected instances are used purely as a texture source — they are never modified. GetAllTextureParameterInfo returns every parameter the instance exposes, and GetTextureParameterValue resolves the actual texture asset assigned to each one. This snapshot is taken before any new assets are created.

TArray<FMaterialParameterInfo> ParamInfos;
TArray<FGuid> ParamGuids;
OldMI->GetAllTextureParameterInfo(ParamInfos, ParamGuids);

TMap<FName, UTexture*> OldParamValues;
for (const FMaterialParameterInfo& Info : ParamInfos)
{
    UTexture* Value;
    if (OldMI->GetTextureParameterValue(Info, Value))
        OldParamValues.Add(Info.Name, Value);
}

Name Normalisation & Uniqueness

The new asset name is derived from the old instance's name with common material prefixes stripped first — M_, MI_, and MLI_ — so the output is always consistently prefixed MI_ regardless of what convention the source asset used. CreateUniqueAssetName is then called to avoid collisions with anything already in the same content folder, and the final name is extracted from the returned package path.

OldName.RemoveFromStart(TEXT("M_"));
OldName.RemoveFromStart(TEXT("MI_"));
OldName.RemoveFromStart(TEXT("MLI_"));
FString NewName = "MI_" + OldName;

FString PackageName, UniqueAssetName;
AssetTools.Get().CreateUniqueAssetName(Path / NewName, TEXT(""), PackageName, UniqueAssetName);
FString FinalName = FPaths::GetBaseFilename(PackageName);

Fuzzy Parameter Matching

Once the new instance exists, the tool queries the new parent's parameter list and cross-references it against the harvested textures using keyword matching. Both parameter names are lowercased and checked for shared semantic tokens — color, normal, rough, mask, metal, emissive — so textures land in the right slots even when the selected source instances and the new parent use different naming conventions. Common normal map variants (NAM, NA) are also covered for studio-specific conventions.

if (ParamOldName.Contains("color")    && ParamNewName.Contains("color")    ||
    ParamOldName.Contains("normal")   && ParamNewName.Contains("normal")   ||
    ParamOldName.Contains("NAM")      && ParamNewName.Contains("normal")   ||
    ParamOldName.Contains("rough")    && ParamNewName.Contains("rough")    ||
    ParamOldName.Contains("mask")     && ParamNewName.Contains("mask")     ||
    ParamOldName.Contains("metal")    && ParamNewName.Contains("metal")    ||
    ParamOldName.Contains("emissive") && ParamNewName.Contains("emissive"))
{
    NewMI->SetTextureParameterValueEditorOnly(NewParam.Name, OldPair.Value);
    UE_LOG(LogTemp, Log, TEXT("Mapped %s → %s"),
           *OldPair.Key.ToString(), *NewParam.Name.ToString());
    break;
}

Each successful mapping is logged so it is easy to verify in the Output Log that every texture landed on the expected slot.

Finalisation & Save

After texture assignment, any material layer functions are cleared by assigning an empty FMaterialLayersFunctions struct — ensuring the new instance starts clean under its parent rather than carrying over any layer data. The instance is then finalised with PostEditChange, marked dirty, and saved to disk immediately so the Content Browser reflects the new assets without a manual save step.

// Clear stale material layers from old parent
FMaterialLayersFunctions EmptyLayers;
NewMI->SetMaterialLayers(EmptyLayers);

NewMI->PostEditChange();
(void)NewMI->MarkPackageDirty();
UEditorAssetLibrary::SaveLoadedAsset(NewMI);