247 lines
8.7 KiB
C++
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);
|
|
}
|
|
}
|
|
};
|