// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= Main implementation of FFbxExporter : export FBX data from Unreal =============================================================================*/ #include "CoreMinimal.h" #include "EngineDefines.h" #include "Misc/MessageDialog.h" #include "Misc/Guid.h" #include "Misc/ConfigCacheIni.h" #include "Misc/EngineVersion.h" #include "Misc/App.h" #include "Components/ActorComponent.h" #include "GameFramework/Actor.h" #include "Engine/Blueprint.h" #include "RawIndexBuffer.h" #include "CinematicExporter.h" #include "Components/StaticMeshComponent.h" #include "Components/LightComponent.h" #include "Model.h" #include "Channels/MovieSceneDoubleChannel.h" #include "Channels/MovieSceneFloatChannel.h" #include "Channels/MovieSceneIntegerChannel.h" #include "Channels/MovieSceneStringChannel.h" #include "Channels/MovieSceneByteChannel.h" #include "Animation/AnimTypes.h" #include "Animation/AnimSequenceBase.h" #include "Animation/AnimSequence.h" #include "Components/SkeletalMeshComponent.h" #include "Editor/EditorPerProjectUserSettings.h" #include "Engine/Brush.h" #include "Camera/CameraActor.h" #include "Camera/CameraComponent.h" #include "Particles/Emitter.h" #include "Components/PointLightComponent.h" #include "Components/SpotLightComponent.h" #include "Engine/Light.h" #include "Engine/StaticMeshActor.h" #include "Components/ChildActorComponent.h" #include "Components/DirectionalLightComponent.h" #include "Components/InstancedStaticMeshComponent.h" #include "Engine/Polys.h" #include "Engine/SkeletalMesh.h" #include "Engine/StaticMesh.h" #include "Editor.h" #include "Channels/MovieSceneChannelProxy.h" #include "Materials/Material.h" #include "Materials/MaterialExpressionConstant.h" #include "Materials/MaterialExpressionVectorParameter.h" #include "Materials/MaterialExpressionConstant2Vector.h" #include "Materials/MaterialExpressionConstant3Vector.h" #include "Materials/MaterialExpressionConstant4Vector.h" #include "Materials/MaterialExpressionTextureSample.h" #include "Materials/MaterialInstance.h" #include "LandscapeProxy.h" #include "LandscapeInfo.h" #include "LandscapeComponent.h" #include "LandscapeDataAccess.h" #include "Components/SplineMeshComponent.h" #include "StaticMeshComponentLODInfo.h" #include "StaticMeshResources.h" #include "FbxExporter.h" #include "Exporters/FbxExportOption.h" #include "FbxExportOptionsWindow.h" #include "FbxAnimUtils.h" #include "INodeAndChannelMappings.h" #include "StaticMeshAttributes.h" #include "StaticMeshOperations.h" #include "Components/BrushComponent.h" #include "CineCameraComponent.h" #include "Math/UnitConversion.h" #include "IMovieScenePlayer.h" #include "MovieScene.h" #include "Tracks/MovieScene3DTransformTrack.h" #include "Tracks/MovieSceneColorTrack.h" #include "Tracks/MovieSceneDoubleTrack.h" #include "Tracks/MovieSceneFloatTrack.h" #include "Tracks/MovieSceneSkeletalAnimationTrack.h" #include "Tracks/MovieSceneVectorTrack.h" #include "Sections/MovieSceneSkeletalAnimationSection.h" #include "Sections/MovieScene3DTransformSection.h" #include "Sections/MovieSceneColorSection.h" #include "Sections/MovieSceneDoubleSection.h" #include "Sections/MovieSceneFloatSection.h" #include "Sections/MovieSceneVectorSection.h" #include "Evaluation/MovieScenePlayback.h" #include "Evaluation/MovieSceneEvaluationTemplateInstance.h" #include "MovieSceneSequenceID.h" #include "Evaluation/MovieSceneSequenceHierarchy.h" #include "Compilation/MovieSceneCompiledDataManager.h" #include "MovieSceneSequence.h" #include "MovieSceneTimeHelpers.h" #include "DynamicMeshBuilder.h" #include "PhysicsEngine/AggregateGeom.h" #include "PhysicsEngine/BodySetup.h" #include "PhysicsEngine/ConvexElem.h" #include "PhysicsEngine/SphereElem.h" #include "Chaos/Core.h" #include "Chaos/Particles.h" #include "Chaos/Plane.h" #include "ChaosCheck.h" #include "Animation/AnimationSettings.h" #include "Chaos/Convex.h" #include "Widgets/SWindow.h" #include "Framework/Application/SlateApplication.h" #include "Interfaces/IMainFrameModule.h" #include "UObject/MetaData.h" #include "FbxMaterialExportUtilities.h" #include "Exporters/Exporter.h" #include "MaterialPropertyEx.h" namespace UnFbx { TSharedPtr FFbxExporter::StaticInstance; FFbxExporter::FFbxExporter() { bBakeKeys = true; bKeepHierarchy = true; //We use the FGCObject pattern to keep the fbx export option alive during the editor session ExportOptionsUI = NewObject(); //Load the option from the user save ini file ExportOptionsUI->LoadOptions(); ExportOptionsOverride = nullptr; // Create the SdkManager SdkManager = FbxManager::Create(); // create an IOSettings object FbxIOSettings * ios = FbxIOSettings::Create(SdkManager, IOSROOT ); SdkManager->SetIOSettings(ios); DefaultCamera = NULL; } FFbxExporter::~FFbxExporter() { if (SdkManager) { SdkManager->Destroy(); SdkManager = NULL; } } FFbxExporter* FFbxExporter::GetInstance() { if (!StaticInstance.IsValid()) { StaticInstance = MakeShareable( new FFbxExporter() ); } return StaticInstance.Get(); } void FFbxExporter::DeleteInstance() { StaticInstance.Reset(); } void FFbxExporter::AddReferencedObjects(FReferenceCollector& Collector) { if (ExportOptionsUI != nullptr) { Collector.AddReferencedObject(ExportOptionsUI); } if (ExportOptionsOverride != nullptr) { Collector.AddReferencedObject(ExportOptionsOverride); } } void FFbxExporter::FillExportOptions(bool BatchMode, bool bShowOptionDialog, const FString& FullPath, bool& OutOperationCanceled, bool& bOutExportAll) { OutOperationCanceled = false; //Export option should have been set in the constructor check(ExportOptionsUI != nullptr); //Load the option from the user save ini file ExportOptionsUI->LoadOptions(); //Return if we do not show the export options or we are running automation test or we are unattended if (!bShowOptionDialog || GIsAutomationTesting || FApp::IsUnattended()) { return; } bOutExportAll = false; TSharedPtr ParentWindow; if (FModuleManager::Get().IsModuleLoaded("MainFrame")) { IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked("MainFrame"); ParentWindow = MainFrame.GetParentWindow(); } TSharedRef Window = SNew(SWindow) .Title(NSLOCTEXT("UnrealEd", "FBXExportOpionsTitle", "FBX Export Options")) .SizingRule(ESizingRule::UserSized) .AutoCenter(EAutoCenter::PrimaryWorkArea) .ClientSize(FVector2D(500, 445)); TSharedPtr FbxOptionWindow; Window->SetContent ( SAssignNew(FbxOptionWindow, SFbxExportOptionsWindow) .ExportOptions(ExportOptionsUI) .WidgetWindow(Window) .FullPath(FText::FromString(FullPath)) .BatchMode(BatchMode) ); FSlateApplication::Get().AddModalWindow(Window, ParentWindow, false); //Respect the edit condition if(ExportOptionsUI->bExportSourceMesh) { ExportOptionsUI->Collision = false; ExportOptionsUI->LevelOfDetail = false; } ExportOptionsUI->SaveOptions(); if (FbxOptionWindow->ShouldExport()) { bOutExportAll = FbxOptionWindow->ShouldExportAll(); } else { OutOperationCanceled = true; } } void FFbxExporter::SetExportOptionsOverride(UFbxExportOption* OverrideOptions) { ExportOptionsOverride = OverrideOptions; } UFbxExportOption* FFbxExporter::GetExportOptions() { return ExportOptionsOverride ? ExportOptionsOverride : ExportOptionsUI; } void FFbxExporter::CreateDocument() { Scene = FbxScene::Create(SdkManager,""); // create scene info FbxDocumentInfo* SceneInfo = FbxDocumentInfo::Create(SdkManager,"SceneInfo"); SceneInfo->mTitle = "Unreal FBX Exporter"; SceneInfo->mSubject = "Export FBX meshes from Unreal"; SceneInfo->Original_ApplicationVendor.Set( "Epic Games" ); SceneInfo->Original_ApplicationName.Set( "Unreal Engine" ); SceneInfo->Original_ApplicationVersion.Set( TCHAR_TO_UTF8(*FEngineVersion::Current().ToString()) ); SceneInfo->LastSaved_ApplicationVendor.Set( "Epic Games" ); SceneInfo->LastSaved_ApplicationName.Set( "Unreal Engine" ); SceneInfo->LastSaved_ApplicationVersion.Set( TCHAR_TO_UTF8(*FEngineVersion::Current().ToString()) ); Scene->SetSceneInfo(SceneInfo); //FbxScene->GetGlobalSettings().SetOriginalUpAxis(KFbxAxisSystem::Max); FbxAxisSystem::EFrontVector FrontVector = (FbxAxisSystem::EFrontVector)-FbxAxisSystem::eParityOdd; if (GetExportOptions()->bForceFrontXAxis) FrontVector = FbxAxisSystem::eParityEven; const FbxAxisSystem UnrealZUp(FbxAxisSystem::eZAxis, FrontVector, FbxAxisSystem::eRightHanded); Scene->GetGlobalSettings().SetAxisSystem(UnrealZUp); Scene->GetGlobalSettings().SetOriginalUpAxis(UnrealZUp); // Maya use cm by default Scene->GetGlobalSettings().SetSystemUnit(FbxSystemUnit::cm); //FbxScene->GetGlobalSettings().SetOriginalSystemUnit( KFbxSystemUnit::m ); bSceneGlobalTimeLineSet = false; Scene->GetGlobalSettings().SetTimeMode(FbxTime::eDefaultMode); // setup anim stack AnimStack = FbxAnimStack::Create(Scene, "Unreal Take"); //KFbxSet(AnimStack->LocalStart, KTIME_ONE_SECOND); AnimStack->Description.Set("Animation Take for Unreal."); // this take contains one base layer. In fact having at least one layer is mandatory. AnimLayer = FbxAnimLayer::Create(Scene, "Base Layer"); AnimStack->AddMember(AnimLayer); } #ifdef IOS_REF #undef IOS_REF #define IOS_REF (*(SdkManager->GetIOSettings())) #endif void FFbxExporter::WriteToFile(const TCHAR* Filename) { int32 Major, Minor, Revision; bool Status = true; int32 FileFormat = -1; bool bEmbedMedia = false; bool bASCII = GetExportOptions()->bASCII; // Create an exporter. FbxExporter* Exporter = FbxExporter::Create(SdkManager, ""); // set file format // Write in fall back format if pEmbedMedia is true if (bASCII) { FileFormat = SdkManager->GetIOPluginRegistry()->FindWriterIDByDescription("FBX ascii (*.fbx)"); } else { FileFormat = SdkManager->GetIOPluginRegistry()->GetNativeWriterFormat(); } // Set the export states. By default, the export states are always set to // true except for the option eEXPORT_TEXTURE_AS_EMBEDDED. The code below // shows how to change these states. IOS_REF.SetBoolProp(EXP_FBX_MATERIAL, true); IOS_REF.SetBoolProp(EXP_FBX_TEXTURE, true); IOS_REF.SetBoolProp(EXP_FBX_EMBEDDED, bEmbedMedia); IOS_REF.SetBoolProp(EXP_FBX_SHAPE, true); IOS_REF.SetBoolProp(EXP_FBX_GOBO, true); IOS_REF.SetBoolProp(EXP_FBX_ANIMATION, true); IOS_REF.SetBoolProp(EXP_FBX_GLOBAL_SETTINGS, true); IOS_REF.SetBoolProp(EXP_ASCIIFBX, bASCII); //Get the compatibility from the editor settings const char* CompatibilitySetting = FBX_2013_00_COMPATIBLE; const EFbxExportCompatibility FbxExportCompatibility = GetExportOptions()->FbxExportCompatibility; switch (FbxExportCompatibility) { case EFbxExportCompatibility::FBX_2011: CompatibilitySetting = FBX_2011_00_COMPATIBLE; break; case EFbxExportCompatibility::FBX_2012: CompatibilitySetting = FBX_2012_00_COMPATIBLE; break; case EFbxExportCompatibility::FBX_2013: CompatibilitySetting = FBX_2013_00_COMPATIBLE; break; case EFbxExportCompatibility::FBX_2014: CompatibilitySetting = FBX_2014_00_COMPATIBLE; break; case EFbxExportCompatibility::FBX_2016: CompatibilitySetting = FBX_2016_00_COMPATIBLE; break; case EFbxExportCompatibility::FBX_2018: CompatibilitySetting = FBX_2018_00_COMPATIBLE; break; case EFbxExportCompatibility::FBX_2019: CompatibilitySetting = FBX_2019_00_COMPATIBLE; break; case EFbxExportCompatibility::FBX_2020: CompatibilitySetting = FBX_2020_00_COMPATIBLE; break; default: CompatibilitySetting = FBX_2013_00_COMPATIBLE; break; } // We export using FBX 2013 format because many users are still on that version and FBX 2014 files has compatibility issues with // normals when importing to an earlier version of the plugin if (!Exporter->SetFileExportVersion(CompatibilitySetting, FbxSceneRenamer::eNone)) { UE_LOG(LogFbx, Warning, TEXT("Call to KFbxExporter::SetFileExportVersion(FBX_2013_00_COMPATIBLE) to export 2013 fbx file format failed.\n")); } // Initialize the exporter by providing a filename. if( !Exporter->Initialize(TCHAR_TO_UTF8(Filename), FileFormat, SdkManager->GetIOSettings()) ) { UE_LOG(LogFbx, Warning, TEXT("Call to KFbxExporter::Initialize() failed.\n")); UE_LOG(LogFbx, Warning, TEXT("Error returned: %hs\n\n"), Exporter->GetStatus().GetErrorString() ); return; } FbxManager::GetFileFormatVersion(Major, Minor, Revision); UE_LOG(LogFbx, Log, TEXT("FBX version number for this version of the FBX SDK is %d.%d.%d\n\n"), Major, Minor, Revision); // Export the scene. Status = Exporter->Export(Scene); // Destroy the exporter. Exporter->Destroy(); CloseDocument(); return; } /** * Release the FBX scene, releasing its memory. */ void FFbxExporter::CloseDocument() { FbxActors.Reset(); FbxSkeletonRoots.Reset(); FbxMaterials.Reset(); FbxMeshes.Reset(); FbxCollisionMeshes.Reset(); FbxNodeNameToIndexMap.Reset(); if (Scene) { Scene->Destroy(); Scene = NULL; } bSceneGlobalTimeLineSet = false; } template void FFbxExporter::CreateAnimatableUserProperty(FbxNode* Node, T Value, const char* Name, const char* Label, FbxDataType DataType) { // Add one user property for recording the animation FbxProperty UserProp = FbxProperty::Create(Node, DataType, Name, Label); UserProp.Set(Value); UserProp.ModifyFlag(FbxPropertyFlags::eUserDefined, true); UserProp.ModifyFlag(FbxPropertyFlags::eAnimatable, true); } /** * Sorts actors such that parent actors will appear before children actors in the list * Stable sort */ static void SortActorsHierarchy(TArray& Actors) { auto CalcAttachDepth = [](AActor* InActor) -> int32 { int32 Depth = MAX_int32; if (InActor) { Depth = 0; if (InActor->GetRootComponent()) { for (const USceneComponent* Test = InActor->GetRootComponent()->GetAttachParent(); Test != nullptr; Test = Test->GetAttachParent(), Depth++); } } return Depth; }; // Unfortunately TArray.StableSort assumes no null entries in the array // So it forces me to use internal unrestricted version StableSortInternal(Actors.GetData(), Actors.Num(), [&](AActor* L, AActor* R) { return CalcAttachDepth(L) < CalcAttachDepth(R); }); } /** * Exports the basic scene information to the FBX document. */ void FFbxExporter::ExportLevelMesh(ULevel* InLevel, bool bExportLevelGeometry, TArray& ActorToExport, INodeNameAdapter& NodeNameAdapter, bool bSaveAnimSeq) { if (InLevel == NULL) { return; } if (bExportLevelGeometry) { // Exports the level's scene geometry // the vertex number of Model must be more than 2 (at least a triangle panel) if (InLevel->Model != NULL && InLevel->Model->VertexBuffer.Vertices.Num() > 2 && InLevel->Model->MaterialIndexBuffers.Num() > 0) { // create a FbxNode FbxNode* Node = FbxNode::Create(Scene, "LevelMesh"); // set the shading mode to view texture Node->SetShadingMode(FbxNode::eTextureShading); Node->LclScaling.Set(FbxVector4(1.0, 1.0, 1.0)); Scene->GetRootNode()->AddChild(Node); // Export the mesh for the world ExportModel(InLevel->Model, Node, "Level Mesh", FFbxMaterialBakingMeshData(InLevel->Model)); } } //Sort the hierarchy to make sure parent come first SortActorsHierarchy(ActorToExport); int32 ActorCount = ActorToExport.Num(); for (int32 ActorIndex = 0; ActorIndex < ActorCount; ++ActorIndex) { AActor* Actor = ActorToExport[ActorIndex]; //We export only valid actor if (Actor == nullptr) continue; bool bIsBlueprintClass = false; if (UClass* ActorClass = Actor->GetClass()) { // Check if we export the actor as a blueprint bIsBlueprintClass = UBlueprint::GetBlueprintFromClass(ActorClass) != nullptr; } //Blueprint can be any type of actor so it must be done first if (bIsBlueprintClass) { // Export blueprint actors and all their components. ExportActor(Actor, true, NodeNameAdapter, bSaveAnimSeq); } else if (Actor->IsA(ALight::StaticClass())) { ExportLight((ALight*)Actor, NodeNameAdapter); } else if (Actor->IsA(AStaticMeshActor::StaticClass())) { ExportStaticMesh(Actor, CastChecked(Actor)->GetStaticMeshComponent(), NodeNameAdapter); } else if (Actor->IsA(ALandscapeProxy::StaticClass())) { ExportLandscape(CastChecked(Actor), false, NodeNameAdapter); } else if (Actor->IsA(ABrush::StaticClass())) { // All brushes should be included within the world geometry exported above. ExportBrush((ABrush*)Actor, NULL, 0, NodeNameAdapter); } else if (Actor->IsA(AEmitter::StaticClass())) { ExportActor(Actor, false, NodeNameAdapter, bSaveAnimSeq); // Just export the placement of the particle emitter. } else if (Actor->IsA(ACameraActor::StaticClass())) { ExportCamera(CastChecked(Actor), false, NodeNameAdapter); } else { // Export any other type of actors and all their components. ExportActor(Actor, true, NodeNameAdapter, bSaveAnimSeq); } } } /** * Exports the basic scene information to the FBX document. */ void FFbxExporter::ExportLevelMesh( ULevel* InLevel, bool bSelectedOnly, INodeNameAdapter& NodeNameAdapter, bool bSaveAnimSeq) { if (InLevel == NULL) { return; } TArray ActorToExport; int32 ActorCount = InLevel->Actors.Num(); for (int32 ActorIndex = 0; ActorIndex < ActorCount; ++ActorIndex) { AActor* Actor = InLevel->Actors[ActorIndex]; if (Actor != NULL && (!bSelectedOnly || (bSelectedOnly && Actor->IsSelected()))) { ActorToExport.Add(Actor); } } ExportLevelMesh(InLevel, !bSelectedOnly, ActorToExport, NodeNameAdapter, bSaveAnimSeq); } void FFbxExporter::FillFbxLightAttribute(FbxLight* Light, FbxNode* FbxParentNode, ULightComponent* BaseLight) { Light->Intensity.Set(BaseLight->Intensity); Light->Color.Set(Converter.ConvertToFbxColor(BaseLight->LightColor)); // Add one user property for recording the Brightness animation CreateAnimatableUserProperty(FbxParentNode, BaseLight->Intensity, "UE_Intensity", "UE_Matinee_Light_Intensity"); CreateAnimatableUserProperty(FbxParentNode, BaseLight->bUseTemperature, "UE_UseTemperature", "UE_Matinee_Light_UseTemperature", FbxBoolDT); CreateAnimatableUserProperty(FbxParentNode, BaseLight->GetLightUnits(), "UE_IntensityUnits", "UE_Matinee_UE_IntensityUnits", FbxEnumDT); // Look for the higher-level light types and determine the lighting method if (BaseLight->IsA(UPointLightComponent::StaticClass())) { UPointLightComponent* PointLight = (UPointLightComponent*)BaseLight; if (BaseLight->IsA(USpotLightComponent::StaticClass())) { USpotLightComponent* SpotLight = (USpotLightComponent*)BaseLight; Light->LightType.Set(FbxLight::eSpot); // Export the spot light parameters. if (!FMath::IsNearlyZero(SpotLight->InnerConeAngle*2.0f)) { Light->InnerAngle.Set(SpotLight->InnerConeAngle*2.0f); } else // Maya requires a non-zero inner cone angle { Light->InnerAngle.Set(0.01f); } Light->OuterAngle.Set(SpotLight->OuterConeAngle*2.0f); } else { Light->LightType.Set(FbxLight::ePoint); } // Export the point light parameters. Light->EnableFarAttenuation.Set(true); Light->FarAttenuationEnd.Set(PointLight->AttenuationRadius); // Add one user property for recording the FalloffExponent animation CreateAnimatableUserProperty(FbxParentNode, PointLight->AttenuationRadius, "UE_Radius", "UE_Matinee_Light_Radius"); // Add one user property for recording the FalloffExponent animation CreateAnimatableUserProperty(FbxParentNode, PointLight->LightFalloffExponent, "UE_FalloffExponent", "UE_Matinee_Light_FalloffExponent"); } else if (BaseLight->IsA(UDirectionalLightComponent::StaticClass())) { // The directional light has no interesting properties. Light->LightType.Set(FbxLight::eDirectional); Light->Intensity.Set(BaseLight->Intensity*100.0f); } } /** * Exports the light-specific information for a light actor. */ void FFbxExporter::ExportLight( ALight* Actor, INodeNameAdapter& NodeNameAdapter ) { if (Scene == NULL || Actor == NULL || !Actor->GetLightComponent()) return; // Export the basic actor information. FbxNode* FbxActor = ExportActor( Actor, false, NodeNameAdapter ); // this is the pivot node // The real fbx light node FbxNode* FbxLightNode = FbxActor->GetParent(); ULightComponent* BaseLight = Actor->GetLightComponent(); FString FbxNodeName = NodeNameAdapter.GetActorNodeName(Actor); // Export the basic light information FbxLight* Light = FbxLight::Create(Scene, TCHAR_TO_UTF8(*FbxNodeName)); FillFbxLightAttribute(Light, FbxLightNode, BaseLight); FbxActor->SetNodeAttribute(Light); } void FFbxExporter::FillFbxCameraAttribute(FbxNode* ParentNode, FbxCamera* Camera, UCameraComponent *CameraComponent) { float ApertureHeightInInches = 0.612f; // 0.612f is a magic number from Maya that represents the ApertureHeight float ApertureWidthInInches = CameraComponent->AspectRatio * ApertureHeightInInches; float FocalLength = Camera->ComputeFocalLength(CameraComponent->FieldOfView); TOptional FocusDistance; if (CameraComponent->IsA(UCineCameraComponent::StaticClass())) { UCineCameraComponent* CineCameraComponent = Cast(CameraComponent); if (CineCameraComponent) { ApertureWidthInInches = FUnitConversion::Convert(CineCameraComponent->Filmback.SensorWidth, EUnit::Millimeters, EUnit::Inches); ApertureHeightInInches = FUnitConversion::Convert(CineCameraComponent->Filmback.SensorHeight, EUnit::Millimeters, EUnit::Inches); FocalLength = CineCameraComponent->CurrentFocalLength; FocusDistance = CineCameraComponent->FocusSettings.ManualFocusDistance; } } // Export the view area information Camera->ProjectionType.Set(CameraComponent->ProjectionMode == ECameraProjectionMode::Type::Perspective ? FbxCamera::ePerspective : FbxCamera::eOrthogonal); Camera->SetAspect(FbxCamera::eFixedRatio, CameraComponent->AspectRatio, 1.0f); Camera->FilmAspectRatio.Set(CameraComponent->AspectRatio); Camera->SetApertureWidth(ApertureWidthInInches); Camera->SetApertureHeight(ApertureHeightInInches); Camera->SetApertureMode(FbxCamera::eFocalLength); Camera->FocalLength.Set(FocalLength); if (FocusDistance.IsSet()) { Camera->FocusDistance.Set(FocusDistance.GetValue()); } // Add one user property for recording the AspectRatio animation CreateAnimatableUserProperty(ParentNode, CameraComponent->AspectRatio, "UE_AspectRatio", "UE_Matinee_Camera_AspectRatio"); // Push the near/far clip planes away, as the engine uses larger values than the default. Camera->SetNearPlane(10.0f); Camera->SetFarPlane(100000.0f); } void FFbxExporter::ExportCamera( ACameraActor* Actor, bool bExportComponents,INodeNameAdapter& NodeNameAdapter ) { if (Scene == NULL || Actor == NULL) return; UCameraComponent *CameraComponent = Actor->GetCameraComponent(); // Export the basic actor information. FbxNode* FbxActor = ExportActor( Actor, bExportComponents, NodeNameAdapter ); // this is the pivot node // The real fbx camera node FbxNode* FbxCameraNode = FbxActor->GetParent(); FString FbxNodeName = NodeNameAdapter.GetActorNodeName(Actor); // Create a properly-named FBX camera structure and instantiate it in the FBX scene graph FbxCamera* Camera = FbxCamera::Create(Scene, TCHAR_TO_UTF8(*FbxNodeName)); FillFbxCameraAttribute(FbxCameraNode, Camera, CameraComponent); FbxActor->SetNodeAttribute(Camera); DefaultCamera = Camera; } /** * Exports the mesh and the actor information for a brush actor. */ void FFbxExporter::ExportBrush(ABrush* Actor, UModel* InModel, bool bConvertToStaticMesh, INodeNameAdapter& NodeNameAdapter ) { if (Scene == NULL || Actor == NULL || !Actor->GetBrushComponent()) return; if (!bConvertToStaticMesh) { // Retrieve the information structures, verifying the integrity of the data. UModel* Model = Actor->GetBrushComponent()->Brush; if (Model == NULL || Model->VertexBuffer.Vertices.Num() < 3 || Model->MaterialIndexBuffers.Num() == 0) return; // Create the FBX actor, the FBX geometry and instantiate it. FbxNode* FbxActor = ExportActor( Actor, false, NodeNameAdapter ); Scene->GetRootNode()->AddChild(FbxActor); // Export the mesh information ExportModel(Model, FbxActor, TCHAR_TO_UTF8(*Actor->GetName()), FFbxMaterialBakingMeshData(InModel, Actor)); } else { FMeshDescription Mesh; FStaticMeshAttributes MeshAttributes(Mesh); MeshAttributes.Register(); TArray Materials; GetBrushMesh(Actor,Actor->Brush,Mesh,Materials); if( Mesh.Vertices().Num() ) { UStaticMesh* StaticMesh = CreateStaticMesh(Mesh,Materials,GetTransientPackage(),Actor->GetFName()); ExportStaticMesh( StaticMesh, &Materials ); } } } void FFbxExporter::ExportModel(UModel* Model, FbxNode* Node, const char* Name, const FFbxMaterialBakingMeshData& MaterialBakingMeshData) { //int32 VertexCount = Model->VertexBuffer.Vertices.Num(); int32 MaterialCount = Model->MaterialIndexBuffers.Num(); const float BiasedHalfWorldExtent = HALF_WORLD_MAX * 0.95f; // Create the mesh and three data sources for the vertex positions, normals and texture coordinates. FbxMesh* Mesh = FbxMesh::Create(Scene, Name); // Create control points. uint32 VertCount(Model->VertexBuffer.Vertices.Num()); Mesh->InitControlPoints(VertCount); FbxVector4* ControlPoints = Mesh->GetControlPoints(); // Set the normals on Layer 0. FbxLayer* Layer = Mesh->GetLayer(0); if (Layer == NULL) { Mesh->CreateLayer(); Layer = Mesh->GetLayer(0); } // We want to have one normal for each vertex (or control point), // so we set the mapping mode to eBY_CONTROL_POINT. FbxLayerElementNormal* LayerElementNormal= FbxLayerElementNormal::Create(Mesh, ""); LayerElementNormal->SetMappingMode(FbxLayerElement::eByControlPoint); // Set the normal values for every control point. LayerElementNormal->SetReferenceMode(FbxLayerElement::eDirect); // Create UV for Diffuse channel. FbxLayerElementUV* UVDiffuseLayer = FbxLayerElementUV::Create(Mesh, "DiffuseUV"); UVDiffuseLayer->SetMappingMode(FbxLayerElement::eByControlPoint); UVDiffuseLayer->SetReferenceMode(FbxLayerElement::eDirect); Layer->SetUVs(UVDiffuseLayer, FbxLayerElement::eTextureDiffuse); for (uint32 VertexIdx = 0; VertexIdx < VertCount; ++VertexIdx) { FModelVertex& Vertex = Model->VertexBuffer.Vertices[VertexIdx]; FVector Normal = (FVector4)Vertex.TangentZ; // If the vertex is outside of the world extent, snap it to the origin. The faces associated with // these vertices will be removed before exporting. We leave the snapped vertex in the buffer so // we won't have to deal with reindexing everything. FVector FinalVertexPos = (FVector)Vertex.Position; if( FMath::Abs( Vertex.Position.X ) > BiasedHalfWorldExtent || FMath::Abs( Vertex.Position.Y ) > BiasedHalfWorldExtent || FMath::Abs( Vertex.Position.Z ) > BiasedHalfWorldExtent ) { FinalVertexPos = FVector::ZeroVector; } ControlPoints[VertexIdx] = FbxVector4(FinalVertexPos.X, -FinalVertexPos.Y, FinalVertexPos.Z); FbxVector4 FbxNormal = FbxVector4(Normal.X, -Normal.Y, Normal.Z); FbxAMatrix NodeMatrix; FbxVector4 Trans = Node->LclTranslation.Get(); NodeMatrix.SetT(FbxVector4(Trans[0], Trans[1], Trans[2])); FbxVector4 Rot = Node->LclRotation.Get(); NodeMatrix.SetR(FbxVector4(Rot[0], Rot[1], Rot[2])); NodeMatrix.SetS(Node->LclScaling.Get()); FbxNormal = NodeMatrix.MultT(FbxNormal); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); // update the index array of the UVs that map the texture to the face UVDiffuseLayer->GetDirectArray().Add(FbxVector2(Vertex.TexCoord.X, -Vertex.TexCoord.Y)); } Layer->SetNormals(LayerElementNormal); Layer->SetUVs(UVDiffuseLayer); FbxLayerElementMaterial* MatLayer = FbxLayerElementMaterial::Create(Mesh, ""); MatLayer->SetMappingMode(FbxLayerElement::eByPolygon); MatLayer->SetReferenceMode(FbxLayerElement::eIndexToDirect); Layer->SetMaterials(MatLayer); //Make sure the Index buffer is accessible. for (auto MaterialIterator = Model->MaterialIndexBuffers.CreateIterator(); MaterialIterator; ++MaterialIterator) { BeginReleaseResource(MaterialIterator->Value.Get()); } FlushRenderingCommands(); // Create the materials and the per-material tesselation structures. for (auto MaterialIterator = Model->MaterialIndexBuffers.CreateIterator(); MaterialIterator; ++MaterialIterator) { UMaterialInterface* MaterialInterface = MaterialIterator.Key(); FRawIndexBuffer16or32& IndexBuffer = *MaterialIterator.Value(); int32 IndexCount = IndexBuffer.Indices.Num(); if (IndexCount < 3) continue; // Are NULL materials okay? int32 MaterialIndex = -1; FbxSurfaceMaterial* FbxMaterial; if (MaterialInterface != NULL && MaterialInterface->GetMaterial() != NULL) { FbxMaterial = ExportMaterial(MaterialInterface, MaterialBakingMeshData.GetUModelStaticMeshMaterialIndex(MaterialInterface), MaterialBakingMeshData); } else { // Set default material FbxMaterial = CreateDefaultMaterial(); } MaterialIndex = Node->AddMaterial(FbxMaterial); // Create the Fbx polygons set. // Retrieve and fill in the index buffer. const int32 TriangleCount = IndexCount / 3; for( int32 TriangleIdx = 0; TriangleIdx < TriangleCount; ++TriangleIdx ) { bool bSkipTriangle = false; for( int32 IndexIdx = 0; IndexIdx < 3; ++IndexIdx ) { // Skip triangles that belong to BSP geometry close to the world extent, since its probably // the automatically-added-brush for new levels. The vertices will be left in the buffer (unreferenced) FVector VertexPos = (FVector)Model->VertexBuffer.Vertices[ IndexBuffer.Indices[ TriangleIdx * 3 + IndexIdx ] ].Position; if( FMath::Abs( VertexPos.X ) > BiasedHalfWorldExtent || FMath::Abs( VertexPos.Y ) > BiasedHalfWorldExtent || FMath::Abs( VertexPos.Z ) > BiasedHalfWorldExtent ) { bSkipTriangle = true; break; } } if( !bSkipTriangle ) { // all faces of the cube have the same texture Mesh->BeginPolygon(MaterialIndex); for( int32 IndexIdx = 0; IndexIdx < 3; ++IndexIdx ) { // Control point index Mesh->AddPolygon(IndexBuffer.Indices[ TriangleIdx * 3 + IndexIdx ]); } Mesh->EndPolygon (); } } BeginInitResource(&IndexBuffer); } FlushRenderingCommands(); Node->SetNodeAttribute(Mesh); } FString FFbxExporter::GetFbxObjectName(const FString &FbxObjectNode, INodeNameAdapter& NodeNameAdapter) { FString FbxTestName = FbxObjectNode; int32 *NodeIndex = FbxNodeNameToIndexMap.Find(FbxTestName); if (NodeIndex) { FbxTestName = FString::Printf(TEXT("%s%d"), *FbxTestName, *NodeIndex); ++(*NodeIndex); } else { FbxNodeNameToIndexMap.Add(FbxTestName, 1); } return FbxTestName; } void FFbxExporter::ExportStaticMesh(AActor* Actor, UStaticMeshComponent* StaticMeshComponent, INodeNameAdapter& NodeNameAdapter) { if (Scene == NULL || Actor == NULL || StaticMeshComponent == NULL) { return; } // Retrieve the static mesh rendering information at the correct LOD level. UStaticMesh* StaticMesh = StaticMeshComponent->GetStaticMesh(); if (StaticMesh == NULL || !StaticMesh->HasValidRenderData()) { return; } FString FbxNodeName = NodeNameAdapter.GetActorNodeName(Actor); FString FbxMeshName = StaticMesh->GetName().Replace(TEXT("-"), TEXT("_")); FColorVertexBuffer* ColorBuffer = NULL; if(GetExportOptions()->LevelOfDetail && StaticMesh->GetNumLODs() > 1) { //Create a fbx LOD Group node FbxNode* FbxActor = ExportActor(Actor, false, NodeNameAdapter); FString FbxLODGroupName = NodeNameAdapter.GetActorNodeName(Actor); FbxLODGroupName += TEXT("_LodGroup"); FbxLODGroupName = GetFbxObjectName(FbxLODGroupName, NodeNameAdapter); FbxLODGroup *FbxLodGroupAttribute = FbxLODGroup::Create(Scene, TCHAR_TO_UTF8(*FbxLODGroupName)); FbxActor->AddNodeAttribute(FbxLodGroupAttribute); FbxLodGroupAttribute->ThresholdsUsedAsPercentage = true; //Export an Fbx Mesh Node for every LOD and child them to the fbx node (LOD Group) for (int CurrentLodIndex = 0; CurrentLodIndex < StaticMesh->GetNumLODs(); ++CurrentLodIndex) { if (CurrentLodIndex < StaticMeshComponent->LODData.Num()) { ColorBuffer = StaticMeshComponent->LODData[CurrentLodIndex].OverrideVertexColors; } else { ColorBuffer = nullptr; } FString FbxLODNodeName = NodeNameAdapter.GetActorNodeName(Actor); FbxLODNodeName += TEXT("_LOD") + FString::FromInt(CurrentLodIndex); FbxLODNodeName = GetFbxObjectName(FbxLODNodeName, NodeNameAdapter); FbxNode* FbxActorLOD = FbxNode::Create(Scene, TCHAR_TO_UTF8(*FbxLODNodeName)); FbxActor->AddChild(FbxActorLOD); if (CurrentLodIndex + 1 < StaticMesh->GetNumLODs()) { //Convert the screen size to a threshold, it is just to be sure that we set some threshold, there is no way to convert this precisely double LodScreenSize = (double)(10.0f / StaticMesh->GetRenderData()->ScreenSize[CurrentLodIndex].Default); FbxLodGroupAttribute->AddThreshold(LodScreenSize); } const int32 LightmapUVChannel = -1; const TArray* MaterialOrderOverride = nullptr; ExportStaticMeshToFbx(StaticMesh, CurrentLodIndex, *FbxMeshName, FbxActorLOD, FFbxMaterialBakingMeshData(StaticMesh, StaticMeshComponent, CurrentLodIndex), LightmapUVChannel, ColorBuffer, MaterialOrderOverride, &ToRawPtrTArrayUnsafe(StaticMeshComponent->OverrideMaterials)); } } else { const int32 LODIndex = (StaticMeshComponent->ForcedLodModel > 0 ? FMath::Min(StaticMesh->GetNumLODs(), StaticMeshComponent->ForcedLodModel) - 1 : /* auto-select*/ 0); if (StaticMeshComponent->LODData.IsValidIndex(LODIndex)) { ColorBuffer = StaticMeshComponent->LODData[LODIndex].OverrideVertexColors; } FbxNode* FbxActor = ExportActor(Actor, false, NodeNameAdapter); const int32 LightmapUVChannel = -1; const TArray* MaterialOrderOverride = nullptr; ExportStaticMeshToFbx(StaticMesh, LODIndex, *FbxMeshName, FbxActor, FFbxMaterialBakingMeshData(StaticMesh, StaticMeshComponent, LODIndex), LightmapUVChannel, ColorBuffer, MaterialOrderOverride, &ToRawPtrTArrayUnsafe(StaticMeshComponent->OverrideMaterials)); } } struct FBSPExportData { FMeshDescription Mesh; TArray Materials; TArray SmoothGroups; uint32 NumVerts; uint32 NumFaces; uint32 CurrentVertAddIndex; uint32 CurrentFaceAddIndex; bool bInitialised; FBSPExportData() :NumVerts(0) ,NumFaces(0) ,CurrentVertAddIndex(0) ,CurrentFaceAddIndex(0) ,bInitialised(false) { } }; void FFbxExporter::ExportBSP( UModel* Model, bool bSelectedOnly ) { TMap< ABrush*, FBSPExportData > BrushToMeshMap; TArray AllMaterials; for(int32 NodeIndex = 0;NodeIndex < Model->Nodes.Num();NodeIndex++) { FBspNode& Node = Model->Nodes[NodeIndex]; if( Node.NumVertices >= 3 ) { FBspSurf& Surf = Model->Surfs[Node.iSurf]; ABrush* BrushActor = Surf.Actor; if( (Surf.PolyFlags & PF_Selected) || !bSelectedOnly || (BrushActor && BrushActor->IsSelected() ) ) { FBSPExportData& Data = BrushToMeshMap.FindOrAdd( BrushActor ); FPoly Poly; GEditor->polyFindBrush(Model, Node.iSurf, Poly); Data.NumVerts += Node.NumVertices; Data.NumFaces += Node.NumVertices-2; UMaterialInterface* Material = Poly.Material; FName MaterialName = Material != nullptr ? Material->GetFName() : NAME_None; Data.Materials.AddUnique(FStaticMaterial(Material, MaterialName, MaterialName)); AllMaterials.AddUnique(FStaticMaterial(Material, MaterialName, MaterialName)); } } } for(int32 NodeIndex = 0;NodeIndex < Model->Nodes.Num();NodeIndex++) { FBspNode& Node = Model->Nodes[NodeIndex]; FBspSurf& Surf = Model->Surfs[Node.iSurf]; ABrush* BrushActor = Surf.Actor; if( (Surf.PolyFlags & PF_Selected) || !bSelectedOnly || (BrushActor && BrushActor->IsSelected() && Node.NumVertices >= 3) ) { FPoly Poly; GEditor->polyFindBrush( Model, Node.iSurf, Poly ); FBSPExportData* ExportData = BrushToMeshMap.Find( BrushActor ); if( NULL == ExportData ) { UE_LOG(LogFbx, Fatal, TEXT("Error in FBX export of BSP.")); return; } TArray& Materials = ExportData->Materials; FMeshDescription& Mesh = ExportData->Mesh; //Pre-allocate space for this mesh. if( !ExportData->bInitialised ) { uint32 NumWedges = ExportData->NumFaces * 3; FStaticMeshAttributes InitMeshAttributes(Mesh); InitMeshAttributes.Register(); ExportData->bInitialised = true; Mesh.Empty(); Mesh.ReserveNewVertices(ExportData->NumVerts); Mesh.ReserveNewVertexInstances(NumWedges); Mesh.ReserveNewPolygons(ExportData->NumFaces); Mesh.ReserveNewEdges(ExportData->NumFaces * 2); Mesh.ReserveNewPolygonGroups(Materials.Num()); //We need to get the PolygonGroupImportedMaterial TPolygonGroupAttributesRef InitPolygonGroupNames = InitMeshAttributes.GetPolygonGroupMaterialSlotNames(); for (const FStaticMaterial& StaticMaterial : Materials) { const FPolygonGroupID PolygonGroupID = Mesh.CreatePolygonGroup(); InitPolygonGroupNames[PolygonGroupID] = StaticMaterial.ImportedMaterialSlotName; } ExportData->SmoothGroups.Empty(ExportData->NumFaces); } //Get the Attributes FStaticMeshAttributes MeshAttributes(Mesh); TVertexAttributesRef VertexPositions = MeshAttributes.GetVertexPositions(); TVertexInstanceAttributesRef UVs = MeshAttributes.GetVertexInstanceUVs(); TVertexInstanceAttributesRef Colors = MeshAttributes.GetVertexInstanceColors(); TVertexInstanceAttributesRef Normals = MeshAttributes.GetVertexInstanceNormals(); TEdgeAttributesRef EdgeHardnesses = MeshAttributes.GetEdgeHardnesses(); UMaterialInterface* Material = Poly.Material; FName MaterialName = Material != nullptr ? Material->GetFName() : NAME_None; int32 MaterialIndex = INDEX_NONE; if (!ExportData->Materials.Find(FStaticMaterial(Material, MaterialName, MaterialName), MaterialIndex)) { MaterialIndex = 0; } const FPolygonGroupID CurrentPolygonGroupID(MaterialIndex); //The material ID should follow the unique materials check(Mesh.IsPolygonGroupValid(CurrentPolygonGroupID)); const FVector& TextureBase = (FVector)Model->Points[Surf.pBase]; const FVector& TextureX = (FVector)Model->Vectors[Surf.vTextureU]; const FVector& TextureY = (FVector)Model->Vectors[Surf.vTextureV]; const FVector& Normal = (FVector)Model->Vectors[Surf.vNormal]; const int32 StartIndex = ExportData->CurrentVertAddIndex; for(int32 VertexIndex = 0; VertexIndex < Node.NumVertices ; VertexIndex++ ) { const FVert& Vert = Model->Verts[Node.iVertPool + VertexIndex]; const FVector& Vertex = (FVector)Model->Points[Vert.pVertex]; FVertexID VertexID = Mesh.CreateVertex(); VertexPositions[VertexID] = (FVector3f)Vertex; } ExportData->CurrentVertAddIndex += Node.NumVertices; for (int32 StartVertexIndex = 1; StartVertexIndex < Node.NumVertices - 1; ++StartVertexIndex) { TArray VertexInstanceIDs; VertexInstanceIDs.SetNum(3); FVertexID VertexIDs[3]; // These map the node's vertices to the 3 triangle indices to triangulate the convex polygon. int32 TriVertIndices[3] = { Node.iVertPool + StartVertexIndex + 1, Node.iVertPool + StartVertexIndex, Node.iVertPool }; int32 WedgeIndices[3] = { StartIndex + StartVertexIndex + 1, StartIndex + StartVertexIndex, StartIndex }; for (uint32 Corner = 0; Corner < 3; ++Corner) { VertexIDs[Corner] = FVertexID(WedgeIndices[Corner]); VertexInstanceIDs[Corner] = Mesh.CreateVertexInstance(VertexIDs[Corner]); const FVert& Vert = Model->Verts[TriVertIndices[Corner]]; const FVector& Vertex = (FVector)Model->Points[Vert.pVertex]; float U = ((Vertex - TextureBase) | TextureX) / UModel::GetGlobalBSPTexelScale(); float V = ((Vertex - TextureBase) | TextureY) / UModel::GetGlobalBSPTexelScale(); UVs.Set(VertexInstanceIDs[Corner], 0, FVector2f(U, V)); //This is not exported when exporting the whole level via ExportModel so leaving out here for now. //UVs.Set(VertexInstanceIDs[Corner], 1, Vert.ShadowTexCoord); Colors[VertexInstanceIDs[Corner]] = FVector4f(FLinearColor::White); Normals[VertexInstanceIDs[Corner]] = (FVector3f)Normal; } // Insert a polygon into the mesh TArray NewEdgeIDs; const FPolygonID NewPolygonID = Mesh.CreatePolygon(CurrentPolygonGroupID, VertexInstanceIDs, &NewEdgeIDs); for (const FEdgeID& EdgeID : NewEdgeIDs) { EdgeHardnesses[EdgeID] = false; } //Add to the smoothGroup array so we can compute hard edge later ExportData->SmoothGroups.Add((1 << (Node.iSurf % 32))); ++ExportData->CurrentFaceAddIndex; } } } for( TMap< ABrush*, FBSPExportData >::TIterator It(BrushToMeshMap); It; ++It ) { if( It.Value().Mesh.Vertices().Num() ) { FStaticMeshOperations::ConvertSmoothGroupToHardEdges(It.Value().SmoothGroups, It.Value().Mesh); UStaticMesh* NewMesh = CreateStaticMesh( It.Value().Mesh, It.Value().Materials, GetTransientPackage(), It.Key()->GetFName() ); ExportStaticMesh( NewMesh, &It.Value().Materials); } } } void FFbxExporter::ExportStaticMesh( UStaticMesh* StaticMesh, const TArray* MaterialOrder ) { if (Scene == NULL || StaticMesh == NULL || !StaticMesh->HasValidRenderData()) return; FString MeshName; StaticMesh->GetName(MeshName); FbxNode* MeshNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*MeshName)); Scene->GetRootNode()->AddChild(MeshNode); if (GetExportOptions()->LevelOfDetail && StaticMesh->GetNumLODs() > 1) { FString LodGroup_MeshName = MeshName + ("_LodGroup"); FbxLODGroup *FbxLodGroupAttribute = FbxLODGroup::Create(Scene, TCHAR_TO_UTF8(*LodGroup_MeshName)); MeshNode->AddNodeAttribute(FbxLodGroupAttribute); FbxLodGroupAttribute->ThresholdsUsedAsPercentage = true; //Export an Fbx Mesh Node for every LOD and child them to the fbx node (LOD Group) for (int CurrentLodIndex = 0; CurrentLodIndex < StaticMesh->GetNumLODs(); ++CurrentLodIndex) { FString FbxLODNodeName = MeshName + TEXT("_LOD") + FString::FromInt(CurrentLodIndex); FbxNode* FbxActorLOD = FbxNode::Create(Scene, TCHAR_TO_UTF8(*FbxLODNodeName)); MeshNode->AddChild(FbxActorLOD); if (CurrentLodIndex + 1 < StaticMesh->GetNumLODs()) { //Convert the screen size to a threshold, it is just to be sure that we set some threshold, there is no way to convert this precisely double LodScreenSize = (double)(10.0f / StaticMesh->GetRenderData()->ScreenSize[CurrentLodIndex].Default); FbxLodGroupAttribute->AddThreshold(LodScreenSize); } ExportStaticMeshToFbx(StaticMesh, CurrentLodIndex, *MeshName, FbxActorLOD, FFbxMaterialBakingMeshData(StaticMesh, nullptr, CurrentLodIndex), -1, nullptr, MaterialOrder); } } else { ExportStaticMeshToFbx(StaticMesh, 0, *MeshName, MeshNode, FFbxMaterialBakingMeshData(StaticMesh), -1, NULL, MaterialOrder); } } void FFbxExporter::ExportStaticMeshLightMap( UStaticMesh* StaticMesh, int32 LODIndex, int32 UVChannel ) { if (Scene == NULL || StaticMesh == NULL || !StaticMesh->HasValidRenderData()) return; FString MeshName; StaticMesh->GetName(MeshName); FbxNode* MeshNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*MeshName)); Scene->GetRootNode()->AddChild(MeshNode); ExportStaticMeshToFbx(StaticMesh, LODIndex, *MeshName, MeshNode, FFbxMaterialBakingMeshData(StaticMesh, nullptr, LODIndex), UVChannel); } void FFbxExporter::ExportSkeletalMesh( USkeletalMesh* SkeletalMesh ) { if (Scene == NULL || SkeletalMesh == NULL) return; FString MeshName; SkeletalMesh->GetName(MeshName); FbxNode* MeshNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*MeshName)); Scene->GetRootNode()->AddChild(MeshNode); ExportSkeletalMeshToFbx(SkeletalMesh, NULL, *MeshName, MeshNode); } void FFbxExporter::ExportSkeletalMesh( AActor* Actor, USkeletalMeshComponent* SkeletalMeshComponent, INodeNameAdapter& NodeNameAdapter ) { if (Scene == NULL || Actor == NULL || SkeletalMeshComponent == NULL) return; // Retrieve the skeletal mesh rendering information. USkeletalMesh* SkeletalMesh = SkeletalMeshComponent->GetSkeletalMeshAsset(); FString FbxNodeName = NodeNameAdapter.GetActorNodeName(Actor); ExportActor( Actor, true, NodeNameAdapter ); } FbxSurfaceMaterial* FFbxExporter::CreateDefaultMaterial() { // TODO(sbc): the below cast is needed to avoid clang warning. The upstream // signature in FBX should really use 'const char *'. FbxSurfaceMaterial* FbxMaterial = Scene->GetMaterial(const_cast("Fbx Default Material")); if (!FbxMaterial) { FbxMaterial = FbxSurfaceLambert::Create(Scene, "Fbx Default Material"); ((FbxSurfaceLambert*)FbxMaterial)->Diffuse.Set(FbxDouble3(0.72, 0.72, 0.72)); } return FbxMaterial; } void FFbxExporter::ExportLandscape(ALandscapeProxy* Actor, bool bSelectedOnly, INodeNameAdapter& NodeNameAdapter) { if (Scene == NULL || Actor == NULL) { return; } FString FbxNodeName = NodeNameAdapter.GetActorNodeName(Actor); FbxNode* FbxActor = ExportActor(Actor, true, NodeNameAdapter); ExportLandscapeToFbx(Actor, *FbxNodeName, FbxActor, bSelectedOnly); } FbxDouble3 SetMaterialComponent(FColorMaterialInput& MatInput, bool ToLinear) { FColor RGBColor; FLinearColor LinearColor = FLinearColor::Black; bool LinearSet = false; if (MatInput.Expression) { if (Cast(MatInput.Expression)) { UMaterialExpressionConstant* Expr = Cast(MatInput.Expression); RGBColor = FColor(Expr->R); } else if (Cast(MatInput.Expression)) { UMaterialExpressionVectorParameter* Expr = Cast(MatInput.Expression); LinearColor = Expr->DefaultValue; LinearSet = true; //Linear to sRGB color space conversion RGBColor = Expr->DefaultValue.ToFColor(true); } else if (Cast(MatInput.Expression)) { UMaterialExpressionConstant3Vector* Expr = Cast(MatInput.Expression); RGBColor.R = Expr->Constant.R; RGBColor.G = Expr->Constant.G; RGBColor.B = Expr->Constant.B; } else if (Cast(MatInput.Expression)) { UMaterialExpressionConstant4Vector* Expr = Cast(MatInput.Expression); RGBColor.R = Expr->Constant.R; RGBColor.G = Expr->Constant.G; RGBColor.B = Expr->Constant.B; //RGBColor.A = Expr->A; } else if (Cast(MatInput.Expression)) { UMaterialExpressionConstant2Vector* Expr = Cast(MatInput.Expression); RGBColor.R = Expr->R; RGBColor.G = Expr->G; RGBColor.B = 0; } else { RGBColor.R = MatInput.Constant.R; RGBColor.G = MatInput.Constant.G; RGBColor.B = MatInput.Constant.B; } } else { RGBColor.R = MatInput.Constant.R; RGBColor.G = MatInput.Constant.G; RGBColor.B = MatInput.Constant.B; } if (ToLinear) { if (!LinearSet) { //sRGB to linear color space conversion LinearColor = FLinearColor(RGBColor); } return FbxDouble3(LinearColor.R, LinearColor.G, LinearColor.B); } return FbxDouble3(RGBColor.R, RGBColor.G, RGBColor.B); } bool FFbxExporter::FillFbxTextureProperty(const char *PropertyName, const FExpressionInput& MaterialInput, FbxSurfaceMaterial* FbxMaterial) { if (Scene == NULL) { return false; } FbxProperty FbxColorProperty = FbxMaterial->FindProperty(PropertyName); if (FbxColorProperty.IsValid()) { if (MaterialInput.IsConnected() && MaterialInput.Expression) { if (MaterialInput.Expression->IsA(UMaterialExpressionTextureSample::StaticClass())) { UMaterialExpressionTextureSample *TextureSample = Cast(MaterialInput.Expression); if (TextureSample && TextureSample->Texture && TextureSample->Texture->AssetImportData) { FString TextureSourceFullPath = TextureSample->Texture->AssetImportData->GetFirstFilename(); //Create a fbx property FbxFileTexture* lTexture = FbxFileTexture::Create(Scene, "EnvSamplerTex"); lTexture->SetFileName(TCHAR_TO_UTF8(*TextureSourceFullPath)); lTexture->SetTextureUse(FbxTexture::eStandard); lTexture->SetMappingType(FbxTexture::eUV); lTexture->ConnectDstProperty(FbxColorProperty); return true; } } } } return false; } /** * Exports the profile_COMMON information for a material. */ FbxSurfaceMaterial* FFbxExporter::ExportMaterial(UMaterialInterface* MaterialInterface, const int32& MaterialIndex, const FFbxMaterialBakingMeshData& BakingMeshData) { FString ExportFolderPath = FPaths::GetPath(UExporter::CurrentFilename); if (Scene == nullptr || MaterialInterface == nullptr || MaterialInterface->GetMaterial() == nullptr) return nullptr; // Verify that this material has not already been exported: TMap* MaterialIndexToFbxMaterials = FbxMaterials.Find(MaterialInterface); if (MaterialIndexToFbxMaterials && MaterialIndexToFbxMaterials->Find(MaterialIndex)) { return *MaterialIndexToFbxMaterials->Find(MaterialIndex); } // Create the Fbx material FbxSurfaceMaterial* FbxMaterial = nullptr; UMaterial *Material = MaterialInterface->GetMaterial(); if (!Material) { //Nothing to export return nullptr; } if (!MaterialIndexToFbxMaterials) { MaterialIndexToFbxMaterials = &FbxMaterials.Add(MaterialInterface); } UMaterialEditorOnlyData* MaterialEditorOnly = Material->GetEditorOnlyData(); // Set the shading model bool bUseLambert = false; bool bInterchangeOriginated = FFbxMaterialExportUtilities::GetInterchangeShadingModel(MaterialInterface, bUseLambert); FString FbxMaterialName = MaterialInterface->GetName(); if (MaterialIndexToFbxMaterials && MaterialIndexToFbxMaterials->Num() > 0) { FbxMaterialName += FString(TEXT("_")) + FString::FromInt(MaterialIndex); } if ((bInterchangeOriginated && !bUseLambert) || (!bInterchangeOriginated && Material->GetShadingModels().HasOnlyShadingModel(MSM_DefaultLit))) { FbxMaterial = FbxSurfacePhong::Create(Scene, TCHAR_TO_UTF8(*FbxMaterialName)); //((FbxSurfacePhong*)FbxMaterial)->Specular.Set(Material->Specular)); //((FbxSurfacePhong*)FbxMaterial)->Shininess.Set(Material->SpecularPower.Constant); } else // if (Material->ShadingModel == MSM_Unlit) { FbxMaterial = FbxSurfaceLambert::Create(Scene, TCHAR_TO_UTF8(*FbxMaterialName)); } if (bInterchangeOriginated) { FFbxMaterialExportUtilities::ProcessInterchangeMaterials(MaterialInterface, Scene, FbxMaterial); MaterialIndexToFbxMaterials->Add(MaterialIndex, FbxMaterial); return FbxMaterial; } //Get the base material connected expression parameter name for all the supported fbx material exported properties //We only support material input where the connected expression is a parameter of type (constant, scalar, vector, texture, TODO virtual texture) bool bBaseColorNonConstAndNonDefault = (!MaterialEditorOnly->BaseColor.UseConstant && MaterialEditorOnly->BaseColor.Expression); bool bEmissiveNonConstAndNonDefault = (!MaterialEditorOnly->EmissiveColor.UseConstant && MaterialEditorOnly->EmissiveColor.Expression); bool bNormalNonConstAndNonDefault = (MaterialEditorOnly->Normal.Expression != nullptr); bool bOpacityNonConstAndNonDefault = (!MaterialEditorOnly->Opacity.UseConstant && MaterialEditorOnly->Opacity.Expression); bool bOpacityMaskNonConstAndNonDefault = (!MaterialEditorOnly->OpacityMask.UseConstant && MaterialEditorOnly->OpacityMask.Expression); FName BaseColorParamName = bBaseColorNonConstAndNonDefault ? MaterialEditorOnly->BaseColor.Expression->GetParameterName() : NAME_None; FName EmissiveParamName = bEmissiveNonConstAndNonDefault ? MaterialEditorOnly->EmissiveColor.Expression->GetParameterName() : NAME_None; FName NormalParamName = bNormalNonConstAndNonDefault ? MaterialEditorOnly->Normal.Expression->GetParameterName() : NAME_None; FName OpacityParamName = bOpacityNonConstAndNonDefault ? MaterialEditorOnly->Opacity.Expression->GetParameterName() : NAME_None; FName OpacityMaskParamName = bOpacityMaskNonConstAndNonDefault ? MaterialEditorOnly->OpacityMask.Expression->GetParameterName() : NAME_None; bool BaseColorParamSet = false; bool EmissiveParamSet = false; bool NormalParamSet = false; bool OpacityParamSet = false; bool OpacityMaskParamSet = false; UMaterialInstance* MaterialInstance = Cast(MaterialInterface); EBlendMode BlendMode = MaterialInstance != nullptr && MaterialInstance->BasePropertyOverrides.bOverride_BlendMode ? MaterialInstance->BasePropertyOverrides.BlendMode : Material->BlendMode; // Loop through all types of parameters for this base material and add the supported one to the fbx material. //The order is important we search Texture, Vector, Scalar. TArray ParameterInfos; TArray Guids; //Query all base material texture parameters. Material->GetAllTextureParameterInfo(ParameterInfos, Guids); for (int32 ParameterIdx = 0; ParameterIdx < ParameterInfos.Num(); ParameterIdx++) { const FMaterialParameterInfo& ParameterInfo = ParameterInfos[ParameterIdx]; FName ParameterName = ParameterInfo.Name; UTexture* Value; //Query the material instance parameter overridden value if (MaterialInterface->GetTextureParameterValue(ParameterInfo, Value) && Value && Value->AssetImportData) { //This lambda extract the source file path and create the fbx file texture node and connect it to the fbx material auto SetTextureProperty = [&FbxMaterial, &Value](const char* FbxPropertyName, FbxScene* InScene)->bool { FbxProperty FbxColorProperty = FbxMaterial->FindProperty(FbxPropertyName); if (FbxColorProperty.IsValid()) { const FString TextureName = Value->GetName(); const FString TextureSourceFullPath = Value->AssetImportData->GetFirstFilename(); if(!TextureSourceFullPath.IsEmpty()) { //Create a fbx property FbxFileTexture* lTexture = FbxFileTexture::Create(InScene, TCHAR_TO_UTF8(*TextureName)); lTexture->SetFileName(TCHAR_TO_UTF8(*TextureSourceFullPath)); lTexture->SetTextureUse(FbxTexture::eStandard); lTexture->SetMappingType(FbxTexture::eUV); lTexture->ConnectDstProperty(FbxColorProperty); return true; } } return false; }; if (!BaseColorParamSet && BaseColorParamName != NAME_None && ParameterName == BaseColorParamName) { BaseColorParamSet = SetTextureProperty(FbxSurfaceMaterial::sDiffuse, Scene); } if (!EmissiveParamSet && EmissiveParamName != NAME_None && ParameterName == EmissiveParamName) { EmissiveParamSet = SetTextureProperty(FbxSurfaceMaterial::sEmissive, Scene); } if (BlendMode == BLEND_Translucent) { if (!OpacityParamSet && OpacityParamName != NAME_None && ParameterName == OpacityParamName) { OpacityParamSet = SetTextureProperty(FbxSurfaceMaterial::sTransparentColor, Scene); } if (!OpacityMaskParamSet && OpacityMaskParamName != NAME_None && ParameterName == OpacityMaskParamName) { OpacityMaskParamSet = SetTextureProperty(FbxSurfaceMaterial::sTransparencyFactor, Scene); } } else { //There is no normal input in Blend translucent mode if (!NormalParamSet && NormalParamName != NAME_None && ParameterName == NormalParamName) { NormalParamSet = SetTextureProperty(FbxSurfaceMaterial::sNormalMap, Scene); } } } } Material->GetAllVectorParameterInfo(ParameterInfos, Guids); //Query all base material vector parameters. for (int32 ParameterIdx = 0; ParameterIdx < ParameterInfos.Num(); ParameterIdx++) { const FMaterialParameterInfo& ParameterInfo = ParameterInfos[ParameterIdx]; FName ParameterName = ParameterInfo.Name; FLinearColor LinearColor; //Query the material instance parameter overridden value if (MaterialInterface->GetVectorParameterValue(ParameterInfo, LinearColor)) { FbxDouble3 LinearFbxValue(LinearColor.R, LinearColor.G, LinearColor.B); if (!BaseColorParamSet && BaseColorParamName != NAME_None && ParameterName == BaseColorParamName) { ((FbxSurfaceLambert*)FbxMaterial)->Diffuse.Set(LinearFbxValue); BaseColorParamSet = true; } if (!EmissiveParamSet && EmissiveParamName != NAME_None && ParameterName == EmissiveParamName) { ((FbxSurfaceLambert*)FbxMaterial)->Emissive.Set(LinearFbxValue); EmissiveParamSet = true; } } } //Query all base material scalar parameters. Material->GetAllScalarParameterInfo(ParameterInfos, Guids); for (int32 ParameterIdx = 0; ParameterIdx < ParameterInfos.Num(); ParameterIdx++) { const FMaterialParameterInfo& ParameterInfo = ParameterInfos[ParameterIdx]; FName ParameterName = ParameterInfo.Name; float Value; //Query the material instance parameter overridden value if (MaterialInterface->GetScalarParameterValue(ParameterInfo, Value)) { FbxDouble FbxValue = (FbxDouble)Value; FbxDouble3 FbxVector(FbxValue, FbxValue, FbxValue); if (!BaseColorParamSet && BaseColorParamName != NAME_None && ParameterName == BaseColorParamName) { ((FbxSurfaceLambert*)FbxMaterial)->Diffuse.Set(FbxVector); BaseColorParamSet = true; } if (!EmissiveParamSet && EmissiveParamName != NAME_None && ParameterName == EmissiveParamName) { ((FbxSurfaceLambert*)FbxMaterial)->Emissive.Set(FbxVector); EmissiveParamSet = true; } if (BlendMode == BLEND_Translucent) { if (!OpacityParamSet && OpacityParamName != NAME_None && ParameterName == OpacityParamName) { ((FbxSurfaceLambert*)FbxMaterial)->TransparentColor.Set(FbxVector); OpacityParamSet = true; } if (!OpacityMaskParamSet && OpacityMaskParamName != NAME_None && ParameterName == OpacityMaskParamName) { ((FbxSurfaceLambert*)FbxMaterial)->TransparencyFactor.Set(FbxValue); OpacityMaskParamSet = true; } } } } //TODO: export virtual texture to fbx //Query all base material runtime virtual texture parameters. // Material->GetAllRuntimeVirtualTextureParameterInfo(ParameterInfos, Guids); // for (int32 ParameterIdx = 0; ParameterIdx < ParameterInfos.Num(); ParameterIdx++) // { // const FMaterialParameterInfo& ParameterInfo = ParameterInfos[ParameterIdx]; // FName ParameterName = ParameterInfo.Name; // URuntimeVirtualTexture* Value; // //Query the material instance parameter overridden value // if (MaterialInterface->GetRuntimeVirtualTextureParameterValue(ParameterInfo, Value, bOveriddenOnly)) // { // } // } // //In case there is no material instance override, we extract the value from the base material // //The OpacityMaskParam set the transparency factor, so set it only if it was not set if(!OpacityMaskParamSet) { ((FbxSurfaceLambert*)FbxMaterial)->TransparencyFactor.Set(MaterialEditorOnly->Opacity.Constant); } UFbxExportOption* FbxExportOptions = GetExportOptions(); // Fill in the profile_COMMON effect with the material information. //Fill the texture or constant if(!BaseColorParamSet) { if (!FillFbxTextureProperty(FbxSurfaceMaterial::sDiffuse, MaterialEditorOnly->BaseColor, FbxMaterial)) { if (bBaseColorNonConstAndNonDefault) { FFbxMaterialExportUtilities::BakeMaterialProperty(FbxExportOptions, Scene, FbxMaterial, FbxSurfaceMaterial::sDiffuse, MP_BaseColor, MaterialInterface, MaterialIndex, BakingMeshData, ExportFolderPath); } else { ((FbxSurfaceLambert*)FbxMaterial)->Diffuse.Set(SetMaterialComponent(MaterialEditorOnly->BaseColor, true)); } } } if (!EmissiveParamSet) { if (!FillFbxTextureProperty(FbxSurfaceMaterial::sEmissive, MaterialEditorOnly->EmissiveColor, FbxMaterial)) { if (bEmissiveNonConstAndNonDefault) { FFbxMaterialExportUtilities::BakeMaterialProperty(FbxExportOptions, Scene, FbxMaterial, FbxSurfaceMaterial::sEmissive, MP_EmissiveColor, MaterialInterface, MaterialIndex, BakingMeshData, ExportFolderPath); } else { ((FbxSurfaceLambert*)FbxMaterial)->Emissive.Set(SetMaterialComponent(MaterialEditorOnly->EmissiveColor, true)); } } } //Always set the ambient to zero since we dont have ambient in unreal we want to avoid default value in DCCs ((FbxSurfaceLambert*)FbxMaterial)->Ambient.Set(FbxDouble3(0.0, 0.0, 0.0)); if (BlendMode == BLEND_Translucent) { if (!OpacityParamSet) { if (!FillFbxTextureProperty(FbxSurfaceMaterial::sTransparentColor, MaterialEditorOnly->Opacity, FbxMaterial)) { if (bOpacityNonConstAndNonDefault) { FFbxMaterialExportUtilities::BakeMaterialProperty(FbxExportOptions, Scene, FbxMaterial, FbxSurfaceMaterial::sTransparentColor, MP_Opacity, MaterialInterface, MaterialIndex, BakingMeshData, ExportFolderPath); } else { FbxDouble3 OpacityValue((FbxDouble)(MaterialEditorOnly->Opacity.Constant), (FbxDouble)(MaterialEditorOnly->Opacity.Constant), (FbxDouble)(MaterialEditorOnly->Opacity.Constant)); ((FbxSurfaceLambert*)FbxMaterial)->TransparentColor.Set(OpacityValue); } } } if (!OpacityMaskParamSet) { if (!FillFbxTextureProperty(FbxSurfaceMaterial::sTransparencyFactor, MaterialEditorOnly->OpacityMask, FbxMaterial)) { if (bOpacityMaskNonConstAndNonDefault) { FFbxMaterialExportUtilities::BakeMaterialProperty(FbxExportOptions, Scene, FbxMaterial, FbxSurfaceMaterial::sTransparencyFactor, MP_OpacityMask, MaterialInterface, MaterialIndex, BakingMeshData, ExportFolderPath); } else { ((FbxSurfaceLambert*)FbxMaterial)->TransparencyFactor.Set(MaterialEditorOnly->OpacityMask.Constant); } } } } else { //There is no normal input in Blend translucent mode if (!NormalParamSet) { //Set the Normal map only if there is a texture sampler if (!FillFbxTextureProperty(FbxSurfaceMaterial::sNormalMap, MaterialEditorOnly->Normal, FbxMaterial)) { if (bNormalNonConstAndNonDefault) { FFbxMaterialExportUtilities::BakeMaterialProperty(FbxExportOptions, Scene, FbxMaterial, FbxSurfaceMaterial::sNormalMap, MP_Normal, MaterialInterface, MaterialIndex, BakingMeshData, ExportFolderPath); } } } } MaterialIndexToFbxMaterials->Add(MaterialIndex, FbxMaterial); return FbxMaterial; } FFbxExporter::FLevelSequenceNodeNameAdapter::FLevelSequenceNodeNameAdapter( UMovieScene* InMovieScene, IMovieScenePlayer* InMovieScenePlayer, FMovieSceneSequenceIDRef InSequenceID ) { MovieScene = InMovieScene; MovieScenePlayer = InMovieScenePlayer; SequenceID = InSequenceID; } FString FFbxExporter::FLevelSequenceNodeNameAdapter::GetActorNodeName(const AActor* Actor) { FString NodeName = Actor->GetName(); for ( const FMovieSceneBinding& MovieSceneBinding : MovieScene->GetBindings() ) { for ( TWeakObjectPtr RuntimeObject : MovieScenePlayer->FindBoundObjects(MovieSceneBinding.GetObjectGuid(), SequenceID) ) { if (RuntimeObject.Get() == Actor) { NodeName = MovieScene->GetObjectDisplayName(MovieSceneBinding.GetObjectGuid()).ToString(); } } } // Maya does not support dashes. Change all dashes to underscores NodeName = NodeName.Replace(TEXT("-"), TEXT("_") ); // Maya does not support spaces. Change all spaces to underscores NodeName = NodeName.Replace(TEXT(" "), TEXT("_") ); return NodeName; } void FFbxExporter::FLevelSequenceNodeNameAdapter::AddFbxNode(UObject* InObject, FbxNode* FbxNode) { FGuid ObjectGuid = MovieScenePlayer->FindObjectId(*InObject, SequenceID); if (ObjectGuid.IsValid()) { if (!GuidToFbxNodeMap.Contains(ObjectGuid)) { GuidToFbxNodeMap.Add(ObjectGuid); } GuidToFbxNodeMap[ObjectGuid] = FbxNode; } } FbxNode* FFbxExporter::FLevelSequenceNodeNameAdapter::GetFbxNode(UObject* InObject) { FGuid ObjectGuid = MovieScenePlayer->FindObjectId(*InObject, SequenceID); if (ObjectGuid.IsValid()) { if (GuidToFbxNodeMap.Contains(ObjectGuid)) { return GuidToFbxNodeMap[ObjectGuid]; } } return nullptr; } FLevelSequenceAnimTrackAdapter::FLevelSequenceAnimTrackAdapter(const FAnimTrackSettings& Settings) { MovieScenePlayer = Settings.MovieScenePlayer; MovieSceneSequence = Settings.MovieSceneSequence; RootMovieSceneSequence = Settings.RootMovieSceneSequence; MovieScene = Settings.MovieSceneSequence->GetMovieScene(); RootToLocalTransform = Settings.RootToLocalTransform; AnimTrack = Settings.AnimTrack; bForceUseOfMovieScenePlaybackRange = Settings.bForceUseOfMovieScenePlaybackRange; } //5.6 deprecrated FLevelSequenceAnimTrackAdapter::FLevelSequenceAnimTrackAdapter(IMovieScenePlayer* InMovieScenePlayer, UMovieSceneSequence* InMovieSceneSequence, UMovieSceneSequence* InRootMovieSceneSequence, const FMovieSceneSequenceTransform& InRootToLocalTransform, UMovieSceneSkeletalAnimationTrack* InAnimTrack) { MovieScenePlayer = InMovieScenePlayer; MovieSceneSequence = InMovieSceneSequence; RootMovieSceneSequence = InRootMovieSceneSequence; MovieScene = MovieSceneSequence->GetMovieScene(); RootToLocalTransform = InRootToLocalTransform; AnimTrack = InAnimTrack; bForceUseOfMovieScenePlaybackRange = false; } //5.5 deprecrated FLevelSequenceAnimTrackAdapter::FLevelSequenceAnimTrackAdapter(IMovieScenePlayer* InMovieScenePlayer, UMovieScene* InMovieScene, const FMovieSceneSequenceTransform& InRootToLocalTransform, UMovieSceneSkeletalAnimationTrack* InAnimTrack) { MovieScenePlayer = InMovieScenePlayer; MovieScene = InMovieScene; RootToLocalTransform = InRootToLocalTransform; AnimTrack = InAnimTrack; MovieSceneSequence = nullptr; RootMovieSceneSequence = nullptr; if (MovieScene) { if (UMovieSceneSequence* OwnerSceneSequence = MovieScene->GetTypedOuter()) { MovieSceneSequence = OwnerSceneSequence; RootMovieSceneSequence = OwnerSceneSequence; } } bForceUseOfMovieScenePlaybackRange = false; } void FLevelSequenceAnimTrackAdapter::SetRange(const FFrameNumber& StartFrame, const FFrameNumber& EndFrame) { TPair Range(StartFrame, EndFrame); OptionalRange = Range; } TRange FLevelSequenceAnimTrackAdapter::GetSequenceRange() const { if (OptionalRange.IsSet()) { TRange SetRange(OptionalRange.GetValue().Key, (OptionalRange.GetValue().Value)); return SetRange; } FMovieSceneEvaluationState* State = MovieScenePlayer->GetEvaluationState(); State->AssignSequence(MovieSceneSequenceID::Root, *RootMovieSceneSequence, *MovieScenePlayer); FMovieSceneSequenceIDRef Template = State->FindSequenceId(MovieSceneSequence); FMovieSceneSequenceHierarchy Hierarchy = FMovieSceneSequenceHierarchy(); UMovieSceneCompiledDataManager::CompileHierarchy(RootMovieSceneSequence, &Hierarchy,EMovieSceneServerClientMask::All); const FMovieSceneSubSequenceData* SubSequenceData = Hierarchy.FindSubData(Template); TRange Range = MovieScene->GetPlaybackRange(); FFrameTime StartTime = FFrameRate::TransformTime(UE::MovieScene::DiscreteInclusiveLower(MovieScene->GetPlaybackRange()).Value, MovieScene->GetTickResolution(), GetDisplayRate()); if (SubSequenceData && bForceUseOfMovieScenePlaybackRange == false) { Range = SubSequenceData->PlayRange.Value; } return Range; } int32 FLevelSequenceAnimTrackAdapter::GetLocalStartFrame() const { TRange Range = GetSequenceRange(); FFrameRate TickResolution = MovieScene->GetTickResolution(); FFrameRate DisplayRate = GetDisplayRate(); return FFrameRate::TransformTime(FFrameTime(UE::MovieScene::DiscreteInclusiveLower(Range)), TickResolution, DisplayRate).RoundToFrame().Value; } int32 FLevelSequenceAnimTrackAdapter::GetStartFrame() const { TRange Range = GetSequenceRange(); FFrameRate TickResolution = MovieScene->GetTickResolution(); FFrameRate DisplayRate = GetDisplayRate(); FFrameNumber StartFrame = UE::MovieScene::DiscreteInclusiveLower(Range); FMovieSceneInverseSequenceTransform Inverse = RootToLocalTransform.Inverse(); TOptional RootTime = Inverse.TryTransformTime(StartFrame); return FFrameRate::TransformTime(RootTime.Get(StartFrame), TickResolution, DisplayRate).RoundToFrame().Value; } int32 FLevelSequenceAnimTrackAdapter::GetLength() const { TRange Range = GetSequenceRange(); FFrameRate TickResolution = MovieScene->GetTickResolution(); FFrameRate DisplayRate = GetDisplayRate(); return FFrameRate::TransformTime(FFrameTime(UE::MovieScene::DiscreteSize(Range)), TickResolution, DisplayRate).RoundToFrame().Value; } void FLevelSequenceAnimTrackAdapter::UpdateAnimation( int32 LocalFrame ) { FFrameRate TickResolution = MovieScene->GetTickResolution(); FFrameRate DisplayRate = GetDisplayRate(); FFrameTime LocalTime = FFrameRate::TransformTime(FFrameTime(LocalFrame), DisplayRate, TickResolution); TOptional GlobalTime = RootToLocalTransform.Inverse().TryTransformTime(LocalTime); if (GlobalTime) { FMovieSceneContext Context = FMovieSceneContext(FMovieSceneEvaluationRange(GlobalTime.GetValue(), TickResolution), MovieScenePlayer->GetPlaybackStatus()).SetHasJumped(true); MovieScenePlayer->GetEvaluationTemplate().EvaluateSynchronousBlocking( Context ); } } void FLevelSequenceAnimTrackAdapter::SetFrameRate(FFrameRate InFrameRate) { OptionalFrameRate = InFrameRate; } double FLevelSequenceAnimTrackAdapter::GetFrameRate() const { return OptionalFrameRate.IsSet() ? OptionalFrameRate.GetValue().AsDecimal() : MovieScene->GetDisplayRate().AsDecimal(); } FFrameRate FLevelSequenceAnimTrackAdapter::GetDisplayRate() const { return OptionalFrameRate.IsSet() ? OptionalFrameRate.GetValue() : MovieScene->GetDisplayRate(); } UAnimSequence* FLevelSequenceAnimTrackAdapter::GetAnimSequence(int32 LocalFrame) const { if (!AnimTrack) { return nullptr; } FFrameRate TickResolution = MovieScene->GetTickResolution(); FFrameRate DisplayRate = GetDisplayRate(); FFrameTime LocalTime = FFrameRate::TransformTime(FFrameTime(LocalFrame), DisplayRate, TickResolution); for (UMovieSceneSection* Section : AnimTrack->GetAnimSectionsAtTime(LocalTime.FrameNumber)) { if (UMovieSceneSkeletalAnimationSection* AnimSection = Cast(Section)) { return Cast(AnimSection->Params.Animation); } } return nullptr; } float FLevelSequenceAnimTrackAdapter::GetAnimTime(int32 LocalFrame) const { if (!AnimTrack) { return 0.f; } FFrameRate TickResolution = MovieScene->GetTickResolution(); FFrameRate DisplayRate = GetDisplayRate(); FFrameTime LocalTime = FFrameRate::TransformTime(FFrameTime(LocalFrame), DisplayRate, TickResolution); for (UMovieSceneSection* Section : AnimTrack->GetAnimSectionsAtTime(LocalTime.FrameNumber)) { if (UMovieSceneSkeletalAnimationSection* AnimSection = Cast(Section)) { if (AnimSection->Params.Animation) { return static_cast(AnimSection->MapTimeToAnimation(LocalTime, TickResolution)); } } } return 0.f; } bool FFbxExporter::ExportLevelSequenceTracks(UMovieScene* MovieScene, IMovieScenePlayer* MovieScenePlayer, FMovieSceneSequenceIDRef InSequenceID, FbxNode* FbxActor, UObject* BoundObject, const TArray& Tracks, const FMovieSceneSequenceTransform& RootToLocalTransform) { if (MovieScene) { if (UMovieSceneSequence* OwnerSceneSequence = MovieScene->GetTypedOuter()) { return ExportLevelSequenceTracks(OwnerSceneSequence, OwnerSceneSequence, MovieScenePlayer, InSequenceID, FbxActor, BoundObject, Tracks, RootToLocalTransform); } } return false; } bool FFbxExporter::ExportLevelSequenceTracks(UMovieSceneSequence* MovieSceneSequence, UMovieSceneSequence* RootMovieSceneSequence, IMovieScenePlayer* MovieScenePlayer, FMovieSceneSequenceIDRef InSequenceID, FbxNode* FbxActor, UObject* BoundObject, const TArray& Tracks, const FMovieSceneSequenceTransform& RootToLocalTransform) { AActor* Actor = Cast(BoundObject); if (!Actor) { UActorComponent* Component = Cast(BoundObject); if (Component) { Actor = Component->GetOwner(); } } USkeletalMeshComponent* SkeletalMeshComp = Cast(BoundObject); if (!SkeletalMeshComp) { SkeletalMeshComp = Actor ? Cast(Actor->GetComponentByClass(USkeletalMeshComponent::StaticClass())) : nullptr; } UMovieScene* MovieScene = MovieSceneSequence->GetMovieScene(); FFrameRate DisplayRate = MovieScene->GetDisplayRate(); bool bSkip3DTransformTrack = SkeletalMeshComp && GetExportOptions()->MapSkeletalMotionToRoot; // Get all the transform tracks that affect this binding TArray > TransformTracks; if (BoundObject) { for ( const FMovieSceneBinding& MovieSceneBinding : MovieScene->GetBindings() ) { for ( TWeakObjectPtr RuntimeObject : MovieScenePlayer->FindBoundObjects(MovieSceneBinding.GetObjectGuid(), InSequenceID) ) { if (RuntimeObject.IsValid()) { AActor* RuntimeActor = Cast(RuntimeObject); UActorComponent* RuntimeComponent = nullptr; if (!RuntimeActor) { RuntimeComponent = Cast(RuntimeObject); if (RuntimeComponent) { RuntimeActor = RuntimeComponent->GetOwner(); } } if (RuntimeActor == Actor || RuntimeComponent == BoundObject) { for (UMovieSceneTrack* Track : MovieSceneBinding.GetTracks()) { if (Track->IsA(UMovieScene3DTransformTrack::StaticClass())) { TransformTracks.Add(Cast(Track)); } } } } } } // Also need to skip 3d transform track if this object is an actor and the component has a transform track because otherwise if (BoundObject->IsA()) { for ( const FMovieSceneBinding& MovieSceneBinding : MovieScene->GetBindings() ) { for ( TWeakObjectPtr RuntimeObject : MovieScenePlayer->FindBoundObjects(MovieSceneBinding.GetObjectGuid(), InSequenceID) ) { if (RuntimeObject.IsValid()) { UActorComponent* RuntimeComponent = Cast(RuntimeObject); if (RuntimeComponent && RuntimeComponent->GetOwner() == BoundObject) { for (UMovieSceneTrack* Track : MovieSceneBinding.GetTracks()) { if (Track->IsA(UMovieScene3DTransformTrack::StaticClass())) { bSkip3DTransformTrack = true; } } } } } } } } AActor* BoundActor = Cast(BoundObject); USceneComponent* BoundComponent = Cast(BoundObject); const bool bIsCameraActor = BoundActor ? BoundActor->IsA(ACameraActor::StaticClass()) : BoundComponent ? BoundComponent->IsA(UCameraComponent::StaticClass()) : false; const bool bIsLightActor = BoundActor ? BoundActor->IsA(ALight::StaticClass()) : BoundComponent ? BoundComponent->IsA(ULightComponent::StaticClass()) : false; bool bBakeAllTransformChannels = false; if (bIsCameraActor || bIsLightActor) { bBakeAllTransformChannels = (static_cast(GetExportOptions()->BakeCameraAndLightAnimation) & static_cast(EMovieSceneBakeType::BakeTransforms)); } else { bBakeAllTransformChannels = (static_cast(GetExportOptions()->BakeActorAnimation) & static_cast(EMovieSceneBakeType::BakeTransforms)); } // If this actor has attached actors, and the bound component isn't the root component, skip the 3d transform track because we don't want the // component transform to be assigned to the actor, which would subsequently apply to the attached children if (Actor && BoundComponent && Actor->GetRootComponent() != BoundComponent) { TArray AttachedActors; Actor->GetAttachedActors(AttachedActors); if (AttachedActors.Num() != 0) { bSkip3DTransformTrack = true; } } // If there's more than one transform track for this actor (ie. on the actor and on the root component) or if there's more than one section, evaluate baked if (bBakeAllTransformChannels || TransformTracks.Num() > 1 || (TransformTracks.Num() != 0 && TransformTracks[0].Get()->GetAllSections().Num() > 1)) { if (!bSkip3DTransformTrack) { UnFbx::FLevelSequenceAnimTrackAdapter::FAnimTrackSettings Settings; Settings.MovieScenePlayer = MovieScenePlayer; Settings.MovieSceneSequence = MovieSceneSequence; Settings.RootMovieSceneSequence = RootMovieSceneSequence; Settings.RootToLocalTransform = RootToLocalTransform; Settings.bForceUseOfMovieScenePlaybackRange = false; FLevelSequenceAnimTrackAdapter AnimTrackAdapter(Settings); ExportLevelSequenceBaked3DTransformTrack(AnimTrackAdapter, FbxActor, MovieScenePlayer, InSequenceID, TransformTracks, BoundObject, MovieScene->GetPlaybackRange(), RootToLocalTransform); } bSkip3DTransformTrack = true; } // Look for the tracks that we currently support UMovieSceneSkeletalAnimationTrack* SkeletalAnimationTrack = nullptr; bool IsControlRigTrack = false; for (UMovieSceneTrack* Track : Tracks) { if (Track->IsA(UMovieScene3DTransformTrack::StaticClass())) { if (!bSkip3DTransformTrack) { UMovieScene3DTransformTrack* TransformTrack = (UMovieScene3DTransformTrack*)Track; ExportLevelSequence3DTransformTrack(FbxActor, MovieScenePlayer, InSequenceID, *TransformTrack, BoundObject, MovieScene->GetPlaybackRange(), RootToLocalTransform); } } else if (Track->IsA(UMovieSceneSkeletalAnimationTrack::StaticClass())) { SkeletalAnimationTrack = Cast(Track); } else if (Track->IsA(UMovieSceneColorTrack::StaticClass())) { ExportLevelSequenceColorTrack(FbxActor, *Cast(Track), BoundObject, MovieScene->GetPlaybackRange(), RootToLocalTransform); } else if (Track->IsA(UMovieSceneDoubleVectorTrack::StaticClass())) { ExportLevelSequenceVectorTrack(FbxActor, *Cast(Track), BoundObject, MovieScene->GetPlaybackRange(), RootToLocalTransform); } else if(Cast(Track)) { IsControlRigTrack = true; } else { bool bBakeChannels = false; if (bIsCameraActor || bIsLightActor) { bBakeChannels = (static_cast(GetExportOptions()->BakeCameraAndLightAnimation) & static_cast(EMovieSceneBakeType::BakeChannels)); } else { bBakeChannels = (static_cast(GetExportOptions()->BakeActorAnimation) & static_cast(EMovieSceneBakeType::BakeChannels)); } ExportLevelSequenceTrackChannels(FbxActor, *Track, MovieScene->GetPlaybackRange(), RootToLocalTransform, bBakeChannels); } } // Export all of the skeletal animation components for this actor if (SkeletalMeshComp && (SkeletalAnimationTrack || IsControlRigTrack)) { TArray SkeletalMeshComponents; SkeletalMeshComp->GetOwner()->GetComponents(SkeletalMeshComponents); UnFbx::FLevelSequenceAnimTrackAdapter::FAnimTrackSettings Settings; Settings.MovieScenePlayer = MovieScenePlayer; Settings.MovieSceneSequence = MovieSceneSequence; Settings.RootMovieSceneSequence = RootMovieSceneSequence; Settings.RootToLocalTransform = RootToLocalTransform; Settings.bForceUseOfMovieScenePlaybackRange = false; for (USkeletalMeshComponent* SkeletalMeshComponent : SkeletalMeshComponents) { Settings.AnimTrack = SkeletalAnimationTrack; FLevelSequenceAnimTrackAdapter AnimTrackAdapter(Settings); ExportAnimTrack(AnimTrackAdapter, Actor, SkeletalMeshComponent, 1.0 / DisplayRate.AsDecimal()); } } return true; } bool FFbxExporter::ExportLevelSequence(UMovieScene* MovieScene, const TArray& Bindings, IMovieScenePlayer* MovieScenePlayer, INodeNameAdapter& NodeNameAdapter, FMovieSceneSequenceIDRef SequenceID, const FMovieSceneSequenceTransform& RootToLocalTransform) { if (MovieScene) { if (UMovieSceneSequence* OwnerSceneSequence = MovieScene->GetTypedOuter()) { return ExportLevelSequence(OwnerSceneSequence, OwnerSceneSequence, Bindings, MovieScenePlayer, NodeNameAdapter, SequenceID, RootToLocalTransform); } } return false; } bool FFbxExporter::ExportLevelSequence(UMovieSceneSequence* MovieSceneSequence, UMovieSceneSequence* RootMovieSceneSequence, const TArray& Bindings, IMovieScenePlayer* MovieScenePlayer, INodeNameAdapter& NodeNameAdapter, FMovieSceneSequenceIDRef SequenceID, const FMovieSceneSequenceTransform& RootToLocalTransform) { if (MovieSceneSequence == nullptr || RootMovieSceneSequence == nullptr || MovieScenePlayer == nullptr) { return false; } UMovieScene* MovieScene = MovieSceneSequence->GetMovieScene(); double FrameRate = MovieScene->GetDisplayRate().AsDecimal(); FbxTime::EMode TimeMode = FbxTime::ConvertFrameRateToTimeMode(FrameRate); // Unknown frame rates, (ie. 12 fps) return eDefaultMode, so override with custom so that the custom frame rate takes effect if (TimeMode == FbxTime::eDefaultMode) { TimeMode = FbxTime::eCustom; } Scene->GetGlobalSettings().SetTimeMode(TimeMode); if (TimeMode == FbxTime::eCustom) { Scene->GetGlobalSettings().SetCustomFrameRate(FrameRate); } for (const FMovieSceneBinding& MovieSceneBinding : MovieScene->GetBindings()) { // If there are specific bindings to export, export those only if (Bindings.Num() != 0 && !Bindings.Contains(MovieSceneBinding.GetObjectGuid())) { continue; } bool bAnyBindingsExported = false; for (TWeakObjectPtr RuntimeObject : MovieScenePlayer->FindBoundObjects(MovieSceneBinding.GetObjectGuid(), SequenceID)) { if (RuntimeObject.IsValid()) { AActor* Actor = Cast(RuntimeObject.Get()); UActorComponent* Component = Cast(RuntimeObject.Get()); if (Actor == nullptr && Component != nullptr) { Actor = Component->GetOwner(); } if (Actor == nullptr) { continue; } FbxNode* FbxActor = FindActor(Actor, &NodeNameAdapter); // now it should export everybody if (FbxActor) { ExportLevelSequenceTracks(MovieSceneSequence, RootMovieSceneSequence, MovieScenePlayer, SequenceID, FbxActor, RuntimeObject.Get(), MovieSceneBinding.GetTracks(), RootToLocalTransform); bAnyBindingsExported = true; } } } // If no bindings exported, create a dummy actor to export tracks onto if (!bAnyBindingsExported) { ExportLevelSequenceTracks(MovieSceneSequence, RootMovieSceneSequence, MovieScenePlayer, SequenceID, nullptr, nullptr, MovieSceneBinding.GetTracks(), RootToLocalTransform); } } return true; } void FFbxExporter::AddTimecodeAttributesAndSetKey(const UMovieSceneSection* InSection, FbxNode* InFbxNode, const FMovieSceneSequenceTransform& RootToLocalTransform) { FbxAnimLayer* BaseLayer = AnimStack->GetMember(0); const FFrameRate TickResolution = InSection->GetTypedOuter()->GetTickResolution(); FName TCHourAttrName(TEXT("TCHour")); FName TCMinuteAttrName(TEXT("TCMinute")); FName TCSecondAttrName(TEXT("TCSecond")); FName TCFrameAttrName(TEXT("TCFrame")); if (const UAnimationSettings* AnimationSettings = UAnimationSettings::Get()) { TCHourAttrName = AnimationSettings->BoneTimecodeCustomAttributeNameSettings.HourAttributeName; TCMinuteAttrName = AnimationSettings->BoneTimecodeCustomAttributeNameSettings.MinuteAttributeName; TCSecondAttrName = AnimationSettings->BoneTimecodeCustomAttributeNameSettings.SecondAttributeName; TCFrameAttrName = AnimationSettings->BoneTimecodeCustomAttributeNameSettings.FrameAttributeName; } const TArray TimecodePropertyNames = { TCHourAttrName.ToString(), TCMinuteAttrName.ToString(), TCSecondAttrName.ToString(), TCFrameAttrName.ToString() }; const TArray TimecodeValues = { InSection->TimecodeSource.Timecode.Hours, InSection->TimecodeSource.Timecode.Minutes, InSection->TimecodeSource.Timecode.Seconds, InSection->TimecodeSource.Timecode.Frames }; const FFrameNumber KeyTime = InSection->GetTypedOuter()->GetPlaybackRange().GetLowerBoundValue(); for (int i = 0; i < TimecodePropertyNames.Num(); i++) { const FString PropertyName = TimecodePropertyNames[i]; CreateAnimatableUserProperty(InFbxNode, 0.0, StringCast(*PropertyName).Get(), StringCast(*PropertyName).Get(), FbxFloatDT); FbxProperty Property = InFbxNode->FindProperty(StringCast(*PropertyName).Get()); if (Property.IsValid()) { if (FbxAnimCurve* AnimCurve = Property.GetCurve(BaseLayer, nullptr, true)) { const float KeyValue = (float)TimecodeValues[i]; AnimCurve->KeyModifyBegin(); FbxTime FbxTime; TOptional GlobalKeyTime = RootToLocalTransform.Inverse().TryTransformTime(KeyTime); const double KeyTimeSeconds = GetExportOptions()->bExportLocalTime ? KeyTime / TickResolution : GlobalKeyTime.Get(KeyTime) / TickResolution; FbxTime.SetSecondDouble(KeyTimeSeconds); const int FbxKeyIndex = AnimCurve->KeyAdd(FbxTime); AnimCurve->KeySet(FbxKeyIndex, FbxTime, KeyValue, FbxAnimCurveDef::eInterpolationConstant); AnimCurve->KeySetConstantMode(FbxKeyIndex, FbxAnimCurveDef::EConstantMode::eConstantStandard); AnimCurve->KeyModifyEnd(); } } } } bool FFbxExporter::ExportControlRigSection(const UMovieSceneSection* Section, const TArray& ChannelsMapping, const TArray& FilterControls, const FMovieSceneSequenceTransform& RootToLocalTransform) { if (!Section) { return false; } UMovieSceneTrack* Track = Section->GetTypedOuter(); FbxAnimLayer* BaseLayer = AnimStack->GetMember(0); const FFrameRate TickResolution = Track->GetTypedOuter()->GetTickResolution(); // Helpers to convert channel/property names to indices and vice versa const TArray TransformComponentsArray = {"X", "Y", "Z"}; const TArray TransformPropertyArray = {"Location", "Rotation", "Scale"}; // Cached transform channels for a single property to bake and export the 3 channels in one go TPair CachedTransformChannels[3] = {{nullptr, false}, {nullptr, false}, {nullptr, false}}; // Cached transform property index keep track of which property we are currently caching to bake & export Transforms & TransformNoScale int CurrentTmPropertyIndex = 0; // The control rig parent group node FbxNode* RootFbxNode = CreateNode(Track->GetDisplayName().ToString()); // Export timecode AddTimecodeAttributesAndSetKey(Section, RootFbxNode, RootToLocalTransform); const FName BoolChannelTypeName = FMovieSceneBoolChannel::StaticStruct()->GetFName(); const FName ByteChannelTypeName = FMovieSceneByteChannel::StaticStruct()->GetFName(); const FName DoubleChannelTypeName = FMovieSceneDoubleChannel::StaticStruct()->GetFName(); const FName FloatChannelTypeName = FMovieSceneFloatChannel::StaticStruct()->GetFName(); const FName IntegerChannelTypeName = FMovieSceneIntegerChannel::StaticStruct()->GetFName(); FMovieSceneChannelProxy& ChannelProxy = Section->GetChannelProxy(); for (const FMovieSceneChannelEntry& Entry : ChannelProxy.GetAllEntries()) { TArrayView Channels = Entry.GetChannels(); TArrayView AllMetaData = Entry.GetMetaData(); for (int32 Index = 0; Index < Channels.Num(); ++Index) { const FName ChannelTypeName = Entry.GetChannelTypeName(); FMovieSceneChannelHandle ChannelHandle = ChannelProxy.MakeHandle(ChannelTypeName, Index); FMovieSceneBoolChannel* BoolChannel = ChannelTypeName == BoolChannelTypeName ? ChannelHandle.Cast().Get() : nullptr; FMovieSceneByteChannel* ByteChannel = ChannelTypeName == ByteChannelTypeName ? ChannelHandle.Cast().Get() : nullptr; FMovieSceneDoubleChannel* DoubleChannel = ChannelTypeName == DoubleChannelTypeName ? ChannelHandle.Cast().Get() : nullptr; FMovieSceneFloatChannel* FloatChannel = ChannelTypeName == FloatChannelTypeName ? ChannelHandle.Cast().Get() : nullptr; FMovieSceneIntegerChannel* IntegerChannel = ChannelTypeName == IntegerChannelTypeName ? ChannelHandle.Cast().Get() : nullptr; if (!BoolChannel && !ByteChannel && !DoubleChannel && !FloatChannel && !IntegerChannel) { continue; } // Find the node and attribute names const FMovieSceneChannelMetaData& MetaData = AllMetaData[Index]; const FString ChannelName = MetaData.Name.ToString(); FControlRigFbxCurveData FbxCurveData; if (INodeAndChannelMappings* ChannelMapping = Cast(Track)) { if (!ChannelMapping->GetFbxCurveDataFromChannelMetadata(MetaData, FbxCurveData)) { continue; } } // Skip any control not in the filter if the filter isn't empty if (!FilterControls.IsEmpty() && !FilterControls.Contains(FbxCurveData.ControlName)) { continue; } // Find or create the node FbxNode* ControlFbxNode = RootFbxNode->FindChild(StringCast(*FbxCurveData.NodeName).Get()); if (ControlFbxNode == nullptr) { ControlFbxNode = FbxNode::Create(Scene, StringCast(*FbxCurveData.NodeName).Get()); RootFbxNode->AddChild(ControlFbxNode); } // Remap channel, optionally baking & exporting it for transforms & rotations bool bNegate = false; int TmComponentIndex = TransformComponentsArray.Find(FbxCurveData.AttributeName); // Find the attribute to remap onto and whether to negate the channel if required const FControlRigFbxNodeMapping* NodeMapping = ChannelsMapping.FindByPredicate( [ChannelTypeName, FbxCurveData, TmComponentIndex](const FControlRigFbxNodeMapping& AMapping) { const bool bAttrMatch = AMapping.ChannelAttrIndex == TmComponentIndex; const bool bControlTypeOk = AMapping.ControlType == FbxCurveData.ControlType; // todo: am: need to allow mapping of position/rotation/scale for transforms & euler transforms to allow export of negated transform curves const bool bChannelTypeOk = AMapping.ChannelType == ChannelTypeName; // If the control is treated as an attribute of another control, we don't need to check the attribute to remap const bool bAttrOk = !FbxCurveData.IsControlNode() || bAttrMatch; const bool bRemapControl = bControlTypeOk && bChannelTypeOk && bAttrOk; return bRemapControl; }); if (TmComponentIndex != INDEX_NONE) { // Retrieve property name for Vector2D, Position, Scale if (FbxCurveData.ControlType == FFBXControlRigTypeProxyEnum::Vector2D || FbxCurveData.ControlType == FFBXControlRigTypeProxyEnum::Position) { FbxCurveData.AttributePropertyName = "Location"; } if (FbxCurveData.ControlType == FFBXControlRigTypeProxyEnum::Scale) { FbxCurveData.AttributePropertyName = "Scale"; } // Export converted & baked for Rotations & Transforms if ((uint8)FbxCurveData.ControlType >= (uint8)FFBXControlRigTypeProxyEnum::Rotator) { const bool bExportCachedTransforms = TmComponentIndex == 2; const bool bRotator = FbxCurveData.ControlType == FFBXControlRigTypeProxyEnum::Rotator; int TmPropertyIndex = bRotator ? 1 : CurrentTmPropertyIndex; // todo: am: does not handle remapping to another property (i.e. rotation to location) // Remap transform channel if (NodeMapping) { TmPropertyIndex = NodeMapping->FbxAttrIndex / 3; TmComponentIndex = NodeMapping->FbxAttrIndex % 3; bNegate = NodeMapping->bNegate; } // Cache a single transform property channel CachedTransformChannels[TmComponentIndex].Key = FloatChannel; CachedTransformChannels[TmComponentIndex].Value = bNegate; // Once all 3 channels in the current property have been cached, export baked if (bExportCachedTransforms) { for (int i = 0; i < 3; i++) { if (CachedTransformChannels[i].Key && CachedTransformChannels[i].Key->GetNumKeys() > 0) { // Only export baked if at least one curve has keys ExportTransformChannelsToFbxCurve(ControlFbxNode, CachedTransformChannels[0], CachedTransformChannels[1], CachedTransformChannels[2], TmPropertyIndex, Track, RootToLocalTransform); break; } } // Increase or reset CurrentTmPropertyIndex to keep track of which property in a Transform we are currently caching const bool bNoScale = FbxCurveData.ControlType == FFBXControlRigTypeProxyEnum::TransformNoScale; const bool bTransformProcessed = bRotator || CurrentTmPropertyIndex == 2 || (bNoScale && CurrentTmPropertyIndex == 1); CurrentTmPropertyIndex = bTransformProcessed ? 0 : CurrentTmPropertyIndex + 1; } continue; } } // Remap non transform channels if required (rotations & transforms remapped separately) if (NodeMapping) { FbxCurveData.AttributePropertyName = TransformPropertyArray[NodeMapping->FbxAttrIndex / 3]; FbxCurveData.AttributeName = TransformComponentsArray[NodeMapping->FbxAttrIndex % 3]; bNegate = NodeMapping->bNegate; } FbxProperty Property; // Use AttributeName (i.e. Weight) as the property if no AttributePropertyName (i.e. Scale). Use NodeName if AttributeName was also empty... const FString FbxPropertyName = FbxCurveData.AttributePropertyName.IsEmpty() ? (!FbxCurveData.AttributeName.IsEmpty() ? FbxCurveData.AttributeName : FbxCurveData.NodeName) : FbxCurveData.AttributePropertyName; // Try and find the existing property if (FbxCurveData.AttributePropertyName == "Location") { Property = ControlFbxNode->LclTranslation; } else if (FbxCurveData.AttributePropertyName == "Rotation") { Property = ControlFbxNode->LclRotation; } else if (FbxCurveData.AttributePropertyName == "Scale") { Property = ControlFbxNode->LclScaling; } else { Property = ControlFbxNode->FindProperty(StringCast(*FbxPropertyName).Get()); } // Or create it otherwise if (!Property.IsValid()) { if (DoubleChannel) { double Default = DoubleChannel->GetDefault().Get(0); CreateAnimatableUserProperty(ControlFbxNode, Default, StringCast(*FbxPropertyName).Get(), StringCast(*FbxPropertyName).Get(), FbxDoubleDT); } else if (FloatChannel) { float Default = FloatChannel->GetDefault().Get(0); CreateAnimatableUserProperty(ControlFbxNode, Default, StringCast(*FbxPropertyName).Get(), StringCast(*FbxPropertyName).Get(), FbxFloatDT); } else if (IntegerChannel) { int32 Default = IntegerChannel->GetDefault().Get(0); CreateAnimatableUserProperty(ControlFbxNode, Default, StringCast(*FbxPropertyName).Get(), StringCast(*FbxPropertyName).Get(), FbxIntDT); } else if (BoolChannel) { bool Default = BoolChannel->GetDefault().Get(false); CreateAnimatableUserProperty(ControlFbxNode, Default, StringCast(*FbxPropertyName).Get(), StringCast(*FbxPropertyName).Get(), FbxBoolDT); } else if (ByteChannel) { uint8 Default = ByteChannel->GetDefault().Get(0); CreateAnimatableUserProperty(ControlFbxNode, Default, StringCast(*FbxPropertyName).Get(), StringCast(*FbxPropertyName).Get(), FbxEnumDT); } Property = ControlFbxNode->FindProperty(StringCast(*FbxPropertyName).Get()); } if (!Property.IsValid()) { continue; } // Create the anim curve, optionally of the given name if the attribute had a component FbxAnimCurveNode* AnimCurveNode = Property.GetCurveNode(BaseLayer, true); FbxAnimCurve* AnimCurve = Property.GetCurve(BaseLayer, FbxCurveData.AttributePropertyName.IsEmpty() ? nullptr : StringCast(*FbxCurveData.AttributeName).Get(), true); if (!AnimCurve) { continue; } // Export the curve if (BoolChannel) { ExportChannelToFbxCurve(*AnimCurve, *BoolChannel, TickResolution, RootToLocalTransform); } else if (ByteChannel) { ExportChannelToFbxCurve(*AnimCurve, *ByteChannel, TickResolution, RootToLocalTransform); } else if (DoubleChannel) { ExportChannelToFbxCurve(*AnimCurve, *DoubleChannel, TickResolution, ERichCurveValueMode::Default, bNegate, RootToLocalTransform); } else if (FloatChannel) { ExportChannelToFbxCurve(*AnimCurve, *FloatChannel, TickResolution, ERichCurveValueMode::Default, bNegate, RootToLocalTransform); } else if (IntegerChannel) { ExportChannelToFbxCurve(*AnimCurve, *IntegerChannel, TickResolution, RootToLocalTransform); } } } return true; } void FFbxExporter::ExportTransformChannelsToFbxCurve(FbxNode* InFbxNode, TPair ChannelX, TPair ChannelY, TPair ChannelZ, int TmPropertyIndex, const UMovieSceneTrack* Track, const FMovieSceneSequenceTransform& RootToLocalTransform) { FbxAnimLayer* BaseLayer = AnimStack->GetMember(0); FbxProperty Property = TmPropertyIndex == 0 ? InFbxNode->LclTranslation : TmPropertyIndex == 1 ? InFbxNode->LclRotation : InFbxNode->LclScaling; FbxAnimCurve* FbxCurveX = Property.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_X, true); FbxAnimCurve* FbxCurveY = Property.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); FbxAnimCurve* FbxCurveZ = Property.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); FTransform RotationDirectionConvert; FbxCurveX->KeyModifyBegin(); FbxCurveY->KeyModifyBegin(); FbxCurveZ->KeyModifyBegin(); FFrameRate TickResolution = Track->GetTypedOuter()->GetTickResolution(); FFrameRate DisplayRate = Track->GetTypedOuter()->GetDisplayRate(); TRange PlaybackRange = Track->GetTypedOuter()->GetPlaybackRange(); FMovieSceneInverseSequenceTransform SequenceToRootTransform = RootToLocalTransform.Inverse(); int32 LocalStartFrame = FFrameRate::TransformTime(FFrameTime(UE::MovieScene::DiscreteInclusiveLower(PlaybackRange)), TickResolution, DisplayRate).RoundToFrame().Value; int32 AnimationLength = FFrameRate::TransformTime(FFrameTime(FFrameNumber(UE::MovieScene::DiscreteSize(PlaybackRange))), TickResolution, DisplayRate).RoundToFrame().Value; for (int32 FrameCount = 0; FrameCount <= AnimationLength; ++FrameCount) { int32 LocalFrame = LocalStartFrame + FrameCount; FFrameTime LocalTime = FFrameRate::TransformTime(FFrameTime(LocalFrame), DisplayRate, TickResolution); FVector3f Vec = FVector3f::ZeroVector; if (ChannelX.Key) { ChannelX.Key->Evaluate(LocalTime, Vec.X); if (ChannelX.Value) { Vec.X = -Vec.X; } } if (ChannelY.Key) { ChannelY.Key->Evaluate(LocalTime, Vec.Y); if (ChannelY.Value) { Vec.Y = -Vec.Y; } } if (ChannelZ.Key) { ChannelZ.Key->Evaluate(LocalTime, Vec.Z); if (ChannelZ.Value) { Vec.Z = -Vec.Z; } } FbxVector4 KeyVec; if (TmPropertyIndex == 0) { KeyVec = Converter.ConvertToFbxPos((FVector)Vec); } else if (TmPropertyIndex == 1) { KeyVec = Converter.ConvertToFbxRot((FVector)Vec); } else { KeyVec = Converter.ConvertToFbxScale((FVector)Vec); } FbxTime FbxTime; if (GetExportOptions()->bExportLocalTime) { FbxTime.SetSecondDouble(DisplayRate.AsSeconds(LocalFrame)); } // @todo: This code does not handle the root sequence having a different tick resolution than the local space, but we do // not have that information here so we have to just assume they match. This should be improved so that we have // all the information we need to do the right thing else if (TOptional GlobalKeyTime = SequenceToRootTransform.TryTransformTime(LocalTime)) { FbxTime.SetSecondDouble(TickResolution.AsSeconds(GlobalKeyTime.GetValue())); } else { // Doesn't map to a root time continue; } FbxCurveX->KeySet(FbxCurveX->KeyAdd(FbxTime), FbxTime, KeyVec[0]); FbxCurveY->KeySet(FbxCurveY->KeyAdd(FbxTime), FbxTime, KeyVec[1]); FbxCurveZ->KeySet(FbxCurveZ->KeyAdd(FbxTime), FbxTime, KeyVec[2]); } FbxCurveX->KeyModifyEnd(); FbxCurveY->KeyModifyEnd(); FbxCurveZ->KeyModifyEnd(); } /** * Exports a scene node with the placement indicated by a given actor. * This scene node will always have two transformations: one translation vector and one Euler rotation. */ FbxNode* FFbxExporter::ExportActor(AActor* Actor, bool bExportComponents, INodeNameAdapter& NodeNameAdapter, bool bSaveAnimSeq ) { // Verify that this actor isn't already exported, create a structure for it // and buffer it. FbxNode* ActorNode = FindActor(Actor, &NodeNameAdapter); if (ActorNode == NULL) { FString FbxNodeName = NodeNameAdapter.GetActorNodeName(Actor); FbxNodeName = GetFbxObjectName(FbxNodeName, NodeNameAdapter); ActorNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*FbxNodeName)); AActor* ParentActor = Actor->GetAttachParentActor(); // this doesn't work with skeletalmeshcomponent FbxNode* ParentNode = FindActor(ParentActor, &NodeNameAdapter); FVector ActorLocation, ActorRotation, ActorScale; TArray AttachedActors; Actor->GetAttachedActors(AttachedActors); const bool bHasAttachedActors = AttachedActors.Num() != 0; // For cameras and lights: always add a rotation to get the correct coordinate system. FTransform RotationDirectionConvert = FTransform::Identity; const bool bIsCameraActor = Actor->IsA(ACameraActor::StaticClass()); const bool bIsLightActor = Actor->IsA(ALight::StaticClass()); if (bIsCameraActor || bIsLightActor) { if (bIsCameraActor) { FRotator Rotator = FFbxDataConverter::GetCameraRotation().GetInverse(); RotationDirectionConvert = FTransform(Rotator); } else if (bIsLightActor) { FRotator Rotator = FFbxDataConverter::GetLightRotation().GetInverse(); RotationDirectionConvert = FTransform(Rotator); } } //If the parent is the root or is not export use the root node as the parent if (bKeepHierarchy && ParentNode) { // Set the default position of the actor on the transforms // The transformation is different from FBX's Z-up: invert the Y-axis for translations and the Y/Z angle values in rotations. const FTransform RelativeTransform = RotationDirectionConvert * Actor->GetTransform().GetRelativeTransform(ParentActor->GetTransform()); ActorLocation = RelativeTransform.GetTranslation(); ActorRotation = RelativeTransform.GetRotation().Euler(); ActorScale = RelativeTransform.GetScale3D(); } else { ParentNode = Scene->GetRootNode(); // Set the default position of the actor on the transforms // The transformation is different from FBX's Z-up: invert the Y-axis for translations and the Y/Z angle values in rotations. if (ParentActor != NULL) { //In case the parent was not export, get the absolute transform const FTransform AbsoluteTransform = RotationDirectionConvert * Actor->GetTransform(); ActorLocation = AbsoluteTransform.GetTranslation(); ActorRotation = AbsoluteTransform.GetRotation().Euler(); ActorScale = AbsoluteTransform.GetScale3D(); } else { const FTransform ConvertedTransform = RotationDirectionConvert * Actor->GetTransform(); ActorLocation = ConvertedTransform.GetTranslation(); ActorRotation = ConvertedTransform.GetRotation().Euler(); ActorScale = ConvertedTransform.GetScale3D(); } } ParentNode->AddChild(ActorNode); FbxActors.Add(Actor, ActorNode); NodeNameAdapter.AddFbxNode(Actor, ActorNode); // Set the default position of the actor on the transforms // The transformation is different from FBX's Z-up: invert the Y-axis for translations and the Y/Z angle values in rotations. ActorNode->LclTranslation.Set(Converter.ConvertToFbxPos(ActorLocation)); ActorNode->LclRotation.Set(Converter.ConvertToFbxRot(ActorRotation)); ActorNode->LclScaling.Set(Converter.ConvertToFbxScale(ActorScale)); if( bExportComponents ) { TInlineComponentArray ComponentsToExport; for (UActorComponent* ActorComp : Actor->GetComponents()) { USceneComponent* Component = Cast(ActorComp); if (Component == nullptr || Component->bHiddenInGame) { //Skip hidden component like camera mesh or other editor helper continue; } UStaticMeshComponent* StaticMeshComp = Cast( Component ); USkeletalMeshComponent* SkelMeshComp = Cast( Component ); UChildActorComponent* ChildActorComp = Cast( Component ); if( StaticMeshComp && StaticMeshComp->GetStaticMesh()) { ComponentsToExport.Add( StaticMeshComp ); } else if( SkelMeshComp && SkelMeshComp->GetSkeletalMeshAsset()) { ComponentsToExport.Add( SkelMeshComp ); } else if (Component->IsA(UCameraComponent::StaticClass())) { ComponentsToExport.Add(Component); } else if (Component->IsA(ULightComponent::StaticClass())) { ComponentsToExport.Add(Component); } else if (ChildActorComp && ChildActorComp->GetChildActor()) { ComponentsToExport.Add(ChildActorComp); } } for( int32 CompIndex = 0; CompIndex < ComponentsToExport.Num(); ++CompIndex ) { USceneComponent* Component = ComponentsToExport[CompIndex]; RotationDirectionConvert = FTransform::Identity; // For cameras and lights: always add a rotation to get the correct coordinate system // Unless we are parented to an Actor of the same type, since the rotation direction was already added if (Component->IsA(UCameraComponent::StaticClass()) || Component->IsA(ULightComponent::StaticClass())) { if (!bIsCameraActor && Component->IsA(UCameraComponent::StaticClass())) { FRotator Rotator = FFbxDataConverter::GetCameraRotation().GetInverse(); RotationDirectionConvert = FTransform(Rotator); } else if (!bIsLightActor && Component->IsA(ULightComponent::StaticClass())) { FRotator Rotator = FFbxDataConverter::GetLightRotation().GetInverse(); RotationDirectionConvert = FTransform(Rotator); } } FbxNode* ExportNode = ActorNode; if( ComponentsToExport.Num() > 1 ) { // This actor has multiple components // create a child node under the actor for each component FbxNode* CompNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*Component->GetName())); if( Component != Actor->GetRootComponent() ) { // Transform is relative to the root component const FTransform RelativeTransform = RotationDirectionConvert * Component->GetComponentToWorld().GetRelativeTransform(Actor->GetTransform()); CompNode->LclTranslation.Set(Converter.ConvertToFbxPos(RelativeTransform.GetTranslation())); CompNode->LclRotation.Set(Converter.ConvertToFbxRot(RelativeTransform.GetRotation().Euler())); CompNode->LclScaling.Set(Converter.ConvertToFbxScale(RelativeTransform.GetScale3D())); } ExportNode = CompNode; ActorNode->AddChild(CompNode); } // If this actor has attached actors, don't allow its non root component components to contribute to the transform else if(Component != Actor->GetRootComponent() && !bHasAttachedActors) { // Merge the component relative transform in the ActorNode transform since this is the only component to export and its not the root const FTransform RelativeTransform = RotationDirectionConvert * Component->GetComponentToWorld().GetRelativeTransform(Actor->GetTransform()); FTransform ActorTransform(FRotator::MakeFromEuler(ActorRotation).Quaternion(), ActorLocation, ActorScale); FTransform TotalTransform = RelativeTransform * ActorTransform; ActorNode->LclTranslation.Set(Converter.ConvertToFbxPos(TotalTransform.GetLocation())); ActorNode->LclRotation.Set(Converter.ConvertToFbxRot(TotalTransform.GetRotation().Euler())); ActorNode->LclScaling.Set(Converter.ConvertToFbxScale(TotalTransform.GetScale3D())); } UStaticMeshComponent* StaticMeshComp = Cast( Component ); USkeletalMeshComponent* SkelMeshComp = Cast( Component ); UChildActorComponent* ChildActorComp = Cast( Component ); if (StaticMeshComp && StaticMeshComp->GetStaticMesh()) { const int32 LODIndex = (StaticMeshComp->ForcedLodModel > 0 ? FMath::Min(StaticMeshComp->GetStaticMesh()->GetNumLODs(), StaticMeshComp->ForcedLodModel) - 1 : /* auto-select*/ 0); if (USplineMeshComponent* SplineMeshComp = Cast(StaticMeshComp)) { //TODO: Validate Spline material baking //(Spline mesh staticmesh does not take into account the Spline transforms, might not be accurate/enough to use Spline->StaticMesh for material baking.) ExportSplineMeshToFbx(SplineMeshComp, *SplineMeshComp->GetName(), ExportNode, FFbxMaterialBakingMeshData(SplineMeshComp->GetStaticMesh(), SplineMeshComp, LODIndex)); } else if (UInstancedStaticMeshComponent* InstancedMeshComp = Cast(StaticMeshComp)) { ExportInstancedMeshToFbx(InstancedMeshComp, *InstancedMeshComp->GetName(), ExportNode, FFbxMaterialBakingMeshData(InstancedMeshComp->GetStaticMesh(), InstancedMeshComp, LODIndex)); } else { const int32 LightmapUVChannel = -1; const TArray* MaterialOrderOverride = nullptr; const FColorVertexBuffer* ColorBuffer = nullptr; ExportStaticMeshToFbx(StaticMeshComp->GetStaticMesh(), LODIndex, *StaticMeshComp->GetName(), ExportNode, FFbxMaterialBakingMeshData(StaticMeshComp->GetStaticMesh(), StaticMeshComp, LODIndex), LightmapUVChannel, ColorBuffer, MaterialOrderOverride, &ToRawPtrTArrayUnsafe(StaticMeshComp->OverrideMaterials)); } } else if (SkelMeshComp && SkelMeshComp->GetSkeletalMeshAsset()) { ExportSkeletalMeshComponent(SkelMeshComp, *SkelMeshComp->GetName(), ExportNode, NodeNameAdapter, bSaveAnimSeq); } // If this actor has attached actors, don't allow a camera component to determine the node attributes because that would alter the transform else if (Component->IsA(UCameraComponent::StaticClass()) && !bHasAttachedActors) { FbxCamera* Camera = FbxCamera::Create(Scene, TCHAR_TO_UTF8(*Component->GetName())); FillFbxCameraAttribute(ActorNode, Camera, Cast(Component)); ExportNode->SetNodeAttribute(Camera); } else if (Component->IsA(ULightComponent::StaticClass()) && !bHasAttachedActors) { FbxLight* Light = FbxLight::Create(Scene, TCHAR_TO_UTF8(*Component->GetName())); FillFbxLightAttribute(Light, ActorNode, Cast(Component)); ExportNode->SetNodeAttribute(Light); } else if (ChildActorComp && ChildActorComp->GetChildActor()) { FbxNode* ChildActorNode = ExportActor(ChildActorComp->GetChildActor(), true, NodeNameAdapter, bSaveAnimSeq); FbxActors.Add(ChildActorComp->GetChildActor(), ChildActorNode); NodeNameAdapter.AddFbxNode(ChildActorComp->GetChildActor(), ChildActorNode); } } } } return ActorNode; } void ConvertInterpToFBX(uint8 UnrealInterpMode, FbxAnimCurveDef::EInterpolationType& Interpolation, FbxAnimCurveDef::ETangentMode& Tangent) { switch(UnrealInterpMode) { case CIM_Linear: Interpolation = FbxAnimCurveDef::eInterpolationLinear; Tangent = FbxAnimCurveDef::eTangentUser; break; case CIM_CurveAuto: Interpolation = FbxAnimCurveDef::eInterpolationCubic; Tangent = FbxAnimCurveDef::eTangentAuto; break; case CIM_Constant: Interpolation = FbxAnimCurveDef::eInterpolationConstant; Tangent = (FbxAnimCurveDef::ETangentMode)FbxAnimCurveDef::eConstantStandard; break; case CIM_CurveUser: Interpolation = FbxAnimCurveDef::eInterpolationCubic; Tangent = FbxAnimCurveDef::eTangentUser; break; case CIM_CurveBreak: Interpolation = FbxAnimCurveDef::eInterpolationCubic; Tangent = (FbxAnimCurveDef::ETangentMode) FbxAnimCurveDef::eTangentBreak; break; case CIM_CurveAutoClamped: Interpolation = FbxAnimCurveDef::eInterpolationCubic; Tangent = (FbxAnimCurveDef::ETangentMode) (FbxAnimCurveDef::eTangentAuto | FbxAnimCurveDef::eTangentGenericClamp); break; case CIM_Unknown: // ??? Interpolation = FbxAnimCurveDef::eInterpolationConstant; Tangent = FbxAnimCurveDef::eTangentAuto; break; } } // float-float comparison that allows for a certain error in the floating point values // due to floating-point operations never being exact. static bool IsEquivalent(float a, float b, float Tolerance = KINDA_SMALL_NUMBER) { return (a - b) > -Tolerance && (a - b) < Tolerance; } void RichCurveInterpolationToFbxInterpolation(ERichCurveInterpMode InInterpolation, ERichCurveTangentMode InTangentMode, ERichCurveTangentWeightMode InTangentWeightMode, FbxAnimCurveDef::EInterpolationType &OutInterpolation, FbxAnimCurveDef::ETangentMode &OutTangentMode, FbxAnimCurveDef::EWeightedMode &OutTangentWeightMode) { if (InInterpolation == ERichCurveInterpMode::RCIM_Cubic) { OutInterpolation = FbxAnimCurveDef::eInterpolationCubic; OutTangentMode = FbxAnimCurveDef::eTangentUser; // Always set tangent on the fbx key, so OutTangentMode should explicitly be eTangentUser unless Break. if (InTangentMode == RCTM_Break) { OutTangentMode = FbxAnimCurveDef::eTangentBreak; } switch (InTangentWeightMode) { case ERichCurveTangentWeightMode::RCTWM_WeightedBoth: OutTangentWeightMode = FbxAnimCurveDef::eWeightedAll; break; case ERichCurveTangentWeightMode::RCTWM_WeightedArrive: OutTangentWeightMode = FbxAnimCurveDef::eWeightedNextLeft; break; case ERichCurveTangentWeightMode::RCTWM_WeightedLeave: OutTangentWeightMode = FbxAnimCurveDef::eWeightedRight; break; case ERichCurveTangentWeightMode::RCTWM_WeightedNone: default: OutTangentWeightMode = FbxAnimCurveDef::eWeightedNone; break; }; } else if (InInterpolation == ERichCurveInterpMode::RCIM_Linear) { OutInterpolation = FbxAnimCurveDef::eInterpolationLinear; OutTangentMode = FbxAnimCurveDef::eTangentUser; OutTangentWeightMode = FbxAnimCurveDef::eWeightedNone; } else if (InInterpolation == ERichCurveInterpMode::RCIM_Constant) { OutInterpolation = FbxAnimCurveDef::eInterpolationConstant; OutTangentMode = (FbxAnimCurveDef::ETangentMode)FbxAnimCurveDef::eConstantStandard; OutTangentWeightMode = FbxAnimCurveDef::eWeightedNone; } else { OutInterpolation = FbxAnimCurveDef::eInterpolationCubic; OutTangentMode = FbxAnimCurveDef::eTangentUser; OutTangentWeightMode = FbxAnimCurveDef::eWeightedNone; } } template void FFbxExporter::ExportBezierChannelToFbxCurveBaked(FbxAnimCurve& InFbxCurve, const ChannelType& InChannel, FFrameRate TickResolution, const UMovieSceneTrack* Track, ERichCurveValueMode ValueMode, bool bNegative, const FMovieSceneSequenceTransform& RootToLocalTransform) { using ChannelValueType = typename ChannelType::ChannelValueType; using CurveValueType = typename ChannelType::CurveValueType; const float NegateFactor = bNegative ? -1.f : 1.f; const float kOneThird = 1.f / 3.f; InFbxCurve.KeyModifyBegin(); FMovieSceneInverseSequenceTransform SequenceToRootTransform = RootToLocalTransform.Inverse(); FFrameRate DisplayRate = Track->GetTypedOuter()->GetDisplayRate(); TRange PlaybackRange = Track->GetTypedOuter()->GetPlaybackRange(); int32 LocalStartFrame = FFrameRate::TransformTime(FFrameTime(UE::MovieScene::DiscreteInclusiveLower(PlaybackRange)), TickResolution, DisplayRate).RoundToFrame().Value; int32 AnimationLength = FFrameRate::TransformTime(FFrameTime(FFrameNumber(UE::MovieScene::DiscreteSize(PlaybackRange))), TickResolution, DisplayRate).RoundToFrame().Value; for (int32 FrameCount = 0; FrameCount <= AnimationLength; ++FrameCount) { int32 LocalFrame = LocalStartFrame + FrameCount; FFrameTime LocalTime = FFrameRate::TransformTime(FFrameTime(LocalFrame), DisplayRate, TickResolution); float Value; InChannel.Evaluate(LocalTime, Value); FbxTime FbxTime; if (GetExportOptions()->bExportLocalTime) { FbxTime.SetSecondDouble(DisplayRate.AsSeconds(LocalFrame)); } // @todo: This code does not handle the root sequence having a different tick resolution than the local space, but we do // not have that information here so we have to just assume they match. This should be improved so that we have // all the information we need to do the right thing else if (TOptional GlobalKeyTime = SequenceToRootTransform.TryTransformTime(LocalTime)) { FbxTime.SetSecondDouble(TickResolution.AsSeconds(GlobalKeyTime.GetValue())); } else { // Doesn't map to a root time continue; } InFbxCurve.KeySet(InFbxCurve.KeyAdd(FbxTime), FbxTime, Value, FbxAnimCurveDef::eInterpolationLinear); } InFbxCurve.KeyModifyEnd(); } template void FFbxExporter::ExportBezierChannelToFbxCurve(FbxAnimCurve& InFbxCurve, const ChannelType& InChannel, FFrameRate TickResolution, ERichCurveValueMode ValueMode, bool bNegative, const FMovieSceneSequenceTransform& RootToLocalTransform) { using ChannelValueType = typename ChannelType::ChannelValueType; using CurveValueType = typename ChannelType::CurveValueType; const float NegateFactor = bNegative ? -1.f : 1.f; const float kOneThird = 1.f / 3.f; InFbxCurve.KeyModifyBegin(); FMovieSceneInverseSequenceTransform SequenceToRootTransform = RootToLocalTransform.Inverse(); TArrayView Times = InChannel.GetTimes(); TArrayView Values = InChannel.GetValues(); for (int32 Index = 0; Index < Times.Num(); ++Index) { const FFrameNumber KeyTime = Times[Index]; const ChannelValueType KeyValue = Values[Index]; const CurveValueType Value = (ValueMode == ERichCurveValueMode::Fov ? DefaultCamera->ComputeFocalLength( KeyValue.Value ) : KeyValue.Value) * NegateFactor; FbxTime FbxTime; FbxAnimCurveKey FbxKey; if (GetExportOptions()->bExportLocalTime) { FbxTime.SetSecondDouble(TickResolution.AsSeconds(KeyTime)); } // @todo: This code does not handle the root sequence having a different tick resolution than the local space, but we do // not have that information here so we have to just assume they match. This should be improved so that we have // all the information we need to do the right thing else if (TOptional GlobalKeyTime = SequenceToRootTransform.TryTransformTime(KeyTime)) { FbxTime.SetSecondDouble(TickResolution.AsSeconds(GlobalKeyTime.GetValue())); } else { // Doesn't map to a root time continue; } const int FbxKeyIndex = InFbxCurve.KeyAdd(FbxTime); FbxAnimCurveDef::EInterpolationType Interpolation = FbxAnimCurveDef::eInterpolationCubic; FbxAnimCurveDef::ETangentMode Tangent = FbxAnimCurveDef::eTangentAuto; FbxAnimCurveDef::EWeightedMode WeightedMode = FbxAnimCurveDef::eWeightedNone; RichCurveInterpolationToFbxInterpolation(KeyValue.InterpMode, KeyValue.TangentMode, KeyValue.Tangent.TangentWeightMode, Interpolation, Tangent, WeightedMode); if (Interpolation == FbxAnimCurveDef::eInterpolationCubic) { if (Index < Times.Num() - 1) { float LeaveTangentWeight = kOneThird; float NextArriveTangentWeight = kOneThird; const double NextTime = Times[Index + 1] / TickResolution; const float TimeDiff = static_cast(NextTime - FbxTime.GetSecondDouble()); float LeaveTangent = KeyValue.Tangent.LeaveTangent * TickResolution.AsDecimal(); float NextArriveTangent = Values[Index + 1].Tangent.ArriveTangent * TickResolution.AsDecimal(); //Need to convert UE tangent weight which is the length of the hypotenuse to FBX normalized X(time) weight if (WeightedMode == FbxAnimCurveDef::eWeightedAll || WeightedMode == FbxAnimCurveDef::eWeightedRight) { const CurveValueType XVal = FMath::Sqrt((KeyValue.Tangent.LeaveTangentWeight * KeyValue.Tangent.LeaveTangentWeight) / (1.0f + LeaveTangent * LeaveTangent)); LeaveTangentWeight = XVal / TimeDiff; } //make sure next tangent is weighted else use default weight if ((Values[Index + 1].Tangent.TangentWeightMode == ERichCurveTangentWeightMode::RCTWM_WeightedBoth || Values[Index + 1].Tangent.TangentWeightMode == ERichCurveTangentWeightMode::RCTWM_WeightedArrive) ) { const CurveValueType XVal = FMath::Sqrt((Values[Index + 1].Tangent.ArriveTangentWeight * Values[Index + 1].Tangent.ArriveTangentWeight) / (1.0f + NextArriveTangent * NextArriveTangent)); NextArriveTangentWeight = XVal / TimeDiff; } if (bNegative) { LeaveTangent = -LeaveTangent; NextArriveTangent = -NextArriveTangent; } InFbxCurve.KeySet(FbxKeyIndex, FbxTime, Value, Interpolation, Tangent, LeaveTangent, NextArriveTangent, WeightedMode, LeaveTangentWeight, NextArriveTangentWeight ); } else { InFbxCurve.KeySet(FbxKeyIndex, FbxTime, Value, Interpolation, Tangent, 0.f, 0.f, WeightedMode); } } else { InFbxCurve.KeySet(FbxKeyIndex, FbxTime, Value, Interpolation, Tangent); } } InFbxCurve.KeyModifyEnd(); } void FFbxExporter::ExportChannelToFbxCurve(FbxAnimCurve& InFbxCurve, const FMovieSceneFloatChannel& InChannel, FFrameRate TickResolution, ERichCurveValueMode ValueMode, bool bNegative, const FMovieSceneSequenceTransform& RootToLocalTransform) { ExportBezierChannelToFbxCurve(InFbxCurve, InChannel, TickResolution, ValueMode, bNegative, RootToLocalTransform); } void FFbxExporter::ExportChannelToFbxCurve(FbxAnimCurve& InFbxCurve, const FMovieSceneDoubleChannel& InChannel, FFrameRate TickResolution, ERichCurveValueMode ValueMode, bool bNegative, const FMovieSceneSequenceTransform& RootToLocalTransform) { ExportBezierChannelToFbxCurve(InFbxCurve, InChannel, TickResolution, ValueMode, bNegative, RootToLocalTransform); } void FFbxExporter::ExportChannelToFbxCurve(FbxAnimCurve& InFbxCurve, const FMovieSceneIntegerChannel& InChannel, FFrameRate TickResolution, const FMovieSceneSequenceTransform& RootToLocalTransform) { ExportConstantChannelToFbxCurve(InFbxCurve, InChannel, TickResolution, RootToLocalTransform); } void FFbxExporter::ExportChannelToFbxCurve(FbxAnimCurve& InFbxCurve, const FMovieSceneBoolChannel& InChannel, FFrameRate TickResolution, const FMovieSceneSequenceTransform& RootToLocalTransform) { ExportConstantChannelToFbxCurve(InFbxCurve, InChannel, TickResolution, RootToLocalTransform); } void FFbxExporter::ExportChannelToFbxCurve(FbxAnimCurve& InFbxCurve, const FMovieSceneByteChannel& InChannel, FFrameRate TickResolution, const FMovieSceneSequenceTransform& RootToLocalTransform) { ExportConstantChannelToFbxCurve(InFbxCurve, InChannel, TickResolution, RootToLocalTransform); } template void FFbxExporter::ExportConstantChannelToFbxCurve(FbxAnimCurve& InFbxCurve, const ChannelType& InChannel, FFrameRate TickResolution, const FMovieSceneSequenceTransform& RootToLocalTransform) { InFbxCurve.KeyModifyBegin(); const TArrayView Times = InChannel.GetTimes(); const TArrayView Values = InChannel.GetValues(); FMovieSceneInverseSequenceTransform SequenceToRootTransform = RootToLocalTransform.Inverse(); for (int32 Index = 0; Index < Times.Num(); ++Index) { const FFrameNumber KeyTime = Times[Index]; const T KeyValue = Values[Index]; FbxTime FbxTime; if (GetExportOptions()->bExportLocalTime) { FbxTime.SetSecondDouble(TickResolution.AsSeconds(KeyTime)); } // @todo: This code does not handle the root sequence having a different tick resolution than the local space, but we do // not have that information here so we have to just assume they match. This should be improved so that we have // all the information we need to do the right thing else if (TOptional GlobalKeyTime = SequenceToRootTransform.TryTransformTime(KeyTime)) { FbxTime.SetSecondDouble(TickResolution.AsSeconds(GlobalKeyTime.GetValue())); } else { // Doesn't map to a root time continue; } const int FbxKeyIndex = InFbxCurve.KeyAdd(FbxTime); InFbxCurve.KeySet(FbxKeyIndex, FbxTime, KeyValue, FbxAnimCurveDef::eInterpolationConstant); InFbxCurve.KeySetConstantMode(FbxKeyIndex, FbxAnimCurveDef::EConstantMode::eConstantStandard); } InFbxCurve.KeyModifyEnd(); } void FFbxExporter::ExportLevelSequenceColorTrack(FbxNode* FbxNode, UMovieSceneColorTrack& ColorTrack, UObject* BoundObject, const TRange& InPlaybackRange, const FMovieSceneSequenceTransform& RootToLocalTransform) { UMovieSceneColorSection* ColorSection = ColorTrack.GetAllSections().Num() > 0 ? Cast(ColorTrack.GetAllSections()[0]) : nullptr; if(!ColorSection) { return; } if(!FbxNode) { FbxNode = CreateNode(ColorTrack.GetDisplayName().ToString()); } FbxLight* FbxLight = FbxNode->GetLight(); const FString PropertyName = ColorTrack.GetTrackName().ToString(); FbxProperty Property; if(PropertyName == TEXT("LightColor") && FbxLight) { Property = FbxLight->Color; } if(Property == 0) { CreateAnimatableUserProperty(FbxNode, FbxDouble3{ 0 }, TCHAR_TO_UTF8(*PropertyName), TCHAR_TO_UTF8(*PropertyName), FbxDouble3DT); Property = FbxNode->FindProperty(TCHAR_TO_UTF8(*PropertyName)); } if(Property == 0) { return; } FbxAnimLayer* BaseLayer = AnimStack->GetMember(0); FbxAnimCurve* CurveRed = Property.GetCurve(BaseLayer, FBXSDK_CURVENODE_COLOR_RED, true); FbxAnimCurve* CurveGreen = Property.GetCurve(BaseLayer, FBXSDK_CURVENODE_COLOR_GREEN, true); FbxAnimCurve* CurveBlue = Property.GetCurve(BaseLayer, FBXSDK_CURVENODE_COLOR_BLUE, true); FFrameRate TickResolution = ColorTrack.GetTypedOuter()->GetTickResolution(); ExportChannelToFbxCurve(*CurveRed, ColorSection->GetRedChannel(), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); ExportChannelToFbxCurve(*CurveGreen, ColorSection->GetGreenChannel(), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); ExportChannelToFbxCurve(*CurveBlue, ColorSection->GetBlueChannel(), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); } void FFbxExporter::ExportLevelSequenceVectorTrack(FbxNode* FbxNode, UMovieSceneDoubleVectorTrack& VectorTrack, UObject* BoundObject, const TRange& InPlaybackRange, const FMovieSceneSequenceTransform& RootToLocalTransform) { UMovieSceneDoubleVectorSection* VectorSection = VectorTrack.GetAllSections().Num() > 0 ? Cast(VectorTrack.GetAllSections()[0]) : nullptr; if(!VectorSection) { return; } if(!FbxNode) { FbxNode = CreateNode(VectorTrack.GetDisplayName().ToString()); } const FString PropertyName = VectorTrack.GetTrackName().ToString(); //VectorSections supports up to 2-4 channels const int32 NumChannelsUsed = VectorSection->GetChannelsUsed(); CreateAnimatableUserProperty(FbxNode, NumChannelsUsed == 2 ? FbxDouble2{ 0 } : NumChannelsUsed == 3 ? FbxDouble3{ 0 } : FbxDouble4{ 0 }, TCHAR_TO_UTF8(*PropertyName), TCHAR_TO_UTF8(*PropertyName), NumChannelsUsed == 2 ? FbxDouble2DT : NumChannelsUsed == 3 ? FbxDouble3DT : FbxDouble4DT); FbxProperty Property = FbxNode->FindProperty(TCHAR_TO_UTF8(*PropertyName)); if(Property == 0) { return; } const char* CurvesNames[] = { "X", "Y", "Z", "W" }; FbxAnimLayer* BaseLayer = AnimStack->GetMember(0); FFrameRate TickResolution = VectorTrack.GetTypedOuter()->GetTickResolution(); for(int32 Index = 0; Index < NumChannelsUsed; ++Index) { FbxAnimCurve* Curve = Property.GetCurve(BaseLayer, CurvesNames[Index], true); ExportChannelToFbxCurve(*Curve, VectorSection->GetChannel(Index), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); } } void FFbxExporter::ExportLevelSequence3DTransformTrack(FbxNode* FbxNode, IMovieScenePlayer* MovieScenePlayer, FMovieSceneSequenceIDRef InSequenceID, UMovieScene3DTransformTrack& TransformTrack, UObject* BoundObject, const TRange& InPlaybackRange, const FMovieSceneSequenceTransform& RootToLocalTransform) { // TODO: Support more than one section? UMovieScene3DTransformSection* TransformSection = TransformTrack.GetAllSections().Num() > 0 ? Cast(TransformTrack.GetAllSections()[0]) : nullptr; if (TransformSection == nullptr) { return; } FMovieSceneInverseSequenceTransform SequenceToRootTransform = RootToLocalTransform.Inverse(); FbxAnimLayer* BaseLayer = AnimStack->GetMember(0); AActor* BoundActor = Cast(BoundObject); USceneComponent* BoundComponent = Cast(BoundObject); const bool bIsCameraActor = BoundActor ? BoundActor->IsA(ACameraActor::StaticClass()) : BoundComponent ? BoundComponent->IsA(UCameraComponent::StaticClass()) : false; const bool bIsLightActor = BoundActor ? BoundActor->IsA(ALight::StaticClass()) : BoundComponent ? BoundComponent->IsA(ULightComponent::StaticClass()) : false; // If bake rotations is set, and actor is a camera or light actor, only bake rotation channel const bool bBakeRotations = (bIsCameraActor || bIsLightActor) && (static_cast(GetExportOptions()->BakeCameraAndLightAnimation) & static_cast(EMovieSceneBakeType::BakeTransforms)); if (!FbxNode) { FbxNode = CreateNode(TransformTrack.GetDisplayName().ToString()); } FFrameRate TickResolution = TransformTrack.GetTypedOuter()->GetTickResolution(); FFrameRate DisplayRate = TransformTrack.GetTypedOuter()->GetDisplayRate(); FbxAnimCurveNode* TranslationNode = FbxNode->LclTranslation.GetCurveNode(BaseLayer, true); FbxAnimCurveNode* RotationNode = FbxNode->LclRotation.GetCurveNode(BaseLayer, true); FbxAnimCurveNode* ScaleNode = FbxNode->LclScaling.GetCurveNode(BaseLayer, true); FbxAnimCurve* FbxCurveTransX = FbxNode->LclTranslation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_X, true); FbxAnimCurve* FbxCurveTransY = FbxNode->LclTranslation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); FbxAnimCurve* FbxCurveTransZ = FbxNode->LclTranslation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); FbxAnimCurve* FbxCurveRotX = FbxNode->LclRotation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_X, true); FbxAnimCurve* FbxCurveRotY = FbxNode->LclRotation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); FbxAnimCurve* FbxCurveRotZ = FbxNode->LclRotation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); FbxAnimCurve* FbxCurveScaleX = FbxNode->LclScaling.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_X, true); FbxAnimCurve* FbxCurveScaleY = FbxNode->LclScaling.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); FbxAnimCurve* FbxCurveScaleZ = FbxNode->LclScaling.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); FMovieSceneChannelProxy& SectionChannelProxy = TransformSection->GetChannelProxy(); TMovieSceneChannelHandle DoubleChannels[] = { SectionChannelProxy.GetChannelByName("Location.X"), SectionChannelProxy.GetChannelByName("Location.Y"), SectionChannelProxy.GetChannelByName("Location.Z"), SectionChannelProxy.GetChannelByName("Rotation.X"), SectionChannelProxy.GetChannelByName("Rotation.Y"), SectionChannelProxy.GetChannelByName("Rotation.Z"), SectionChannelProxy.GetChannelByName("Scale.X"), SectionChannelProxy.GetChannelByName("Scale.Y"), SectionChannelProxy.GetChannelByName("Scale.Z") }; // Translation if (DoubleChannels[0].Get()) { ExportChannelToFbxCurve(*FbxCurveTransX, *DoubleChannels[0].Get(), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); } if (DoubleChannels[1].Get()) { ExportChannelToFbxCurve(*FbxCurveTransY, *DoubleChannels[1].Get(), TickResolution, ERichCurveValueMode::Default, true, RootToLocalTransform); } if (DoubleChannels[2].Get()) { ExportChannelToFbxCurve(*FbxCurveTransZ, *DoubleChannels[2].Get(), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); } // Scale - don't generate scale keys for cameras if (!bIsCameraActor) { if (DoubleChannels[6].Get()) { ExportChannelToFbxCurve(*FbxCurveScaleX, *DoubleChannels[6].Get(), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); } if (DoubleChannels[7].Get()) { ExportChannelToFbxCurve(*FbxCurveScaleY, *DoubleChannels[7].Get(), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); } if (DoubleChannels[8].Get()) { ExportChannelToFbxCurve(*FbxCurveScaleZ, *DoubleChannels[8].Get(), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); } } // Rotation - bake rotation for cameras and lights if (!bBakeRotations) { if (DoubleChannels[3].Get()) { ExportChannelToFbxCurve(*FbxCurveRotX, *DoubleChannels[3].Get(), TickResolution, ERichCurveValueMode::Default, false, RootToLocalTransform); } if (DoubleChannels[4].Get()) { ExportChannelToFbxCurve(*FbxCurveRotY, *DoubleChannels[4].Get(), TickResolution, ERichCurveValueMode::Default, true, RootToLocalTransform); } if (DoubleChannels[5].Get()) { ExportChannelToFbxCurve(*FbxCurveRotZ, *DoubleChannels[5].Get(), TickResolution, ERichCurveValueMode::Default, true, RootToLocalTransform); } } else { FTransform RotationDirectionConvert; if (bIsCameraActor) { FRotator Rotator = FFbxDataConverter::GetCameraRotation().GetInverse(); RotationDirectionConvert = FTransform(Rotator); } else if (bIsLightActor) { FRotator Rotator = FFbxDataConverter::GetLightRotation().GetInverse(); RotationDirectionConvert = FTransform(Rotator); } FbxCurveRotX->KeyModifyBegin(); FbxCurveRotY->KeyModifyBegin(); FbxCurveRotZ->KeyModifyBegin(); int32 LocalStartFrame = FFrameRate::TransformTime(FFrameTime(UE::MovieScene::DiscreteInclusiveLower(InPlaybackRange)), TickResolution, DisplayRate).RoundToFrame().Value; int32 AnimationLength = FFrameRate::TransformTime(FFrameTime(FFrameNumber(UE::MovieScene::DiscreteSize(InPlaybackRange))), TickResolution, DisplayRate).RoundToFrame().Value; for (int32 FrameCount = 0; FrameCount <= AnimationLength; ++FrameCount) { int32 LocalFrame = LocalStartFrame + FrameCount; FFrameTime LocalTime = FFrameRate::TransformTime(FFrameTime(LocalFrame), DisplayRate, TickResolution); FVector3f Trans = FVector3f::ZeroVector; if (DoubleChannels[0].Get()) { DoubleChannels[0].Get()->Evaluate(LocalTime, Trans.X); } if (DoubleChannels[1].Get()) { DoubleChannels[1].Get()->Evaluate(LocalTime, Trans.Y); } if (DoubleChannels[2].Get()) { DoubleChannels[2].Get()->Evaluate(LocalTime, Trans.Z); } FRotator Rotator; if (DoubleChannels[3].Get()) { DoubleChannels[3].Get()->Evaluate(LocalTime, Rotator.Roll); } if (DoubleChannels[4].Get()) { DoubleChannels[4].Get()->Evaluate(LocalTime, Rotator.Pitch); } if (DoubleChannels[5].Get()) { DoubleChannels[5].Get()->Evaluate(LocalTime, Rotator.Yaw); } FVector3f Scale; if (DoubleChannels[6].Get()) { DoubleChannels[6].Get()->Evaluate(LocalTime, Scale.X); } if (DoubleChannels[7].Get()) { DoubleChannels[7].Get()->Evaluate(LocalTime, Scale.Y); } if (DoubleChannels[8].Get()) { DoubleChannels[8].Get()->Evaluate(LocalTime, Scale.Z); } FTransform RelativeTransform; RelativeTransform.SetTranslation((FVector)Trans); RelativeTransform.SetRotation(Rotator.Quaternion()); RelativeTransform.SetScale3D((FVector)Scale); RelativeTransform = RotationDirectionConvert * RelativeTransform; FbxVector4 KeyTrans = Converter.ConvertToFbxPos(RelativeTransform.GetTranslation()); FbxVector4 KeyRot = Converter.ConvertToFbxRot(RelativeTransform.GetRotation().Euler()); FbxVector4 KeyScale = Converter.ConvertToFbxScale(RelativeTransform.GetScale3D()); FbxTime FbxTime; if (GetExportOptions()->bExportLocalTime) { FbxTime.SetSecondDouble(DisplayRate.AsSeconds(LocalFrame)); } // @todo: This code does not handle the root sequence having a different tick resolution than the local space, but we do // not have that information here so we have to just assume they match. This should be improved so that we have // all the information we need to do the right thing else if (TOptional GlobalKeyTime = SequenceToRootTransform.TryTransformTime(LocalTime)) { FbxTime.SetSecondDouble(TickResolution.AsSeconds(GlobalKeyTime.GetValue())); } else { // Doesn't map to a root time continue; } FbxCurveRotX->KeySet(FbxCurveRotX->KeyAdd(FbxTime), FbxTime, KeyRot[0]); FbxCurveRotY->KeySet(FbxCurveRotY->KeyAdd(FbxTime), FbxTime, KeyRot[1]); FbxCurveRotZ->KeySet(FbxCurveRotZ->KeyAdd(FbxTime), FbxTime, KeyRot[2]); } FbxCurveRotX->KeyModifyEnd(); FbxCurveRotY->KeyModifyEnd(); FbxCurveRotZ->KeyModifyEnd(); } } void FFbxExporter::ExportLevelSequenceBaked3DTransformTrack(IAnimTrackAdapter& AnimTrackAdapter, FbxNode* FbxNode, IMovieScenePlayer* MovieScenePlayer, FMovieSceneSequenceIDRef InSequenceID, TArray > TransformTracks, UObject* BoundObject, const TRange& InPlaybackRange, const FMovieSceneSequenceTransform& RootToLocalTransform) { using namespace UE::MovieScene; UMovieScene3DTransformTrack* TransformTrack = nullptr; int32 NumSections = 0; for (TWeakObjectPtr WeakTransformTrack : TransformTracks) { if (WeakTransformTrack.IsValid()) { TransformTrack = WeakTransformTrack.Get(); NumSections += TransformTrack->GetAllSections().Num(); } } if (NumSections <= 0 || !TransformTrack) { return; } FbxAnimLayer* BaseLayer = AnimStack->GetMember(0); AActor* BoundActor = Cast(BoundObject); USceneComponent* BoundComponent = Cast(BoundObject); const bool bIsCameraActor = BoundActor ? BoundActor->IsA(ACameraActor::StaticClass()) : BoundComponent ? BoundComponent->IsA(UCameraComponent::StaticClass()) : false; const bool bIsLightActor = BoundActor ? BoundActor->IsA(ALight::StaticClass()) : BoundComponent ? BoundComponent->IsA(ULightComponent::StaticClass()) : false; const bool bBakeRotations = bIsCameraActor || bIsLightActor; USceneComponent* InterrogatedComponent = nullptr; if (BoundComponent) { InterrogatedComponent = BoundComponent; } else if (BoundActor) { InterrogatedComponent = BoundActor->GetRootComponent(); } if (bIsCameraActor) { if (InterrogatedComponent && InterrogatedComponent->IsA()) { // all set } else if (BoundActor && BoundActor->IsA(ACameraActor::StaticClass())) { ACameraActor* CameraActor = Cast(BoundActor); InterrogatedComponent = CameraActor->GetCameraComponent(); } } if (!InterrogatedComponent) { UE_LOG(LogFbx, Warning, TEXT("Export transform track for %s failed because could not find suitable scene component"), *BoundObject->GetName()); return; } if (!FbxNode) { FbxNode = CreateNode(TransformTrack->GetDisplayName().ToString()); } FFrameRate TickResolution = TransformTrack->GetTypedOuter()->GetTickResolution(); FFrameRate DisplayRate = TransformTrack->GetTypedOuter()->GetDisplayRate(); FbxAnimCurveNode* TranslationNode = FbxNode->LclTranslation.GetCurveNode(BaseLayer, true); FbxAnimCurveNode* RotationNode = FbxNode->LclRotation.GetCurveNode(BaseLayer, true); FbxAnimCurveNode* ScaleNode = FbxNode->LclScaling.GetCurveNode(BaseLayer, true); FbxAnimCurve* FbxCurveTransX = FbxNode->LclTranslation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_X, true); FbxAnimCurve* FbxCurveTransY = FbxNode->LclTranslation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); FbxAnimCurve* FbxCurveTransZ = FbxNode->LclTranslation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); FbxAnimCurve* FbxCurveRotX = FbxNode->LclRotation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_X, true); FbxAnimCurve* FbxCurveRotY = FbxNode->LclRotation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); FbxAnimCurve* FbxCurveRotZ = FbxNode->LclRotation.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); FbxAnimCurve* FbxCurveScaleX = FbxNode->LclScaling.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_X, true); FbxAnimCurve* FbxCurveScaleY = FbxNode->LclScaling.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); FbxAnimCurve* FbxCurveScaleZ = FbxNode->LclScaling.GetCurve(BaseLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); FTransform RotationDirectionConvert; if (bIsCameraActor) { FRotator Rotator = FFbxDataConverter::GetCameraRotation().GetInverse(); RotationDirectionConvert = FTransform(Rotator); } else if (bIsLightActor) { FRotator Rotator = FFbxDataConverter::GetLightRotation().GetInverse(); RotationDirectionConvert = FTransform(Rotator); } FbxCurveTransX->KeyModifyBegin(); FbxCurveTransY->KeyModifyBegin(); FbxCurveTransZ->KeyModifyBegin(); FbxCurveRotX->KeyModifyBegin(); FbxCurveRotY->KeyModifyBegin(); FbxCurveRotZ->KeyModifyBegin(); if (!bIsCameraActor) { FbxCurveScaleX->KeyModifyBegin(); FbxCurveScaleY->KeyModifyBegin(); FbxCurveScaleZ->KeyModifyBegin(); } FMovieSceneInverseSequenceTransform LocalToRootTransform = RootToLocalTransform.Inverse(); TArray RelativeTransforms; int32 LocalStartFrame = FFrameRate::TransformTime(FFrameTime(DiscreteInclusiveLower(InPlaybackRange)), TickResolution, DisplayRate).RoundToFrame().Value; int32 AnimationLength = FFrameRate::TransformTime(FFrameTime(FFrameNumber(DiscreteSize(InPlaybackRange))), TickResolution, DisplayRate).RoundToFrame().Value + 1; // Add one so that we export a key for the end frame const double SampleRate = 1.0/DisplayRate.AsDecimal(); for (int32 FrameNumber = LocalStartFrame; FrameNumber < LocalStartFrame + AnimationLength; ++FrameNumber) { const FFrameTime FrameTime = FFrameRate::TransformTime(FrameNumber, DisplayRate, TickResolution); // This will call UpdateSkelPose on the skeletal mesh component to move bones based on animations in the matinee group AnimTrackAdapter.UpdateAnimation(FrameNumber); USceneComponent* Child = InterrogatedComponent; while (Child) { if (USkeletalMeshComponent* ChildSkeletalMeshComponent = Cast(Child)) { ChildSkeletalMeshComponent->TickAnimation(SampleRate, false); ChildSkeletalMeshComponent->RefreshBoneTransforms(); ChildSkeletalMeshComponent->RefreshFollowerComponents(); ChildSkeletalMeshComponent->UpdateComponentToWorld(); ChildSkeletalMeshComponent->FinalizeBoneTransform(); ChildSkeletalMeshComponent->MarkRenderTransformDirty(); ChildSkeletalMeshComponent->MarkRenderDynamicDataDirty(); } if (Child->GetOwner()) { Child->GetOwner()->Tick(SampleRate); } Child = Child->GetAttachParent(); } // Get the relative transform for this component. This can be complicated because we don't export scene components in the hierarchy. For example, // ParentActor // ChildActor // SceneComponent // CameraComponent // When exporting CameraComponent, we don't export SceneComponent, so we need to get CameraComponent's world transform relative to ParentActor // AActor* InterrogatedOwner = InterrogatedComponent->GetOwner(); AActor* AttachParentActor = InterrogatedOwner ? InterrogatedOwner->GetAttachParentActor() : nullptr; FTransform ParentTransform = AttachParentActor ? AttachParentActor->GetTransform() : FTransform::Identity; FTransform RelativeTransform = InterrogatedComponent->GetComponentToWorld().GetRelativeTransform(ParentTransform); RelativeTransforms.Add(RelativeTransform); } // Reset AnimTrackAdapter.UpdateAnimation(LocalStartFrame); for (int32 TransformIndex = 0; TransformIndex < RelativeTransforms.Num(); ++TransformIndex) { FTransform RelativeTransform = RotationDirectionConvert * RelativeTransforms[TransformIndex]; FbxVector4 KeyTrans = Converter.ConvertToFbxPos(RelativeTransform.GetTranslation()); FbxVector4 KeyRot = Converter.ConvertToFbxRot(RelativeTransform.GetRotation().Euler()); FbxVector4 KeyScale = Converter.ConvertToFbxScale(RelativeTransform.GetScale3D()); const int32 CurrentFrame = LocalStartFrame + TransformIndex; const FFrameTime LocalTime = FFrameRate::TransformTime(CurrentFrame, DisplayRate, TickResolution); FbxTime FbxTime; if (GetExportOptions()->bExportLocalTime) { FbxTime.SetSecondDouble(DisplayRate.AsSeconds(CurrentFrame)); } // @todo: This code does not handle the root sequence having a different tick resolution than the local space, but we do // not have that information here so we have to just assume they match. This should be improved so that we have // all the information we need to do the right thing else if (TOptional GlobalKeyTime = LocalToRootTransform.TryTransformTime(LocalTime)) { FbxTime.SetSecondDouble(TickResolution.AsSeconds(GlobalKeyTime.GetValue())); } else { // Doesn't map to a root time continue; } FbxCurveTransX->KeySet(FbxCurveTransX->KeyAdd(FbxTime), FbxTime, KeyTrans[0]); FbxCurveTransY->KeySet(FbxCurveTransY->KeyAdd(FbxTime), FbxTime, KeyTrans[1]); FbxCurveTransZ->KeySet(FbxCurveTransZ->KeyAdd(FbxTime), FbxTime, KeyTrans[2]); FbxCurveRotX->KeySet(FbxCurveRotX->KeyAdd(FbxTime), FbxTime, KeyRot[0]); FbxCurveRotY->KeySet(FbxCurveRotY->KeyAdd(FbxTime), FbxTime, KeyRot[1]); FbxCurveRotZ->KeySet(FbxCurveRotZ->KeyAdd(FbxTime), FbxTime, KeyRot[2]); if (!bIsCameraActor) { FbxCurveScaleX->KeySet(FbxCurveScaleX->KeyAdd(FbxTime), FbxTime, KeyScale[0]); FbxCurveScaleY->KeySet(FbxCurveScaleY->KeyAdd(FbxTime), FbxTime, KeyScale[1]); FbxCurveScaleZ->KeySet(FbxCurveScaleZ->KeyAdd(FbxTime), FbxTime, KeyScale[2]); } } FbxCurveTransX->KeyModifyEnd(); FbxCurveTransY->KeyModifyEnd(); FbxCurveTransZ->KeyModifyEnd(); FbxCurveRotX->KeyModifyEnd(); FbxCurveRotY->KeyModifyEnd(); FbxCurveRotZ->KeyModifyEnd(); if (!bIsCameraActor) { FbxCurveScaleX->KeyModifyEnd(); FbxCurveScaleY->KeyModifyEnd(); FbxCurveScaleZ->KeyModifyEnd(); } } void FFbxExporter::ExportLevelSequenceTrackChannels( FbxNode* FbxNode, UMovieSceneTrack& Track, const TRange& InPlaybackRange, const FMovieSceneSequenceTransform& RootToLocalTransform, bool bBakeBezierCurves) { // TODO: Support more than one section? UMovieSceneSection* Section = Track.GetAllSections().Num() > 0 ? Track.GetAllSections()[0] : nullptr; if (!Section) { return; } if (!FbxNode) { FbxNode = CreateNode(Track.GetDisplayName().ToString()); } FbxCamera* FbxCamera = FbxNode->GetCamera(); FbxLight* FbxLight = FbxNode->GetLight(); FFrameRate TickResolution = Track.GetTypedOuter()->GetTickResolution(); const FName BoolChannelTypeName = FMovieSceneBoolChannel::StaticStruct()->GetFName(); const FName DoubleChannelTypeName = FMovieSceneDoubleChannel::StaticStruct()->GetFName(); const FName EnumChannelTypeName = FMovieSceneByteChannel::StaticStruct()->GetFName(); const FName FloatChannelTypeName = FMovieSceneFloatChannel::StaticStruct()->GetFName(); const FName IntegerChannelTypeName = FMovieSceneIntegerChannel::StaticStruct()->GetFName(); const FName StringChannelTypeName = FMovieSceneStringChannel::StaticStruct()->GetFName(); FMovieSceneChannelProxy& ChannelProxy = Section->GetChannelProxy(); for (const FMovieSceneChannelEntry& Entry : Section->GetChannelProxy().GetAllEntries()) { const FName ChannelTypeName = Entry.GetChannelTypeName(); if (ChannelTypeName != BoolChannelTypeName && ChannelTypeName != DoubleChannelTypeName && ChannelTypeName != EnumChannelTypeName && ChannelTypeName != FloatChannelTypeName && ChannelTypeName != IntegerChannelTypeName && ChannelTypeName != StringChannelTypeName) { continue; } TArrayView Channels = Entry.GetChannels(); TArrayView AllMetaData = Entry.GetMetaData(); for (int32 Index = 0; Index < Channels.Num(); ++Index) { FMovieSceneChannelHandle Channel = ChannelProxy.MakeHandle(ChannelTypeName, Index); FMovieSceneBoolChannel* BoolChannel = Entry.GetChannelTypeName() == BoolChannelTypeName ? Channel.Cast().Get() : nullptr; FMovieSceneDoubleChannel* DoubleChannel = Entry.GetChannelTypeName() == DoubleChannelTypeName ? Channel.Cast().Get() : nullptr; FMovieSceneByteChannel* EnumChannel = Entry.GetChannelTypeName() == EnumChannelTypeName ? Channel.Cast().Get() : nullptr; FMovieSceneFloatChannel* FloatChannel = Entry.GetChannelTypeName() == FloatChannelTypeName ? Channel.Cast().Get() : nullptr; FMovieSceneIntegerChannel* IntegerChannel = Entry.GetChannelTypeName() == IntegerChannelTypeName ? Channel.Cast().Get() : nullptr; FMovieSceneStringChannel* StringChannel = Entry.GetChannelTypeName() == StringChannelTypeName ? Channel.Cast().Get() : nullptr; if (!BoolChannel && !DoubleChannel && !EnumChannel && !FloatChannel && !IntegerChannel && !StringChannel) { continue; } const FMovieSceneChannelMetaData& MetaData = AllMetaData[Index]; FText Name = FText::FromName(MetaData.Name); FbxProperty Property; FString PropertyName = MetaData.Name.IsNone() ? Track.GetTrackName().ToString() : MetaData.Name.ToString(); bool IsFoV = false; // most properties are created as user property, only FOV of camera in FBX supports animation if (PropertyName == "Intensity" && FbxLight) { Property = FbxLight->Intensity; } else if (PropertyName == "FalloffExponent") { Property = FbxNode->FindProperty("UE_FalloffExponent", false); } else if (PropertyName == "AttenuationRadius") { Property = FbxNode->FindProperty("UE_Radius", false); } else if (PropertyName == "FieldOfView" && FbxCamera) { Property = FbxCamera->FieldOfView; IsFoV = true; } else if (PropertyName == "FOVAngle" && FbxCamera) { Property = FbxCamera->FocalLength; IsFoV = true; } else if (PropertyName == "CurrentFocalLength" && FbxCamera) { Property = FbxCamera->FocalLength; } else if (PropertyName == "AspectRatio") { Property = FbxNode->FindProperty("UE_AspectRatio", false); } else if (PropertyName == "MotionBlur_Amount") { Property = FbxNode->FindProperty("UE_MotionBlur_Amount", false); } else if ( PropertyName == "FocusSettings.ManualFocusDistance" && FbxCamera ) { Property = FbxCamera->FocusDistance; } else if(PropertyName == "bUseTemperature") { Property = FbxNode->FindProperty("UE_UseTemperature", false); } else if(PropertyName == "IntensityUnits") { Property = FbxNode->FindProperty("UE_IntensityUnits", false); } else if(PropertyName == "Filmback.SensorAspectRatio" && FbxCamera) { Property = FbxCamera->FilmAspectRatio; } else if(PropertyName == "Filmback.SensorHeight" && FbxCamera) { Property = FbxCamera->FilmHeight; } else if(PropertyName == "Filmback.SensorWidth" && FbxCamera) { Property = FbxCamera->FilmWidth; } if (Property == 0) { if(BoolChannel) { CreateAnimatableUserProperty(FbxNode, BoolChannel->GetDefault().Get(false), TCHAR_TO_UTF8(*PropertyName), TCHAR_TO_UTF8(*PropertyName), FbxBoolDT); } else if (DoubleChannel) { CreateAnimatableUserProperty(FbxNode, DoubleChannel->GetDefault().Get(MAX_flt), TCHAR_TO_UTF8(*PropertyName), TCHAR_TO_UTF8(*PropertyName)); } else if (EnumChannel) { CreateAnimatableUserProperty(FbxNode, EnumChannel->GetDefault().Get(0), TCHAR_TO_UTF8(*PropertyName), TCHAR_TO_UTF8(*PropertyName), FbxEnumDT); } else if (FloatChannel) { CreateAnimatableUserProperty(FbxNode, FloatChannel->GetDefault().Get(MAX_flt), TCHAR_TO_UTF8(*PropertyName), TCHAR_TO_UTF8(*PropertyName)); } else if (IntegerChannel) { CreateAnimatableUserProperty(FbxNode, IntegerChannel->GetDefault().Get(0), TCHAR_TO_UTF8(*PropertyName), TCHAR_TO_UTF8(*PropertyName), FbxIntDT); } else if (StringChannel) { FbxString FbxValueString(TCHAR_TO_UTF8(*StringChannel->GetDefault().Get(TEXT("")))); CreateAnimatableUserProperty(FbxNode, FbxValueString, TCHAR_TO_UTF8(*PropertyName), TCHAR_TO_UTF8(*PropertyName), FbxStringDT); } Property = FbxNode->FindProperty(TCHAR_TO_UTF8(*PropertyName), false); } if (Property == 0) { continue; } // Ensure that the property is animatable so that GetCurveNode succeeds Property.ModifyFlag(FbxPropertyFlags::eAnimatable, true); FbxAnimCurve* AnimCurve = FbxAnimCurve::Create(Scene, ""); FbxAnimCurveNode* CurveNode = Property.GetCurveNode(true); if (!CurveNode) { continue; } if (DoubleChannel) { CurveNode->SetChannelValue(0U, DoubleChannel->GetDefault().Get(MAX_dbl)); CurveNode->ConnectToChannel(AnimCurve, 0U); if (bBakeBezierCurves) { ExportBezierChannelToFbxCurveBaked(*AnimCurve, *DoubleChannel, TickResolution, &Track, IsFoV ? ERichCurveValueMode::Fov : ERichCurveValueMode::Default, false, RootToLocalTransform); } else { ExportChannelToFbxCurve(*AnimCurve, *DoubleChannel, TickResolution, IsFoV ? ERichCurveValueMode::Fov : ERichCurveValueMode::Default, false, RootToLocalTransform); } } else if (FloatChannel) { CurveNode->SetChannelValue(0U, FloatChannel->GetDefault().Get(MAX_flt)); CurveNode->ConnectToChannel(AnimCurve, 0U); if (bBakeBezierCurves) { ExportBezierChannelToFbxCurveBaked(*AnimCurve, *FloatChannel, TickResolution, &Track, IsFoV ? ERichCurveValueMode::Fov : ERichCurveValueMode::Default, false, RootToLocalTransform); } else { ExportChannelToFbxCurve(*AnimCurve, *FloatChannel, TickResolution, IsFoV ? ERichCurveValueMode::Fov : ERichCurveValueMode::Default, false, RootToLocalTransform); } } else if (IntegerChannel) { CurveNode->SetChannelValue(0U, IntegerChannel->GetDefault().Get(0)); CurveNode->ConnectToChannel(AnimCurve, 0U); ExportChannelToFbxCurve(*AnimCurve, *IntegerChannel, TickResolution, RootToLocalTransform); } else if (BoolChannel) { CurveNode->SetChannelValue(0U, BoolChannel->GetDefault().Get(false)); CurveNode->ConnectToChannel(AnimCurve, 0U); ExportChannelToFbxCurve(*AnimCurve, *BoolChannel, TickResolution, RootToLocalTransform); } else if (EnumChannel) { CurveNode->SetChannelValue(0U, EnumChannel->GetDefault().Get(0U)); CurveNode->ConnectToChannel(AnimCurve, 0U); ExportChannelToFbxCurve(*AnimCurve, *EnumChannel, TickResolution, RootToLocalTransform); } } } } /** * Finds the given actor in the already-exported list of structures */ FbxNode* FFbxExporter::FindActor(AActor* Actor, INodeNameAdapter* NodeNameAdapter) { if (NodeNameAdapter) { FbxNode* ActorNode = NodeNameAdapter->GetFbxNode(Actor); if (ActorNode) { return ActorNode; } } if (FbxActors.Find(Actor)) { return *FbxActors.Find(Actor); } else { return NULL; } } FbxNode* FFbxExporter::CreateNode(const FString& NodeName) { FbxNode* FbxNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*NodeName)); Scene->GetRootNode()->AddChild(FbxNode); return FbxNode; } bool FFbxExporter::FindSkeleton(USkeletalMeshComponent* SkelComp, TArray& BoneNodes, INodeNameAdapter* NodeNameAdapter) { if (NodeNameAdapter) { FbxNode* SkelRoot = NodeNameAdapter->GetFbxNode(SkelComp); if (SkelRoot) { BoneNodes.Empty(); GetSkeleton(SkelRoot, BoneNodes); return true; } } FbxNode** SkelRoot = FbxSkeletonRoots.Find(SkelComp); if (SkelRoot) { BoneNodes.Empty(); GetSkeleton(*SkelRoot, BoneNodes); return true; } return false; } /** * Determines the UVS to weld when exporting a Static Mesh * * @param VertRemap Index of each UV (out) * @param UniqueVerts */ void DetermineUVsToWeld(TArray& VertRemap, TArray& UniqueVerts, const FStaticMeshVertexBuffer& VertexBuffer, int32 TexCoordSourceIndex) { const int32 VertexCount = VertexBuffer.GetNumVertices(); // Maps unreal verts to reduced list of verts VertRemap.Empty(VertexCount); VertRemap.AddUninitialized(VertexCount); // List of Unreal Verts to keep UniqueVerts.Empty(VertexCount); // Combine matching verts using hashed search to maintain good performance TMap HashedVerts; for(int32 Vertex=0; Vertex < VertexCount; Vertex++) { const FVector2D& PositionA = FVector2D(VertexBuffer.GetVertexUV(Vertex,TexCoordSourceIndex)); const int32* FoundIndex = HashedVerts.Find(PositionA); if ( !FoundIndex ) { int32 NewIndex = UniqueVerts.Add(Vertex); VertRemap[Vertex] = NewIndex; HashedVerts.Add(PositionA, NewIndex); } else { VertRemap[Vertex] = *FoundIndex; } } } void DetermineVertsToWeld(TArray& VertRemap, TArray& UniqueVerts, const FStaticMeshLODResources& RenderMesh) { const int32 VertexCount = RenderMesh.VertexBuffers.StaticMeshVertexBuffer.GetNumVertices(); // Maps unreal verts to reduced list of verts VertRemap.Empty(VertexCount); VertRemap.AddUninitialized(VertexCount); // List of Unreal Verts to keep UniqueVerts.Empty(VertexCount); // Combine matching verts using hashed search to maintain good performance TMap HashedVerts; for(int32 a=0; a < VertexCount; a++) { const FVector& PositionA = (FVector)RenderMesh.VertexBuffers.PositionVertexBuffer.VertexPosition(a); const int32* FoundIndex = HashedVerts.Find(PositionA); if ( !FoundIndex ) { int32 NewIndex = UniqueVerts.Add(a); VertRemap[a] = NewIndex; HashedVerts.Add(PositionA, NewIndex); } else { VertRemap[a] = *FoundIndex; } } } class FCollisionFbxExporter { public: FCollisionFbxExporter(const UStaticMesh *StaticMeshToExport, FbxMesh* ExportMesh, int32 ActualMatIndexToExport) { BoxPositions[0] = FVector(-1, -1, +1); BoxPositions[1] = FVector(-1, +1, +1); BoxPositions[2] = FVector(+1, +1, +1); BoxPositions[3] = FVector(+1, -1, +1); BoxFaceRotations[0] = FRotator(0, 0, 0); BoxFaceRotations[1] = FRotator(90.f, 0, 0); BoxFaceRotations[2] = FRotator(-90.f, 0, 0); BoxFaceRotations[3] = FRotator(0, 0, 90.f); BoxFaceRotations[4] = FRotator(0, 0, -90.f); BoxFaceRotations[5] = FRotator(180.f, 0, 0); DrawCollisionSides = 16; SpherNumSides = DrawCollisionSides; SphereNumRings = DrawCollisionSides / 2; SphereNumVerts = (SpherNumSides + 1) * (SphereNumRings + 1); CapsuleNumSides = DrawCollisionSides; CapsuleNumRings = (DrawCollisionSides / 2) + 1; CapsuleNumVerts = (CapsuleNumSides + 1) * (CapsuleNumRings + 1); CurrentVertexOffset = 0; StaticMesh = StaticMeshToExport; Mesh = ExportMesh; ActualMatIndex = ActualMatIndexToExport; } void ExportCollisions() { const FKAggregateGeom& AggGeo = StaticMesh->GetBodySetup()->AggGeom; int32 VerticeNumber = 0; for (const FKConvexElem &ConvexElem : AggGeo.ConvexElems) { VerticeNumber += GetConvexVerticeNumber(ConvexElem); } for (const FKBoxElem &BoxElem : AggGeo.BoxElems) { VerticeNumber += GetBoxVerticeNumber(); } for (const FKSphereElem &SphereElem : AggGeo.SphereElems) { VerticeNumber += GetSphereVerticeNumber(); } for (const FKSphylElem &CapsuleElem : AggGeo.SphylElems) { VerticeNumber += GetCapsuleVerticeNumber(); } Mesh->InitControlPoints(VerticeNumber); ControlPoints = Mesh->GetControlPoints(); CurrentVertexOffset = 0; ////////////////////////////////////////////////////////////////////////// // Set all vertex for (const FKConvexElem &ConvexElem : AggGeo.ConvexElems) { AddConvexVertex(ConvexElem); } for (const FKBoxElem &BoxElem : AggGeo.BoxElems) { AddBoxVertex(BoxElem); } for (const FKSphereElem &SphereElem : AggGeo.SphereElems) { AddSphereVertex(SphereElem); } for (const FKSphylElem &CapsuleElem : AggGeo.SphylElems) { AddCapsuleVertex(CapsuleElem); } // Set the normals on Layer 0. FbxLayer* Layer = Mesh->GetLayer(0); if (Layer == nullptr) { Mesh->CreateLayer(); Layer = Mesh->GetLayer(0); } // Create and fill in the per-face-vertex normal data source. LayerElementNormal = FbxLayerElementNormal::Create(Mesh, ""); // Set the normals per polygon instead of storing normals on positional control points LayerElementNormal->SetMappingMode(FbxLayerElement::eByPolygonVertex); // Set the normal values for every polygon vertex. LayerElementNormal->SetReferenceMode(FbxLayerElement::eDirect); ////////////////////////////////////////////////////////////////////////// //Set the Normals for (const FKConvexElem &ConvexElem : AggGeo.ConvexElems) { AddConvexNormals(ConvexElem); } for (const FKBoxElem &BoxElem : AggGeo.BoxElems) { AddBoxNormal(BoxElem); } int32 SphereIndex = 0; for (const FKSphereElem &SphereElem : AggGeo.SphereElems) { AddSphereNormals(SphereElem, SphereIndex); SphereIndex++; } int32 CapsuleIndex = 0; for (const FKSphylElem &CapsuleElem : AggGeo.SphylElems) { AddCapsuleNormals(CapsuleElem, CapsuleIndex); CapsuleIndex++; } Layer->SetNormals(LayerElementNormal); ////////////////////////////////////////////////////////////////////////// // Set polygons // Build list of polygon re-used multiple times to lookup Normals, UVs, other per face vertex information CurrentVertexOffset = 0; //Reset the current VertexCount for (const FKConvexElem &ConvexElem : AggGeo.ConvexElems) { AddConvexPolygon(ConvexElem); } for (const FKBoxElem &BoxElem : AggGeo.BoxElems) { AddBoxPolygons(); } for (const FKSphereElem &SphereElem : AggGeo.SphereElems) { AddSpherePolygons(); } for (const FKSphylElem &CapsuleElem : AggGeo.SphylElems) { AddCapsulePolygons(); } ////////////////////////////////////////////////////////////////////////// //Free the sphere resources for (FDynamicMeshVertex* DynamicMeshVertex : SpheresVerts) { FMemory::Free(DynamicMeshVertex); } SpheresVerts.Empty(); ////////////////////////////////////////////////////////////////////////// //Free the capsule resources for (FDynamicMeshVertex* DynamicMeshVertex : CapsuleVerts) { FMemory::Free(DynamicMeshVertex); } CapsuleVerts.Empty(); } private: uint32 GetConvexVerticeNumber(const FKConvexElem &ConvexElem) { return ConvexElem.VertexData.Num(); } uint32 GetBoxVerticeNumber() { return 24; } uint32 GetSphereVerticeNumber() { return SphereNumVerts; } uint32 GetCapsuleVerticeNumber() { return CapsuleNumVerts; } void AddConvexVertex(const FKConvexElem &ConvexElem) { const TArray& VertexArray = ConvexElem.VertexData; for (int32 PosIndex = 0; PosIndex < VertexArray.Num(); ++PosIndex) { FVector Position = VertexArray[PosIndex]; ControlPoints[CurrentVertexOffset + PosIndex] = FbxVector4(Position.X, -Position.Y, Position.Z); } CurrentVertexOffset += VertexArray.Num(); } void AddConvexNormals(const FKConvexElem &ConvexElem) { const auto& ConvexMesh = ConvexElem.GetChaosConvexMesh(); if (!ConvexMesh.IsValid()) { return; } const TArray& Faces = ConvexMesh->GetFaces(); for (int32 PolyIndex = 0; PolyIndex < Faces.Num(); ++PolyIndex) { FVector Normal = (FVector)Faces[PolyIndex].Normal().GetSafeNormal(); FbxVector4 FbxNormal = FbxVector4(Normal.X, -Normal.Y, Normal.Z); // add vertices for (int32 j = 0; j < 3; ++j) { LayerElementNormal->GetDirectArray().Add(FbxNormal); } } } void AddConvexPolygon(const FKConvexElem &ConvexElem) { const auto& ConvexMesh = ConvexElem.GetChaosConvexMesh(); if (!ConvexMesh.IsValid()) { return; } TArray IndexData(ConvexElem.IndexData); if (IndexData.Num() == 0) { IndexData = ConvexElem.GetChaosConvexIndices(); } check(IndexData.Num() % 3 == 0); for (int32 VertexIndex = 0; VertexIndex < IndexData.Num(); VertexIndex += 3) { Mesh->BeginPolygon(ActualMatIndex); Mesh->AddPolygon(CurrentVertexOffset + IndexData[VertexIndex]); Mesh->AddPolygon(CurrentVertexOffset + IndexData[VertexIndex + 1]); Mesh->AddPolygon(CurrentVertexOffset + IndexData[VertexIndex + 2]); Mesh->EndPolygon(); } CurrentVertexOffset += ConvexElem.VertexData.Num(); } void AddBoxVertex(const FKBoxElem &BoxElem) { FScaleMatrix ExtendScale(0.5f * FVector(BoxElem.X, BoxElem.Y, BoxElem.Z)); // Calculate verts for a face pointing down Z FMatrix BoxTransform = BoxElem.GetTransform().ToMatrixWithScale(); for (int32 f = 0; f < 6; f++) { FMatrix FaceTransform = FRotationMatrix(BoxFaceRotations[f])*ExtendScale*BoxTransform; for (int32 VertexIndex = 0; VertexIndex < 4; VertexIndex++) { FVector4 VertexPosition = FaceTransform.TransformPosition(BoxPositions[VertexIndex]); ControlPoints[CurrentVertexOffset + VertexIndex] = FbxVector4(VertexPosition.X, -VertexPosition.Y, VertexPosition.Z); } CurrentVertexOffset += 4; } } void AddBoxNormal(const FKBoxElem &BoxElem) { FScaleMatrix ExtendScale(0.5f * FVector(BoxElem.X, BoxElem.Y, BoxElem.Z)); FMatrix BoxTransform = BoxElem.GetTransform().ToMatrixWithScale(); for (int32 f = 0; f < 6; f++) { FMatrix FaceTransform = FRotationMatrix(BoxFaceRotations[f])*ExtendScale*BoxTransform; FVector4 TangentZ = FaceTransform.TransformVector(FVector(0, 0, 1)); FbxVector4 FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); for (int32 VertexIndex = 0; VertexIndex < 4; VertexIndex++) { LayerElementNormal->GetDirectArray().Add(FbxNormal); } } } void AddBoxPolygons() { for (int32 f = 0; f < 6; f++) { Mesh->BeginPolygon(ActualMatIndex); for (int32 VertexIndex = 0; VertexIndex < 4; VertexIndex++) { const uint32 VertIndex = CurrentVertexOffset + VertexIndex; Mesh->AddPolygon(VertIndex); } Mesh->EndPolygon(); CurrentVertexOffset += 4; } } void AddSphereVertex(const FKSphereElem &SphereElem) { FMatrix SphereTransform = FScaleMatrix(SphereElem.Radius * FVector(1.0f)) * SphereElem.GetTransform().ToMatrixWithScale(); FDynamicMeshVertex* Verts = (FDynamicMeshVertex*)FMemory::Malloc(SphereNumVerts * sizeof(FDynamicMeshVertex)); // Calculate verts for one arc FDynamicMeshVertex* ArcVerts = (FDynamicMeshVertex*)FMemory::Malloc((SphereNumRings + 1) * sizeof(FDynamicMeshVertex)); for (int32 i = 0; i < SphereNumRings + 1; i++) { FDynamicMeshVertex* ArcVert = &ArcVerts[i]; float angle = ((float)i / SphereNumRings) * PI; // Note- unit sphere, so position always has mag of one. We can just use it for normal! ArcVert->Position.X = 0.0f; ArcVert->Position.Y = FMath::Sin(angle); ArcVert->Position.Z = FMath::Cos(angle); ArcVert->SetTangents( FVector3f(1, 0, 0), FVector3f(0.0f, -ArcVert->Position.Z, ArcVert->Position.Y), ArcVert->Position ); } // Then rotate this arc SpherNumSides+1 times. for (int32 s = 0; s < SpherNumSides + 1; s++) { FRotator3f ArcRotator(0, 360.f * (float)s / SpherNumSides, 0); FRotationMatrix44f ArcRot(ArcRotator); for (int32 v = 0; v < SphereNumRings + 1; v++) { int32 VIx = (SphereNumRings + 1)*s + v; Verts[VIx].Position = ArcRot.TransformPosition(ArcVerts[v].Position); Verts[VIx].SetTangents( ArcRot.TransformVector(ArcVerts[v].TangentX.ToFVector3f()), ArcRot.TransformVector(ArcVerts[v].GetTangentY()), ArcRot.TransformVector(ArcVerts[v].TangentZ.ToFVector3f()) ); } } // Add all of the vertices we generated to the mesh builder. for (int32 VertexIndex = 0; VertexIndex < SphereNumVerts; VertexIndex++) { FVector Position = (FVector)SphereTransform.TransformPosition((FVector)Verts[VertexIndex].Position); ControlPoints[CurrentVertexOffset + VertexIndex] = FbxVector4(Position.X, -Position.Y, Position.Z); } CurrentVertexOffset += SphereNumVerts; // Free our local copy of arc verts FMemory::Free(ArcVerts); SpheresVerts.Add(Verts); } void AddSphereNormals(const FKSphereElem &SphereElem, int32 SphereIndex) { FMatrix SphereTransform = FScaleMatrix(SphereElem.Radius * FVector(1.0f)) * SphereElem.GetTransform().ToMatrixWithScale(); for (int32 s = 0; s < SpherNumSides; s++) { int32 a0start = (s + 0) * (SphereNumRings + 1); int32 a1start = (s + 1) * (SphereNumRings + 1); for (int32 r = 0; r < SphereNumRings; r++) { if (r != 0) { int32 indexV = a0start + r + 0; FVector TangentZ = SphereTransform.TransformVector(SpheresVerts[SphereIndex][indexV].TangentZ.ToFVector()); FbxVector4 FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); indexV = a1start + r + 0; TangentZ = SphereTransform.TransformVector(SpheresVerts[SphereIndex][indexV].TangentZ.ToFVector()); FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); indexV = a0start + r + 1; TangentZ = SphereTransform.TransformVector(SpheresVerts[SphereIndex][indexV].TangentZ.ToFVector()); FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); } if (r != SphereNumRings - 1) { int32 indexV = a1start + r + 0; FVector TangentZ = SphereTransform.TransformVector(SpheresVerts[SphereIndex][indexV].TangentZ.ToFVector()); FbxVector4 FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); indexV = a1start + r + 1; TangentZ = SphereTransform.TransformVector(SpheresVerts[SphereIndex][indexV].TangentZ.ToFVector()); FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); indexV = a0start + r + 1; TangentZ = SphereTransform.TransformVector(SpheresVerts[SphereIndex][indexV].TangentZ.ToFVector()); FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); } } } } void AddSpherePolygons() { for (int32 s = 0; s < SpherNumSides; s++) { int32 a0start = (s + 0) * (SphereNumRings + 1); int32 a1start = (s + 1) * (SphereNumRings + 1); for (int32 r = 0; r < SphereNumRings; r++) { if (r != 0) { Mesh->BeginPolygon(ActualMatIndex); Mesh->AddPolygon(CurrentVertexOffset + a0start + r + 0); Mesh->AddPolygon(CurrentVertexOffset + a1start + r + 0); Mesh->AddPolygon(CurrentVertexOffset + a0start + r + 1); Mesh->EndPolygon(); } if (r != SphereNumRings - 1) { Mesh->BeginPolygon(ActualMatIndex); Mesh->AddPolygon(CurrentVertexOffset + a1start + r + 0); Mesh->AddPolygon(CurrentVertexOffset + a1start + r + 1); Mesh->AddPolygon(CurrentVertexOffset + a0start + r + 1); Mesh->EndPolygon(); } } } CurrentVertexOffset += SphereNumVerts; } void AddCapsuleVertex(const FKSphylElem &CapsuleElem) { FMatrix CapsuleTransform = CapsuleElem.GetTransform().ToMatrixWithScale(); float Length = CapsuleElem.Length; float Radius = CapsuleElem.Radius; FDynamicMeshVertex* Verts = (FDynamicMeshVertex*)FMemory::Malloc(CapsuleNumVerts * sizeof(FDynamicMeshVertex)); // Calculate verts for one arc FDynamicMeshVertex* ArcVerts = (FDynamicMeshVertex*)FMemory::Malloc((CapsuleNumRings + 1) * sizeof(FDynamicMeshVertex)); for (int32 RingIdx = 0; RingIdx < CapsuleNumRings + 1; RingIdx++) { FDynamicMeshVertex* ArcVert = &ArcVerts[RingIdx]; float Angle; float ZOffset; if (RingIdx <= DrawCollisionSides / 4) { Angle = ((float)RingIdx / (CapsuleNumRings - 1)) * PI; ZOffset = 0.5 * Length; } else { Angle = ((float)(RingIdx - 1) / (CapsuleNumRings - 1)) * PI; ZOffset = -0.5 * Length; } // Note- unit sphere, so position always has mag of one. We can just use it for normal! FVector SpherePos; SpherePos.X = 0.0f; SpherePos.Y = Radius * FMath::Sin(Angle); SpherePos.Z = Radius * FMath::Cos(Angle); ArcVert->Position = (FVector3f)SpherePos + FVector3f(0, 0, ZOffset); ArcVert->SetTangents( FVector3f(1, 0, 0), FVector3f(0.0f, -SpherePos.Z, SpherePos.Y), (FVector3f)SpherePos ); } // Then rotate this arc NumSides+1 times. for (int32 SideIdx = 0; SideIdx < CapsuleNumSides + 1; SideIdx++) { const FRotator3f ArcRotator(0, 360.f * ((float)SideIdx / CapsuleNumSides), 0); const FRotationMatrix44f ArcRot(ArcRotator); for (int32 VertIdx = 0; VertIdx < CapsuleNumRings + 1; VertIdx++) { int32 VIx = (CapsuleNumRings + 1)*SideIdx + VertIdx; Verts[VIx].Position = ArcRot.TransformPosition(ArcVerts[VertIdx].Position); Verts[VIx].SetTangents( ArcRot.TransformVector(ArcVerts[VertIdx].TangentX.ToFVector3f()), ArcRot.TransformVector(ArcVerts[VertIdx].GetTangentY()), ArcRot.TransformVector(ArcVerts[VertIdx].TangentZ.ToFVector3f()) ); } } // Add all of the vertices we generated to the mesh builder. for (int32 VertexIndex = 0; VertexIndex < CapsuleNumVerts; VertexIndex++) { FVector Position = (FVector)CapsuleTransform.TransformPosition((FVector)Verts[VertexIndex].Position); ControlPoints[CurrentVertexOffset + VertexIndex] = FbxVector4(Position.X, -Position.Y, Position.Z); } CurrentVertexOffset += CapsuleNumVerts; // Free our local copy of arc verts FMemory::Free(ArcVerts); CapsuleVerts.Add(Verts); } void AddCapsuleNormals(const FKSphylElem &CapsuleElem, int32 CapsuleIndex) { FMatrix CapsuleTransform = CapsuleElem.GetTransform().ToMatrixWithScale(); // Add all of the triangles to the mesh. for (int32 SideIdx = 0; SideIdx < CapsuleNumSides; SideIdx++) { const int32 a0start = (SideIdx + 0) * (CapsuleNumRings + 1); const int32 a1start = (SideIdx + 1) * (CapsuleNumRings + 1); for (int32 RingIdx = 0; RingIdx < CapsuleNumRings; RingIdx++) { if (RingIdx != 0) { int32 indexV = a0start + RingIdx + 0; FVector TangentZ = CapsuleTransform.TransformVector(CapsuleVerts[CapsuleIndex][indexV].TangentZ.ToFVector()); FbxVector4 FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); indexV = a1start + RingIdx + 0; TangentZ = CapsuleTransform.TransformVector(CapsuleVerts[CapsuleIndex][indexV].TangentZ.ToFVector()); FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); indexV = a0start + RingIdx + 1; TangentZ = CapsuleTransform.TransformVector(CapsuleVerts[CapsuleIndex][indexV].TangentZ.ToFVector()); FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); } if (RingIdx != CapsuleNumRings - 1) { int32 indexV = a1start + RingIdx + 0; FVector TangentZ = CapsuleTransform.TransformVector(CapsuleVerts[CapsuleIndex][indexV].TangentZ.ToFVector()); FbxVector4 FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); indexV = a1start + RingIdx + 1; TangentZ = CapsuleTransform.TransformVector(CapsuleVerts[CapsuleIndex][indexV].TangentZ.ToFVector()); FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); indexV = a0start + RingIdx + 1; TangentZ = CapsuleTransform.TransformVector(CapsuleVerts[CapsuleIndex][indexV].TangentZ.ToFVector()); FbxNormal = FbxVector4(TangentZ.X, -TangentZ.Y, TangentZ.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); } } } } void AddCapsulePolygons() { // Add all of the triangles to the mesh. for (int32 SideIdx = 0; SideIdx < CapsuleNumSides; SideIdx++) { const int32 a0start = (SideIdx + 0) * (CapsuleNumRings + 1); const int32 a1start = (SideIdx + 1) * (CapsuleNumRings + 1); for (int32 RingIdx = 0; RingIdx < CapsuleNumRings; RingIdx++) { if (RingIdx != 0) { Mesh->BeginPolygon(ActualMatIndex); Mesh->AddPolygon(CurrentVertexOffset + a0start + RingIdx + 0); Mesh->AddPolygon(CurrentVertexOffset + a1start + RingIdx + 0); Mesh->AddPolygon(CurrentVertexOffset + a0start + RingIdx + 1); Mesh->EndPolygon(); } if (RingIdx != CapsuleNumRings - 1) { Mesh->BeginPolygon(ActualMatIndex); Mesh->AddPolygon(CurrentVertexOffset + a1start + RingIdx + 0); Mesh->AddPolygon(CurrentVertexOffset + a1start + RingIdx + 1); Mesh->AddPolygon(CurrentVertexOffset + a0start + RingIdx + 1); Mesh->EndPolygon(); } } } CurrentVertexOffset += CapsuleNumVerts; } ////////////////////////////////////////////////////////////////////////// //Box data FVector BoxPositions[4]; FRotator BoxFaceRotations[6]; int32 DrawCollisionSides; ////////////////////////////////////////////////////////////////////////// //Sphere data int32 SpherNumSides; int32 SphereNumRings; int32 SphereNumVerts; TArray SpheresVerts; ////////////////////////////////////////////////////////////////////////// //Capsule data int32 CapsuleNumSides; int32 CapsuleNumRings; int32 CapsuleNumVerts; TArray CapsuleVerts; ////////////////////////////////////////////////////////////////////////// //Mesh Data uint32 CurrentVertexOffset; const UStaticMesh *StaticMesh; FbxMesh* Mesh; int32 ActualMatIndex; FbxVector4* ControlPoints; FbxLayerElementNormal* LayerElementNormal; }; FbxNode* FFbxExporter::ExportCollisionMesh(const UStaticMesh* StaticMesh, const TCHAR* MeshName, FbxNode* ParentActor) { const FKAggregateGeom& AggGeo = StaticMesh->GetBodySetup()->AggGeom; if (AggGeo.GetElementCount() <= 0) { return nullptr; } FbxMesh* Mesh = FbxMeshes.FindRef(StaticMesh); if (!Mesh) { //We export collision only if the mesh is already exported return nullptr; } //Name the node with the actor name FString MeshCollisionName = TEXT("UCX_"); MeshCollisionName += UTF8_TO_TCHAR(ParentActor->GetName()); //-V595 FbxNode* FbxActor = FbxNode::Create(Scene, TCHAR_TO_UTF8(*MeshCollisionName)); if (ParentActor != nullptr) { // Collision meshes are added directly to the scene root, so we need to use the global transform instead of the relative one. FbxAMatrix& GlobalTransform = ParentActor->EvaluateGlobalTransform(); FbxActor->LclTranslation.Set(GlobalTransform.GetT()); FbxActor->LclRotation.Set(GlobalTransform.GetR()); FbxActor->LclScaling.Set(GlobalTransform.GetS()); } Scene->GetRootNode()->AddChild(FbxActor); FbxMesh* CollisionMesh = FbxCollisionMeshes.FindRef(StaticMesh); if (!CollisionMesh) { //Name the mesh attribute with the mesh name MeshCollisionName = TEXT("UCX_"); MeshCollisionName += MeshName; CollisionMesh = FbxMesh::Create(Scene, TCHAR_TO_UTF8(*MeshCollisionName)); //Export all collision elements in one mesh FbxSurfaceMaterial* FbxMaterial = nullptr; int32 ActualMatIndex = FbxActor->AddMaterial(FbxMaterial); FCollisionFbxExporter CollisionFbxExporter(StaticMesh, CollisionMesh, ActualMatIndex); CollisionFbxExporter.ExportCollisions(); FbxCollisionMeshes.Add(StaticMesh, CollisionMesh); } //Set the original meshes in case it was already existing FbxActor->SetNodeAttribute(CollisionMesh); return FbxActor; } void FFbxExporter::ExportObjectMetadata(const UObject* ObjectToExport, FbxNode* Node) { if (ObjectToExport && Node) { // Retrieve the metadata map without creating it const TMap* MetadataMap = FMetaData::GetMapForObject(ObjectToExport); if (MetadataMap) { static const FString MetadataPrefix(FBX_METADATA_PREFIX); for (const auto& MetadataIt : *MetadataMap) { // Export object metadata tags that are prefixed as FBX custom user-defined properties // Remove the prefix since it's for Unreal use only (and '.' is considered an invalid character for user property names in DCC like Maya) FString TagAsString = MetadataIt.Key.ToString(); if (TagAsString.RemoveFromStart(MetadataPrefix)) { // Remaining tag follows the format NodeName.PropertyName, so replace '.' with '_' TagAsString.ReplaceInline(TEXT("."), TEXT("_")); if (MetadataIt.Value == TEXT("true") || MetadataIt.Value == TEXT("false")) { FbxProperty Property = FbxProperty::Create(Node, FbxBoolDT, TCHAR_TO_UTF8(*TagAsString)); FbxBool ValueBool = MetadataIt.Value == TEXT("true") ? true : false; Property.Set(ValueBool); Property.ModifyFlag(FbxPropertyFlags::eUserDefined, true); } else { FbxProperty Property = FbxProperty::Create(Node, FbxStringDT, TCHAR_TO_UTF8(*TagAsString)); FbxString ValueString(TCHAR_TO_UTF8(*MetadataIt.Value)); Property.Set(ValueString); Property.ModifyFlag(FbxPropertyFlags::eUserDefined, true); } } } } } } bool FFbxExporter::ExportStaticMeshFromMeshDescription(FbxMesh* Mesh , const UStaticMesh* StaticMesh , const FMeshDescription* MeshDescription , FbxNode* FbxActor , int32 LightmapUVChannel , const TArray* MaterialOrderOverride , const TArray* OverrideMaterials , const FFbxMaterialBakingMeshData& MaterialBakingMeshData) { if (MeshDescription->IsEmpty() || MeshDescription->Vertices().Num() == 0) { return false; } FStaticMeshConstAttributes Attributes(*MeshDescription); TVertexAttributesConstRef VertexPositions = Attributes.GetVertexPositions(); TVertexInstanceAttributesConstRef VertexInstanceNormals = Attributes.GetVertexInstanceNormals(); TVertexInstanceAttributesConstRef VertexInstanceTangents = Attributes.GetVertexInstanceTangents(); TVertexInstanceAttributesConstRef VertexInstanceBinormalSigns = Attributes.GetVertexInstanceBinormalSigns(); TVertexInstanceAttributesConstRef VertexInstanceUVs = Attributes.GetVertexInstanceUVs(); TVertexInstanceAttributesConstRef VertexInstanceColors = Attributes.GetVertexInstanceColors(); TPolygonGroupAttributesConstRef PolygonGroupMaterialSlotNames = Attributes.GetPolygonGroupMaterialSlotNames(); TEdgeAttributesConstRef EdgeHardnesses = Attributes.GetEdgeHardnesses(); const int32 VertexCount = MeshDescription->Vertices().Num(); const int32 VertexInstanceCount = MeshDescription->VertexInstances().Num(); Mesh->InitControlPoints(VertexCount); FbxVector4* ControlPoints = Mesh->GetControlPoints(); for (int32 PosIndex = 0; PosIndex < VertexCount; ++PosIndex) { FVector Position = (FVector)VertexPositions[FVertexID(PosIndex)]; ControlPoints[PosIndex] = FbxVector4(Position.X, -Position.Y, Position.Z); } // Set the normals on Layer 0. FbxLayer* Layer = Mesh->GetLayer(0); if (Layer == nullptr) { Mesh->CreateLayer(); Layer = Mesh->GetLayer(0); } TArray Indices; Indices.Reserve(VertexInstanceCount); for (const FPolygonGroupID& PolygonGroupID : MeshDescription->PolygonGroups().GetElementIDs()) { for(const FTriangleID& TriangleID : MeshDescription->GetPolygonGroupTriangles(PolygonGroupID)) { TArrayView TriangleVertexInstanceIDs = MeshDescription->GetTriangleVertexInstances(TriangleID); for(const FVertexInstanceID& VertexInstanceID : TriangleVertexInstanceIDs) { Indices.Add(MeshDescription->GetVertexInstanceVertex(VertexInstanceID).GetValue()); } } } // Create and fill in the per-face-vertex normal data source. // We extract the Z-tangent and the X/Y-tangents which are also stored in the render mesh. FbxLayerElementNormal* LayerElementNormal = FbxLayerElementNormal::Create(Mesh, ""); FbxLayerElementTangent* LayerElementTangent = FbxLayerElementTangent::Create(Mesh, ""); FbxLayerElementBinormal* LayerElementBinormal = FbxLayerElementBinormal::Create(Mesh, ""); // Set 3 NTBs per triangle instead of storing on positional control points LayerElementNormal->SetMappingMode(FbxLayerElement::eByPolygonVertex); LayerElementTangent->SetMappingMode(FbxLayerElement::eByPolygonVertex); LayerElementBinormal->SetMappingMode(FbxLayerElement::eByPolygonVertex); // Set the NTBs values for every polygon vertex. LayerElementNormal->SetReferenceMode(FbxLayerElement::eDirect); LayerElementTangent->SetReferenceMode(FbxLayerElement::eDirect); LayerElementBinormal->SetReferenceMode(FbxLayerElement::eDirect); //Extract the tangent space { for (const FPolygonGroupID& PolygonGroupID : MeshDescription->PolygonGroups().GetElementIDs()) { for (const FTriangleID& TriangleID : MeshDescription->GetPolygonGroupTriangles(PolygonGroupID)) { TArrayView TriangleVertexInstanceIDs = MeshDescription->GetTriangleVertexInstances(TriangleID); for (const FVertexInstanceID& VertexInstanceID : TriangleVertexInstanceIDs) { //Query the tangent space FVector3f Normal, Tangent, Binormal; Tangent = VertexInstanceTangents[VertexInstanceID]; Normal = VertexInstanceNormals[VertexInstanceID]; float BinormalSign = VertexInstanceBinormalSigns[VertexInstanceID]; Binormal = (FVector3f::CrossProduct(Normal, Tangent).GetSafeNormal() * BinormalSign); FbxVector4 FbxNormal = FbxVector4(Normal.X, -Normal.Y, Normal.Z); FbxNormal.Normalize(); LayerElementNormal->GetDirectArray().Add(FbxNormal); FbxVector4 FbxTangent = FbxVector4(Tangent.X, -Tangent.Y, Tangent.Z); FbxTangent.Normalize(); LayerElementTangent->GetDirectArray().Add(FbxTangent); FbxVector4 FbxBinormal = FbxVector4(-Binormal.X, Binormal.Y, -Binormal.Z); FbxBinormal.Normalize(); LayerElementBinormal->GetDirectArray().Add(FbxBinormal); } } } } Layer->SetNormals(LayerElementNormal); Layer->SetTangents(LayerElementTangent); Layer->SetBinormals(LayerElementBinormal); // Create and fill in the per-face-vertex texture coordinate data source(s). // Create UV for Diffuse channel. int32 TexCoordSourceCount = (LightmapUVChannel == -1) ? MeshDescription->GetNumUVElementChannels() : LightmapUVChannel + 1; int32 TexCoordSourceIndex = (LightmapUVChannel == -1) ? 0 : LightmapUVChannel; for (; TexCoordSourceIndex < TexCoordSourceCount; ++TexCoordSourceIndex) { FbxLayer* UVsLayer = (LightmapUVChannel == -1) ? Mesh->GetLayer(TexCoordSourceIndex) : Mesh->GetLayer(0); if (UVsLayer == NULL) { Mesh->CreateLayer(); UVsLayer = (LightmapUVChannel == -1) ? Mesh->GetLayer(TexCoordSourceIndex) : Mesh->GetLayer(0); } check(UVsLayer); FString UVChannelNameBuilder = TEXT("UVmap_") + FString::FromInt(TexCoordSourceIndex); const auto UVChannelNameUTF8 = TStringConversion(*UVChannelNameBuilder); // Do not inline it! The lifetime of this object needs to extend over the usage of the converted buffer. const char* UVChannelName = UVChannelNameUTF8.Get(); // actually UTF8 as required by Fbx, but can't use UE's UTF8CHAR type because that's a uint8 aka *unsigned* char if ((LightmapUVChannel >= 0) || ((LightmapUVChannel == -1) && (TexCoordSourceIndex == StaticMesh->GetLightMapCoordinateIndex()))) { UVChannelName = "LightMapUV"; } FbxLayerElementUV* UVDiffuseLayer = FbxLayerElementUV::Create(Mesh, UVChannelName); UVDiffuseLayer->SetMappingMode(FbxLayerElement::eByPolygonVertex); UVDiffuseLayer->SetReferenceMode(FbxLayerElement::eDirect); for (const FPolygonGroupID& PolygonGroupID : MeshDescription->PolygonGroups().GetElementIDs()) { for (const FTriangleID& TriangleID : MeshDescription->GetPolygonGroupTriangles(PolygonGroupID)) { TArrayView TriangleVertexInstanceIDs = MeshDescription->GetTriangleVertexInstances(TriangleID); for (const FVertexInstanceID& VertexInstanceID : TriangleVertexInstanceIDs) { const FVector2f& VertexUV = VertexInstanceUVs.Get(VertexInstanceID, TexCoordSourceIndex); UVDiffuseLayer->GetDirectArray().Add(FbxVector2(VertexUV.X, -VertexUV.Y + 1.0)); } } } UVsLayer->SetUVs(UVDiffuseLayer, FbxLayerElement::eTextureDiffuse); } FbxLayerElementMaterial* MatLayer = FbxLayerElementMaterial::Create(Mesh, ""); MatLayer->SetMappingMode(FbxLayerElement::eByPolygon); MatLayer->SetReferenceMode(FbxLayerElement::eIndexToDirect); Layer->SetMaterials(MatLayer); FbxLayerElementSmoothing* SmoothingInfoLayer = FbxLayerElementSmoothing::Create(Mesh, ""); SmoothingInfoLayer->SetMappingMode(FbxLayerElement::eByEdge); SmoothingInfoLayer->SetReferenceMode(FbxLayerElement::eDirect); Layer->SetSmoothing(SmoothingInfoLayer); //Create the polygon with the correct material { int32 IndiceIndex = 0; TSet ProcessEdges; ProcessEdges.Reserve(Indices.Num()); for(const FPolygonGroupID& PolygonGroupID : MeshDescription->PolygonGroups().GetElementIDs()) { UMaterialInterface* Material = nullptr; FName CurrentMaterialSlotName = PolygonGroupMaterialSlotNames[PolygonGroupID]; int32 MaterialIndex = StaticMesh->GetMaterialIndexFromImportedMaterialSlotName(CurrentMaterialSlotName); if (MaterialIndex == INDEX_NONE) { MaterialIndex = PolygonGroupID.GetValue(); } if (!StaticMesh->GetStaticMaterials().IsValidIndex(MaterialIndex)) { MaterialIndex = 0; } if (OverrideMaterials && OverrideMaterials->IsValidIndex(PolygonGroupID.GetValue())) { Material = (*OverrideMaterials)[PolygonGroupID.GetValue()]; } else { Material = StaticMesh->GetMaterial(MaterialIndex); } FbxSurfaceMaterial* FbxMaterial = Material ? ExportMaterial(Material, MaterialIndex, MaterialBakingMeshData) : nullptr; if (!FbxMaterial) { FbxMaterial = CreateDefaultMaterial(); } int32 MatIndex = FbxActor->AddMaterial(FbxMaterial); // Determine the actual material index int32 ActualMatIndex = MatIndex; if (MaterialOrderOverride) { ActualMatIndex = MaterialOrderOverride->Find(Material); } //Create the triangles { for (const FTriangleID& TriangleID : MeshDescription->GetPolygonGroupTriangles(PolygonGroupID)) { FVertexID Corner[3]; int32 CornerIndex = 0; Mesh->BeginPolygon(ActualMatIndex); TArrayView TriangleVertexInstanceIDs = MeshDescription->GetTriangleVertexInstances(TriangleID); for (const FVertexInstanceID& VertexInstanceID : TriangleVertexInstanceIDs) { Corner[CornerIndex] = MeshDescription->GetVertexInstanceVertex(VertexInstanceID); Mesh->AddPolygon(Indices[IndiceIndex]); IndiceIndex++; CornerIndex++; } Mesh->EndPolygon(); } } } //Build the edge so we can set the edge hardness Mesh->BuildMeshEdgeArray(); Mesh->BeginGetMeshEdgeIndexForPolygon(); int32 PolygonIndex = 0; for (const FPolygonGroupID& PolygonGroupID : MeshDescription->PolygonGroups().GetElementIDs()) { for (const FTriangleID& TriangleID : MeshDescription->GetPolygonGroupTriangles(PolygonGroupID)) { FVertexID Corner[3]; int32 CornerIndex = 0; TArrayView TriangleVertexInstanceIDs = MeshDescription->GetTriangleVertexInstances(TriangleID); for (const FVertexInstanceID& VertexInstanceID : TriangleVertexInstanceIDs) { Corner[CornerIndex] = MeshDescription->GetVertexInstanceVertex(VertexInstanceID); CornerIndex++; } //Add the smoothing group for the triangle for (CornerIndex = 0; CornerIndex < 3; ++CornerIndex) { FVertexID EdgeStart = Corner[CornerIndex]; FVertexID EdgeEnd = Corner[(CornerIndex + 1) % 3]; FEdgeID MatchEdgeId = MeshDescription->GetVertexPairEdge(EdgeStart, EdgeEnd); if (ProcessEdges.Contains(MatchEdgeId)) { continue; } ProcessEdges.Add(MatchEdgeId); int32 FbxEdgeIndex = Mesh->GetMeshEdgeIndexForPolygon(PolygonIndex, CornerIndex); if (FbxEdgeIndex == -1) { continue; } int32 EdgeHardnessValue = 1; if (MatchEdgeId != INDEX_NONE) { EdgeHardnessValue = EdgeHardnesses[MatchEdgeId] ? 0 : 1; } int32 LayerAddIndex = SmoothingInfoLayer->GetDirectArray().Add(EdgeHardnessValue); ensure(LayerAddIndex == FbxEdgeIndex); } PolygonIndex++; } } Mesh->EndGetMeshEdgeIndexForPolygon(); } // Create and fill in the vertex color data source. uint32 ColorVertexCount = MeshDescription->VertexInstanceAttributes().HasAttribute(MeshAttribute::VertexInstance::Color) ? MeshDescription->VertexInstances().Num() : 0; // Only export vertex colors if they exist if (GetExportOptions()->VertexColor && ColorVertexCount > 0) { FbxLayerElementVertexColor* VertexColor = FbxLayerElementVertexColor::Create(Mesh, ""); VertexColor->SetMappingMode(FbxLayerElement::eByPolygonVertex); VertexColor->SetReferenceMode(FbxLayerElement::eDirect); FbxLayerElementArrayTemplate& VertexColorArray = VertexColor->GetDirectArray(); Layer->SetVertexColors(VertexColor); for (const FPolygonGroupID& PolygonGroupID : MeshDescription->PolygonGroups().GetElementIDs()) { for (const FTriangleID& TriangleID : MeshDescription->GetPolygonGroupTriangles(PolygonGroupID)) { TArrayView TriangleVertexInstanceIDs = MeshDescription->GetTriangleVertexInstances(TriangleID); for (const FVertexInstanceID& VertexInstanceID : TriangleVertexInstanceIDs) { const FVector4f& SourceVertexColor = VertexInstanceColors[VertexInstanceID]; FLinearColor LinearColor(SourceVertexColor); //Convert to sRGB FColor Color = LinearColor.ToFColor(true); VertexColorArray.Add(FbxColor(static_cast(Color.R)/255.0f, static_cast(Color.G) / 255.0f, static_cast(Color.B) / 255.0f, static_cast(Color.A) / 255.0f)); } } } } return true; } bool FFbxExporter::ExportStaticMeshFromRenderData(FbxMesh* Mesh , const UStaticMesh* StaticMesh , const FStaticMeshLODResources& RenderMesh , FbxNode* FbxActor , int32 LightmapUVChannel , const FColorVertexBuffer* ColorBuffer , const TArray* MaterialOrderOverride , const TArray* OverrideMaterials , const FFbxMaterialBakingMeshData& MaterialBakingMeshData) { // Verify the integrity of the static mesh. if (RenderMesh.VertexBuffers.StaticMeshVertexBuffer.GetNumVertices() == 0) { return false; } if (RenderMesh.Sections.Num() == 0) { return false; } // Remaps an Unreal vert to final reduced vertex list TArray VertRemap; TArray UniqueVerts; // Weld verts DetermineVertsToWeld(VertRemap, UniqueVerts, RenderMesh); // Create and fill in the vertex position data source. // The position vertices are duplicated, for some reason, retrieve only the first half vertices. const int32 VertexCount = VertRemap.Num(); const int32 PolygonsCount = RenderMesh.Sections.Num(); Mesh->InitControlPoints(UniqueVerts.Num()); FbxVector4* ControlPoints = Mesh->GetControlPoints(); for (int32 PosIndex = 0; PosIndex < UniqueVerts.Num(); ++PosIndex) { int32 UnrealPosIndex = UniqueVerts[PosIndex]; FVector Position = (FVector)RenderMesh.VertexBuffers.PositionVertexBuffer.VertexPosition(UnrealPosIndex); ControlPoints[PosIndex] = FbxVector4(Position.X, -Position.Y, Position.Z); } // Set the normals on Layer 0. FbxLayer* Layer = Mesh->GetLayer(0); if (Layer == nullptr) { Mesh->CreateLayer(); Layer = Mesh->GetLayer(0); } // Build list of Indices re-used multiple times to lookup Normals, UVs, other per face vertex information TArray Indices; for (int32 PolygonsIndex = 0; PolygonsIndex < PolygonsCount; ++PolygonsIndex) { FIndexArrayView RawIndices = RenderMesh.IndexBuffer.GetArrayView(); const FStaticMeshSection& Polygons = RenderMesh.Sections[PolygonsIndex]; const uint32 TriangleCount = Polygons.NumTriangles; for (uint32 TriangleIndex = 0; TriangleIndex < TriangleCount; ++TriangleIndex) { for (uint32 PointIndex = 0; PointIndex < 3; PointIndex++) { uint32 UnrealVertIndex = RawIndices[Polygons.FirstIndex + ((TriangleIndex * 3) + PointIndex)]; Indices.Add(UnrealVertIndex); } } } // Create and fill in the per-face-vertex normal data source. // We extract the Z-tangent and the X/Y-tangents which are also stored in the render mesh. FbxLayerElementNormal* LayerElementNormal = FbxLayerElementNormal::Create(Mesh, ""); FbxLayerElementTangent* LayerElementTangent = FbxLayerElementTangent::Create(Mesh, ""); FbxLayerElementBinormal* LayerElementBinormal = FbxLayerElementBinormal::Create(Mesh, ""); // Set 3 NTBs per triangle instead of storing on positional control points LayerElementNormal->SetMappingMode(FbxLayerElement::eByPolygonVertex); LayerElementTangent->SetMappingMode(FbxLayerElement::eByPolygonVertex); LayerElementBinormal->SetMappingMode(FbxLayerElement::eByPolygonVertex); // Set the NTBs values for every polygon vertex. LayerElementNormal->SetReferenceMode(FbxLayerElement::eDirect); LayerElementTangent->SetReferenceMode(FbxLayerElement::eDirect); LayerElementBinormal->SetReferenceMode(FbxLayerElement::eDirect); TArray FbxNormals; TArray FbxTangents; TArray FbxBinormals; FbxNormals.AddUninitialized(VertexCount); FbxTangents.AddUninitialized(VertexCount); FbxBinormals.AddUninitialized(VertexCount); for (int32 NTBIndex = 0; NTBIndex < VertexCount; ++NTBIndex) { FVector3f Normal = (FVector3f)(RenderMesh.VertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(NTBIndex)); FbxVector4& FbxNormal = FbxNormals[NTBIndex]; FbxNormal = FbxVector4(Normal.X, -Normal.Y, Normal.Z); FbxNormal.Normalize(); FVector3f Tangent = (FVector3f)(RenderMesh.VertexBuffers.StaticMeshVertexBuffer.VertexTangentX(NTBIndex)); FbxVector4& FbxTangent = FbxTangents[NTBIndex]; FbxTangent = FbxVector4(Tangent.X, -Tangent.Y, Tangent.Z); FbxTangent.Normalize(); FVector3f Binormal = -(FVector3f)(RenderMesh.VertexBuffers.StaticMeshVertexBuffer.VertexTangentY(NTBIndex)); FbxVector4& FbxBinormal = FbxBinormals[NTBIndex]; FbxBinormal = FbxVector4(Binormal.X, -Binormal.Y, Binormal.Z); FbxBinormal.Normalize(); } // Add one normal per each face index (3 per triangle) for (int32 FbxVertIndex = 0; FbxVertIndex < Indices.Num(); FbxVertIndex++) { uint32 UnrealVertIndex = Indices[FbxVertIndex]; LayerElementNormal->GetDirectArray().Add(FbxNormals[UnrealVertIndex]); LayerElementTangent->GetDirectArray().Add(FbxTangents[UnrealVertIndex]); LayerElementBinormal->GetDirectArray().Add(FbxBinormals[UnrealVertIndex]); } Layer->SetNormals(LayerElementNormal); Layer->SetTangents(LayerElementTangent); Layer->SetBinormals(LayerElementBinormal); FbxNormals.Empty(); FbxTangents.Empty(); FbxBinormals.Empty(); // Create and fill in the per-face-vertex texture coordinate data source(s). // Create UV for Diffuse channel. int32 TexCoordSourceCount = (LightmapUVChannel == -1) ? RenderMesh.VertexBuffers.StaticMeshVertexBuffer.GetNumTexCoords() : LightmapUVChannel + 1; int32 TexCoordSourceIndex = (LightmapUVChannel == -1) ? 0 : LightmapUVChannel; for (; TexCoordSourceIndex < TexCoordSourceCount; ++TexCoordSourceIndex) { FbxLayer* UVsLayer = (LightmapUVChannel == -1) ? Mesh->GetLayer(TexCoordSourceIndex) : Mesh->GetLayer(0); if (UVsLayer == NULL) { Mesh->CreateLayer(); UVsLayer = (LightmapUVChannel == -1) ? Mesh->GetLayer(TexCoordSourceIndex) : Mesh->GetLayer(0); } check(UVsLayer); FString UVChannelNameBuilder = TEXT("UVmap_") + FString::FromInt(TexCoordSourceIndex); const auto UVChannelNameUTF8 = TStringConversion(*UVChannelNameBuilder); // Do not inline it! The lifetime of this object needs to extend over the usage of the converted buffer. const char* UVChannelName = UVChannelNameUTF8.Get(); // actually UTF8 as required by Fbx, but can't use UE's UTF8CHAR type because that's a uint8 aka *unsigned* char if ((LightmapUVChannel >= 0) || ((LightmapUVChannel == -1) && (TexCoordSourceIndex == StaticMesh->GetLightMapCoordinateIndex()))) { UVChannelName = "LightMapUV"; } FbxLayerElementUV* UVDiffuseLayer = FbxLayerElementUV::Create(Mesh, UVChannelName); // Note: when eINDEX_TO_DIRECT is used, IndexArray must be 3xTriangle count, DirectArray can be smaller UVDiffuseLayer->SetMappingMode(FbxLayerElement::eByPolygonVertex); UVDiffuseLayer->SetReferenceMode(FbxLayerElement::eIndexToDirect); TArray UvsRemap; TArray UniqueUVs; // Weld UVs DetermineUVsToWeld(UvsRemap, UniqueUVs, RenderMesh.VertexBuffers.StaticMeshVertexBuffer, TexCoordSourceIndex); // Create the texture coordinate data source. for (int32 FbxVertIndex = 0; FbxVertIndex < UniqueUVs.Num(); FbxVertIndex++) { int32 UnrealVertIndex = UniqueUVs[FbxVertIndex]; const FVector2f& TexCoord = RenderMesh.VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(UnrealVertIndex, TexCoordSourceIndex); UVDiffuseLayer->GetDirectArray().Add(FbxVector2(TexCoord.X, -TexCoord.Y + 1.0)); } // For each face index, point to a texture uv UVDiffuseLayer->GetIndexArray().SetCount(Indices.Num()); for (int32 FbxVertIndex = 0; FbxVertIndex < Indices.Num(); FbxVertIndex++) { uint32 UnrealVertIndex = Indices[FbxVertIndex]; int32 NewVertIndex = UvsRemap[UnrealVertIndex]; UVDiffuseLayer->GetIndexArray().SetAt(FbxVertIndex, NewVertIndex); } UVsLayer->SetUVs(UVDiffuseLayer, FbxLayerElement::eTextureDiffuse); } FbxLayerElementMaterial* MatLayer = FbxLayerElementMaterial::Create(Mesh, ""); MatLayer->SetMappingMode(FbxLayerElement::eByPolygon); MatLayer->SetReferenceMode(FbxLayerElement::eIndexToDirect); Layer->SetMaterials(MatLayer); // Keep track of the number of tri's we export uint32 AccountedTriangles = 0; for (int32 PolygonsIndex = 0; PolygonsIndex < PolygonsCount; ++PolygonsIndex) { const FStaticMeshSection& Polygons = RenderMesh.Sections[PolygonsIndex]; FIndexArrayView RawIndices = RenderMesh.IndexBuffer.GetArrayView(); UMaterialInterface* Material = nullptr; if (OverrideMaterials && OverrideMaterials->IsValidIndex(Polygons.MaterialIndex)) { Material = (*OverrideMaterials)[Polygons.MaterialIndex]; } else { Material = StaticMesh->GetMaterial(Polygons.MaterialIndex); } FbxSurfaceMaterial* FbxMaterial = Material ? ExportMaterial(Material, Polygons.MaterialIndex, MaterialBakingMeshData) : NULL; if (!FbxMaterial) { FbxMaterial = CreateDefaultMaterial(); } int32 MatIndex = FbxActor->AddMaterial(FbxMaterial); // Determine the actual material index int32 ActualMatIndex = MatIndex; if (MaterialOrderOverride) { ActualMatIndex = MaterialOrderOverride->Find(Material); } // Static meshes contain one triangle list per element. // [GLAFORTE] Could it occasionally contain triangle strips? How do I know? uint32 TriangleCount = Polygons.NumTriangles; // Copy over the index buffer into the FBX polygons set. for (uint32 TriangleIndex = 0; TriangleIndex < TriangleCount; ++TriangleIndex) { Mesh->BeginPolygon(ActualMatIndex); for (uint32 PointIndex = 0; PointIndex < 3; PointIndex++) { uint32 OriginalUnrealVertIndex = RawIndices[Polygons.FirstIndex + ((TriangleIndex * 3) + PointIndex)]; int32 RemappedVertIndex = VertRemap[OriginalUnrealVertIndex]; Mesh->AddPolygon(RemappedVertIndex); } Mesh->EndPolygon(); } AccountedTriangles += TriangleCount; } #ifdef TODO_FBX // Throw a warning if this is a lightmap export and the exported poly count does not match the raw triangle data count if (LightmapUVChannel != -1 && AccountedTriangles != RenderMesh.RawTriangles.GetElementCount()) { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "StaticMeshEditor_LightmapExportFewerTriangles", "Fewer polygons have been exported than the raw triangle count. This Lightmapped UV mesh may contain fewer triangles than the destination mesh on import.")); } // Create and fill in the smoothing data source. FbxLayerElementSmoothing* SmoothingInfo = FbxLayerElementSmoothing::Create(Mesh, ""); SmoothingInfo->SetMappingMode(FbxLayerElement::eByPolygon); SmoothingInfo->SetReferenceMode(FbxLayerElement::eDirect); FbxLayerElementArrayTemplate& SmoothingArray = SmoothingInfo->GetDirectArray(); Layer->SetSmoothing(SmoothingInfo); // This is broken. We are exporting the render mesh but providing smoothing // information from the source mesh. The render triangles are not in the // same order. Therefore we should export the raw mesh or not export // smoothing group information! int32 TriangleCount = RenderMesh.RawTriangles.GetElementCount(); FStaticMeshTriangle* RawTriangleData = (FStaticMeshTriangle*)RenderMesh.RawTriangles.Lock(LOCK_READ_ONLY); for (int32 TriangleIndex = 0; TriangleIndex < TriangleCount; TriangleIndex++) { FStaticMeshTriangle* Triangle = (RawTriangleData++); SmoothingArray.Add(Triangle->SmoothingMask); } RenderMesh.RawTriangles.Unlock(); #endif // #if TODO_FBX // Create and fill in the vertex color data source. const FColorVertexBuffer* ColorBufferToUse = ColorBuffer ? ColorBuffer : &RenderMesh.VertexBuffers.ColorVertexBuffer; uint32 ColorVertexCount = ColorBufferToUse->GetNumVertices(); // Only export vertex colors if they exist if (GetExportOptions()->VertexColor && ColorVertexCount > 0) { FbxLayerElementVertexColor* VertexColor = FbxLayerElementVertexColor::Create(Mesh, ""); VertexColor->SetMappingMode(FbxLayerElement::eByPolygonVertex); VertexColor->SetReferenceMode(FbxLayerElement::eIndexToDirect); FbxLayerElementArrayTemplate& VertexColorArray = VertexColor->GetDirectArray(); Layer->SetVertexColors(VertexColor); for (int32 FbxVertIndex = 0; FbxVertIndex < Indices.Num(); FbxVertIndex++) { FLinearColor VertColor(1.0f, 1.0f, 1.0f); uint32 UnrealVertIndex = Indices[FbxVertIndex]; if (UnrealVertIndex < ColorVertexCount) { VertColor = ColorBufferToUse->VertexColor(UnrealVertIndex).ReinterpretAsLinear(); } VertexColorArray.Add(FbxColor(VertColor.R, VertColor.G, VertColor.B, VertColor.A)); } VertexColor->GetIndexArray().SetCount(Indices.Num()); for (int32 FbxVertIndex = 0; FbxVertIndex < Indices.Num(); FbxVertIndex++) { VertexColor->GetIndexArray().SetAt(FbxVertIndex, FbxVertIndex); } } return true; } /** * Exports a static mesh * @param StaticMesh The static mesh to export * @param MeshName The name of the mesh for the FBX file * @param FbxActor The fbx node representing the mesh * @param ExportLOD The LOD of the mesh to export * @param LightmapUVChannel If set, performs a "lightmap export" and exports only the single given UV channel * @param ColorBuffer Vertex color overrides to export * @param MaterialOrderOverride Optional ordering of materials to set up correct material ID's across multiple meshes being export such as BSP surfaces which share common materials. Should be used sparingly */ FbxNode* FFbxExporter::ExportStaticMeshToFbx(const UStaticMesh* StaticMesh, int32 ExportLOD, const TCHAR* MeshName, FbxNode* FbxActor, const FFbxMaterialBakingMeshData& MaterialBakingMeshData, int32 LightmapUVChannel /*= -1*/, const FColorVertexBuffer* ColorBuffer /*= NULL*/, const TArray* MaterialOrderOverride /*= NULL*/, const TArray* OverrideMaterials /*= NULL*/) { FbxMesh* Mesh = nullptr; if ((ExportLOD == 0 || ExportLOD == -1) && LightmapUVChannel == -1 && ColorBuffer == nullptr && MaterialOrderOverride == nullptr) { Mesh = FbxMeshes.FindRef(StaticMesh); } if (!Mesh) { Mesh = FbxMesh::Create(Scene, TCHAR_TO_UTF8(MeshName)); int32 LodIndex = ExportLOD == INDEX_NONE ? 0 : ExportLOD; bool bExportSourceMesh = GetExportOptions()->bExportSourceMesh && !GetExportOptions()->LevelOfDetail && !GetExportOptions()->Collision; const bool bUseNaniteData = LodIndex == 0 && StaticMesh->IsNaniteEnabled() && StaticMesh->IsHiResMeshDescriptionValid(); const bool bUseLodData = !bUseNaniteData && StaticMesh->IsMeshDescriptionValid(LodIndex); bExportSourceMesh &= (bUseNaniteData || bUseLodData); if (bExportSourceMesh) { if (bUseNaniteData) { //Export the nanite mesh description if (!ExportStaticMeshFromMeshDescription(Mesh, StaticMesh, StaticMesh->GetHiResMeshDescription(), FbxActor, LightmapUVChannel, MaterialOrderOverride, OverrideMaterials, MaterialBakingMeshData)) { return nullptr; } } else { ensure(bUseLodData); //Export the lod mesh description if(!ExportStaticMeshFromMeshDescription(Mesh, StaticMesh, StaticMesh->GetMeshDescription(LodIndex), FbxActor, LightmapUVChannel, MaterialOrderOverride, OverrideMaterials, MaterialBakingMeshData)) { return nullptr; } } } else { //Export the render data const FStaticMeshLODResources& RenderMesh = StaticMesh->GetLODForExport(LodIndex); if (!ExportStaticMeshFromRenderData(Mesh, StaticMesh, RenderMesh, FbxActor, LightmapUVChannel, ColorBuffer, MaterialOrderOverride, OverrideMaterials, MaterialBakingMeshData)) { return nullptr; } } if (LodIndex == 0 && LightmapUVChannel == -1 && ColorBuffer == nullptr && MaterialOrderOverride == nullptr) { FbxMeshes.Add(StaticMesh, Mesh); } } else { //Materials in fbx are store in the node and not in the mesh, so even if the mesh was already export //we have to find and assign the mesh material. const FStaticMeshLODResources& RenderMesh = StaticMesh->GetLODForExport(ExportLOD); const int32 PolygonsCount = RenderMesh.Sections.Num(); uint32 AccountedTriangles = 0; for (int32 PolygonsIndex = 0; PolygonsIndex < PolygonsCount; ++PolygonsIndex) { const FStaticMeshSection& Polygons = RenderMesh.Sections[PolygonsIndex]; FIndexArrayView RawIndices = RenderMesh.IndexBuffer.GetArrayView(); UMaterialInterface* Material = nullptr; if (OverrideMaterials && OverrideMaterials->IsValidIndex(Polygons.MaterialIndex)) { Material = (*OverrideMaterials)[Polygons.MaterialIndex]; } else { Material = StaticMesh->GetMaterial(Polygons.MaterialIndex); } FbxSurfaceMaterial* FbxMaterial = Material ? ExportMaterial(Material, Polygons.MaterialIndex, MaterialBakingMeshData) : NULL; if (!FbxMaterial) { FbxMaterial = CreateDefaultMaterial(); } FbxActor->AddMaterial(FbxMaterial); } } if ((ExportLOD == 0 || ExportLOD == -1) && GetExportOptions()->Collision) { ExportCollisionMesh(StaticMesh, MeshName, FbxActor); } //Set the original meshes in case it was already existing FbxActor->SetNodeAttribute(Mesh); ExportObjectMetadata(StaticMesh, FbxActor); return FbxActor; } void FFbxExporter::ExportSplineMeshToFbx(const USplineMeshComponent* SplineMeshComp, const TCHAR* MeshName, FbxNode* FbxActor, const FFbxMaterialBakingMeshData& MaterialBakingMeshData) { const UStaticMesh* StaticMesh = SplineMeshComp->GetStaticMesh(); check(StaticMesh); const int32 LODIndex = (SplineMeshComp->ForcedLodModel > 0 ? FMath::Min(StaticMesh->GetNumLODs(), SplineMeshComp->ForcedLodModel) - 1 : /* auto-select*/ 0); if (!SplineMeshComp->LODData.IsValidIndex(LODIndex)) { return; } const FStaticMeshLODResources& RenderMesh = StaticMesh->GetLODForExport(LODIndex); // Verify the integrity of the static mesh. if (RenderMesh.VertexBuffers.StaticMeshVertexBuffer.GetNumVertices() == 0) { return; } if (RenderMesh.Sections.Num() == 0) { return; } // Remaps an Unreal vert to final reduced vertex list TArray VertRemap; TArray UniqueVerts; // Weld verts DetermineVertsToWeld(VertRemap, UniqueVerts, RenderMesh); FbxMesh* Mesh = FbxMesh::Create(Scene, TCHAR_TO_UTF8(MeshName)); // Create and fill in the vertex position data source. // The position vertices are duplicated, for some reason, retrieve only the first half vertices. const int32 VertexCount = VertRemap.Num(); const int32 PolygonsCount = RenderMesh.Sections.Num(); Mesh->InitControlPoints(UniqueVerts.Num()); FbxVector4* ControlPoints = Mesh->GetControlPoints(); for (int32 PosIndex = 0; PosIndex < UniqueVerts.Num(); ++PosIndex) { int32 UnrealPosIndex = UniqueVerts[PosIndex]; FVector Position = (FVector)RenderMesh.VertexBuffers.PositionVertexBuffer.VertexPosition(UnrealPosIndex); const FTransform SliceTransform = SplineMeshComp->CalcSliceTransform(USplineMeshComponent::GetAxisValueRef(Position, SplineMeshComp->ForwardAxis)); USplineMeshComponent::GetAxisValueRef(Position, SplineMeshComp->ForwardAxis) = 0; Position = SliceTransform.TransformPosition(Position); ControlPoints[PosIndex] = FbxVector4(Position.X, -Position.Y, Position.Z); } // Set the normals on Layer 0. FbxLayer* Layer = Mesh->GetLayer(0); if (Layer == NULL) { Mesh->CreateLayer(); Layer = Mesh->GetLayer(0); } // Build list of Indices re-used multiple times to lookup Normals, UVs, other per face vertex information TArray Indices; for (int32 PolygonsIndex = 0; PolygonsIndex < PolygonsCount; ++PolygonsIndex) { FIndexArrayView RawIndices = RenderMesh.IndexBuffer.GetArrayView(); const FStaticMeshSection& Polygons = RenderMesh.Sections[PolygonsIndex]; const uint32 TriangleCount = Polygons.NumTriangles; for (uint32 TriangleIndex = 0; TriangleIndex < TriangleCount; ++TriangleIndex) { for (uint32 PointIndex = 0; PointIndex < 3; PointIndex++) { uint32 UnrealVertIndex = RawIndices[Polygons.FirstIndex + ((TriangleIndex * 3) + PointIndex)]; Indices.Add(UnrealVertIndex); } } } // Create and fill in the per-face-vertex normal data source. // We extract the Z-tangent and drop the X/Y-tangents which are also stored in the render mesh. FbxLayerElementNormal* LayerElementNormal = FbxLayerElementNormal::Create(Mesh, ""); // Set 3 normals per triangle instead of storing normals on positional control points LayerElementNormal->SetMappingMode(FbxLayerElement::eByPolygonVertex); // Set the normal values for every polygon vertex. LayerElementNormal->SetReferenceMode(FbxLayerElement::eDirect); TArray FbxNormals; FbxNormals.AddUninitialized(VertexCount); for (int32 VertIndex = 0; VertIndex < VertexCount; ++VertIndex) { FVector Position = (FVector)RenderMesh.VertexBuffers.PositionVertexBuffer.VertexPosition(VertIndex); const FTransform SliceTransform = SplineMeshComp->CalcSliceTransform(USplineMeshComponent::GetAxisValueRef(Position, SplineMeshComp->ForwardAxis)); FVector Normal = FVector(RenderMesh.VertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(VertIndex)); Normal = SliceTransform.TransformVector(Normal); FbxVector4& FbxNormal = FbxNormals[VertIndex]; FbxNormal = FbxVector4(Normal.X, -Normal.Y, Normal.Z); FbxNormal.Normalize(); } // Add one normal per each face index (3 per triangle) for (uint32 UnrealVertIndex : Indices) { LayerElementNormal->GetDirectArray().Add(FbxNormals[UnrealVertIndex]); } Layer->SetNormals(LayerElementNormal); FbxNormals.Empty(); // Create and fill in the per-face-vertex texture coordinate data source(s). // Create UV for Diffuse channel. int32 TexCoordSourceCount = RenderMesh.VertexBuffers.StaticMeshVertexBuffer.GetNumTexCoords(); for (int32 TexCoordSourceIndex = 0; TexCoordSourceIndex < TexCoordSourceCount; ++TexCoordSourceIndex) { FbxLayer* UVsLayer = Mesh->GetLayer(TexCoordSourceIndex); if (UVsLayer == NULL) { Mesh->CreateLayer(); UVsLayer = Mesh->GetLayer(TexCoordSourceIndex); } FString UVChannelNameBuilder = TEXT("UVmap_") + FString::FromInt(TexCoordSourceIndex); const auto UVChannelNameUTF8 = TStringConversion(*UVChannelNameBuilder); // Do not inline it! The lifetime of this object needs to extend over the usage of the converted buffer. const char* UVChannelName = UVChannelNameUTF8.Get(); // actually UTF8 as required by Fbx, but can't use UE's UTF8CHAR type because that's a uint8 aka *unsigned* char if (TexCoordSourceIndex == StaticMesh->GetLightMapCoordinateIndex()) { UVChannelName = "LightMapUV"; } FbxLayerElementUV* UVDiffuseLayer = FbxLayerElementUV::Create(Mesh, UVChannelName); // Note: when eINDEX_TO_DIRECT is used, IndexArray must be 3xTriangle count, DirectArray can be smaller UVDiffuseLayer->SetMappingMode(FbxLayerElement::eByPolygonVertex); UVDiffuseLayer->SetReferenceMode(FbxLayerElement::eIndexToDirect); TArray UvsRemap; TArray UniqueUVs; // Weld UVs DetermineUVsToWeld(UvsRemap, UniqueUVs, RenderMesh.VertexBuffers.StaticMeshVertexBuffer, TexCoordSourceIndex); // Create the texture coordinate data source. for (int32 UnrealVertIndex : UniqueUVs) { const FVector2f& TexCoord = RenderMesh.VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(UnrealVertIndex, TexCoordSourceIndex); UVDiffuseLayer->GetDirectArray().Add(FbxVector2(TexCoord.X, -TexCoord.Y + 1.0)); } // For each face index, point to a texture uv UVDiffuseLayer->GetIndexArray().SetCount(Indices.Num()); for (int32 FbxVertIndex = 0; FbxVertIndex < Indices.Num(); FbxVertIndex++) { uint32 UnrealVertIndex = Indices[FbxVertIndex]; int32 NewVertIndex = UvsRemap[UnrealVertIndex]; UVDiffuseLayer->GetIndexArray().SetAt(FbxVertIndex, NewVertIndex); } UVsLayer->SetUVs(UVDiffuseLayer, FbxLayerElement::eTextureDiffuse); } FbxLayerElementMaterial* MatLayer = FbxLayerElementMaterial::Create(Mesh, ""); MatLayer->SetMappingMode(FbxLayerElement::eByPolygon); MatLayer->SetReferenceMode(FbxLayerElement::eIndexToDirect); Layer->SetMaterials(MatLayer); for (int32 PolygonsIndex = 0; PolygonsIndex < PolygonsCount; ++PolygonsIndex) { const FStaticMeshSection& Polygons = RenderMesh.Sections[PolygonsIndex]; FIndexArrayView RawIndices = RenderMesh.IndexBuffer.GetArrayView(); UMaterialInterface* Material = StaticMesh->GetMaterial(Polygons.MaterialIndex); FbxSurfaceMaterial* FbxMaterial = Material ? ExportMaterial(Material, Polygons.MaterialIndex, MaterialBakingMeshData) : NULL; if (!FbxMaterial) { FbxMaterial = CreateDefaultMaterial(); } int32 MatIndex = FbxActor->AddMaterial(FbxMaterial); // Static meshes contain one triangle list per element. uint32 TriangleCount = Polygons.NumTriangles; // Copy over the index buffer into the FBX polygons set. for (uint32 TriangleIndex = 0; TriangleIndex < TriangleCount; ++TriangleIndex) { Mesh->BeginPolygon(MatIndex); for (uint32 PointIndex = 0; PointIndex < 3; PointIndex++) { uint32 OriginalUnrealVertIndex = RawIndices[Polygons.FirstIndex + ((TriangleIndex * 3) + PointIndex)]; int32 RemappedVertIndex = VertRemap[OriginalUnrealVertIndex]; Mesh->AddPolygon(RemappedVertIndex); } Mesh->EndPolygon(); } } #ifdef TODO_FBX // This is broken. We are exporting the render mesh but providing smoothing // information from the source mesh. The render triangles are not in the // same order. Therefore we should export the raw mesh or not export // smoothing group information! int32 TriangleCount = RenderMesh.RawTriangles.GetElementCount(); FStaticMeshTriangle* RawTriangleData = (FStaticMeshTriangle*)RenderMesh.RawTriangles.Lock(LOCK_READ_ONLY); for (int32 TriangleIndex = 0; TriangleIndex < TriangleCount; TriangleIndex++) { FStaticMeshTriangle* Triangle = (RawTriangleData++); SmoothingArray.Add(Triangle->SmoothingMask); } RenderMesh.RawTriangles.Unlock(); #endif // #if TODO_FBX // Create and fill in the vertex color data source. const FColorVertexBuffer* ColorBufferToUse = &RenderMesh.VertexBuffers.ColorVertexBuffer; uint32 ColorVertexCount = ColorBufferToUse->GetNumVertices(); // Only export vertex colors if they exist if (GetExportOptions()->VertexColor && ColorVertexCount > 0) { FbxLayerElementVertexColor* VertexColor = FbxLayerElementVertexColor::Create(Mesh, ""); VertexColor->SetMappingMode(FbxLayerElement::eByPolygonVertex); VertexColor->SetReferenceMode(FbxLayerElement::eIndexToDirect); FbxLayerElementArrayTemplate& VertexColorArray = VertexColor->GetDirectArray(); Layer->SetVertexColors(VertexColor); for (int32 FbxVertIndex = 0; FbxVertIndex < Indices.Num(); FbxVertIndex++) { FLinearColor VertColor(1.0f, 1.0f, 1.0f); uint32 UnrealVertIndex = Indices[FbxVertIndex]; if (UnrealVertIndex < ColorVertexCount) { VertColor = ColorBufferToUse->VertexColor(UnrealVertIndex).ReinterpretAsLinear(); } VertexColorArray.Add(FbxColor(VertColor.R, VertColor.G, VertColor.B, VertColor.A)); } VertexColor->GetIndexArray().SetCount(Indices.Num()); for (int32 FbxVertIndex = 0; FbxVertIndex < Indices.Num(); FbxVertIndex++) { VertexColor->GetIndexArray().SetAt(FbxVertIndex, FbxVertIndex); } } FbxActor->SetNodeAttribute(Mesh); } void FFbxExporter::ExportInstancedMeshToFbx(const UInstancedStaticMeshComponent* InstancedMeshComp, const TCHAR* MeshName, FbxNode* FbxActor, const FFbxMaterialBakingMeshData& MaterialBakingMeshData) { const UStaticMesh* StaticMesh = InstancedMeshComp->GetStaticMesh(); check(StaticMesh); const int32 LODIndex = (InstancedMeshComp->ForcedLodModel > 0 ? FMath::Min(StaticMesh->GetNumLODs(), InstancedMeshComp->ForcedLodModel) - 1 : /* auto-select*/ 0); const int32 NumInstances = InstancedMeshComp->GetInstanceCount(); for (int32 InstanceIndex = 0; InstanceIndex < NumInstances; ++InstanceIndex) { FTransform RelativeTransform; if (ensure(InstancedMeshComp->GetInstanceTransform(InstanceIndex, RelativeTransform, /*bWorldSpace=*/false))) { FbxNode* InstNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*FString::Printf(TEXT("%d"), InstanceIndex))); InstNode->LclTranslation.Set(Converter.ConvertToFbxPos(RelativeTransform.GetTranslation())); InstNode->LclRotation.Set(Converter.ConvertToFbxRot(RelativeTransform.GetRotation().Euler())); InstNode->LclScaling.Set(Converter.ConvertToFbxScale(RelativeTransform.GetScale3D())); // Todo - export once and then clone the node const int32 LightmapUVChannel = -1; const TArray* MaterialOrderOverride = nullptr; const FColorVertexBuffer* ColorBuffer = nullptr; ExportStaticMeshToFbx(StaticMesh, LODIndex, *FString::Printf(TEXT("%d"), InstanceIndex), InstNode, MaterialBakingMeshData, LightmapUVChannel, ColorBuffer, MaterialOrderOverride, &ToRawPtrTArrayUnsafe(InstancedMeshComp->OverrideMaterials)); FbxActor->AddChild(InstNode); } } } /** * Exports a Landscape */ void FFbxExporter::ExportLandscapeToFbx(ALandscapeProxy* Landscape, const TCHAR* MeshName, FbxNode* FbxActor, bool bSelectedOnly) { const ULandscapeInfo* LandscapeInfo = Landscape->GetLandscapeInfo(); TSet SelectedComponents; if (bSelectedOnly && LandscapeInfo) { SelectedComponents = LandscapeInfo->GetSelectedComponents(); } bSelectedOnly = bSelectedOnly && SelectedComponents.Num() > 0; int32 MinX = MAX_int32, MinY = MAX_int32; int32 MaxX = MIN_int32, MaxY = MIN_int32; // Find range of entire landscape for (int32 ComponentIndex = 0; ComponentIndex < Landscape->LandscapeComponents.Num(); ComponentIndex++) { ULandscapeComponent* Component = Landscape->LandscapeComponents[ComponentIndex]; if (bSelectedOnly && !SelectedComponents.Contains(Component)) { continue; } Component->GetComponentExtent(MinX, MinY, MaxX, MaxY); } FbxMesh* Mesh = FbxMesh::Create(Scene, TCHAR_TO_UTF8(MeshName)); // Create and fill in the vertex position data source. const int32 ComponentSizeQuads = ((Landscape->ComponentSizeQuads + 1) >> Landscape->ExportLOD) - 1; const float ScaleFactor = (float)Landscape->ComponentSizeQuads / (float)ComponentSizeQuads; const int32 NumComponents = bSelectedOnly ? SelectedComponents.Num() : Landscape->LandscapeComponents.Num(); const int32 VertexCountPerComponent = FMath::Square(ComponentSizeQuads + 1); const int32 VertexCount = NumComponents * VertexCountPerComponent; const int32 TriangleCount = NumComponents * FMath::Square(ComponentSizeQuads) * 2; Mesh->InitControlPoints(VertexCount); // Normals and Tangents FbxLayerElementNormal* LayerElementNormals= FbxLayerElementNormal::Create(Mesh, ""); LayerElementNormals->SetMappingMode(FbxLayerElement::eByControlPoint); LayerElementNormals->SetReferenceMode(FbxLayerElement::eDirect); FbxLayerElementTangent* LayerElementTangents= FbxLayerElementTangent::Create(Mesh, ""); LayerElementTangents->SetMappingMode(FbxLayerElement::eByControlPoint); LayerElementTangents->SetReferenceMode(FbxLayerElement::eDirect); FbxLayerElementBinormal* LayerElementBinormals= FbxLayerElementBinormal::Create(Mesh, ""); LayerElementBinormals->SetMappingMode(FbxLayerElement::eByControlPoint); LayerElementBinormals->SetReferenceMode(FbxLayerElement::eDirect); // Add Texture UVs (which are simply incremented 1.0 per vertex) FbxLayerElementUV* LayerElementTextureUVs = FbxLayerElementUV::Create(Mesh, "TextureUVs"); LayerElementTextureUVs->SetMappingMode(FbxLayerElement::eByControlPoint); LayerElementTextureUVs->SetReferenceMode(FbxLayerElement::eDirect); // Add Weightmap UVs (to match up with an exported weightmap, not the original weightmap UVs, which are per-component) const FVector2D UVScale = FVector2D(1.0f, 1.0f) / FVector2D((MaxX - MinX) + 1, (MaxY - MinY) + 1); FbxLayerElementUV* LayerElementWeightmapUVs = FbxLayerElementUV::Create(Mesh, "WeightmapUVs"); LayerElementWeightmapUVs->SetMappingMode(FbxLayerElement::eByControlPoint); LayerElementWeightmapUVs->SetReferenceMode(FbxLayerElement::eDirect); FbxVector4* ControlPoints = Mesh->GetControlPoints(); FbxLayerElementArrayTemplate& Normals = LayerElementNormals->GetDirectArray(); Normals.Resize(VertexCount); FbxLayerElementArrayTemplate& Tangents = LayerElementTangents->GetDirectArray(); Tangents.Resize(VertexCount); FbxLayerElementArrayTemplate& Binormals = LayerElementBinormals->GetDirectArray(); Binormals.Resize(VertexCount); FbxLayerElementArrayTemplate& TextureUVs = LayerElementTextureUVs->GetDirectArray(); TextureUVs.Resize(VertexCount); FbxLayerElementArrayTemplate& WeightmapUVs = LayerElementWeightmapUVs->GetDirectArray(); WeightmapUVs.Resize(VertexCount); TArray VisibilityData; VisibilityData.Empty(VertexCount); VisibilityData.AddZeroed(VertexCount); for (int32 ComponentIndex = 0, SelectedComponentIndex = 0; ComponentIndex < Landscape->LandscapeComponents.Num(); ComponentIndex++) { ULandscapeComponent* Component = Landscape->LandscapeComponents[ComponentIndex]; if (bSelectedOnly && !SelectedComponents.Contains(Component)) { continue; } FLandscapeComponentDataInterface CDI(Component, Landscape->ExportLOD); const int32 BaseVertIndex = SelectedComponentIndex++ * VertexCountPerComponent; const TArray& ComponentWeightmapLayerAllocations = Component->GetWeightmapLayerAllocations(); TArray CompVisData; for (int32 AllocIdx = 0; AllocIdx < ComponentWeightmapLayerAllocations.Num(); AllocIdx++) { const FWeightmapLayerAllocationInfo& AllocInfo = ComponentWeightmapLayerAllocations[AllocIdx]; if (AllocInfo.LayerInfo == ALandscapeProxy::VisibilityLayer) { CDI.GetWeightmapTextureData(AllocInfo.LayerInfo, CompVisData); } } if (CompVisData.Num() > 0) { for (int32 i = 0; i < VertexCountPerComponent; ++i) { VisibilityData[BaseVertIndex + i] = CompVisData[CDI.VertexIndexToTexel(i)]; } } for (int32 VertIndex = 0; VertIndex < VertexCountPerComponent; VertIndex++) { int32 VertX, VertY; CDI.VertexIndexToXY(VertIndex, VertX, VertY); FVector Position = CDI.GetLocalVertex(VertX, VertY) + Component->GetRelativeLocation(); FbxVector4 FbxPosition = FbxVector4(Position.X, -Position.Y, Position.Z); ControlPoints[BaseVertIndex + VertIndex] = FbxPosition; FVector Normal, TangentX, TangentY; CDI.GetLocalTangentVectors(VertX, VertY, TangentX, TangentY, Normal); Normal /= Component->GetComponentTransform().GetScale3D(); Normal.Normalize(); TangentX /= Component->GetComponentTransform().GetScale3D(); TangentX.Normalize(); TangentY /= Component->GetComponentTransform().GetScale3D(); TangentY.Normalize(); FbxVector4 FbxNormal = FbxVector4(Normal.X, -Normal.Y, Normal.Z); FbxNormal.Normalize(); Normals.SetAt(BaseVertIndex + VertIndex, FbxNormal); FbxVector4 FbxTangent = FbxVector4(TangentX.X, -TangentX.Y, TangentX.Z); FbxTangent.Normalize(); Tangents.SetAt(BaseVertIndex + VertIndex, FbxTangent); FbxVector4 FbxBinormal = FbxVector4(TangentY.X, -TangentY.Y, TangentY.Z); FbxBinormal.Normalize(); Binormals.SetAt(BaseVertIndex + VertIndex, FbxBinormal); FVector2D TextureUV = FVector2D(VertX * ScaleFactor + Component->GetSectionBase().X, VertY * ScaleFactor + Component->GetSectionBase().Y); FbxVector2 FbxTextureUV = FbxVector2(TextureUV.X, TextureUV.Y); TextureUVs.SetAt(BaseVertIndex + VertIndex, FbxTextureUV); FVector2D WeightmapUV = (TextureUV - FVector2D(MinX, MinY)) * UVScale; FbxVector2 FbxWeightmapUV = FbxVector2(WeightmapUV.X, WeightmapUV.Y); WeightmapUVs.SetAt(BaseVertIndex + VertIndex, FbxWeightmapUV); } } FbxLayer* Layer0 = Mesh->GetLayer(0); if (Layer0 == NULL) { Mesh->CreateLayer(); Layer0 = Mesh->GetLayer(0); } Layer0->SetNormals(LayerElementNormals); Layer0->SetTangents(LayerElementTangents); Layer0->SetBinormals(LayerElementBinormals); Layer0->SetUVs(LayerElementTextureUVs); Layer0->SetUVs(LayerElementWeightmapUVs, FbxLayerElement::eTextureBump); // this doesn't seem to work, on import the mesh has no smoothing layer at all //FbxLayerElementSmoothing* SmoothingInfo = FbxLayerElementSmoothing::Create(Mesh, ""); //SmoothingInfo->SetMappingMode(FbxLayerElement::eAllSame); //SmoothingInfo->SetReferenceMode(FbxLayerElement::eDirect); //FbxLayerElementArrayTemplate& Smoothing = SmoothingInfo->GetDirectArray(); //Smoothing.Add(0); //Layer0->SetSmoothing(SmoothingInfo); FbxLayerElementMaterial* LayerElementMaterials = FbxLayerElementMaterial::Create(Mesh, ""); LayerElementMaterials->SetMappingMode(FbxLayerElement::eAllSame); LayerElementMaterials->SetReferenceMode(FbxLayerElement::eIndexToDirect); Layer0->SetMaterials(LayerElementMaterials); UMaterialInterface* Material = Landscape->GetLandscapeMaterial(); FbxSurfaceMaterial* FbxMaterial = Material ? ExportMaterial(Material, 0, FFbxMaterialBakingMeshData()) : NULL; if (!FbxMaterial) { FbxMaterial = CreateDefaultMaterial(); } const int32 FbxMaterialIndex = FbxActor->AddMaterial(FbxMaterial); LayerElementMaterials->GetIndexArray().Add(FbxMaterialIndex); const int32 VisThreshold = 170; // Copy over the index buffer into the FBX polygons set. for (int32 ComponentIndex = 0; ComponentIndex < NumComponents; ComponentIndex++) { int32 BaseVertIndex = ComponentIndex * VertexCountPerComponent; for (int32 Y = 0; Y < ComponentSizeQuads; Y++) { for (int32 X = 0; X < ComponentSizeQuads; X++) { if (VisibilityData[BaseVertIndex + Y * (ComponentSizeQuads + 1) + X] < VisThreshold) { Mesh->BeginPolygon(); Mesh->AddPolygon(BaseVertIndex + (X + 0) + (Y + 0)*(ComponentSizeQuads + 1)); Mesh->AddPolygon(BaseVertIndex + (X + 1) + (Y + 1)*(ComponentSizeQuads + 1)); Mesh->AddPolygon(BaseVertIndex + (X + 1) + (Y + 0)*(ComponentSizeQuads + 1)); Mesh->EndPolygon(); Mesh->BeginPolygon(); Mesh->AddPolygon(BaseVertIndex + (X + 0) + (Y + 0)*(ComponentSizeQuads + 1)); Mesh->AddPolygon(BaseVertIndex + (X + 0) + (Y + 1)*(ComponentSizeQuads + 1)); Mesh->AddPolygon(BaseVertIndex + (X + 1) + (Y + 1)*(ComponentSizeQuads + 1)); Mesh->EndPolygon(); } } } } FbxActor->SetNodeAttribute(Mesh); } } // namespace UnFbx