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);