963 lines
35 KiB
C++
963 lines
35 KiB
C++
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "MCPHandler.h"
|
|
#include "MCPAssetFinder.h"
|
|
#include "MCPUtils.h"
|
|
#include "Materials/Material.h"
|
|
#include "MaterialDomain.h"
|
|
#include "Materials/MaterialInstanceConstant.h"
|
|
#include "Materials/MaterialFunction.h"
|
|
#include "Materials/MaterialExpression.h"
|
|
#include "Materials/MaterialExpressionScalarParameter.h"
|
|
#include "Materials/MaterialExpressionVectorParameter.h"
|
|
#include "Materials/MaterialExpressionTextureObjectParameter.h"
|
|
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
|
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
|
#include "Materials/MaterialExpressionConstant.h"
|
|
#include "Materials/MaterialExpressionConstant3Vector.h"
|
|
#include "Materials/MaterialExpressionConstant4Vector.h"
|
|
#include "Materials/MaterialExpressionTextureSample.h"
|
|
#include "Materials/MaterialExpressionTextureCoordinate.h"
|
|
#include "Materials/MaterialExpressionComponentMask.h"
|
|
#include "Materials/MaterialExpressionCustom.h"
|
|
#include "Materials/MaterialExpressionFunctionInput.h"
|
|
#include "Materials/MaterialExpressionFunctionOutput.h"
|
|
#include "Materials/MaterialExpressionMaterialFunctionCall.h"
|
|
#include "MaterialGraph/MaterialGraph.h"
|
|
#include "MaterialGraph/MaterialGraphNode.h"
|
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
|
#include "Kismet2/BlueprintEditorUtils.h"
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "AssetRegistry/IAssetRegistry.h"
|
|
#include "EdGraph/EdGraph.h"
|
|
#include "EdGraph/EdGraphNode.h"
|
|
#include "MCPHandlers_MaterialRead.generated.h"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="list_material_assets"))
|
|
class UMCPHandler_ListMaterials : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Optional, Description="Filter string to match against material name or path"))
|
|
FString Filter;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Type filter: 'all', 'material', or 'instance'"))
|
|
FString Type;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("List Material and MaterialInstance assets, optionally filtered by name and type.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
bool bIncludeMaterials = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("material");
|
|
bool bIncludeInstances = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("instance");
|
|
|
|
MCPAssets<UMaterial> Assets;
|
|
if (bIncludeMaterials) Assets.Scan(UMaterial::StaticClass());
|
|
if (bIncludeInstances) Assets.Scan(UMaterialInstanceConstant::StaticClass());
|
|
Assets.Substring(Filter).NoDerived().Info();
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Entries;
|
|
for (const FAssetData& Asset : Assets.AllData())
|
|
{
|
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString());
|
|
Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString());
|
|
Entry->SetStringField(TEXT("type"),
|
|
Asset.AssetClassPath.GetAssetName() == TEXT("MaterialInstanceConstant")
|
|
? TEXT("MaterialInstance") : TEXT("Material"));
|
|
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
|
}
|
|
|
|
Result->SetNumberField(TEXT("count"), Entries.Num());
|
|
Result->SetArrayField(TEXT("materials"), Entries);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="dump_material"))
|
|
class UMCPHandler_GetMaterial : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Material or MaterialInstance name or package path"))
|
|
FString Material;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Get detailed info about a material or material instance, including parameters, usage flags, and referenced textures.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
FString DecodedName = MCPUtils::UrlDecode(Material);
|
|
|
|
// Try loading as UMaterial or UMaterialInstanceConstant
|
|
MCPAssets<UMaterialInterface> Assets;
|
|
Assets.Scan(UMaterial::StaticClass());
|
|
Assets.Scan(UMaterialInstanceConstant::StaticClass());
|
|
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
|
|
UMaterialInterface* LoadedObj = Assets.Object();
|
|
|
|
if (UMaterial* MaterialObj = Cast<UMaterial>(LoadedObj))
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material '%s'"), *MaterialObj->GetName());
|
|
|
|
Result->SetStringField(TEXT("name"), MaterialObj->GetName());
|
|
Result->SetStringField(TEXT("path"), MaterialObj->GetPathName());
|
|
Result->SetStringField(TEXT("type"), TEXT("Material"));
|
|
|
|
// Material domain
|
|
FString DomainStr = TEXT("Unknown");
|
|
if (const UEnum* DomainEnum = StaticEnum<EMaterialDomain>())
|
|
{
|
|
DomainStr = DomainEnum->GetNameStringByValue((int64)MaterialObj->MaterialDomain);
|
|
}
|
|
Result->SetStringField(TEXT("domain"), DomainStr);
|
|
|
|
// Blend mode
|
|
FString BlendModeStr = TEXT("Unknown");
|
|
if (const UEnum* BlendEnum = StaticEnum<EBlendMode>())
|
|
{
|
|
BlendModeStr = BlendEnum->GetNameStringByValue((int64)MaterialObj->BlendMode);
|
|
}
|
|
Result->SetStringField(TEXT("blendMode"), BlendModeStr);
|
|
|
|
// Shading models
|
|
TArray<TSharedPtr<FJsonValue>> ShadingModels;
|
|
FMaterialShadingModelField SMField = MaterialObj->GetShadingModels();
|
|
if (const UEnum* SMEnum = StaticEnum<EMaterialShadingModel>())
|
|
{
|
|
for (int32 i = 0; i < SMEnum->NumEnums() - 1; ++i)
|
|
{
|
|
EMaterialShadingModel SM = (EMaterialShadingModel)SMEnum->GetValueByIndex(i);
|
|
if (SMField.HasShadingModel(SM))
|
|
{
|
|
ShadingModels.Add(MakeShared<FJsonValueString>(SMEnum->GetNameStringByIndex(i)));
|
|
}
|
|
}
|
|
}
|
|
Result->SetArrayField(TEXT("shadingModels"), ShadingModels);
|
|
|
|
// Two-sided
|
|
Result->SetBoolField(TEXT("twoSided"), MaterialObj->IsTwoSided());
|
|
|
|
// Expression count
|
|
auto Expressions = MaterialObj->GetExpressions();
|
|
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
|
|
|
|
// Parameters — iterate expressions for parameter types
|
|
TArray<TSharedPtr<FJsonValue>> Parameters;
|
|
for (UMaterialExpression* Expr : Expressions)
|
|
{
|
|
if (!Expr) continue;
|
|
|
|
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
|
bool bIsParam = false;
|
|
|
|
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
|
{
|
|
bIsParam = true;
|
|
ParamObj->SetStringField(TEXT("name"), SP->ParameterName.ToString());
|
|
ParamObj->SetStringField(TEXT("type"), TEXT("Scalar"));
|
|
ParamObj->SetStringField(TEXT("group"), SP->Group.ToString());
|
|
ParamObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
|
|
}
|
|
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
|
{
|
|
bIsParam = true;
|
|
ParamObj->SetStringField(TEXT("name"), VP->ParameterName.ToString());
|
|
ParamObj->SetStringField(TEXT("type"), TEXT("Vector"));
|
|
ParamObj->SetStringField(TEXT("group"), VP->Group.ToString());
|
|
TSharedRef<FJsonObject> DefVal = MakeShared<FJsonObject>();
|
|
DefVal->SetNumberField(TEXT("r"), VP->DefaultValue.R);
|
|
DefVal->SetNumberField(TEXT("g"), VP->DefaultValue.G);
|
|
DefVal->SetNumberField(TEXT("b"), VP->DefaultValue.B);
|
|
DefVal->SetNumberField(TEXT("a"), VP->DefaultValue.A);
|
|
ParamObj->SetObjectField(TEXT("defaultValue"), DefVal);
|
|
}
|
|
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
|
{
|
|
bIsParam = true;
|
|
ParamObj->SetStringField(TEXT("name"), TP->ParameterName.ToString());
|
|
ParamObj->SetStringField(TEXT("type"), TEXT("Texture"));
|
|
ParamObj->SetStringField(TEXT("group"), TP->Group.ToString());
|
|
if (TP->Texture)
|
|
ParamObj->SetStringField(TEXT("defaultValue"), TP->Texture->GetPathName());
|
|
}
|
|
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
|
{
|
|
bIsParam = true;
|
|
ParamObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString());
|
|
ParamObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
|
|
ParamObj->SetStringField(TEXT("group"), SSP->Group.ToString());
|
|
ParamObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
|
|
}
|
|
|
|
if (bIsParam)
|
|
{
|
|
Parameters.Add(MakeShared<FJsonValueObject>(ParamObj));
|
|
}
|
|
}
|
|
Result->SetArrayField(TEXT("parameters"), Parameters);
|
|
|
|
// Referenced textures
|
|
TArray<TSharedPtr<FJsonValue>> ReferencedTextures;
|
|
auto RefTexObjs = MaterialObj->GetReferencedTextures();
|
|
for (const TObjectPtr<UObject>& TexObj : RefTexObjs)
|
|
{
|
|
if (TexObj)
|
|
{
|
|
ReferencedTextures.Add(MakeShared<FJsonValueString>(TexObj->GetPathName()));
|
|
}
|
|
}
|
|
Result->SetArrayField(TEXT("referencedTextures"), ReferencedTextures);
|
|
|
|
// Graph node count
|
|
int32 GraphNodeCount = 0;
|
|
if (MaterialObj->MaterialGraph)
|
|
{
|
|
GraphNodeCount = MaterialObj->MaterialGraph->Nodes.Num();
|
|
}
|
|
Result->SetNumberField(TEXT("graphNodeCount"), GraphNodeCount);
|
|
|
|
// Usage flags
|
|
TSharedRef<FJsonObject> UsageFlags = MakeShared<FJsonObject>();
|
|
UsageFlags->SetBoolField(TEXT("bUsedWithSkeletalMesh"), MaterialObj->bUsedWithSkeletalMesh != 0);
|
|
UsageFlags->SetBoolField(TEXT("bUsedWithMorphTargets"), MaterialObj->bUsedWithMorphTargets != 0);
|
|
UsageFlags->SetBoolField(TEXT("bUsedWithNiagaraSprites"), MaterialObj->bUsedWithNiagaraSprites != 0);
|
|
UsageFlags->SetBoolField(TEXT("bUsedWithParticleSprites"), MaterialObj->bUsedWithParticleSprites != 0);
|
|
UsageFlags->SetBoolField(TEXT("bUsedWithStaticLighting"), MaterialObj->bUsedWithStaticLighting != 0);
|
|
Result->SetObjectField(TEXT("usageFlags"), UsageFlags);
|
|
|
|
// Opacity mask clip value
|
|
Result->SetNumberField(TEXT("opacityMaskClipValue"), MaterialObj->OpacityMaskClipValue);
|
|
|
|
// Additional settings
|
|
Result->SetBoolField(TEXT("ditheredLODTransition"), MaterialObj->DitheredLODTransition != 0);
|
|
Result->SetBoolField(TEXT("bAllowNegativeEmissiveColor"), MaterialObj->bAllowNegativeEmissiveColor != 0);
|
|
|
|
// Texture sample count (simple expression scan)
|
|
int32 TextureSampleCount = 0;
|
|
for (UMaterialExpression* Expr : Expressions)
|
|
{
|
|
if (Expr && Expr->IsA<UMaterialExpressionTextureSample>())
|
|
{
|
|
TextureSampleCount++;
|
|
}
|
|
}
|
|
Result->SetNumberField(TEXT("textureSampleCount"), TextureSampleCount);
|
|
|
|
return;
|
|
}
|
|
|
|
if (UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(LoadedObj))
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material instance '%s'"), *MI->GetName());
|
|
|
|
Result->SetStringField(TEXT("name"), MI->GetName());
|
|
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
|
Result->SetStringField(TEXT("type"), TEXT("MaterialInstance"));
|
|
|
|
if (MI->Parent)
|
|
{
|
|
Result->SetStringField(TEXT("parent"), MI->Parent->GetName());
|
|
Result->SetStringField(TEXT("parentPath"), MI->Parent->GetPathName());
|
|
}
|
|
|
|
// Overridden parameters
|
|
TArray<TSharedPtr<FJsonValue>> OverriddenParams;
|
|
|
|
// Scalar parameters
|
|
for (const FScalarParameterValue& Param : MI->ScalarParameterValues)
|
|
{
|
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
PObj->SetStringField(TEXT("type"), TEXT("Scalar"));
|
|
PObj->SetNumberField(TEXT("value"), Param.ParameterValue);
|
|
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
|
}
|
|
|
|
// Vector parameters
|
|
for (const FVectorParameterValue& Param : MI->VectorParameterValues)
|
|
{
|
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
PObj->SetStringField(TEXT("type"), TEXT("Vector"));
|
|
TSharedRef<FJsonObject> Val = MakeShared<FJsonObject>();
|
|
Val->SetNumberField(TEXT("r"), Param.ParameterValue.R);
|
|
Val->SetNumberField(TEXT("g"), Param.ParameterValue.G);
|
|
Val->SetNumberField(TEXT("b"), Param.ParameterValue.B);
|
|
Val->SetNumberField(TEXT("a"), Param.ParameterValue.A);
|
|
PObj->SetObjectField(TEXT("value"), Val);
|
|
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
|
}
|
|
|
|
// Texture parameters
|
|
for (const FTextureParameterValue& Param : MI->TextureParameterValues)
|
|
{
|
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
PObj->SetStringField(TEXT("type"), TEXT("Texture"));
|
|
if (Param.ParameterValue)
|
|
PObj->SetStringField(TEXT("value"), Param.ParameterValue->GetPathName());
|
|
else
|
|
PObj->SetStringField(TEXT("value"), TEXT("None"));
|
|
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
|
}
|
|
|
|
// Static switch parameters
|
|
for (const FStaticSwitchParameter& Param : MI->GetStaticParameters().StaticSwitchParameters)
|
|
{
|
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
PObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
|
|
PObj->SetBoolField(TEXT("value"), Param.Value);
|
|
PObj->SetBoolField(TEXT("overridden"), Param.bOverride);
|
|
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
|
}
|
|
|
|
Result->SetArrayField(TEXT("overriddenParameters"), OverriddenParams);
|
|
|
|
return;
|
|
}
|
|
|
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName));
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="dump_material_expression_graph"))
|
|
class UMCPHandler_GetMaterialGraph : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Material name or package path"))
|
|
FString Material;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Get the serialized expression graph for a material, including all nodes and connections.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
FString DecodedName = MCPUtils::UrlDecode(Material);
|
|
|
|
MCPAssets<UMaterial> Assets;
|
|
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
|
|
UMaterial* MaterialObj = Assets.Object();
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *MaterialObj->GetName());
|
|
|
|
// Ensure the material graph is built
|
|
if (!MaterialObj->MaterialGraph)
|
|
{
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — MaterialGraph is null, attempting rebuild"));
|
|
// The material graph is built lazily by the material editor; force-create it
|
|
MaterialObj->MaterialGraph = CastChecked<UMaterialGraph>(
|
|
FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
|
|
MaterialObj->MaterialGraph->Material = MaterialObj;
|
|
MaterialObj->MaterialGraph->RebuildGraph();
|
|
}
|
|
|
|
if (!MaterialObj->MaterialGraph)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
|
|
}
|
|
|
|
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(MaterialObj->MaterialGraph);
|
|
if (!GraphJson.IsValid())
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to serialize material graph"));
|
|
}
|
|
|
|
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
|
|
|
|
// Add material name context
|
|
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
|
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="describe_material_in_english"))
|
|
class UMCPHandler_DescribeMaterial : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Material name or package path"))
|
|
FString Material;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Generate a human-readable description of a material by tracing its expression graph from the root node inputs.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
MCPAssets<UMaterial> Assets;
|
|
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
UMaterial* MaterialObj = Assets.Object();
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *MaterialObj->GetName());
|
|
|
|
// Ensure material graph is built
|
|
if (!MaterialObj->MaterialGraph)
|
|
{
|
|
MaterialObj->MaterialGraph = CastChecked<UMaterialGraph>(
|
|
FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
|
|
MaterialObj->MaterialGraph->Material = MaterialObj;
|
|
MaterialObj->MaterialGraph->RebuildGraph();
|
|
}
|
|
|
|
if (!MaterialObj->MaterialGraph)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
|
|
}
|
|
|
|
// Recursive helper: trace backwards from a pin and build a description string
|
|
TFunction<FString(UEdGraphPin*, int32)> TracePin = [&TracePin](UEdGraphPin* Pin, int32 Depth) -> FString
|
|
{
|
|
if (!Pin || Depth > 10)
|
|
return TEXT("(unknown)");
|
|
|
|
// If no connections, report as unconnected
|
|
if (Pin->LinkedTo.Num() == 0)
|
|
{
|
|
if (!Pin->DefaultValue.IsEmpty())
|
|
return FString::Printf(TEXT("(default: %s)"), *Pin->DefaultValue);
|
|
return TEXT("(unconnected)");
|
|
}
|
|
|
|
TArray<FString> Sources;
|
|
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
|
|
{
|
|
if (!LinkedPin || !LinkedPin->GetOwningNode()) continue;
|
|
|
|
UEdGraphNode* SourceNode = LinkedPin->GetOwningNode();
|
|
FString NodeDesc;
|
|
|
|
// Check if this is a material graph node
|
|
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(SourceNode))
|
|
{
|
|
UMaterialExpression* Expr = MatNode->MaterialExpression;
|
|
if (!Expr)
|
|
{
|
|
NodeDesc = TEXT("(null expression)");
|
|
}
|
|
else if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
|
{
|
|
NodeDesc = FString::Printf(TEXT("ScalarParam \"%s\" (default: %.4f)"), *SP->ParameterName.ToString(), SP->DefaultValue);
|
|
}
|
|
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
|
{
|
|
NodeDesc = FString::Printf(TEXT("VectorParam \"%s\" (default: R=%.2f G=%.2f B=%.2f A=%.2f)"),
|
|
*VP->ParameterName.ToString(), VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
|
|
}
|
|
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
|
{
|
|
FString TexName = TP->Texture ? TP->Texture->GetName() : TEXT("None");
|
|
NodeDesc = FString::Printf(TEXT("TextureParam \"%s\" (%s)"), *TP->ParameterName.ToString(), *TexName);
|
|
}
|
|
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
|
{
|
|
NodeDesc = FString::Printf(TEXT("StaticSwitchParam \"%s\" (default: %s)"),
|
|
*SSP->ParameterName.ToString(), SSP->DefaultValue ? TEXT("true") : TEXT("false"));
|
|
}
|
|
else if (auto* SC = Cast<UMaterialExpressionConstant>(Expr))
|
|
{
|
|
NodeDesc = FString::Printf(TEXT("Constant(%.4f)"), SC->R);
|
|
}
|
|
else if (auto* C3 = Cast<UMaterialExpressionConstant3Vector>(Expr))
|
|
{
|
|
NodeDesc = FString::Printf(TEXT("Constant3(R=%.2f G=%.2f B=%.2f)"), C3->Constant.R, C3->Constant.G, C3->Constant.B);
|
|
}
|
|
else if (auto* C4 = Cast<UMaterialExpressionConstant4Vector>(Expr))
|
|
{
|
|
NodeDesc = FString::Printf(TEXT("Constant4(R=%.2f G=%.2f B=%.2f A=%.2f)"), C4->Constant.R, C4->Constant.G, C4->Constant.B, C4->Constant.A);
|
|
}
|
|
else if (auto* TS = Cast<UMaterialExpressionTextureSample>(Expr))
|
|
{
|
|
FString TexName = TS->Texture ? TS->Texture->GetName() : TEXT("None");
|
|
NodeDesc = FString::Printf(TEXT("TextureSample(%s)"), *TexName);
|
|
}
|
|
else if (auto* MFC = Cast<UMaterialExpressionMaterialFunctionCall>(Expr))
|
|
{
|
|
FString FuncName = MFC->MaterialFunction ? MFC->MaterialFunction->GetName() : TEXT("None");
|
|
NodeDesc = FString::Printf(TEXT("FunctionCall(%s)"), *FuncName);
|
|
}
|
|
else
|
|
{
|
|
NodeDesc = Expr->GetClass()->GetName();
|
|
}
|
|
|
|
// If the source node has input pins with connections, recurse
|
|
TArray<FString> InputDescs;
|
|
for (UEdGraphPin* InputPin : SourceNode->Pins)
|
|
{
|
|
if (!InputPin || InputPin->Direction != EGPD_Input || InputPin->LinkedTo.Num() == 0) continue;
|
|
FString InputDesc = TracePin(InputPin, Depth + 1);
|
|
InputDescs.Add(InputDesc);
|
|
}
|
|
|
|
if (InputDescs.Num() > 0)
|
|
{
|
|
NodeDesc += TEXT(" <- (") + FString::Join(InputDescs, TEXT(", ")) + TEXT(")");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Non-material node (e.g., root, comment), just use title
|
|
NodeDesc = SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
}
|
|
|
|
Sources.Add(NodeDesc);
|
|
}
|
|
|
|
if (Sources.Num() == 1)
|
|
return Sources[0];
|
|
|
|
return TEXT("(") + FString::Join(Sources, TEXT(", ")) + TEXT(")");
|
|
};
|
|
|
|
// Find root node and trace each input
|
|
TArray<TSharedPtr<FJsonValue>> InputDescriptions;
|
|
|
|
UMaterialGraphNode_Root* RootNode = nullptr;
|
|
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
|
|
{
|
|
RootNode = Cast<UMaterialGraphNode_Root>(Node);
|
|
if (RootNode) break;
|
|
}
|
|
|
|
if (!RootNode)
|
|
{
|
|
return MCPUtils::MakeErrorJson(Result, TEXT("Could not find root node in material graph"));
|
|
}
|
|
|
|
for (UEdGraphPin* Pin : RootNode->Pins)
|
|
{
|
|
if (!Pin || Pin->Direction != EGPD_Input) continue;
|
|
|
|
FString PinName = Pin->PinName.ToString();
|
|
FString Description;
|
|
|
|
if (Pin->LinkedTo.Num() == 0)
|
|
{
|
|
Description = TEXT("(unconnected)");
|
|
}
|
|
else
|
|
{
|
|
Description = TracePin(Pin, 0);
|
|
}
|
|
|
|
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
|
|
InputObj->SetStringField(TEXT("input"), PinName);
|
|
InputObj->SetStringField(TEXT("chain"), Description);
|
|
InputObj->SetBoolField(TEXT("connected"), Pin->LinkedTo.Num() > 0);
|
|
InputDescriptions.Add(MakeShared<FJsonValueObject>(InputObj));
|
|
}
|
|
|
|
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
|
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
|
|
Result->SetArrayField(TEXT("inputs"), InputDescriptions);
|
|
|
|
// Also include a compact text description
|
|
FString TextDesc;
|
|
for (const TSharedPtr<FJsonValue>& Val : InputDescriptions)
|
|
{
|
|
TSharedPtr<FJsonObject> Obj = Val->AsObject();
|
|
if (!Obj.IsValid()) continue;
|
|
FString InputName = Obj->GetStringField(TEXT("input"));
|
|
FString Chain = Obj->GetStringField(TEXT("chain"));
|
|
bool bConnected = Obj->GetBoolField(TEXT("connected"));
|
|
if (bConnected)
|
|
{
|
|
TextDesc += FString::Printf(TEXT("%s <- %s\n"), *InputName, *Chain);
|
|
}
|
|
}
|
|
if (!TextDesc.IsEmpty())
|
|
{
|
|
Result->SetStringField(TEXT("description"), TextDesc);
|
|
}
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="search_within_materials"))
|
|
class UMCPHandler_SearchMaterials : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Search query string to match against material names, expression classes, and parameter names"))
|
|
FString Query;
|
|
|
|
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)"))
|
|
int32 MaxResults = 50;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Search across all materials for matching material names, expression types, and parameter names.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
FString DecodedQuery = MCPUtils::UrlDecode(Query);
|
|
|
|
MaxResults = FMath::Clamp(MaxResults, 1, 200);
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Results;
|
|
|
|
MCPAssets<UMaterial> AllMaterials;
|
|
AllMaterials.Info();
|
|
|
|
for (const FAssetData& Asset : AllMaterials.AllData())
|
|
{
|
|
if (Results.Num() >= MaxResults) break;
|
|
|
|
FString MatName = Asset.AssetName.ToString();
|
|
|
|
// Check material name first
|
|
bool bNameMatch = MatName.Contains(DecodedQuery, ESearchCase::IgnoreCase);
|
|
|
|
UMaterial* MaterialObj = Cast<UMaterial>(const_cast<FAssetData&>(Asset).GetAsset());
|
|
if (!MaterialObj) continue;
|
|
|
|
auto Expressions = MaterialObj->GetExpressions();
|
|
|
|
if (bNameMatch)
|
|
{
|
|
// Add a match for the material itself
|
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
R->SetStringField(TEXT("material"), MatName);
|
|
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
|
|
R->SetStringField(TEXT("matchType"), TEXT("materialName"));
|
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
}
|
|
|
|
// Search expressions
|
|
for (UMaterialExpression* Expr : Expressions)
|
|
{
|
|
if (!Expr || Results.Num() >= MaxResults) continue;
|
|
|
|
FString ExprDesc = Expr->GetDescription();
|
|
FString ExprClass = Expr->GetClass()->GetName();
|
|
|
|
// Check parameter name
|
|
FString ParamName;
|
|
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
|
ParamName = SP->ParameterName.ToString();
|
|
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
|
ParamName = VP->ParameterName.ToString();
|
|
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
|
ParamName = TP->ParameterName.ToString();
|
|
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
|
ParamName = SSP->ParameterName.ToString();
|
|
|
|
bool bExprMatch = ExprDesc.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
|
|
ExprClass.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
|
|
(!ParamName.IsEmpty() && ParamName.Contains(DecodedQuery, ESearchCase::IgnoreCase));
|
|
|
|
if (bExprMatch)
|
|
{
|
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
R->SetStringField(TEXT("material"), MatName);
|
|
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
|
|
R->SetStringField(TEXT("matchType"), TEXT("expression"));
|
|
R->SetStringField(TEXT("expressionClass"), ExprClass);
|
|
if (!ExprDesc.IsEmpty())
|
|
R->SetStringField(TEXT("description"), ExprDesc);
|
|
if (!ParamName.IsEmpty())
|
|
R->SetStringField(TEXT("parameterName"), ParamName);
|
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
}
|
|
}
|
|
}
|
|
|
|
Result->SetStringField(TEXT("query"), DecodedQuery);
|
|
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
|
Result->SetArrayField(TEXT("results"), Results);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="find_material_references"))
|
|
class UMCPHandler_FindMaterialReferences : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Material or MaterialInstance name or package path"))
|
|
FString Material;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Find all assets that reference a given material or material instance.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
// Try to find the material's package path (search both Material and MaterialInstance)
|
|
MCPAssets<UMaterial> Assets;
|
|
Assets.Scan<UMaterial>().Scan<UMaterialInstanceConstant>();
|
|
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Info()) return;
|
|
FString PackagePath = Assets.OneData().PackageName.ToString();
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindMaterialReferences — '%s' (package: %s)"), *Material, *PackagePath);
|
|
|
|
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
|
|
|
TArray<FName> Referencers;
|
|
Registry.GetReferencers(FName(*PackagePath), Referencers);
|
|
|
|
TArray<TSharedPtr<FJsonValue>> RefArray;
|
|
for (const FName& Ref : Referencers)
|
|
{
|
|
FString RefStr = Ref.ToString();
|
|
// Skip self-reference
|
|
if (RefStr == PackagePath) continue;
|
|
RefArray.Add(MakeShared<FJsonValueString>(RefStr));
|
|
}
|
|
|
|
Result->SetStringField(TEXT("packagePath"), PackagePath);
|
|
Result->SetNumberField(TEXT("totalReferencers"), RefArray.Num());
|
|
Result->SetArrayField(TEXT("referencers"), RefArray);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="list_material_function_assets"))
|
|
class UMCPHandler_ListMaterialFunctions : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Optional, Description="Filter string to match against function name or path"))
|
|
FString Filter;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("List MaterialFunction assets, optionally filtered by name or path.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
MCPAssets<UMaterialFunction> Assets;
|
|
Assets.Substring(Filter).Info();
|
|
|
|
TArray<TSharedPtr<FJsonValue>> Entries;
|
|
for (const FAssetData& Asset : Assets.AllData())
|
|
{
|
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString());
|
|
Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString());
|
|
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
|
}
|
|
|
|
Result->SetNumberField(TEXT("count"), Entries.Num());
|
|
Result->SetArrayField(TEXT("functions"), Entries);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="dump_material_function"))
|
|
class UMCPHandler_GetMaterialFunction : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="MaterialFunction name or package path"))
|
|
FString MaterialFunction;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Get detailed info about a material function, including its inputs, outputs, and expressions.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
FString DecodedName = MCPUtils::UrlDecode(MaterialFunction);
|
|
|
|
MCPAssets<UMaterialFunction> Assets;
|
|
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
|
|
UMaterialFunction* MF = Assets.Object();
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName());
|
|
|
|
Result->SetStringField(TEXT("name"), MF->GetName());
|
|
Result->SetStringField(TEXT("path"), MF->GetPathName());
|
|
Result->SetStringField(TEXT("description"), MF->GetDescription());
|
|
|
|
// Expression count
|
|
auto Expressions = MF->GetExpressions();
|
|
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
|
|
|
|
// List function inputs and outputs from expressions
|
|
TArray<TSharedPtr<FJsonValue>> Inputs;
|
|
TArray<TSharedPtr<FJsonValue>> Outputs;
|
|
TArray<TSharedPtr<FJsonValue>> ExpressionList;
|
|
|
|
{
|
|
for (UMaterialExpression* Expr : Expressions)
|
|
{
|
|
if (!Expr) continue;
|
|
|
|
if (auto* FI = Cast<UMaterialExpressionFunctionInput>(Expr))
|
|
{
|
|
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
|
|
InputObj->SetStringField(TEXT("name"), FI->InputName.ToString());
|
|
InputObj->SetStringField(TEXT("type"), TEXT("FunctionInput"));
|
|
InputObj->SetNumberField(TEXT("posX"), FI->MaterialExpressionEditorX);
|
|
InputObj->SetNumberField(TEXT("posY"), FI->MaterialExpressionEditorY);
|
|
Inputs.Add(MakeShared<FJsonValueObject>(InputObj));
|
|
}
|
|
else if (auto* FO = Cast<UMaterialExpressionFunctionOutput>(Expr))
|
|
{
|
|
TSharedRef<FJsonObject> OutputObj = MakeShared<FJsonObject>();
|
|
OutputObj->SetStringField(TEXT("name"), FO->OutputName.ToString());
|
|
OutputObj->SetStringField(TEXT("type"), TEXT("FunctionOutput"));
|
|
OutputObj->SetNumberField(TEXT("posX"), FO->MaterialExpressionEditorX);
|
|
OutputObj->SetNumberField(TEXT("posY"), FO->MaterialExpressionEditorY);
|
|
Outputs.Add(MakeShared<FJsonValueObject>(OutputObj));
|
|
}
|
|
|
|
// Serialize every expression
|
|
TSharedPtr<FJsonObject> ExprJson = MCPUtils::SerializeMaterialExpression(Expr);
|
|
if (ExprJson.IsValid())
|
|
{
|
|
ExpressionList.Add(MakeShared<FJsonValueObject>(ExprJson.ToSharedRef()));
|
|
}
|
|
}
|
|
}
|
|
|
|
Result->SetArrayField(TEXT("inputs"), Inputs);
|
|
Result->SetArrayField(TEXT("outputs"), Outputs);
|
|
Result->SetArrayField(TEXT("expressions"), ExpressionList);
|
|
|
|
// If the function has an editor graph, serialize it
|
|
UEdGraph* FuncGraph = MF->MaterialGraph;
|
|
if (FuncGraph)
|
|
{
|
|
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(FuncGraph);
|
|
if (GraphJson.IsValid())
|
|
{
|
|
Result->SetObjectField(TEXT("graph"), GraphJson);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
// ---------------------------------------------------------------------------
|
|
|
|
UCLASS(meta=(ToolName="compile_material"))
|
|
class UMCPHandler_ValidateMaterial : public UObject, public IMCPHandler
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
UPROPERTY(meta=(Description="Material name or package path"))
|
|
FString Material;
|
|
|
|
virtual FString GetDescription() const override
|
|
{
|
|
return TEXT("Force recompile a material and check for compilation errors.");
|
|
}
|
|
|
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
{
|
|
// Load material
|
|
MCPAssets<UMaterial> Assets;
|
|
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
UMaterial* MaterialObj = Assets.Object();
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *MaterialObj->GetName());
|
|
|
|
// Force recompile by triggering PreEditChange/PostEditChange
|
|
MaterialObj->PreEditChange(nullptr);
|
|
MaterialObj->PostEditChange();
|
|
|
|
// Collect compilation errors
|
|
TArray<TSharedPtr<FJsonValue>> ErrorArray;
|
|
bool bValid = true;
|
|
|
|
// Check for compilation errors via FMaterialResource on current platform
|
|
FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel);
|
|
if (Resource)
|
|
{
|
|
const TArray<FString>& CompileErrors = Resource->GetCompileErrors();
|
|
for (const FString& Err : CompileErrors)
|
|
{
|
|
bValid = false;
|
|
ErrorArray.Add(MakeShared<FJsonValueString>(Err));
|
|
}
|
|
}
|
|
|
|
// Count expressions and connections
|
|
auto Expressions = MaterialObj->GetExpressions();
|
|
int32 ExprCount = Expressions.Num();
|
|
int32 ConnectionCount = 0;
|
|
if (MaterialObj->MaterialGraph)
|
|
{
|
|
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
|
|
{
|
|
if (!Node) continue;
|
|
for (UEdGraphPin* Pin : Node->Pins)
|
|
{
|
|
if (Pin && Pin->Direction == EGPD_Output)
|
|
{
|
|
ConnectionCount += Pin->LinkedTo.Num();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Result->SetBoolField(TEXT("valid"), bValid);
|
|
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
|
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
|
|
Result->SetNumberField(TEXT("expressionCount"), ExprCount);
|
|
Result->SetNumberField(TEXT("connectionCount"), ConnectionCount);
|
|
Result->SetArrayField(TEXT("errors"), ErrorArray);
|
|
Result->SetNumberField(TEXT("errorCount"), ErrorArray.Num());
|
|
|
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material '%s' validation %s (%d errors)"),
|
|
*MaterialObj->GetName(), bValid ? TEXT("passed") : TEXT("failed"), ErrorArray.Num());
|
|
}
|
|
};
|