#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> TagDefaults; std::unordered_map NestedBlocks; }; std::unordered_map> 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 ParseGlobalDefaults(tinyxml2::XMLElement* GlobalDefault) { std::unordered_map Mapping; Mapping.emplace("global", ParseDefaultNode(GlobalDefault)); return Mapping; } void MergeAttributes(tinyxml2::XMLElement* Element, const std::unordered_map& 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* 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 Parse(const char* FileName) { auto Document = MakeUnique(); 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 DetailsView; TObjectPtr ImportSettings; UFactory* Factory = nullptr; TSharedPtr MainVerticalBox; TSharedPtr ImportButton; TSharedPtr CancelButton; TSharedPtr ErrorText; TSharedPtr ProgressText; TSharedPtr>> ClassPropertyEntryBox; FString Filename; TArray AllowedClasses; TArray> AllowedClassNames; TUniquePtr 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("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(ParentPackage); ImportSettings->SetFlags(RF_Transient | RF_Standalone | RF_Transactional); ImportSettings->ParentClass = AActor::StaticClass(); ImportSettings->LoadConfig(); SelectedClass = const_cast(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(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 Item) { return *Item == SelectedClass->GetName(); }); SelectedClassText = FText::FromString(*AllowedClassNames[SelectedIndex].Get()); SAssignNew(ClassPropertyEntryBox, SComboBox>) .OptionsSource(&AllowedClassNames) .OnGenerateWidget_Lambda([](TSharedPtr InItem) { return SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(FText::FromString(*InItem)); }) .OnSelectionChanged_Lambda([this](TSharedPtr InItem, ESelectInfo::Type SelectInfo) { int32 SelectedIndex = AllowedClassNames.IndexOfByPredicate([InItem](TSharedPtr Item) { return *Item == *InItem; }); ImportSettings->ParentClass = AllowedClasses[SelectedIndex]; SelectedClass = const_cast(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(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(SelectedClass); BPParams.bReplaceActor = false; BPParams.bOpenBlueprint = false; // first check if the blueprint already exists auto ExistingBlueprint = Cast(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()->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(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(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 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 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 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 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 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 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 HandledAttributes; FString ElementName = UTF8_TO_TCHAR(Element->Name()); for (FProperty* Prop : TFieldRange(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 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> ImportedMeshFiles; TMap> ImportedMaterials; TMap ToBeImportedMeshFiles; TMap ToBeImportedMaterials; TMap ToBeImportedTextures; TMap MeshNameToPath; TMap 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("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(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(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("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(); if (UMaterial* NewMaterial = Cast(AssetTools.Get().CreateAsset(MaterialAssetName, MaterialPath, UMaterial::StaticClass(), MaterialFactory))) { UMaterialEditorOnlyData& Data = *NewMaterial->GetEditorOnlyData(); if (MaterialColorCStr) { TArray 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(RedValue * 255.f), static_cast(GreenValue * 255.f), static_cast(BlueValue * 255.f), static_cast(AlphaValue * 255.f) }; UMaterialExpressionVectorParameter* DiffuseParam = NewObject(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(NewMaterial); auto* Texture = Cast(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(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(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(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 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& 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 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(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*>(UMujocoBodyComponent::StaticClass()->FindPropertyByName(TEXT("Inertial"))->ContainerPtrToValuePtr(BodyComp))) { FMujocoInertial Inertial; *OptionalInertial = TOptional{ 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(JointNode->ComponentTemplate); FMujocoJoint* JointCompJoint = static_cast(UMujocoJointComponent::StaticClass()->FindPropertyByName(TEXT("Joint"))->ContainerPtrToValuePtr(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(SiteNode->ComponentTemplate); FMujocoSite* SiteCompSite = static_cast(UMujocoSiteComponent::StaticClass()->FindPropertyByName(TEXT("Site"))->ContainerPtrToValuePtr(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(GeomNode->ComponentTemplate); FMujocoGeom* GeomCompGeom = static_cast(UMujocoGeomComponent::StaticClass()->FindPropertyByName(TEXT("Geom"))->ContainerPtrToValuePtr(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 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 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 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 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 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(TendonNode->ComponentTemplate); FMujocoTendon* TendonStruct = static_cast(UMujocoTendonComponent::StaticClass()->FindPropertyByName(TEXT("Tendon"))->ContainerPtrToValuePtr(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(TendonNode->ComponentTemplate); FMujocoTendon* TendonStruct = static_cast(UMujocoTendonComponent::StaticClass()->FindPropertyByName(TEXT("Tendon"))->ContainerPtrToValuePtr(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(EqualityNode->ComponentTemplate); ApplyAttributes(UMujocoEqualityComponent::StaticClass(), EqualityComp, Elem); FMujocoEquality* Equality = static_cast(UMujocoEqualityComponent::StaticClass()->FindPropertyByName(TEXT("Equality"))->ContainerPtrToValuePtr(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(Node->ComponentTemplate); ApplyAttributes(UMujocoActuatorComponent::StaticClass(), Comp, Elem); FMujocoActuatorV2* Data = static_cast(UMujocoActuatorComponent::StaticClass()->FindPropertyByName(TEXT("Actuator"))->ContainerPtrToValuePtr(Comp)); Elem->SetAttribute("type", Elem->Name()); ApplyAttributes(FMujocoActuatorV2::StaticStruct(), Data, Elem, true); SCS->AddNode(Node); } } } } // Import meshes. if (!ToBeImportedMeshFiles.IsEmpty()) { IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); TObjectPtr ImportData = NewObject(); UFbxFactory* FbxFactory = NewObject(); FbxFactory->AddToRoot(); UUnrealSTLFactory* STLFactory = NewObject(); 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 STLFiles; TArray 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& Nodes = MeshPair.Value; FString MeshAssetPath = MeshNameToPath.FindRef(MeshName); if (MeshAssetPath.IsEmpty()) { UE_LOG(LogMujoco, Error, TEXT("MeshAssetPath empty: %s"), *MeshName); continue; } UStaticMesh* Mesh = LoadObject(nullptr, *MeshAssetPath); if (!Mesh) { UE_LOG(LogMujoco, Error, TEXT("Failed to load mesh: %s"), *MeshAssetPath); continue; } for (USCS_Node* Node : Nodes) { UMujocoGeomComponent* GeomComp = Cast(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& Nodes = MaterialPair.Value; FString MaterialAssetPath = ToBeImportedMaterials.FindRef(MaterialName); if (MaterialAssetPath.IsEmpty()) { UE_LOG(LogMujoco, Error, TEXT("MaterialAssetPath empty: %s"), *MaterialName); continue; } UMaterial* Material = LoadObject(nullptr, *MaterialAssetPath); if (!Material) { UE_LOG(LogMujoco, Error, TEXT("Failed to load material: %s"), *MaterialAssetPath); continue; } for (USCS_Node* Node : Nodes) { UMujocoGeomComponent* GeomComp = Cast(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()->BroadcastAssetPostImport(Factory, Blueprint); GeneratedPackage->MarkPackageDirty(); RequestDestroyWindow(); return FReply::Handled(); } FReply OnCancelClicked() { RequestDestroyWindow(); return FReply::Handled(); } static TSharedPtr ShowDialog(FString InFilename, UPackage* InParent, FName InName, EObjectFlags InFlags, UFactory* InFactory) { TSharedRef Window = SNew(SMujocoImportWindow, InFilename, InParent, InName, InFlags, InFactory); TSharedPtr ParentWindow; if (FModuleManager::Get().IsModuleLoaded("MainFrame")) { IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked("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 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(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