2025 lines
64 KiB
C++
Raw Normal View History

#include "MujocoFactory.h"
#include "EngineUtils.h"
#include "Math/TransformVectorized.h"
#include "ObjectTools.h"
#include "PackageTools.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "KismetCompilerModule.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "UObject/SavePackage.h"
#include "mujoco/mujoco.h"
#include "StaticMeshAttributes.h"
#include "Containers/StringConv.h"
#include "IAssetTools.h"
#include "AssetToolsModule.h"
#include "Factories/FbxFactory.h"
#include "Factories/FbxImportUI.h"
#include "Factories/FbxStaticMeshImportData.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "StaticMeshDescription.h"
#include "MeshTypes.h"
#include "Misc/FileHelper.h"
#include "Widgets/SWindow.h"
#include "IDetailsView.h"
#include "PropertyEditorModule.h"
#include "Interfaces/IMainFrameModule.h"
#include "PropertyCustomizationHelpers.h"
#include "GameFramework/Character.h"
#include "Widgets/Input/SComboBox.h"
#include "Widgets/Input/SButton.h"
#include "tinyxml2.h"
#include "Misc/MujocoWrapper.h"
#include "UObject/SoftObjectPath.h"
#include "DetailLayoutBuilder.h"
#include "Misc/Optional.h"
#include "Components/MujocoSiteComponent.h"
#include "Components/MujocoBodyComponent.h"
#include "Components/MujocoJointComponent.h"
#include "Components/MujocoGeomComponent.h"
#include "UObject/UnrealType.h"
#include "STLImportFactory.h"
#include "array"
#include "Factories/MaterialInstanceConstantFactoryNew.h"
#include "Materials/MaterialInstance.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Factories/MaterialFactoryNew.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Factories/TextureFactory.h"
#include "ImageUtils.h"
#include "Materials/MaterialExpressionTextureSample.h"
#include "Materials/MaterialExpressionMultiply.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionTextureObject.h"
#include "MaterialEditingLibrary.h"
#include "Engine/StaticMesh.h"
#include "MeshDescription.h"
#include "MujocoMeshFactory.h"
#include "LuckyMujocoEditor.h"
#include "Engine/Texture2D.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"
#include "Modules/ModuleManager.h"
#include "HAL/FileManager.h"
#include "ImageCore.h"
#include "Formats/HdrImageWrapper.h"
#include "Components/MujocoTendonComponent.h"
#include "Components/MujocoEqualityComponent.h"
#include "Components/MujocoActuatorComponent.h"
#include "MujocoAttributeHandler.h"
#include "unordered_map"
#include "unordered_set"
#define LOCTEXT_NAMESPACE "MujocoFactory"
class FStringUtils
{
public:
static FString ConvertToPascalCase(const FString& Input)
{
FString Output;
bool bNextUpperCase = true;
for (const TCHAR& Char : Input)
{
if (IsSymbol(Char))
{
bNextUpperCase = true;
}
else
{
if (bNextUpperCase)
{
Output.AppendChar(FChar::ToUpper(Char));
bNextUpperCase = false;
}
else
{
Output.AppendChar(FChar::ToLower(Char));
}
}
}
return Output;
}
private:
static bool IsSymbol(const TCHAR& Char) { return !FChar::IsAlnum(Char); }
};
class FDefaultsToInline
{
private:
struct FDefaultBlock
{
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> TagDefaults;
std::unordered_map<std::string, FDefaultBlock> NestedBlocks;
};
std::unordered_map<tinyxml2::XMLElement*, std::unordered_map<std::string, std::string>> InitialAttributes;
FDefaultBlock ParseDefaultNode(tinyxml2::XMLElement* Element)
{
FDefaultBlock Block;
for (auto* Child = Element->FirstChildElement(); Child; Child = Child->NextSiblingElement())
{
if (std::string_view{ Child->Name() } == "default")
{
if (const char* ClassAttr = Child->Attribute("class"))
Block.NestedBlocks.emplace(ClassAttr, ParseDefaultNode(Child));
}
else
{
std::string TagName = Child->Name();
for (auto* Attr = Child->FirstAttribute(); Attr; Attr = Attr->Next())
Block.TagDefaults[TagName][Attr->Name()] = Attr->Value();
}
}
return Block;
}
std::unordered_map<std::string, FDefaultBlock> ParseGlobalDefaults(tinyxml2::XMLElement* GlobalDefault)
{
std::unordered_map<std::string, FDefaultBlock> Mapping;
Mapping.emplace("global", ParseDefaultNode(GlobalDefault));
return Mapping;
}
void MergeAttributes(tinyxml2::XMLElement* Element, const std::unordered_map<std::string, std::string>& Defaults)
{
for (const auto& [AttrName, AttrValue] : Defaults)
Element->SetAttribute(AttrName.c_str(), AttrValue.c_str());
auto It = InitialAttributes.find(Element);
if (It != InitialAttributes.end())
{
for (const auto& [AttrName, AttrValue] : It->second)
{
if (AttrName == "class" || AttrName == "childclass")
continue;
Element->SetAttribute(AttrName.c_str(), AttrValue.c_str());
}
}
}
const FDefaultBlock* GetDefaultForClass(const FDefaultBlock* Base, const std::string& ClassVal)
{
if (!Base)
return nullptr;
if (auto It = Base->NestedBlocks.find(ClassVal); It != Base->NestedBlocks.end())
return &It->second;
for (const auto& Pair : Base->NestedBlocks)
{
if (const FDefaultBlock* Found = GetDefaultForClass(&Pair.second, ClassVal))
return Found;
}
return nullptr;
}
void ProcessNode(tinyxml2::XMLElement* Element, const FDefaultBlock* Context, const std::unordered_map<std::string, FDefaultBlock>* GlobalDefaults)
{
for (auto* Attr = Element->FirstAttribute(); Attr; Attr = Attr->Next())
InitialAttributes[Element][Attr->Name()] = Attr->Value();
if (!Context)
{
if (GlobalDefaults)
{
auto It = GlobalDefaults->find("global");
if (It != GlobalDefaults->end())
Context = &It->second;
}
}
if (Context)
{
if (auto It = Context->TagDefaults.find(Element->Name()); It != Context->TagDefaults.end())
MergeAttributes(Element, It->second);
}
if (const char* ClassAttr = Element->Attribute("class"))
{
Element->DeleteAttribute("class");
std::string ClassVal{ ClassAttr };
const FDefaultBlock* FoundDefault = GetDefaultForClass(Context, ClassVal);
if (!FoundDefault && GlobalDefaults)
{
auto It = GlobalDefaults->find("global");
if (It != GlobalDefaults->end())
FoundDefault = GetDefaultForClass(&It->second, ClassVal);
}
if (FoundDefault)
{
if (auto It = FoundDefault->TagDefaults.find(Element->Name()); It != FoundDefault->TagDefaults.end())
MergeAttributes(Element, It->second);
}
}
const FDefaultBlock* ChildContext = Context;
if (const char* ChildClassAttr = Element->Attribute("childclass"))
{
Element->DeleteAttribute("childclass");
std::string ChildClass{ ChildClassAttr };
const FDefaultBlock* FoundChild = GetDefaultForClass(Context, ChildClass);
if (!FoundChild && GlobalDefaults)
{
auto It = GlobalDefaults->find("global");
if (It != GlobalDefaults->end())
FoundChild = GetDefaultForClass(&It->second, ChildClass);
}
ChildContext = FoundChild;
}
for (auto* Child = Element->FirstChildElement(); Child; Child = Child->NextSiblingElement())
ProcessNode(Child, ChildContext, GlobalDefaults);
}
public:
TUniquePtr<tinyxml2::XMLDocument> Parse(const char* FileName)
{
auto Document = MakeUnique<tinyxml2::XMLDocument>();
if (Document->LoadFile(FileName) != tinyxml2::XML_SUCCESS)
return nullptr;
if (auto* MujocoElement = Document->FirstChildElement("mujoco"))
{
if (auto* GlobalDefault = MujocoElement->FirstChildElement("default"))
{
auto GlobalDefaults = ParseGlobalDefaults(GlobalDefault);
MujocoElement->DeleteChild(GlobalDefault);
for (auto* Child = MujocoElement->FirstChildElement(); Child; Child = Child->NextSiblingElement())
ProcessNode(Child, nullptr, &GlobalDefaults);
}
else
{
for (auto* Child = MujocoElement->FirstChildElement(); Child; Child = Child->NextSiblingElement())
ProcessNode(Child, nullptr, nullptr);
}
}
return MoveTemp(Document);
}
};
class SMujocoImportWindow : public SWindow
{
TSharedPtr<IDetailsView> DetailsView;
TObjectPtr<UMjocoImportSettings> ImportSettings;
UFactory* Factory = nullptr;
TSharedPtr<SVerticalBox> MainVerticalBox;
TSharedPtr<SButton> ImportButton;
TSharedPtr<SButton> CancelButton;
TSharedPtr<STextBlock> ErrorText;
TSharedPtr<STextBlock> ProgressText;
TSharedPtr<SComboBox<TSharedPtr<FString>>> ClassPropertyEntryBox;
FString Filename;
TArray<const UClass*> AllowedClasses;
TArray<TSharedPtr<FString>> AllowedClassNames;
TUniquePtr<tinyxml2::XMLDocument> Doc;
FString MujocoMeshAssetPath;
FString MujocoTextureAssetPath;
FString MujocoAssetPath;
AActor* GeneratedActor = nullptr;
UPackage* ParentPackage = nullptr;
UPackage* GeneratedPackage = nullptr;
UBlueprint* Blueprint = nullptr;
UClass* SelectedClass = nullptr;
FName GeneratedName = NAME_None;
EObjectFlags GeneratedFlags = RF_Transient;
FText SelectedClassText;
TMujocoModelPtr Model;
mjSpec* MjSpec = nullptr;
public:
SLATE_BEGIN_ARGS(SMujocoImportWindow) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, FString InFilename, UPackage* InParent, FName InName, EObjectFlags InFlags, UFactory* InFactory)
{
ParentPackage = InParent;
GeneratedName = InName;
GeneratedFlags = InFlags;
Factory = InFactory;
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsViewArgs;
DetailsViewArgs.bAllowSearch = false;
DetailsViewArgs.bHideSelectionTip = true;
DetailsViewArgs.bShowOptions = false;
DetailsViewArgs.bShowModifiedPropertiesOption = false;
DetailsViewArgs.bShowScrollBar = true;
DetailsViewArgs.bShowOptions = false;
DetailsViewArgs.bShowPropertyMatrixButton = false;
DetailsView = PropertyModule.CreateDetailView(DetailsViewArgs);
ImportSettings = NewObject<UMjocoImportSettings>(ParentPackage);
ImportSettings->SetFlags(RF_Transient | RF_Standalone | RF_Transactional);
ImportSettings->ParentClass = AActor::StaticClass();
ImportSettings->LoadConfig();
SelectedClass = const_cast<UClass*>(ImportSettings->ParentClass);
Filename = InFilename;
DetailsView->SetObject(ImportSettings);
AllowedClasses.Add(AActor::StaticClass());
AllowedClasses.Add(APawn::StaticClass());
AllowedClasses.Add(ACharacter::StaticClass());
FDefaultsToInline MergeXmlDefaults;
Doc = MergeXmlDefaults.Parse(TCHAR_TO_UTF8(*InFilename));
for (const UClass* Class : AllowedClasses)
{
AllowedClassNames.Add(MakeShared<FString>(Class->GetName()));
}
ImportButton = SNew(SButton).Text(FText::FromString(TEXT("Import"))).OnClicked(this, &SMujocoImportWindow::OnImportClicked);
CancelButton = SNew(SButton).Text(FText::FromString(TEXT("Cancel"))).OnClicked(this, &SMujocoImportWindow::OnCancelClicked);
// clang-format off
int32 SelectedIndex = AllowedClassNames.IndexOfByPredicate([this](TSharedPtr<FString> Item) { return *Item == SelectedClass->GetName(); });
SelectedClassText = FText::FromString(*AllowedClassNames[SelectedIndex].Get());
SAssignNew(ClassPropertyEntryBox, SComboBox<TSharedPtr<FString>>)
.OptionsSource(&AllowedClassNames)
.OnGenerateWidget_Lambda([](TSharedPtr<FString> InItem)
{
return SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text(FText::FromString(*InItem));
})
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> InItem, ESelectInfo::Type SelectInfo)
{
int32 SelectedIndex = AllowedClassNames.IndexOfByPredicate([InItem](TSharedPtr<FString> Item) { return *Item == *InItem; });
ImportSettings->ParentClass = AllowedClasses[SelectedIndex];
SelectedClass = const_cast<UClass*>(ImportSettings->ParentClass);
SelectedClassText = FText::FromString(*AllowedClassNames[SelectedIndex].Get());
})
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.Text_Lambda([this]() { return SelectedClassText; })
];
SAssignNew(MainVerticalBox, SVerticalBox)
+ SVerticalBox::Slot()
.HAlign(HAlign_Fill)
.AutoHeight()
.Padding(4, 8)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(5, 5)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Parent Class")))
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0)
[
ClassPropertyEntryBox.ToSharedRef()
]
]
+ SVerticalBox::Slot()
.HAlign(HAlign_Fill)
.FillHeight(1.0f)
.Padding(0, 0)
[
DetailsView.ToSharedRef()
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(6, 4)
.HAlign(HAlign_Fill)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.HAlign(HAlign_Fill)
[
SAssignNew(ProgressText, STextBlock)
.Text(FText::FromString(TEXT("")))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.HAlign(HAlign_Right)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(5)
[
ImportButton.ToSharedRef()
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(5)
[
CancelButton.ToSharedRef()
]
]
];
SAssignNew(ErrorText, STextBlock)
.Text(FText::FromString(TEXT("")))
.Visibility(EVisibility::Collapsed);
if(!Doc)
{
SetErrorText(FString::Printf(TEXT("Failed to parse XML file: %s"), *InFilename));
}
std::string ErrMsg{};
ErrMsg.resize(1024);
tinyxml2::XMLPrinter Printer;
Doc->Print(&Printer);
MjSpec = mj_parseXMLString(Printer.CStr(), nullptr, ErrMsg.data(), ErrMsg.capacity());
if (!MjSpec)
{
SetErrorText(FString::Printf(TEXT("%s"), UTF8_TO_TCHAR(ErrMsg.c_str())));
}
else
{
const char* AssetDir = mjs_getString(MjSpec->modelfiledir);
if (AssetDir)
{
FString BaseDir = FPaths::GetPath(InFilename);
MujocoAssetPath = AssetDir;
if(FPaths::IsRelative(MujocoAssetPath))
MujocoAssetPath = FPaths::Combine(BaseDir, AssetDir);
mjs_setString(MjSpec->modelfiledir, TCHAR_TO_UTF8(*MujocoAssetPath));
}
const char* MeshDir = mjs_getString(MjSpec->meshdir);
if (MeshDir)
{
FString BaseDir = FPaths::GetPath(InFilename);
MujocoMeshAssetPath = MeshDir;
if(FPaths::IsRelative(MujocoMeshAssetPath))
MujocoMeshAssetPath = FPaths::Combine(BaseDir, MeshDir);
mjs_setString(MjSpec->meshdir, TCHAR_TO_UTF8(*MujocoMeshAssetPath));
}
else
{
MujocoMeshAssetPath = MujocoAssetPath;
}
const char* TextureDir = mjs_getString(MjSpec->texturedir);
if (TextureDir)
{
FString BaseDir = FPaths::GetPath(InFilename);
MujocoTextureAssetPath = TextureDir;
if(FPaths::IsRelative(MujocoTextureAssetPath))
MujocoTextureAssetPath = FPaths::Combine(BaseDir, MujocoTextureAssetPath);
mjs_setString(MjSpec->texturedir, TCHAR_TO_UTF8(*MujocoTextureAssetPath));
}
else
{
MujocoTextureAssetPath = MujocoAssetPath;
}
Model = MakeMujocoModelPtr(mj_compile(MjSpec, nullptr));
if (!Model)
{
std::string SpecErrMsg = mjs_getError(MjSpec);
SetErrorText(FString::Printf(TEXT("%s"), UTF8_TO_TCHAR(SpecErrMsg.c_str())));
}
}
SWindow::Construct(SWindow::FArguments()
.Title(FText::FromString(TEXT("Import Mujoco Model")))
.ClientSize(FVector2D(500, 300))
.SupportsMaximize(false)
.SupportsMinimize(false)
.CreateTitleBar(true)
.SizingRule(ESizingRule::UserSized)
.FocusWhenFirstShown(true)
.ActivationPolicy(EWindowActivationPolicy::Always)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.FillHeight(1.0f)
.Padding(4, 8)
[
ErrorText.ToSharedRef()
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(0, 0)
[
MainVerticalBox.ToSharedRef()
]
]
);
// clang-format on
}
UObject* GetGeneratedAsset() const { return ImportSettings; }
void SetErrorText(const FString& Text)
{
ErrorText->SetText(FText::FromString(Text));
ErrorText->SetVisibility(EVisibility::Visible);
MainVerticalBox->SetVisibility(EVisibility::Collapsed);
}
bool GenerateBluePrint()
{
const FString FileExt{ FPaths::GetExtension(Filename) };
const FString BaseFilename{ FPaths::GetBaseFilename(Filename) };
FString ModelName{ UPackageTools::SanitizePackageName(BaseFilename) };
ModelName = ObjectTools::SanitizeObjectName(ModelName);
ModelName = FStringUtils::ConvertToPascalCase(ModelName);
FString PackagePath{ FPaths::GetPath(ParentPackage->GetName()) };
const FString BPName{ FString::Printf(TEXT("BP_%s"), *ModelName) };
const FString BPAssetPath{ FString::Printf(TEXT("%s/%s"), *PackagePath, *BPName) };
GeneratedPackage = CreatePackage(*BPAssetPath);
GeneratedPackage->FullyLoad();
GeneratedActor = NewObject<AActor>(GetTransientPackage(), SelectedClass, GeneratedName, RF_Transactional | RF_Transient);
if (!GeneratedActor)
{
SetErrorText(FString::Printf(TEXT("Failed to create actor: %s"), *BPAssetPath));
return false;
}
GeneratedActor->PostLoad();
FAssetRegistryModule::AssetCreated(GeneratedActor);
FKismetEditorUtilities::FCreateBlueprintFromActorParams BPParams;
BPParams.ParentClassOverride = const_cast<UClass*>(SelectedClass);
BPParams.bReplaceActor = false;
BPParams.bOpenBlueprint = false;
// first check if the blueprint already exists
auto ExistingBlueprint = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), nullptr, *BPAssetPath));
if (ExistingBlueprint)
{
ExistingBlueprint->Rename(nullptr, GetTransientPackage(), REN_DontCreateRedirectors);
ExistingBlueprint->ClearFlags(RF_Public | RF_Standalone | RF_Transactional);
ExistingBlueprint->RemoveFromRoot();
ExistingBlueprint->MarkAsGarbage();
}
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPreImport(Factory, UBlueprint::StaticClass(), Blueprint, FName(*BPName), TEXT("xml"));
Blueprint = FKismetEditorUtilities::CreateBlueprintFromActor(BPAssetPath, GeneratedActor, BPParams);
if (!Blueprint)
{
SetErrorText(FString::Printf(TEXT("Failed to create blueprint: %s"), *BPAssetPath));
return false;
}
Blueprint->PostLoad();
FAssetRegistryModule::AssetCreated(Blueprint);
return true;
}
bool ResolveOrientation(FQuat& OutQuat, bool bDegrees, const FString& Sequence, const FMujocoOrientation& Orient)
{
if (Sequence.Len() < 3 && Orient.Type == EMujocoOrientationType::Euler)
{
return false;
}
switch (Orient.Type)
{
case EMujocoOrientationType::AxisAngle:
{
FVector Axis(Orient.AxisAngle[0], Orient.AxisAngle[1], Orient.AxisAngle[2]);
float Angle = Orient.AxisAngle[3];
if (bDegrees)
{
Angle = FMath::DegreesToRadians(Angle);
}
if (Axis.IsNearlyZero())
{
return false;
}
Axis.Normalize();
OutQuat = FQuat(Axis, Angle);
break;
}
case EMujocoOrientationType::XYAxes:
{
FVector XAxis(Orient.XYAxes[0], Orient.XYAxes[1], Orient.XYAxes[2]);
FVector YAxis(Orient.XYAxes[3], Orient.XYAxes[4], Orient.XYAxes[5]);
if (XAxis.IsNearlyZero())
{
return false;
}
XAxis.Normalize();
YAxis -= XAxis * FVector::DotProduct(XAxis, YAxis);
if (YAxis.IsNearlyZero())
{
return false;
}
YAxis.Normalize();
FVector ZAxis = FVector::CrossProduct(XAxis, YAxis);
if (ZAxis.IsNearlyZero())
{
return false;
}
OutQuat = FQuat(FRotationMatrix::MakeFromXY(XAxis, YAxis));
break;
}
case EMujocoOrientationType::ZAxis:
{
FVector ZAxis(Orient.ZAxis[0], Orient.ZAxis[1], Orient.ZAxis[2]);
if (ZAxis.IsNearlyZero())
{
return false;
}
ZAxis.Normalize();
OutQuat = FQuat::FindBetweenNormals(FVector::UpVector, ZAxis);
break;
}
case EMujocoOrientationType::Euler:
{
float EulerAngles[3] = { Orient.Euler[0], Orient.Euler[1], Orient.Euler[2] };
if (bDegrees)
{
for (float& Angle : EulerAngles)
{
Angle = FMath::DegreesToRadians(Angle);
}
}
OutQuat = FQuat::Identity;
for (int32 i = 0; i < 3; ++i)
{
FVector Axis;
switch (Sequence[i])
{
case 'x':
case 'X':
Axis = FVector::ForwardVector;
break;
case 'y':
case 'Y':
Axis = FVector::RightVector;
break;
case 'z':
case 'Z':
Axis = FVector::UpVector;
break;
default:
return false;
}
FQuat RotQuat(Axis, EulerAngles[i]);
OutQuat = FChar::IsLower(Sequence[i]) ? OutQuat * RotQuat : RotQuat * OutQuat;
}
OutQuat.W *= -1.0f;
OutQuat.Y *= -1.0f;
OutQuat.Normalize();
break;
}
default:
return false;
}
return true;
}
void SetGeomMesh(UMujocoGeomComponent* GeomComp, FVector& MeshScale, UStaticMesh* Mesh)
{
GeomComp->Modify();
bool HasCollision = true;
if (GeomComp->Geom.ConType.IsSet())
{
HasCollision = GeomComp->Geom.ConType.GetValue() != 0;
}
if (!HasCollision)
{
GeomComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
GeomComp->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
}
else
{
GeomComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
GeomComp->SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName);
}
if (MeshScale != FVector::One())
{
GeomComp->SetRelativeScale3D(MeshScale);
}
GeomComp->SetStaticMesh(Mesh);
GeomComp->MujocoStaticMesh = Mesh;
FProperty* ChangedProp = FindFProperty<FProperty>(GeomComp->GetClass(), GET_MEMBER_NAME_CHECKED(UMujocoGeomComponent, MujocoStaticMesh));
FPropertyChangedEvent Event(ChangedProp, EPropertyChangeType::ValueSet);
GeomComp->PostEditChangeProperty(Event);
GeomComp->PostEditChange();
}
bool AddComponents()
{
if (!Blueprint)
{
SetErrorText(FString::Printf(TEXT("Blueprint not found")));
return false;
}
auto* GeneratedCls = CastChecked<UBlueprintGeneratedClass>(Blueprint->GeneratedClass);
USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript;
if (!SCS)
{
SetErrorText(FString::Printf(TEXT("Failed to get SCS")));
return false;
}
if (!Doc)
{
SetErrorText(FString::Printf(TEXT("Failed to parse XML file")));
return false;
}
// Lambda to set a scene component's transform based on XML attributes,
// applying a coordinate conversion.
auto SetSceneComponentTransformFromXml = [&](tinyxml2::XMLElement* Element, USceneComponent* SceneComp) {
if (const char* PosAttr = Element->Attribute("pos"))
{
TArray<FString> PosTokens;
FString PosStr = UTF8_TO_TCHAR(PosAttr);
PosStr.ParseIntoArray(PosTokens, TEXT(" "), true);
if (PosTokens.Num() >= 3)
{
// Parse the position from XML.
const float X = FCString::Atof(*PosTokens[0]);
const float Y = FCString::Atof(*PosTokens[1]);
const float Z = FCString::Atof(*PosTokens[2]);
// Convert from MuJoCo (meters, different axes) to UE coordinate system.
// Example conversion: scale X and Z by 100, invert Y.
const FVector ConvertedPos = FVector(X, Y, Z) * FVector(100.0f, -100.0f, 100.0f);
SceneComp->SetRelativeLocation(ConvertedPos);
}
}
if (const char* QuatAttr = Element->Attribute("quat"))
{
TArray<FString> QuatTokens;
FString QuatStr = UTF8_TO_TCHAR(QuatAttr);
QuatStr.ParseIntoArray(QuatTokens, TEXT(" "), true);
if (QuatTokens.Num() >= 4)
{
// Assume the XML order is "w x y z". Adjust conversion as needed.
const float W = FCString::Atof(*QuatTokens[0]) * -1.0;
const float X = FCString::Atof(*QuatTokens[1]);
const float Y = FCString::Atof(*QuatTokens[2]) * -1.0;
const float Z = FCString::Atof(*QuatTokens[3]);
// For example, invert the Y axis to match UE's coordinate system.
FQuat ConvertedQuat(X, Y, Z, W);
ConvertedQuat.Normalize();
SceneComp->SetRelativeRotation(ConvertedQuat.Rotator());
}
}
if (const char* ScaleAttr = Element->Attribute("euler"))
{
bool IsDegree = MjSpec->compiler.degree != 0;
char EulerSeq[4] = { 0 };
memcpy(EulerSeq, MjSpec->compiler.eulerseq, 3);
TArray<FString> EulerTokens;
FString EulerStr = UTF8_TO_TCHAR(ScaleAttr);
EulerStr.ParseIntoArray(EulerTokens, TEXT(" "), true);
if (EulerTokens.Num() >= 3)
{
FMujocoOrientation Orient = {};
Orient.Euler.Init(0.0f, 3);
Orient.Type = EMujocoOrientationType::Euler;
Orient.Euler[0] = FCString::Atof(*EulerTokens[0]);
Orient.Euler[1] = FCString::Atof(*EulerTokens[1]);
Orient.Euler[2] = FCString::Atof(*EulerTokens[2]);
FQuat Quat;
if (ResolveOrientation(Quat, IsDegree, EulerSeq, Orient))
{
SceneComp->SetRelativeRotation(Quat);
}
}
}
if (const char* ScaleAttr = Element->Attribute("xyaxes"))
{
TArray<FString> ScaleTokens;
FString ScaleStr = UTF8_TO_TCHAR(ScaleAttr);
ScaleStr.ParseIntoArray(ScaleTokens, TEXT(" "), true);
if (ScaleTokens.Num() >= 6)
{
FMujocoOrientation Orient = {};
Orient.XYAxes.Init(0.0f, 6);
Orient.Type = EMujocoOrientationType::XYAxes;
Orient.XYAxes[0] = FCString::Atof(*ScaleTokens[0]);
Orient.XYAxes[1] = FCString::Atof(*ScaleTokens[1]);
Orient.XYAxes[2] = FCString::Atof(*ScaleTokens[2]);
Orient.XYAxes[3] = FCString::Atof(*ScaleTokens[3]);
Orient.XYAxes[4] = FCString::Atof(*ScaleTokens[4]);
Orient.XYAxes[5] = FCString::Atof(*ScaleTokens[5]);
FQuat Quat;
if (ResolveOrientation(Quat, false, TEXT("xy"), Orient))
{
SceneComp->SetRelativeRotation(Quat);
}
}
}
if (const char* ScaleAttr = Element->Attribute("zaxis"))
{
TArray<FString> ScaleTokens;
FString ScaleStr = UTF8_TO_TCHAR(ScaleAttr);
ScaleStr.ParseIntoArray(ScaleTokens, TEXT(" "), true);
if (ScaleTokens.Num() >= 3)
{
FMujocoOrientation Orient = {};
Orient.ZAxis.Init(0.0f, 3);
Orient.Type = EMujocoOrientationType::ZAxis;
Orient.ZAxis[0] = FCString::Atof(*ScaleTokens[0]);
Orient.ZAxis[1] = FCString::Atof(*ScaleTokens[1]);
Orient.ZAxis[2] = FCString::Atof(*ScaleTokens[2]);
FQuat Quat;
if (ResolveOrientation(Quat, false, TEXT("z"), Orient))
{
SceneComp->SetRelativeRotation(Quat);
}
}
}
if (const char* ScaleAttr = Element->Attribute("axisangle"))
{
TArray<FString> ScaleTokens;
FString ScaleStr = UTF8_TO_TCHAR(ScaleAttr);
ScaleStr.ParseIntoArray(ScaleTokens, TEXT(" "), true);
if (ScaleTokens.Num() >= 4)
{
FMujocoOrientation Orient = {};
Orient.AxisAngle.Init(0.0f, 4);
Orient.Type = EMujocoOrientationType::AxisAngle;
Orient.AxisAngle[0] = FCString::Atof(*ScaleTokens[0]);
Orient.AxisAngle[1] = FCString::Atof(*ScaleTokens[1]);
Orient.AxisAngle[2] = FCString::Atof(*ScaleTokens[2]);
Orient.AxisAngle[3] = FCString::Atof(*ScaleTokens[3]);
FQuat Quat;
if (ResolveOrientation(Quat, false, TEXT("xyz"), Orient))
{
SceneComp->SetRelativeRotation(Quat);
}
}
}
};
auto ApplyAttributes = [&](UStruct* StructDef, void* DataPtr, tinyxml2::XMLElement* Element, bool LogMissing = false) -> void {
TSet<FString> HandledAttributes;
FString ElementName = UTF8_TO_TCHAR(Element->Name());
for (FProperty* Prop : TFieldRange<FProperty>(StructDef))
{
FString AttrKey = Prop->GetMetaData(TEXT("Attribute"));
FString PropName = Prop->GetName();
if (PropName == TEXT("Type"))
{
AttrKey = TEXT("type");
}
else if (AttrKey.IsEmpty())
{
continue;
}
if (const char* XmlVal = Element->Attribute(TCHAR_TO_ANSI(*AttrKey)))
{
ParseMujocoElementAttribute(Element, AttrKey, DataPtr, Prop);
FString Combined = ElementName + TEXT(":") + AttrKey;
HandledAttributes.Add(Combined);
}
}
if (LogMissing)
{
static std::unordered_set<std::string> Logged;
for (auto* Attr = Element->FirstAttribute(); Attr; Attr = Attr->Next())
{
FString AttrKey = UTF8_TO_TCHAR(Attr->Name());
FString Combined = ElementName + TEXT(":") + AttrKey;
if (HandledAttributes.Contains(Combined))
{
continue;
}
if (Logged.find(TCHAR_TO_UTF8(*Combined)) == Logged.end())
{
Logged.insert(TCHAR_TO_UTF8(*Combined));
UE_LOG(LogMujoco, Error, TEXT("Unhandled attribute: %s"), *Combined);
}
}
}
};
FString PackagePath{ FPaths::GetPath(GeneratedPackage->GetName()) };
FString PackageName{ FPaths::GetBaseFilename(GeneratedPackage->GetName()) };
TMap<FString, TArray<USCS_Node*>> ImportedMeshFiles;
TMap<FString, TArray<USCS_Node*>> ImportedMaterials;
TMap<FString, FString> ToBeImportedMeshFiles;
TMap<FString, FString> ToBeImportedMaterials;
TMap<FString, FString> ToBeImportedTextures;
TMap<FString, FString> MeshNameToPath;
TMap<FString, FVector> MeshSizes;
if (ImportSettings->bImportMeshes)
{
if (tinyxml2::XMLElement* RootElem = Doc->RootElement())
{
if (tinyxml2::XMLElement* AssetElem = RootElem->FirstChildElement("asset"))
{
if (ImportSettings->bImportTextures)
{
FString TexturePath = FPaths::Combine(PackagePath, PackageName);
if (!ImportSettings->TextureSubdir.IsEmpty())
{
TexturePath = FPaths::Combine(PackagePath, PackageName, ImportSettings->TextureSubdir);
}
FAssetToolsModule& AssetTools = FModuleManager::Get().LoadModuleChecked<FAssetToolsModule>("AssetTools");
for (tinyxml2::XMLElement* MeshElem = AssetElem->FirstChildElement("texture"); MeshElem; MeshElem = MeshElem->NextSiblingElement("texture"))
{
const char* TextureFileCStr = MeshElem->Attribute("file");
if (!TextureFileCStr)
{
UE_LOG(LogMujoco, Error, TEXT("Texture element missing file attribute"));
continue;
}
FString TextureFile = TextureFileCStr ? UTF8_TO_TCHAR(TextureFileCStr) : TEXT("");
FString FileName = FPaths::GetBaseFilename(TextureFile);
FString TextureFilePath = FPaths::Combine(MujocoTextureAssetPath, TextureFile);
TexturePath = FPaths::Combine(TexturePath, FileName);
if (!FPaths::FileExists(TextureFilePath))
{
UE_LOG(LogMujoco, Error, TEXT("Texture file not found: %s"), *TextureFilePath);
continue;
}
UPackage* TexturePackage = CreatePackage(*TexturePath);
if (!TexturePackage)
{
UE_LOG(LogMujoco, Error, TEXT("Failed to create package for texture: %s"), *TextureFilePath);
continue;
}
if (UTexture2D* SourceTexture = FImageUtils::ImportFileAsTexture2D(TextureFilePath))
{
UTexture2D* NewTexture = NewObject<UTexture2D>(TexturePackage, *FPaths::GetBaseFilename(TexturePath), RF_Public | RF_Standalone);
if (!NewTexture)
{
UE_LOG(LogMujoco, Error, TEXT("Failed to create texture object for: %s"), *TextureFilePath);
continue;
}
int32 Width = SourceTexture->GetSizeX();
int32 Height = SourceTexture->GetSizeY();
NewTexture->CompressionSettings = TextureCompressionSettings::TC_Default;
NewTexture->SRGB = true;
NewTexture->LODGroup = TEXTUREGROUP_World;
NewTexture->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
NewTexture->NeverStream = true;
FTexture2DMipMap& Mip = SourceTexture->GetPlatformData()->Mips[0];
const uint8* RawData = static_cast<const uint8*>(Mip.BulkData.Lock(LOCK_READ_ONLY));
if (RawData)
{
NewTexture->Source.Init(Width, Height, 1, 1, ETextureSourceFormat::TSF_BGRA8, RawData);
}
Mip.BulkData.Unlock();
NewTexture->UpdateResource();
NewTexture->PreEditChange(nullptr);
NewTexture->PostEditChange();
NewTexture->MarkPackageDirty();
FAssetRegistryModule::AssetCreated(NewTexture);
UE_LOG(LogTemp, Log, TEXT("Texture Created: %s"), *NewTexture->GetName());
TexturePackage->MarkPackageDirty();
ToBeImportedTextures.Add(FileName, TexturePath);
}
}
}
if (ImportSettings->bImportMaterials)
{
FString MaterialPath = FPaths::Combine(PackagePath, PackageName);
if (!ImportSettings->MaterialSubdir.IsEmpty())
{
MaterialPath = FPaths::Combine(PackagePath, PackageName, ImportSettings->MaterialSubdir);
}
FAssetToolsModule& AssetTools = FModuleManager::Get().LoadModuleChecked<FAssetToolsModule>("AssetTools");
for (tinyxml2::XMLElement* MeshElem = AssetElem->FirstChildElement("material"); MeshElem; MeshElem = MeshElem->NextSiblingElement("material"))
{
const char* MaterialNameCStr = MeshElem->Attribute("name");
if (!MaterialNameCStr)
{
UE_LOG(LogMujoco, Warning, TEXT("Material element missing name attribute"));
continue;
}
FString MaterialName = UTF8_TO_TCHAR(MaterialNameCStr);
FString MaterialAssetName = TEXT("MI_") + FStringUtils::ConvertToPascalCase(MaterialName);
const char* MaterialColorCStr = MeshElem->Attribute("rgba");
const char* TextureNameStr = MeshElem->Attribute("texture");
const char* SpecularStr = MeshElem->Attribute("specular");
const char* ShininessStr = MeshElem->Attribute("shininess");
const char* ReflectanceStr = MeshElem->Attribute("reflectance");
FColor MaterialColor = FColor::White;
UMaterialFactoryNew* MaterialFactory = NewObject<UMaterialFactoryNew>();
if (UMaterial* NewMaterial = Cast<UMaterial>(AssetTools.Get().CreateAsset(MaterialAssetName, MaterialPath, UMaterial::StaticClass(), MaterialFactory)))
{
UMaterialEditorOnlyData& Data = *NewMaterial->GetEditorOnlyData();
if (MaterialColorCStr)
{
TArray<FString> Components;
FString MaterialColorStr = UTF8_TO_TCHAR(MaterialColorCStr);
MaterialColorStr.ParseIntoArray(Components, TEXT(" "), true);
if (Components.Num() >= 4)
{
const float RedValue = FCString::Atof(*Components[0]);
const float GreenValue = FCString::Atof(*Components[1]);
const float BlueValue = FCString::Atof(*Components[2]);
const float AlphaValue = FCString::Atof(*Components[3]);
const FColor ColorValue{ static_cast<uint8>(RedValue * 255.f), static_cast<uint8>(GreenValue * 255.f), static_cast<uint8>(BlueValue * 255.f), static_cast<uint8>(AlphaValue * 255.f) };
UMaterialExpressionVectorParameter* DiffuseParam = NewObject<UMaterialExpressionVectorParameter>(NewMaterial);
DiffuseParam->ParameterName = TEXT("DiffuseColor");
DiffuseParam->DefaultValue = FLinearColor(ColorValue);
NewMaterial->GetExpressionCollection().AddExpression(DiffuseParam);
Data.BaseColor.Expression = DiffuseParam;
}
}
if (TextureNameStr)
{
FString TextureName = UTF8_TO_TCHAR(TextureNameStr);
FString TexturePath = ToBeImportedTextures.FindRef(TextureName);
if (!TexturePath.IsEmpty())
{
UMaterialExpressionTextureSample* TextureExpression = NewObject<UMaterialExpressionTextureSample>(NewMaterial);
auto* Texture = Cast<UTexture>(StaticLoadObject(UTexture::StaticClass(), nullptr, *TexturePath));
TextureExpression->SamplerType = SAMPLERTYPE_Color;
NewMaterial->GetExpressionCollection().AddExpression(TextureExpression);
if (Texture)
{
TextureExpression->Texture = Texture;
}
Data.BaseColor.Expression = TextureExpression;
}
}
if (SpecularStr)
{
float SpecularValue = FCString::Atof(UTF8_TO_TCHAR(SpecularStr));
UMaterialExpressionScalarParameter* SpecularParam = NewObject<UMaterialExpressionScalarParameter>(NewMaterial);
SpecularParam->ParameterName = TEXT("Specular");
SpecularParam->DefaultValue = SpecularValue;
NewMaterial->GetExpressionCollection().AddExpression(SpecularParam);
Data.Specular.Expression = SpecularParam;
}
if (ShininessStr)
{
float ShininessValue = FCString::Atof(UTF8_TO_TCHAR(ShininessStr));
UMaterialExpressionScalarParameter* ShininessParam = NewObject<UMaterialExpressionScalarParameter>(NewMaterial);
ShininessParam->ParameterName = TEXT("Shininess");
ShininessParam->DefaultValue = ShininessValue;
NewMaterial->GetExpressionCollection().AddExpression(ShininessParam);
Data.Roughness.Expression = ShininessParam;
}
if (ReflectanceStr)
{
float ReflectanceValue = FCString::Atof(UTF8_TO_TCHAR(ReflectanceStr));
UMaterialExpressionScalarParameter* ReflectanceParam = NewObject<UMaterialExpressionScalarParameter>(NewMaterial);
ReflectanceParam->ParameterName = TEXT("Reflectance");
ReflectanceParam->DefaultValue = ReflectanceValue;
NewMaterial->GetExpressionCollection().AddExpression(ReflectanceParam);
Data.Metallic.Expression = ReflectanceParam;
}
NewMaterial->PreEditChange(nullptr);
NewMaterial->PostEditChange();
NewMaterial->MarkPackageDirty();
FAssetRegistryModule::AssetCreated(NewMaterial);
ToBeImportedMaterials.Add(MaterialName, NewMaterial->GetPathName());
}
}
}
for (tinyxml2::XMLElement* MeshElem = AssetElem->FirstChildElement("mesh"); MeshElem; MeshElem = MeshElem->NextSiblingElement("mesh"))
{
const char* MeshFileCStr = MeshElem->Attribute("file");
if (!MeshFileCStr)
{
continue;
}
FString MeshFile = MeshFileCStr ? UTF8_TO_TCHAR(MeshFileCStr) : TEXT("");
const char* MeshNameCStr = MeshElem->Attribute("name");
FString MeshName = MeshNameCStr ? UTF8_TO_TCHAR(MeshNameCStr) : TEXT("");
FString FileName = FPaths::GetBaseFilename(MeshFile);
if (MeshName.IsEmpty())
{
MeshName = FileName;
}
const char* MeshSizeCStr = MeshElem->Attribute("scale");
FVector MeshSize = FVector::One();
if (MeshSizeCStr)
{
TArray<FString> Components;
FString MeshSizeStr = UTF8_TO_TCHAR(MeshSizeCStr);
MeshSizeStr.ParseIntoArray(Components, TEXT(" "), true);
if (Components.Num() >= 3)
{
MeshSize = FVector(FCString::Atof(*Components[0]), FCString::Atof(*Components[1]), FCString::Atof(*Components[2]));
}
}
MeshSizes.Add(MeshName, MeshSize);
FString MeshPath = FPaths::Combine(MujocoMeshAssetPath, MeshFile);
ToBeImportedMeshFiles.Add(MeshName, MeshPath);
FString PostFix = "OBJ";
if (MeshFile.EndsWith(".stl"))
{
PostFix = "STL";
}
if (!ImportSettings->MeshSubdir.IsEmpty())
{
FString MeshAssetPath = FPaths::Combine(PackagePath, PackageName, ImportSettings->MeshSubdir, PostFix, FileName);
MeshNameToPath.Add(MeshName, MeshAssetPath);
}
else
{
FString MeshAssetPath = FPaths::Combine(PackagePath, PackageName, PostFix, FileName);
MeshNameToPath.Add(MeshName, MeshAssetPath);
}
}
}
}
}
const TObjectPtr<UMjocoImportSettings>& Settings = ImportSettings;
int32 GeomCounter = 0;
int32 JointCounter = 0;
int32 SiteCounter = 0;
int32 TendonCounter = 0;
int32 ActuatorCounter = 0;
int32 EqualityCounter = 0;
// Recursive lambda to process body elements.
std::function<USCS_Node*(tinyxml2::XMLElement*, USCS_Node*)> ProcessBody = [&](tinyxml2::XMLElement* BodyElem, USCS_Node* ParentNode) -> USCS_Node* {
const char* BodyNameCStr = BodyElem->Attribute("name");
FString BodyName = BodyNameCStr ? UTF8_TO_TCHAR(BodyNameCStr) : TEXT("");
USCS_Node* BodyNode = SCS->CreateNode(UMujocoBodyComponent::StaticClass(), FName(*FString::Printf(TEXT("Body_%s"), *BodyName)));
UMujocoBodyComponent* BodyComp = Cast<UMujocoBodyComponent>(BodyNode->ComponentTemplate);
// Set this body component's transform using its "pos" and "quat" attributes.
SetSceneComponentTransformFromXml(BodyElem, BodyComp);
// Apply other body-specific attributes (e.g. "mocap", "gravcomp").
ApplyAttributes(UMujocoBodyComponent::StaticClass(), BodyComp, BodyElem);
for (tinyxml2::XMLElement* Child = BodyElem->FirstChildElement(); Child; Child = Child->NextSiblingElement())
{
if (!strcmp(Child->Name(), "inertial"))
{
// Process inertial sub-element into the BodyComp's inertial data.
if (auto* OptionalInertial = static_cast<TOptional<FMujocoInertial>*>(UMujocoBodyComponent::StaticClass()->FindPropertyByName(TEXT("Inertial"))->ContainerPtrToValuePtr<void>(BodyComp)))
{
FMujocoInertial Inertial;
*OptionalInertial = TOptional<FMujocoInertial>{ Inertial };
ApplyAttributes(FMujocoInertial::StaticStruct(), &OptionalInertial->GetValue(), Child, true);
}
}
else if (!strcmp(Child->Name(), "joint") || !strcmp(Child->Name(), "freejoint"))
{
const char* JointNameCStr = Child->Attribute("name");
FString JointName = UTF8_TO_TCHAR(JointNameCStr);
if (JointName.IsEmpty())
{
JointName = FString::Printf(TEXT("joint_%d"), JointCounter++);
}
if (!strcmp(Child->Name(), "freejoint"))
{
Child->SetAttribute("type", "free");
}
USCS_Node* JointNode = SCS->CreateNode(UMujocoJointComponent::StaticClass(), FName(*FString::Printf(TEXT("%s"), *JointName)));
UMujocoJointComponent* JointComp = Cast<UMujocoJointComponent>(JointNode->ComponentTemplate);
FMujocoJoint* JointCompJoint = static_cast<FMujocoJoint*>(UMujocoJointComponent::StaticClass()->FindPropertyByName(TEXT("Joint"))->ContainerPtrToValuePtr<void>(JointComp));
// Set joint transform.
SetSceneComponentTransformFromXml(Child, JointComp);
ApplyAttributes(UMujocoJointComponent::StaticClass(), JointComp, Child);
ApplyAttributes(FMujocoJoint::StaticStruct(), JointCompJoint, Child, true);
BodyNode->AddChildNode(JointNode);
}
else if (!strcmp(Child->Name(), "site"))
{
const char* SiteNameCStr = Child->Attribute("name");
FString SiteName = UTF8_TO_TCHAR(SiteNameCStr);
if (SiteName.IsEmpty())
{
SiteName = FString::Printf(TEXT("site_%d"), SiteCounter++);
}
USCS_Node* SiteNode = SCS->CreateNode(UMujocoSiteComponent::StaticClass(), FName(*FString::Printf(TEXT("%s"), *SiteName)));
UMujocoSiteComponent* SiteComp = Cast<UMujocoSiteComponent>(SiteNode->ComponentTemplate);
FMujocoSite* SiteCompSite = static_cast<FMujocoSite*>(UMujocoSiteComponent::StaticClass()->FindPropertyByName(TEXT("Site"))->ContainerPtrToValuePtr<void>(SiteComp));
// Set site transform.
SetSceneComponentTransformFromXml(Child, SiteComp);
ApplyAttributes(UMujocoSiteComponent::StaticClass(), SiteComp, Child);
ApplyAttributes(FMujocoSite::StaticStruct(), SiteCompSite, Child, true);
BodyNode->AddChildNode(SiteNode);
}
else if (!strcmp(Child->Name(), "geom"))
{
const char* GeomNameCStr = Child->Attribute("name");
FString GeomName = UTF8_TO_TCHAR(GeomNameCStr);
if (GeomName.IsEmpty())
{
GeomName = FString::Printf(TEXT("geom_%d"), GeomCounter++);
}
USCS_Node* GeomNode = SCS->CreateNode(UMujocoGeomComponent::StaticClass(), FName(*FString::Printf(TEXT("%s"), *GeomName)));
UMujocoGeomComponent* GeomComp = Cast<UMujocoGeomComponent>(GeomNode->ComponentTemplate);
FMujocoGeom* GeomCompGeom = static_cast<FMujocoGeom*>(UMujocoGeomComponent::StaticClass()->FindPropertyByName(TEXT("Geom"))->ContainerPtrToValuePtr<void>(GeomComp));
// Set geom transform.
SetSceneComponentTransformFromXml(Child, GeomComp);
ApplyAttributes(UMujocoGeomComponent::StaticClass(), GeomComp, Child);
ApplyAttributes(FMujocoGeom::StaticStruct(), GeomCompGeom, Child, true);
bool IsVisible = true;
if (GeomComp->Geom.Group.IsSet())
{
switch (GeomComp->Geom.Group.GetValue())
{
case 0:
IsVisible = Settings->VisibleGeomGroups.bGeomGroup0;
break;
case 1:
IsVisible = Settings->VisibleGeomGroups.bGeomGroup1;
break;
case 2:
IsVisible = Settings->VisibleGeomGroups.bGeomGroup2;
break;
case 3:
IsVisible = Settings->VisibleGeomGroups.bGeomGroup3;
break;
case 4:
IsVisible = Settings->VisibleGeomGroups.bGeomGroup4;
break;
case 5:
IsVisible = Settings->VisibleGeomGroups.bGeomGroup5;
break;
}
}
if (!IsVisible)
{
GeomComp->SetVisibility(false);
}
const char* Material = Child->Attribute("material");
if (Material)
{
FString MaterialStr = UTF8_TO_TCHAR(Material);
auto MaterialPath = ToBeImportedMaterials.Find(MaterialStr);
if (MaterialPath)
{
auto& NodeArray = ImportedMaterials.FindOrAdd(MaterialStr);
NodeArray.Add(GeomNode);
}
}
const char* Type = Child->Attribute("type");
const char* MeshName = Child->Attribute("mesh");
FString MeshAssetPath = FPaths::Combine(PackagePath, PackageName, ImportSettings->MeshSubdir);
FVector OneScale = FVector::One();
if (Type)
{
if (strcmp(Type, "mesh") == 0 && MeshName)
{
FString MeshNameStr = UTF8_TO_TCHAR(MeshName);
auto& NodeArray = ImportedMeshFiles.FindOrAdd(MeshNameStr);
NodeArray.Add(GeomNode);
}
// sphere, capsule, ellipsoid, cylinder, box
/*
sphere 1 Radius of the sphere.
capsule 1 or 2 Radius of the capsule; half-length of the cylinder part when not using the fromto specification.
ellipsoid 3 X radius; Y radius; Z radius.
cylinder 1 or 2 Radius of the cylinder; half-length of the cylinder when not using the fromto specification.
box 3 X half-size; Y half-size; Z half-size.
*/
// PackagePath, PackageName, GeomName
else if (strcmp(Type, "box") == 0)
{
FVector Size = FVector::One() * 100.0f;
const char* SizeCStr = Child->Attribute("size");
if (SizeCStr)
{
TArray<FString> Components;
FString SizeStr = UTF8_TO_TCHAR(SizeCStr);
SizeStr.ParseIntoArray(Components, TEXT(" "), true);
if (Components.Num() >= 3)
{
Size *= FVector(FCString::Atof(*Components[0]), FCString::Atof(*Components[1]), FCString::Atof(*Components[2]));
}
if (UStaticMesh* Mesh = MujocoMeshFactory::CreateBoxMesh(Size, MeshAssetPath, GeomName))
{
SetGeomMesh(GeomComp, OneScale, Mesh);
}
}
}
else if (strcmp(Type, "cylinder") == 0)
{
FVector Size = FVector::One() * 100.0f;
const char* SizeCStr = Child->Attribute("size");
if (SizeCStr)
{
TArray<FString> Components;
FString SizeStr = UTF8_TO_TCHAR(SizeCStr);
SizeStr.ParseIntoArray(Components, TEXT(" "), true);
if (Components.Num() >= 2)
{
Size *= FVector(FCString::Atof(*Components[0]), FCString::Atof(*Components[1]), FCString::Atof(*Components[1]));
}
if (UStaticMesh* Mesh = MujocoMeshFactory::CreateCylinderMesh(Size, MeshAssetPath, GeomName))
{
SetGeomMesh(GeomComp, OneScale, Mesh);
}
}
}
else if (strcmp(Type, "ellipsoid") == 0)
{
FVector Size = FVector::One() * 100.0f;
const char* SizeCStr = Child->Attribute("size");
if (SizeCStr)
{
TArray<FString> Components;
FString SizeStr = UTF8_TO_TCHAR(SizeCStr);
SizeStr.ParseIntoArray(Components, TEXT(" "), true);
if (Components.Num() >= 3)
{
Size *= FVector(FCString::Atof(*Components[0]), FCString::Atof(*Components[1]), FCString::Atof(*Components[2]));
}
if (UStaticMesh* Mesh = MujocoMeshFactory::CreateEllipsoidMesh(Size, MeshAssetPath, GeomName))
{
SetGeomMesh(GeomComp, OneScale, Mesh);
}
}
}
else if (strcmp(Type, "capsule") == 0)
{
FVector Size = FVector::One() * 100.0f;
const char* SizeCStr = Child->Attribute("size");
if (SizeCStr)
{
TArray<FString> Components;
FString SizeStr = UTF8_TO_TCHAR(SizeCStr);
SizeStr.ParseIntoArray(Components, TEXT(" "), true);
if (Components.Num() >= 2)
{
Size *= FVector(FCString::Atof(*Components[0]), FCString::Atof(*Components[1]), FCString::Atof(*Components[1]));
}
if (UStaticMesh* Mesh = MujocoMeshFactory::CreateCapsuleMesh(Size, MeshAssetPath, GeomName))
{
SetGeomMesh(GeomComp, OneScale, Mesh);
}
}
}
else if (strcmp(Type, "sphere") == 0)
{
FVector Size = FVector::One() * 100.0f;
const char* SizeCStr = Child->Attribute("size");
if (SizeCStr)
{
TArray<FString> Components;
FString SizeStr = UTF8_TO_TCHAR(SizeCStr);
SizeStr.ParseIntoArray(Components, TEXT(" "), true);
if (Components.Num() >= 1)
{
Size *= FVector(FCString::Atof(*Components[0]), FCString::Atof(*Components[0]), FCString::Atof(*Components[0]));
}
if (UStaticMesh* Mesh = MujocoMeshFactory::CreateSphereMesh(Size, MeshAssetPath, GeomName))
{
SetGeomMesh(GeomComp, OneScale, Mesh);
}
}
}
else
{
UE_LOG(LogMujoco, Warning, TEXT("Unsupported geom type: %s"), UTF8_TO_TCHAR(Type));
}
}
BodyNode->AddChildNode(GeomNode);
}
else if (!strcmp(Child->Name(), "body"))
{
// Process nested body recursively.
ProcessBody(Child, BodyNode);
}
}
if (ParentNode)
{
ParentNode->AddChildNode(BodyNode);
}
else
{
SCS->AddNode(BodyNode);
}
return BodyNode;
};
// Process the "worldbody" element.
if (tinyxml2::XMLElement* RootElem = Doc->RootElement())
{
for (tinyxml2::XMLElement* Child = RootElem->FirstChildElement(); Child; Child = Child->NextSiblingElement())
{
if (!strcmp(Child->Name(), "worldbody"))
{
for (tinyxml2::XMLElement* BodyElem = Child->FirstChildElement("body"); BodyElem; BodyElem = BodyElem->NextSiblingElement("body"))
{
ProcessBody(BodyElem, nullptr);
}
}
else if (!strcmp(Child->Name(), "tendon"))
{
for (tinyxml2::XMLElement* TendonElem = Child->FirstChildElement(); TendonElem; TendonElem = TendonElem->NextSiblingElement())
{
TendonElem->SetAttribute("type", TendonElem->Name());
if (!strcmp(TendonElem->Name(), "fixed"))
{
const char* TendonNameCStr = TendonElem->Attribute("name");
FString TendonName = UTF8_TO_TCHAR(TendonNameCStr);
if (TendonName.IsEmpty())
{
TendonName = FString::Printf(TEXT("tendon_%d"), TendonCounter++);
}
USCS_Node* TendonNode = SCS->CreateNode(UMujocoTendonComponent::StaticClass(), FName(*FString::Printf(TEXT("%s"), *TendonName)));
UMujocoTendonComponent* TendonComp = Cast<UMujocoTendonComponent>(TendonNode->ComponentTemplate);
FMujocoTendon* TendonStruct = static_cast<FMujocoTendon*>(UMujocoTendonComponent::StaticClass()->FindPropertyByName(TEXT("Tendon"))->ContainerPtrToValuePtr<void>(TendonComp));
ApplyAttributes(FMujocoTendon::StaticStruct(), TendonStruct, TendonElem, true);
for (tinyxml2::XMLElement* JointElem = TendonElem->FirstChildElement(); JointElem; JointElem = JointElem->NextSiblingElement())
{
if (!strcmp(JointElem->Name(), "joint"))
{
const char* JointNameCStr = JointElem->Attribute("joint");
FString JointName = UTF8_TO_TCHAR(JointNameCStr);
if (JointName.IsEmpty())
{
UE_LOG(LogMujoco, Error, TEXT("Joint name missing"));
continue;
}
FMujocoTendonFixedJoint FixedJoint;
FixedJoint.Joint = *JointName;
const char* CoefCStr = JointElem->Attribute("coef");
if (CoefCStr)
{
FixedJoint.Coef = FCString::Atof(UTF8_TO_TCHAR(CoefCStr));
}
TendonStruct->FixedJoint.Add(FixedJoint);
}
}
SCS->AddNode(TendonNode);
}
else if (!strcmp(TendonElem->Name(), "spatial"))
{
const char* TendonNameCStr = TendonElem->Attribute("name");
FString TendonName = UTF8_TO_TCHAR(TendonNameCStr);
if (TendonName.IsEmpty())
{
TendonName = FString::Printf(TEXT("tendon_%d"), TendonCounter++);
}
USCS_Node* TendonNode = SCS->CreateNode(UMujocoTendonComponent::StaticClass(), FName(*FString::Printf(TEXT("%s"), *TendonName)));
UMujocoTendonComponent* TendonComp = Cast<UMujocoTendonComponent>(TendonNode->ComponentTemplate);
FMujocoTendon* TendonStruct = static_cast<FMujocoTendon*>(UMujocoTendonComponent::StaticClass()->FindPropertyByName(TEXT("Tendon"))->ContainerPtrToValuePtr<void>(TendonComp));
ApplyAttributes(FMujocoTendon::StaticStruct(), TendonStruct, TendonElem, true);
for (tinyxml2::XMLElement* Elem = TendonElem->FirstChildElement(); Elem; Elem = Elem->NextSiblingElement())
{
if (!strcmp(Elem->Name(), "site"))
{
const char* SiteNameCStr = Elem->Attribute("site");
FString SiteName = UTF8_TO_TCHAR(SiteNameCStr);
if (SiteName.IsEmpty())
{
UE_LOG(LogMujoco, Error, TEXT("Site name missing"));
continue;
}
FMujocoTendonSpatialSite SpatialSite;
SpatialSite.Site = *SiteName;
TendonStruct->SpatialSite.Add(SpatialSite);
}
else if (!strcmp(Elem->Name(), "geom"))
{
FMujocoTendonSpatialGeom SpatialGeom;
const char* GeomNameCStr = Elem->Attribute("geom");
FString GeomName = UTF8_TO_TCHAR(GeomNameCStr);
if (!GeomName.IsEmpty())
{
SpatialGeom.Geom = *GeomName;
}
const char* SideSiteCStr = Elem->Attribute("sidesite");
FString SideSite = UTF8_TO_TCHAR(SideSiteCStr);
if (!SideSite.IsEmpty())
{
SpatialGeom.SideSite = *SideSite;
}
TendonStruct->SpatialGeom.Add(SpatialGeom);
}
else if (!strcmp(Elem->Name(), "pulley"))
{
const char* PulleyNameCStr = Elem->Attribute("pulley");
FString PulleyName = UTF8_TO_TCHAR(PulleyNameCStr);
if (PulleyName.IsEmpty())
{
UE_LOG(LogMujoco, Error, TEXT("Pulley name missing"));
continue;
}
FMujocoTendonPulley SpatialPulley;
const char* DivisorStr = Elem->Attribute("divisor");
if (DivisorStr)
{
SpatialPulley.Divisor = FCString::Atoi(UTF8_TO_TCHAR(DivisorStr));
}
TendonStruct->Pulley.Add(SpatialPulley);
}
}
SCS->AddNode(TendonNode);
}
else
{
UE_LOG(LogMujoco, Error, TEXT("Unsupported tendon type: %s"), UTF8_TO_TCHAR(TendonElem->Name()));
}
}
}
else if (!strcmp(Child->Name(), "equality"))
{
for (tinyxml2::XMLElement* Elem = Child->FirstChildElement(); Elem; Elem = Elem->NextSiblingElement())
{
const char* EqualityNameCStr = Elem->Attribute("name");
FString EqualityName = UTF8_TO_TCHAR(EqualityNameCStr);
if (EqualityName.IsEmpty())
{
EqualityName = FString::Printf(TEXT("equality_%d"), EqualityCounter++);
}
USCS_Node* EqualityNode = SCS->CreateNode(UMujocoEqualityComponent::StaticClass(), FName(*FString::Printf(TEXT("%s"), *EqualityName)));
UMujocoEqualityComponent* EqualityComp = Cast<UMujocoEqualityComponent>(EqualityNode->ComponentTemplate);
ApplyAttributes(UMujocoEqualityComponent::StaticClass(), EqualityComp, Elem);
FMujocoEquality* Equality = static_cast<FMujocoEquality*>(UMujocoEqualityComponent::StaticClass()->FindPropertyByName(TEXT("Equality"))->ContainerPtrToValuePtr<void>(EqualityComp));
Elem->SetAttribute("type", Elem->Name());
ApplyAttributes(FMujocoEquality::StaticStruct(), Equality, Elem, true);
SCS->AddNode(EqualityNode);
}
}
else if (!strcmp(Child->Name(), "actuator"))
{
for (tinyxml2::XMLElement* Elem = Child->FirstChildElement(); Elem; Elem = Elem->NextSiblingElement())
{
const char* NameCStr = Elem->Attribute("name");
FString Name = UTF8_TO_TCHAR(NameCStr);
if (Name.IsEmpty())
{
Name = FString::Printf(TEXT("actuator_%d"), ActuatorCounter++);
}
USCS_Node* Node = SCS->CreateNode(UMujocoActuatorComponent::StaticClass(), FName(*FString::Printf(TEXT("%s"), *Name)));
UMujocoActuatorComponent* Comp = Cast<UMujocoActuatorComponent>(Node->ComponentTemplate);
ApplyAttributes(UMujocoActuatorComponent::StaticClass(), Comp, Elem);
FMujocoActuatorV2* Data = static_cast<FMujocoActuatorV2*>(UMujocoActuatorComponent::StaticClass()->FindPropertyByName(TEXT("Actuator"))->ContainerPtrToValuePtr<void>(Comp));
Elem->SetAttribute("type", Elem->Name());
ApplyAttributes(FMujocoActuatorV2::StaticStruct(), Data, Elem, true);
SCS->AddNode(Node);
}
}
}
}
// Import meshes.
if (!ToBeImportedMeshFiles.IsEmpty())
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
TObjectPtr<UAutomatedAssetImportData> ImportData = NewObject<UAutomatedAssetImportData>();
UFbxFactory* FbxFactory = NewObject<UFbxFactory>();
FbxFactory->AddToRoot();
UUnrealSTLFactory* STLFactory = NewObject<UUnrealSTLFactory>();
STLFactory->AddToRoot();
float Scale{ 100.0f };
FbxFactory->ImportUI->StaticMeshImportData->ImportUniformScale = Scale;
FbxFactory->ImportUI->StaticMeshImportData->bConvertScene = false;
FbxFactory->ImportUI->StaticMeshImportData->bForceFrontXAxis = true;
FbxFactory->ImportUI->bImportMaterials = false;
STLFactory->ImportConfig.Transform.SetScale3D(FVector(1, 1, 1) * Scale);
STLFactory->ImportConfig.Transform.SetRotation(FRotator(0.0f, 0.0f, 0.0f).Quaternion());
ImportData->bReplaceExisting = true;
ImportData->bSkipReadOnly = true;
TArray<FString> STLFiles;
TArray<FString> FbxFiles;
for (const auto& MeshPair : ToBeImportedMeshFiles)
{
FString MeshName = MeshPair.Key;
FString MeshPath = MeshPair.Value;
FString MeshAssetPath = MeshNameToPath.FindRef(MeshName);
if (MeshAssetPath.IsEmpty())
{
continue;
}
if (MeshPath.EndsWith(".obj"))
{
FbxFiles.Add(MeshPath);
}
else if (MeshPath.EndsWith(".stl"))
{
STLFiles.Add(MeshPath);
}
else
{
UE_LOG(LogMujoco, Error, TEXT("Unsupported mesh format: %s"), *MeshPath);
}
}
if (FbxFiles.Num() > 0)
{
if (!ImportSettings->MeshSubdir.IsEmpty())
ImportData->DestinationPath = FPaths::Combine(PackagePath, PackageName, ImportSettings->MeshSubdir, "OBJ");
else
ImportData->DestinationPath = FPaths::Combine(PackagePath, PackageName, "OBJ");
ImportData->Factory = FbxFactory;
ImportData->Filenames = FbxFiles;
AssetTools.ImportAssetsAutomated(ImportData);
}
if (STLFiles.Num() > 0)
{
if (!ImportSettings->MeshSubdir.IsEmpty())
ImportData->DestinationPath = FPaths::Combine(PackagePath, PackageName, ImportSettings->MeshSubdir, "STL");
else
ImportData->DestinationPath = FPaths::Combine(PackagePath, PackageName, "STL");
ImportData->Factory = STLFactory;
ImportData->Filenames = STLFiles;
AssetTools.ImportAssetsAutomated(ImportData);
}
FbxFactory->RemoveFromRoot();
STLFactory->RemoveFromRoot();
}
if (!ImportedMeshFiles.IsEmpty())
{
for (const auto& MeshPair : ImportedMeshFiles)
{
FString MeshName = MeshPair.Key;
const TArray<USCS_Node*>& Nodes = MeshPair.Value;
FString MeshAssetPath = MeshNameToPath.FindRef(MeshName);
if (MeshAssetPath.IsEmpty())
{
UE_LOG(LogMujoco, Error, TEXT("MeshAssetPath empty: %s"), *MeshName);
continue;
}
UStaticMesh* Mesh = LoadObject<UStaticMesh>(nullptr, *MeshAssetPath);
if (!Mesh)
{
UE_LOG(LogMujoco, Error, TEXT("Failed to load mesh: %s"), *MeshAssetPath);
continue;
}
for (USCS_Node* Node : Nodes)
{
UMujocoGeomComponent* GeomComp = Cast<UMujocoGeomComponent>(Node->ComponentTemplate);
if (GeomComp)
{
FVector MeshScale = MeshSizes.FindRef(MeshName);
SetGeomMesh(GeomComp, MeshScale, Mesh);
}
}
}
}
if (!ImportedMaterials.IsEmpty())
{
for (const auto& MaterialPair : ImportedMaterials)
{
FString MaterialName = MaterialPair.Key;
const TArray<USCS_Node*>& Nodes = MaterialPair.Value;
FString MaterialAssetPath = ToBeImportedMaterials.FindRef(MaterialName);
if (MaterialAssetPath.IsEmpty())
{
UE_LOG(LogMujoco, Error, TEXT("MaterialAssetPath empty: %s"), *MaterialName);
continue;
}
UMaterial* Material = LoadObject<UMaterial>(nullptr, *MaterialAssetPath);
if (!Material)
{
UE_LOG(LogMujoco, Error, TEXT("Failed to load material: %s"), *MaterialAssetPath);
continue;
}
for (USCS_Node* Node : Nodes)
{
UMujocoGeomComponent* GeomComp = Cast<UMujocoGeomComponent>(Node->ComponentTemplate);
if (GeomComp)
{
GeomComp->SetMaterial(0, Material);
}
}
}
}
return true;
}
FReply OnImportClicked()
{
ImportSettings->SaveConfig();
ClassPropertyEntryBox->SetEnabled(false);
DetailsView->SetEnabled(false);
ImportButton->SetEnabled(false);
CancelButton->SetEnabled(false);
if (!GenerateBluePrint())
{
return FReply::Handled();
}
if (!AddComponents())
{
return FReply::Handled();
}
FAssetRegistryModule::AssetCreated(Blueprint);
FKismetEditorUtilities::CompileBlueprint(Blueprint);
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPostImport(Factory, Blueprint);
GeneratedPackage->MarkPackageDirty();
RequestDestroyWindow();
return FReply::Handled();
}
FReply OnCancelClicked()
{
RequestDestroyWindow();
return FReply::Handled();
}
static TSharedPtr<SMujocoImportWindow> ShowDialog(FString InFilename, UPackage* InParent, FName InName, EObjectFlags InFlags, UFactory* InFactory)
{
TSharedRef<SMujocoImportWindow> Window = SNew(SMujocoImportWindow, InFilename, InParent, InName, InFlags, InFactory);
TSharedPtr<SWindow> ParentWindow;
if (FModuleManager::Get().IsModuleLoaded("MainFrame"))
{
IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked<IMainFrameModule>("MainFrame");
ParentWindow = MainFrame.GetParentWindow();
}
if (ParentWindow.IsValid())
{
FSlateApplication::Get().AddModalWindow(Window, ParentWindow.ToSharedRef());
}
return Window;
}
};
UMujocoFactory::UMujocoFactory()
{
SupportedClass = NULL;
Formats.Add(TEXT("xml;Mujoco XML files"));
bCreateNew = false;
bText = false;
bEditorImport = true;
}
bool UMujocoFactory::ConfigureProperties()
{
return true;
}
void UMujocoFactory::PostInitProperties()
{
Super::PostInitProperties();
bEditorImport = true;
bText = false;
}
bool UMujocoFactory::FactoryCanImport(const FString& Filename)
{
return true;
}
bool UMujocoFactory::CanImportBeCanceled() const
{
return false;
}
IImportSettingsParser* UMujocoFactory::GetImportSettingsParser()
{
return nullptr;
}
TArray<FString> UMujocoFactory::GetFormats() const
{
return Formats;
}
bool UMujocoFactory::DoesSupportClass(UClass* Class)
{
return Class == UObject::StaticClass();
}
UClass* UMujocoFactory::ResolveSupportedClass()
{
return UObject::StaticClass();
}
UObject* UMujocoFactory::FactoryCreateFile(UClass* InClass, UObject* InParent, FName InName, EObjectFlags InFlags, const FString& InFilename, const TCHAR* InParms, FFeedbackContext* InWarn, bool& bOutOperationCanceled)
{
auto Window = SMujocoImportWindow::ShowDialog(InFilename, Cast<UPackage>(InParent), InName, InFlags, this);
if (Window->GetGeneratedAsset())
{
FSoftObjectPath FoundAssetPath = FSoftObjectPath(Window->GetGeneratedAsset()->GetPathName());
FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([this, FoundAssetPath](float DeltaTime) {
if (const IAssetRegistry* AssetRegistry = IAssetRegistry::Get())
{
const FAssetData AssetData = AssetRegistry->GetAssetByObjectPath(FoundAssetPath);
if (AssetData.IsValid())
{
ObjectTools::DeleteAssets({}, false);
}
}
if (UObject* AssetObject = FoundAssetPath.ResolveObject())
{
AssetObject->Rename(nullptr, GetTransientPackage(), REN_DontCreateRedirectors);
AssetObject->RemoveFromRoot();
AssetObject->ClearFlags(RF_Public | RF_Standalone);
AssetObject->MarkAsGarbage();
}
return false;
}));
}
return Window->GetGeneratedAsset();
}
#undef LOCTEXT_NAMESPACE