Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/Handlers/UMCPHandler_DescribeMaterialInEnglish.h
2026-03-10 01:42:43 -04:00

247 lines
8.7 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 "Engine/Texture.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 "UMCPHandler_DescribeMaterialInEnglish.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UMCPHandler_DescribeMaterialInEnglish : 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 = MCPUtils::FormatName(Expr->GetClass());
}
// 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 = MCPUtils::FormatName(SourceNode);
}
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 = MCPUtils::FormatName(Pin);
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);
}
}
};