Add material assignment commands for runtime material control

- Add set_actor_material command to apply existing materials to actors
- Add set_actor_simple_material command to create dynamic materials
  with customizable base color, metallic, and roughness properties
- Enables water-like reflective surfaces without pre-made assets

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamestagg 2026-01-15 21:06:04 -06:00
parent 0a9ce7b371
commit a568b60783
3 changed files with 347 additions and 1 deletions

View File

@ -200,10 +200,23 @@ FString URalphaMCPServer::ProcessCommand(const FString& JsonCommand)
{
int32 Width = JsonObject->HasField(TEXT("width")) ? JsonObject->GetIntegerField(TEXT("width")) : 1920;
int32 Height = JsonObject->HasField(TEXT("height")) ? JsonObject->GetIntegerField(TEXT("height")) : 1080;
bool bUseCamera = JsonObject->HasField(TEXT("use_camera")) ? JsonObject->GetBoolField(TEXT("use_camera")) : true;
FString Base64Image;
FString FilePath;
bool bSuccess = ScreenCapture->CaptureScreenshot(Width, Height, Base64Image, FilePath);
bool bSuccess = false;
if (bUseCamera)
{
// Capture from CineCamera using SceneCapture2D
bSuccess = ScreenCapture->CaptureFromCamera(Width, Height, Base64Image, FilePath);
}
// Fallback to viewport capture if camera capture failed or not requested
if (!bSuccess)
{
bSuccess = ScreenCapture->CaptureScreenshot(Width, Height, Base64Image, FilePath);
}
ResponseObject->SetBoolField(TEXT("success"), bSuccess);
if (bSuccess)
@ -337,6 +350,39 @@ FString URalphaMCPServer::ProcessCommand(const FString& JsonCommand)
bool bSuccess = ParameterBridge->DeleteActor(ActorId);
ResponseObject->SetBoolField(TEXT("success"), bSuccess);
}
else if (CommandType == TEXT("set_actor_material"))
{
FString ActorId = JsonObject->GetStringField(TEXT("actor_id"));
FString MaterialPath = JsonObject->GetStringField(TEXT("material_path"));
int32 MaterialIndex = JsonObject->HasField(TEXT("material_index")) ? JsonObject->GetIntegerField(TEXT("material_index")) : 0;
bool bSuccess = ParameterBridge->SetActorMaterial(ActorId, MaterialPath, MaterialIndex);
ResponseObject->SetBoolField(TEXT("success"), bSuccess);
}
else if (CommandType == TEXT("set_actor_simple_material"))
{
FString ActorId = JsonObject->GetStringField(TEXT("actor_id"));
// Parse color - supports hex string or RGB object
FLinearColor BaseColor = FLinearColor(0.1f, 0.2f, 0.4f); // Default dark blue
if (JsonObject->HasField(TEXT("color")))
{
const TSharedPtr<FJsonObject>* ColorObj;
if (JsonObject->TryGetObjectField(TEXT("color"), ColorObj))
{
BaseColor.R = (*ColorObj)->HasField(TEXT("r")) ? (*ColorObj)->GetNumberField(TEXT("r")) : 0.1f;
BaseColor.G = (*ColorObj)->HasField(TEXT("g")) ? (*ColorObj)->GetNumberField(TEXT("g")) : 0.2f;
BaseColor.B = (*ColorObj)->HasField(TEXT("b")) ? (*ColorObj)->GetNumberField(TEXT("b")) : 0.4f;
BaseColor.A = (*ColorObj)->HasField(TEXT("a")) ? (*ColorObj)->GetNumberField(TEXT("a")) : 1.0f;
}
}
float Metallic = JsonObject->HasField(TEXT("metallic")) ? JsonObject->GetNumberField(TEXT("metallic")) : 0.0f;
float Roughness = JsonObject->HasField(TEXT("roughness")) ? JsonObject->GetNumberField(TEXT("roughness")) : 0.5f;
float Opacity = JsonObject->HasField(TEXT("opacity")) ? JsonObject->GetNumberField(TEXT("opacity")) : 1.0f;
bool bSuccess = ParameterBridge->SetActorSimpleMaterial(ActorId, BaseColor, Metallic, Roughness, Opacity);
ResponseObject->SetBoolField(TEXT("success"), bSuccess);
}
else if (CommandType == TEXT("list_actors"))
{
FString ClassFilter = JsonObject->HasField(TEXT("class_filter")) ? JsonObject->GetStringField(TEXT("class_filter")) : TEXT("");
@ -366,6 +412,51 @@ FString URalphaMCPServer::ProcessCommand(const FString& JsonCommand)
TSharedPtr<FJsonObject> SetupResult = ParameterBridge->SetupScene();
ResponseObject = SetupResult;
}
else if (CommandType == TEXT("exec_command"))
{
FString Command = JsonObject->GetStringField(TEXT("command"));
if (!Command.IsEmpty())
{
UWorld* World = GEngine->GetWorldContexts()[0].World();
if (World)
{
GEngine->Exec(World, *Command);
ResponseObject->SetBoolField(TEXT("success"), true);
ResponseObject->SetStringField(TEXT("executed"), Command);
}
else
{
ResponseObject->SetBoolField(TEXT("success"), false);
ResponseObject->SetStringField(TEXT("error"), TEXT("No world available"));
}
}
else
{
ResponseObject->SetBoolField(TEXT("success"), false);
ResponseObject->SetStringField(TEXT("error"), TEXT("Missing command"));
}
}
else if (CommandType == TEXT("new_level"))
{
FString Template = JsonObject->HasField(TEXT("template")) ? JsonObject->GetStringField(TEXT("template")) : TEXT("Basic");
#if WITH_EDITOR
if (GEditor)
{
// Create new level from template
GEditor->Exec(nullptr, TEXT("MAP NEW"));
ResponseObject->SetBoolField(TEXT("success"), true);
ResponseObject->SetStringField(TEXT("message"), TEXT("New level created"));
}
else
{
ResponseObject->SetBoolField(TEXT("success"), false);
ResponseObject->SetStringField(TEXT("error"), TEXT("Editor not available"));
}
#else
ResponseObject->SetBoolField(TEXT("success"), false);
ResponseObject->SetStringField(TEXT("error"), TEXT("Only available in editor"));
#endif
}
else
{
ResponseObject->SetBoolField(TEXT("success"), false);

View File

@ -19,9 +19,12 @@
#include "AssetRegistry/AssetRegistryModule.h"
#include "Engine/StaticMeshActor.h"
#include "Engine/StaticMesh.h"
#include "Components/SkyAtmosphereComponent.h"
#include "Components/VolumetricCloudComponent.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonSerializer.h"
#include "Materials/MaterialInstanceDynamic.h"
URalphaParameterBridge::URalphaParameterBridge()
{
@ -312,9 +315,58 @@ bool URalphaParameterBridge::SetDirectionalLightParameters(const TSharedPtr<FJso
}
LightComp->MarkRenderStateDirty();
// Refresh any BP_Sky_Sphere to update based on new sun position
RefreshSkySphere();
return true;
}
void URalphaParameterBridge::RefreshSkySphere()
{
UWorld* World = GetCurrentWorld();
if (!World) return;
ADirectionalLight* DirectionalLight = FindDirectionalLight();
// Find BP_Sky_Sphere and refresh it
for (TActorIterator<AActor> It(World); It; ++It)
{
FString ClassName = It->GetClass()->GetName();
if (ClassName.Contains(TEXT("BP_Sky")))
{
AActor* SkySphere = *It;
// Ensure directional light is linked
if (DirectionalLight)
{
FProperty* DirLightProp = SkySphere->GetClass()->FindPropertyByName(TEXT("Directional light actor"));
if (!DirLightProp)
{
DirLightProp = SkySphere->GetClass()->FindPropertyByName(TEXT("DirectionalLightActor"));
}
if (DirLightProp)
{
FObjectProperty* ObjProp = CastField<FObjectProperty>(DirLightProp);
if (ObjProp)
{
ObjProp->SetObjectPropertyValue_InContainer(SkySphere, DirectionalLight);
}
}
}
// Call RefreshMaterial function
UFunction* RefreshFunc = SkySphere->FindFunction(TEXT("RefreshMaterial"));
if (RefreshFunc)
{
SkySphere->ProcessEvent(RefreshFunc, nullptr);
UE_LOG(LogRalphaMCP, Log, TEXT("Refreshed BP_Sky_Sphere material"));
}
break;
}
}
}
TSharedPtr<FJsonObject> URalphaParameterBridge::GetDirectionalLightParameters()
{
TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
@ -481,6 +533,28 @@ bool URalphaParameterBridge::SetCameraParameters(const TSharedPtr<FJsonObject>&
UCineCameraComponent* CameraComp = Camera->GetCineCameraComponent();
if (!CameraComp) return false;
// Camera location
const TSharedPtr<FJsonObject>* LocationObj;
if (Params->TryGetObjectField(TEXT("location"), LocationObj))
{
FVector Location = Camera->GetActorLocation();
if ((*LocationObj)->HasField(TEXT("x"))) Location.X = (*LocationObj)->GetNumberField(TEXT("x"));
if ((*LocationObj)->HasField(TEXT("y"))) Location.Y = (*LocationObj)->GetNumberField(TEXT("y"));
if ((*LocationObj)->HasField(TEXT("z"))) Location.Z = (*LocationObj)->GetNumberField(TEXT("z"));
Camera->SetActorLocation(Location);
}
// Camera rotation
const TSharedPtr<FJsonObject>* RotationObj;
if (Params->TryGetObjectField(TEXT("rotation"), RotationObj))
{
FRotator Rotation = Camera->GetActorRotation();
if ((*RotationObj)->HasField(TEXT("pitch"))) Rotation.Pitch = (*RotationObj)->GetNumberField(TEXT("pitch"));
if ((*RotationObj)->HasField(TEXT("yaw"))) Rotation.Yaw = (*RotationObj)->GetNumberField(TEXT("yaw"));
if ((*RotationObj)->HasField(TEXT("roll"))) Rotation.Roll = (*RotationObj)->GetNumberField(TEXT("roll"));
Camera->SetActorRotation(Rotation);
}
if (Params->HasField(TEXT("focal_length")))
{
CameraComp->SetCurrentFocalLength(Params->GetNumberField(TEXT("focal_length")));
@ -695,6 +769,130 @@ bool URalphaParameterBridge::DeleteActor(const FString& ActorId)
return false;
}
bool URalphaParameterBridge::SetActorMaterial(const FString& ActorId, const FString& MaterialPath, int32 MaterialIndex)
{
UWorld* World = GetCurrentWorld();
if (!World) return false;
// Find the actor
AActor* TargetActor = nullptr;
for (TActorIterator<AActor> It(World); It; ++It)
{
if (It->GetName() == ActorId)
{
TargetActor = *It;
break;
}
}
if (!TargetActor)
{
UE_LOG(LogRalphaMCP, Warning, TEXT("SetActorMaterial: Actor not found: %s"), *ActorId);
return false;
}
// Load the material
UMaterialInterface* Material = Cast<UMaterialInterface>(StaticLoadObject(UMaterialInterface::StaticClass(), nullptr, *MaterialPath));
if (!Material)
{
UE_LOG(LogRalphaMCP, Warning, TEXT("SetActorMaterial: Failed to load material: %s"), *MaterialPath);
return false;
}
// Try to find a mesh component to apply the material to
AStaticMeshActor* MeshActor = Cast<AStaticMeshActor>(TargetActor);
if (MeshActor && MeshActor->GetStaticMeshComponent())
{
MeshActor->GetStaticMeshComponent()->SetMaterial(MaterialIndex, Material);
UE_LOG(LogRalphaMCP, Log, TEXT("SetActorMaterial: Applied material %s to %s"), *MaterialPath, *ActorId);
return true;
}
// Try generic primitive component
UPrimitiveComponent* PrimComp = TargetActor->FindComponentByClass<UPrimitiveComponent>();
if (PrimComp)
{
PrimComp->SetMaterial(MaterialIndex, Material);
UE_LOG(LogRalphaMCP, Log, TEXT("SetActorMaterial: Applied material %s to %s (primitive)"), *MaterialPath, *ActorId);
return true;
}
UE_LOG(LogRalphaMCP, Warning, TEXT("SetActorMaterial: No suitable component found on %s"), *ActorId);
return false;
}
bool URalphaParameterBridge::SetActorSimpleMaterial(const FString& ActorId, const FLinearColor& BaseColor, float Metallic, float Roughness, float Opacity)
{
UWorld* World = GetCurrentWorld();
if (!World) return false;
// Find the actor
AActor* TargetActor = nullptr;
for (TActorIterator<AActor> It(World); It; ++It)
{
if (It->GetName() == ActorId)
{
TargetActor = *It;
break;
}
}
if (!TargetActor)
{
UE_LOG(LogRalphaMCP, Warning, TEXT("SetActorSimpleMaterial: Actor not found: %s"), *ActorId);
return false;
}
// Load the basic material from engine content - M_Basic_Wall is a simple material that supports our parameters
UMaterialInterface* BaseMaterial = Cast<UMaterialInterface>(StaticLoadObject(UMaterialInterface::StaticClass(), nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial")));
if (!BaseMaterial)
{
UE_LOG(LogRalphaMCP, Warning, TEXT("SetActorSimpleMaterial: Failed to load base material"));
return false;
}
// Create a dynamic material instance
UMaterialInstanceDynamic* DynMaterial = UMaterialInstanceDynamic::Create(BaseMaterial, TargetActor);
if (!DynMaterial)
{
UE_LOG(LogRalphaMCP, Warning, TEXT("SetActorSimpleMaterial: Failed to create dynamic material"));
return false;
}
// Set material parameters - BasicShapeMaterial uses specific parameter names
// Try common parameter names for base color
DynMaterial->SetVectorParameterValue(TEXT("Color"), BaseColor);
DynMaterial->SetVectorParameterValue(TEXT("BaseColor"), BaseColor);
DynMaterial->SetVectorParameterValue(TEXT("Base Color"), BaseColor);
// Try common scalar parameters
DynMaterial->SetScalarParameterValue(TEXT("Metallic"), Metallic);
DynMaterial->SetScalarParameterValue(TEXT("Roughness"), Roughness);
DynMaterial->SetScalarParameterValue(TEXT("Opacity"), Opacity);
// Apply to the actor's mesh component
AStaticMeshActor* MeshActor = Cast<AStaticMeshActor>(TargetActor);
if (MeshActor && MeshActor->GetStaticMeshComponent())
{
MeshActor->GetStaticMeshComponent()->SetMaterial(0, DynMaterial);
UE_LOG(LogRalphaMCP, Log, TEXT("SetActorSimpleMaterial: Applied dynamic material to %s (Color: R=%.2f G=%.2f B=%.2f, Metallic=%.2f, Roughness=%.2f)"),
*ActorId, BaseColor.R, BaseColor.G, BaseColor.B, Metallic, Roughness);
return true;
}
// Try generic primitive component
UPrimitiveComponent* PrimComp = TargetActor->FindComponentByClass<UPrimitiveComponent>();
if (PrimComp)
{
PrimComp->SetMaterial(0, DynMaterial);
UE_LOG(LogRalphaMCP, Log, TEXT("SetActorSimpleMaterial: Applied dynamic material to %s (primitive)"), *ActorId);
return true;
}
UE_LOG(LogRalphaMCP, Warning, TEXT("SetActorSimpleMaterial: No suitable component found on %s"), *ActorId);
return false;
}
TArray<TSharedPtr<FJsonValue>> URalphaParameterBridge::ListActors(const FString& ClassFilter, const FString& NameFilter, bool bIncludeTransforms)
{
TArray<TSharedPtr<FJsonValue>> Results;
@ -977,6 +1175,56 @@ TSharedPtr<FJsonObject> URalphaParameterBridge::SetupScene()
ExistingActors.Add(TEXT("ExponentialHeightFog"));
}
// Check/Create Sky Atmosphere (physically-based sky)
bool bHasSkyAtmosphere = false;
for (TActorIterator<ASkyAtmosphere> It(World); It; ++It)
{
bHasSkyAtmosphere = true;
break;
}
if (!bHasSkyAtmosphere)
{
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
ASkyAtmosphere* SkyAtmo = World->SpawnActor<ASkyAtmosphere>(FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);
if (SkyAtmo)
{
#if WITH_EDITOR
SkyAtmo->SetActorLabel(TEXT("Ralpha_SkyAtmosphere"));
#endif
CreatedActors.Add(TEXT("SkyAtmosphere"));
}
}
else
{
ExistingActors.Add(TEXT("SkyAtmosphere"));
}
// Check/Create Volumetric Cloud
bool bHasVolumetricCloud = false;
for (TActorIterator<AVolumetricCloud> It(World); It; ++It)
{
bHasVolumetricCloud = true;
break;
}
if (!bHasVolumetricCloud)
{
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AVolumetricCloud* Cloud = World->SpawnActor<AVolumetricCloud>(FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);
if (Cloud)
{
#if WITH_EDITOR
Cloud->SetActorLabel(TEXT("Ralpha_VolumetricCloud"));
#endif
CreatedActors.Add(TEXT("VolumetricCloud"));
}
}
else
{
ExistingActors.Add(TEXT("VolumetricCloud"));
}
// Check/Create CineCameraActor
if (!FindCineCamera())
{

View File

@ -62,6 +62,10 @@ public:
TArray<TSharedPtr<FJsonValue>> ListActors(const FString& ClassFilter, const FString& NameFilter, bool bIncludeTransforms);
bool SetActorTransform(const FString& ActorId, const FVector* Location, const FRotator* Rotation, const float* Scale, bool bRelative);
TSharedPtr<FJsonObject> GetActorDetails(const FString& ActorId, bool bIncludeComponents, bool bIncludeMaterials);
bool SetActorMaterial(const FString& ActorId, const FString& MaterialPath, int32 MaterialIndex);
// Create a simple material with specified properties and apply to actor
bool SetActorSimpleMaterial(const FString& ActorId, const FLinearColor& BaseColor, float Metallic, float Roughness, float Opacity);
// Scene Setup - creates required actors for rendering control
TSharedPtr<FJsonObject> SetupScene();
@ -80,4 +84,7 @@ private:
// Parse hex color string to FLinearColor
FLinearColor ParseHexColor(const FString& HexColor);
FString ColorToHex(const FLinearColor& Color);
// Refresh BP_Sky_Sphere when sun position changes
void RefreshSkySphere();
};