// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "DatasmithMaxExporterDefines.h" #include "DatasmithMaxConverters.h" #include "DatasmithMaxGeomUtils.h" #include "IDatasmithSceneElements.h" #include "DatasmithMesh.h" #include "DatasmithUtils.h" #include "Logging/LogMacros.h" DECLARE_LOG_CATEGORY_EXTERN(LogDatasmithMaxExporter, Log, All); #include "Windows/AllowWindowsPlatformTypes.h" MAX_INCLUDES_START #include "impexp.h" #include "max.h" #include "decomp.h" // for matrix #include "ISceneEventManager.h" MAX_INCLUDES_END // Log all messages // #define LOG_DEBUG_HEAVY_ENABLE 1 // #define LOG_DEBUG_ENABLE 1 // #define CANCEL_DEBUG_ENABLE 1 void AssignMeshMaterials(TSharedPtr&MeshElement, Mtl * Material, const TSet&SupportedChannels); class FDatasmithMaxStaticMeshAttributes; namespace DatasmithMaxDirectLink { class ISceneTracker; class FLayerTracker; class FMaterialsCollectionTracker; /** Global export options, stored in preferences */ class IPersistentExportOptions { public: /** Whether to export animation or not */ virtual void SetAnimatedTransforms(bool) = 0; virtual bool GetAnimatedTransforms() = 0; /** Whether to output export statistics to listener/log */ virtual void SetStatSync(bool) = 0; virtual bool GetStatSync() = 0; virtual void SetTextureResolution(int32 Bool) = 0; virtual int32 GetTextureResolution() = 0; virtual void SetXRefScenes(bool) = 0; virtual bool GetXRefScenes() = 0; }; /** Main class for export/change tracking */ class IExporter { public: virtual ~IExporter() {} virtual void Shutdown() = 0; virtual void SetOutputPath(const TCHAR* Path) = 0; virtual void SetName(const TCHAR* Name) = 0; virtual ISceneTracker& GetSceneTracker() = 0; virtual void InitializeScene() = 0; /** Scene update */ virtual bool UpdateScene(bool bQuiet) = 0; virtual void ResetSceneTracking() = 0; /** ChangeTracking */ virtual void StartSceneChangeTracking() = 0; /** DirectLink */ virtual void InitializeDirectLinkForScene() = 0; virtual void UpdateDirectLinkScene() = 0; virtual bool ToggleAutoSync() = 0; virtual bool IsAutoSyncEnabled() = 0; virtual void SetAutoSyncDelay(float Seconds) = 0; virtual void SetAutoSyncIdleDelay(float Seconds) = 0; virtual void PerformSync(bool bQuiet) = 0; }; /** Create exporter with ability for DirectLink change tracking */ bool CreateExporter(bool bEnableUI, const TCHAR* EnginePath); IExporter* GetExporter(); void ShutdownExporter(); bool Export(const TCHAR* Name, const TCHAR* OutputPath, bool bQuiet, bool bSelected); void ShutdownScripts(); IPersistentExportOptions& GetPersistentExportOptions(); // Max nodes are matched by these keys typedef NodeEventNamespace::NodeKey FNodeKey; typedef MtlBase* FMaterialKey; struct FXRefScene { INode* Tree = nullptr; int32 XRefFileIndex = -1; operator bool(){ return XRefFileIndex != -1; } }; /** Incapsulating time slider value to distinguish usage of TimeValue specifically for point on timeslider where we syncing */ struct FSyncPoint { TimeValue Time; }; /** Incapsulating time interval value to distinguish usage of Interval specifically for validity of synced entity */ struct FValidity { explicit FValidity() { Invalidate(); // Invalid by default(to essentially force update(validate explicitly) } bool IsInvalidated() const { return bIsInvalidated; } void SetValid() { bIsInvalidated = false; } void Invalidate() { bIsInvalidated = true; ValidityInterval.SetEmpty(); } bool IsValidForSyncPoint(const FSyncPoint& State) const { return ValidityInterval.InInterval(State.Time) != 0; } /** Maximize validity interval before updating invalidated */ void ResetValidityInterval() { check(bIsInvalidated); ValidityInterval.SetInfinite(); } /** Intersect current validity interval with the param*/ void NarrowValidityToInterval(const Interval& InValidityInterval) { ValidityInterval &= InValidityInterval; } void NarrowValidityToInterval(const FValidity& Validity) { NarrowValidityToInterval(Validity.ValidityInterval); } /** This Validity contains another validity interval fully. Used to determine if this validity doesn't need to be updated @return True is this validity doesn't need to be updated */ bool Overlaps(const FValidity& Validity) const { return ValidityInterval.IsSubset(Validity.ValidityInterval); } private: Interval ValidityInterval; // todo: this flag indicates that ValidityInterval is determined to be recalculated // this is done in order to distinguish from Empty Interval that might happen somehow when updated // Until ValidityInterval is recalculated bIsInvalidated stays - this supports cancelling Update // at any point. So on the next Update nodes with bIsInvalidated are updated(again, if their update wasn't finished) bool bIsInvalidated; }; /** NodeTracker - associated with INode, plus validity Converter - holds convertion data for SPECIFIC node object type, e.g. geometry, railclone, light, dummy Converted - holds Datasmith converted data for node */ /** Identifies Max node to track its changes, including tracked dependencies */ class FNodeTracker: FNoncopyable { public: explicit FNodeTracker(FNodeKey InNodeKey, INode* InNode) : NodeKey(InNodeKey), Node(InNode) {} void SetXRefIndex(FXRefScene InXRefScene) { XRefScene = InXRefScene; } INode* GetXRefParent() { INode* Result = XRefScene ? XRefScene.Tree->GetXRefParent(XRefScene.XRefFileIndex): nullptr; return Result; } FNodeConverted& CreateConverted() { check(!Converted); Converted.Reset(new FNodeConverted); return *Converted; } FNodeConverted& GetConverted() { check(Converted); return *Converted; } bool HasConverted() { return Converted.IsValid(); } void ReleaseConverted() { Converted.Reset(); } template ConverterType& CreateConverter() { check(!Converter); ConverterType* Result = new ConverterType; Converter.Reset(Result); return *Result; } FNodeConverter& GetConverter() { check(Converter); return *Converter; } bool HasConverter() { return Converter.IsValid(); } FNodeConverter::EType GetConverterType() { if (Converter.IsValid()) { return Converter->ConverterType; } return FNodeConverter::Unknown; } void ReleaseConverter() { Converter.Reset(); } /** Node entity identification data */ FNodeKey NodeKey; INode* const Node; /** Keep root node and xref index when this node is a direct child of an XRef scene This is needed to retrieve parent node(e.g. when updated) Keeping parent node itself doesn't work - it can change and the only way to get it when it's changed is to call INode::GetXRefParent */ FXRefScene XRefScene; FString Name; /** Node validity */ bool bParsed = false; bool bDeleted = false; FValidity Validity; FValidity SubtreeValidity; /** Other related tracked entities */ FNodeTracker* Collision = nullptr; FLayerTracker* Layer = nullptr; TSet MaterialTrackers; /** Node conversion state */ /** Datasmith element that this Node is converted to */ TUniquePtr Converted = nullptr; /** Converter for specific Max object type */ TUniquePtr Converter = nullptr; }; class FMeshConverterSource { public: /** Node this mesh instantiates. When this is a 'regular' node it's just instantiates mesh for Params but it's possible that this Node wants mesh to be bounding-box(when DatasmithAttributes spicifies it) */ INode* Node; /** Suggested Mesh name */ FString MeshName; /*Extracted render mesh*/ GeomUtils::FRenderMeshForConversion RenderMesh; /** Whether to join all materials id into single material slot for render mesh (used when a geometry doesn't have a multimaterial assigned) */ bool bConsolidateMaterialIds; GeomUtils::FRenderMeshForConversion CollisionMesh; Interval GetValidityInterval() { return RenderMesh.GetValidityInterval() & CollisionMesh.GetValidityInterval(); } // Whether this will make a mesh bool IsValid() { return RenderMesh.IsValid(); } }; struct FSceneUpdateStats { void Reset() { *this = FSceneUpdateStats(); } // Explicitly initialize every stat int32 ParseSceneXRefFileEncountered = 0; int32 ParseSceneXRefFileDisabled = 0; int32 ParseSceneXRefFileMissing = 0; int32 ParseSceneXRefFileToParse = 0; int32 ParseNodeNodesEncountered = 0; int32 RemoveDeletedNodesNodes = 0; int32 RefreshCollisionsChangedNodes = 0; int32 UpdateNodeNodesUpdated = 0; int32 UpdateNodeSkippedAsCollisionNode = 0; int32 UpdateNodeSkippedAsHiddenNode = 0; int32 UpdateNodeSkippedAsUnselected = 0; int32 UpdateNodeGeomObjEncontered = 0; int32 UpdateNodeHelpersEncontered = 0; int32 UpdateNodeCamerasEncontered = 0; int32 UpdateNodeLightsEncontered = 0; int32 UpdateNodeLightsSkippedAsUnknown = 0; int32 UpdateNodeGeomObjSkippedAsNonRenderable = 0; int32 UpdateNodeGeomObjConverted = 0; int32 ReparentActorsSkippedWithoutDatasmithActor = 0; int32 ReparentActorsAttached = 0; int32 ReparentActorsAttachedToRoot = 0; int32 ProcessInvalidatedMaterialsInvalidated = 0; int32 ProcessInvalidatedMaterialsActualToUpdate = 0; int32 UpdateMaterialsTotal = 0; int32 UpdateMaterialsSkippedAsAlreadyConverted =0; int32 UpdateMaterialsConverted = 0; int32 UpdateTexturesTotal = 0; int32 CheckTimeSliderTotalChecks = 0; int32 CheckTimeSliderSkippedAsAlreadyInvalidated = 0; int32 CheckTimeSliderSkippedAsSubtreeValid = 0; int32 CheckTimeSliderInvalidated = 0; int32 ConvertNodesConverted = 0; int32 UpdateInstancesGeometryUpdated = 0; int32 UpdateTexturesBaked = 0; int64 UpdateTexturesBakedPixels = 0; }; #define SCENE_UPDATE_STAT_INC(Category, Name) {Stats.##Category##Name++;} #define SCENE_UPDATE_STAT_SET(Category, Name, Value) Stats.##Category##Name = Value #define SCENE_UPDATE_STAT_GET(Category, Name) Stats.##Category##Name /** Modifies Datasmith scene in responce to change notification calls Subscription to various Max notification systems is done separately, see FNotifications */ class ISceneTracker { public: virtual ~ISceneTracker(){} /** Change notifications */ virtual void NodeAdded(INode* Node) = 0; virtual void NodeDeleted(INode* Node) = 0; virtual void NodeGeometryChanged(INode* Node) = 0; virtual void NodeHideChanged(INode* Node) = 0; virtual void NodeNameChanged(FNodeKey NodeKey) = 0; virtual void NodePropertiesChanged(INode* Node) = 0; virtual void NodeLinkChanged(FNodeKey NodeKey) = 0; virtual void NodeTransformChanged(INode* Node) = 0; virtual void NodeMaterialAssignmentChanged(FNodeKey NodeKey) = 0; virtual void NodeMaterialAssignmentChanged(INode* Node) = 0; virtual void NodeMaterialGraphModified(FNodeKey NodeKey) = 0; virtual void NodeMaterialGraphModified(INode* NodeKey) = 0; virtual void MaterialGraphModified(Mtl* Material) = 0; virtual void HideByCategoryChanged() = 0; virtual bool IsUpdateInProgress() = 0; /** Scene modification*/ virtual void AddMeshElement(TSharedPtr& Mesh, FDatasmithMesh& DatasmithMesh, FDatasmithMesh* CollisionMesh) = 0; virtual void ReleaseMeshElement(FMeshConverted& Converted) = 0; virtual void SetupActor(FNodeTracker& NodeTracker) = 0; virtual void SetupDatasmithHISMForNode(FNodeTracker& NodeTracker, FMeshConverterSource& MeshSource, Mtl* Material, int32 MeshIndex, const TArray& Transforms) = 0; virtual void RemoveMaterial(const TSharedPtr& DatasmithMaterial) = 0; virtual void RemoveTexture(const TSharedPtr&) = 0; virtual void NodeXRefMerged(INode* Node) = 0; virtual void RemapConvertedMaterialUVChannels(Mtl* ActualMaterial, const TSharedPtr& DatasmithMaterial) = 0; /** Sync/Update */ FSyncPoint CurrentSyncPoint; virtual void AddGeometryNodeInstance(FNodeTracker& NodeTracker, FMeshNodeConverter& MeshConverter, Object* Obj) = 0; virtual void RemoveGeometryNodeInstance(FNodeTracker& NodeTracker) = 0; virtual void ConvertGeometryNodeToDatasmith(FNodeTracker& NodeTracker, FMeshNodeConverter& MeshConverter) = 0; virtual void UnregisterNodeForMaterial(FNodeTracker& NodeTracker) = 0; virtual const TCHAR* AcquireIesTexture(const FString& IesFilePath) = 0; virtual void ReleaseIesTexture(const FString& IesFilePath) = 0; virtual TSharedRef GetDatasmithSceneRef() = 0; virtual const TCHAR* GetAssetsOutputPath() = 0; /** Utility */ virtual FNodeTracker* GetNodeTrackerByNodeName(const TCHAR* Name) = 0; virtual FSceneUpdateStats& GetStats() = 0; }; /** Input data for mesh conversion*/ struct MeshConversionParams { /** Node, this geom object created from */ INode* Node; /** Extracted render mesh */ const GeomUtils::FRenderMeshForConversion& RenderMesh; /** Whether to join all materials id into single material slot (used when a geometry doesn't have a multimaterial assigned) */ bool bConsolidateMaterialIds; }; /** Holds all the data to export mesh to DatasmithMeshElement */ class FDatasmithMeshConverter { public: FMD5Hash ComputeHash(); FDatasmithMesh RenderMesh; TSet SupportedChannels; TMap UVChannelsMap; int32 SelectedLightmapUVChannel = -1; FDatasmithMesh CollisionMesh; bool bHasCollision = false; FMeshConverted Converted; }; /** Creates Mesh element and converts max mesh into it */ bool ConvertMaxMeshToDatasmith(TimeValue CurrentTime, FMeshConverterSource& MeshSource, FDatasmithMeshConverter& MeshConverter); bool OpenDirectLinkUI(); const TCHAR* GetDirectlinkCacheDirectory(); /** Max Notifications/Events/Callbacks handling */ class FNodeEventCallback; class FNodeObserver; class FMaterialObserver; class FNotifications { public: FNotifications(IExporter& InExporter); ~FNotifications(); void RegisterForSystemNotifications(); void StartSceneChangeTracking(); void StopSceneChangeTracking(); void AddNode(INode*); void AddMaterial(Mtl*); FString ConvertNotificationCodeToString(int code); void PrepareForUpdate(); static void On3dsMaxNotification(void* param, NotifyInfo* info); bool bSceneChangeTracking = false; IExporter& Exporter; TMap NotificationCodetoString; // todo: remove, just for debug to output strings for notification codes TArray NotificationCodesRegistered; TUniquePtr NodeEventCallback; TUniquePtr NodeObserver; TUniquePtr MaterialObserver; }; class FDatasmithConverter { public: const float UnitToCentimeter; FDatasmithConverter(); FVector toDatasmithVector(Point3 Point) const { return FVector(UnitToCentimeter * Point.x, UnitToCentimeter * -Point.y, UnitToCentimeter * Point.z); } FVector toDatasmithNormal(Point3 Point) const { return FVector(Point.x, -Point.y, Point.z); } FColor toDatasmithColor(Point3& Point) { //The 3ds Max vertex colors are encoded from 0 to 1 in float return FColor(Point.x * MAX_uint8, Point.y * MAX_uint8, Point.z * MAX_uint8); } void MaxToUnrealCoordinates(Matrix3 Matrix, FVector& Translation, FQuat& Rotation, FVector& Scale) { Point3 Pos = Matrix.GetTrans(); Translation.X = Pos.x * UnitToCentimeter; Translation.Y = -Pos.y * UnitToCentimeter; Translation.Z = Pos.z * UnitToCentimeter; // Clear the transform on the matrix Matrix.NoTrans(); // We're only doing Scale - save out the // rotation so we can put it back AffineParts Parts; decomp_affine(Matrix, &Parts); ScaleValue ScaleVal = ScaleValue(Parts.k * Parts.f, Parts.u); Scale = FVector(ScaleVal.s.x, ScaleVal.s.y, ScaleVal.s.z); Rotation = FQuat(Parts.q.x, -Parts.q.y, Parts.q.z, Parts.q.w); } }; /** Material change tracking */ class FMaterialTracker { public: explicit FMaterialTracker(Mtl* InMaterial) : Material(InMaterial) {} Mtl* Material; /*Actual materials used for this assigned material*/ TArray Materials; TArray Textures; bool bInvalidated = true; TArray& GetActualMaterials() { return Materials; } TArray& GetActualTexmaps() { return Textures; } void ResetActualMaterialAndTextures() { Materials.Reset(); Textures.Reset(); } void AddActualMaterial(Mtl* ActualMaterial) { Materials.AddUnique(ActualMaterial); } void AddActualTexture(Texmap* Texture) { Textures.AddUnique(Texture); } }; class FMaterialTrackerHandle { public: explicit FMaterialTrackerHandle(Mtl* InMaterial) : Impl(MakeShared(InMaterial)) {} FMaterialTracker* GetMaterialTracker() { return Impl.Get(); } private: TSharedPtr Impl; // todo: reuse material tracker objects(e.g. make a pool) FMaterialTracker& GetImpl() { return *Impl; } }; /** Converts texmap to texture element This is different from IDatasmithMaxTexmapToUEPbr which performs texmap conversion to full MaterialExpression which is used in material graph IDatasmithMaxTexmapToUEPbr uses ToTextureElementConverter when it needs a texture element */ class ITexmapToTextureElementConverter { public: virtual ~ITexmapToTextureElementConverter(){} virtual TSharedPtr Convert(FMaterialsCollectionTracker& MaterialsTracker, const FString& ActualBitmapName) = 0; FString TextureElementName; }; class FMaterialsCollectionTracker { public: FMaterialsCollectionTracker(ISceneTracker& InSceneTracker) : SceneTracker(InSceneTracker), Stats(InSceneTracker.GetStats()) {} void Reset(); /** Add material to track its changes*/ FMaterialTracker* AddMaterial(Mtl* Material); /** When Material is not used in scene anymore - stop tracking it*/ void ReleaseMaterial(FMaterialTracker& MaterialTracker); /** Mark changed*/ void InvalidateMaterial(Mtl* Material); /** Reparse source material*/ void UpdateMaterial(FMaterialTracker* MaterialTracker); /** Convert to Datasmith */ void ConvertMaterial(Mtl* Material, TSharedRef DatasmithScene, const TCHAR* AssetsPath, TSet& TexmapsConverted); /** Clean all converted data, remove from datasmith scene(e.g. before rebuilding material) */ void RemoveConvertedMaterial(FMaterialTracker& MaterialTracker); /** Add material used in a specific tracked material's graph */ void AddActualMaterial(FMaterialTracker& MaterialTracker, Mtl* Material); /** Reverse of AddActualMaterial*/ void ReleaseActualMaterial(FMaterialTracker& MaterialTracker, Mtl* Material); /** Get name used for Datasmith material. Datasmith material names must be unique(used to identify elements) */ const TCHAR* GetMaterialName(Mtl* SubMaterial); /** If material is used on a mesh/actor to create a datamsith element */ bool IsMaterialUsed(Mtl* Material); /** Set datasmith materials to Datasmith static mesh according to assigned max material */ void SetMaterialsForMeshElement(FMeshConverted& MeshConverted, Mtl* Material); void SetMaterialsForMeshElement(const TSharedPtr& MeshElement, Mtl* Material, const TSet& SupportedChannels); void SetMaterialForMeshElement(const TSharedPtr& MeshElement, Mtl* MaterialAssignedToNode, Mtl* ActualMaterial, uint16 SlotId); void UnSetMaterialsForMeshElement(const TSharedPtr& MeshElement); /** Set datasmith materials for mesh actor for assigned max material */ void SetMaterialsForMeshActor(const TSharedPtr& MeshActor, Mtl* Material, TSet& SupportedChannels, const FVector3f& RandomSeed); void SetMaterialForMeshActorElement(const TSharedPtr& MeshActor, Mtl* MaterialAssignedToNode, Mtl* Material, int32 SlotId); void UnSetMaterialsForMeshActor(const TSharedPtr&); /** Record which Datasmith material was created for a Max material, not only for tracked(assigned) materials */ void AddDatasmithMaterialForUsedMaterial(TSharedRef DatasmithScene, Mtl* Material, TSharedPtr DatasmithMaterial); void RemoveDatasmithMaterialForUsedMaterial(Mtl* Material); void ResetInvalidatedMaterials() { InvalidatedMaterialTrackers.Reset(); } TSet GetInvalidatedMaterials() { return InvalidatedMaterialTrackers; } void UpdateTexmap(Texmap* Texmap); /** Adds a texmap that need to be converted to texture element */ void AddTexmapForConversion(Texmap* Texmap, const FString& DesiredTextureElementName, const TSharedPtr& Converter); void AddTextureElement(const TSharedPtr& ); void RemoveTextureElement(const TSharedPtr& TextureElement); ISceneTracker& SceneTracker; /** Tracks all assigned materials */ TMap MaterialTrackers; /** Materials needing update */ TSet InvalidatedMaterialTrackers; /** 3ds Max Materials can be: - Tracked. When a material is assigned to a 3ds Max node. Assigned materials are tracked for changes by the plugin. - Used. When a material is used to create a DatasmithMaterialElement associated with it and this element is used by a DatasmithStaticMesh or MeshActor - Actual. Tracked + Used + - Tracked material won't necessarily be Used, for various reasons, e.g. it can be a composite material(in this case only its submaterials are be Used). Or material is assigned but all geometry is invisible. Used materials can be missing in Tracked - a submaterial of an MultiSubobj material assigned to node is not assigned directly. */ /** Tracked material for every Actual material (i.e. set of material which use an Mtl as a submaterial ) */ TMap> ActualMaterialToMaterialTracker; /** All materials that have a corresponding Datasmith material created */ TMap> UsedMaterialToDatasmithMaterial; /** Cached unique name for each used material for Datasmith Element */ TMap UsedMaterialToDatasmithMaterialName; /** Track materials used for static meshes */ TMap>> UsedMaterialToMeshElementSlot; TMap>> MeshElementToUsedMaterialSlot; /** Track materials used for overrides on mesh actors */ TMap>> UsedMaterialToMeshActorSlot; TMap>> MeshActorToUsedMaterialSlot; /** Texmaps tracking */ /** Materials uses by nodes keep set of assigned textures they are used for */ TMap> UsedTextureToMaterialTracker; /** Keep track of Datasmith Element created for texmap to simplify update/removal(no need to search DatasmithScene) */ TMap>> UsedTextureToDatasmithElement; /** Back reference - which texture elements correspond to which texmaps(different texmaps can make same element) */ TMap, TSet> TextureElementToTexmap; TMap> TexmapConverters; /** Bitmap textures using the same file become same texture element */ TMap> BitmapTextureElements; TSet> TextureElementsAddedToScene; FSceneUpdateStats& Stats; FDatasmithUniqueNameProvider MaterialNameProvider; }; /** Logging */ void LogFlush(); void LogError(const TCHAR* Msg); void LogWarning(const TCHAR* Msg); void LogCompletion(const TCHAR* Msg); void LogInfo(const TCHAR* Msg); void LogError(const FString& Msg); void LogWarning(const FString& Msg); void LogCompletion(const FString& Msg); void LogInfo(const FString& Msg); void LogErrorDialog(const FString& Msg); void LogWarningDialog(const FString& Msg); void LogCompletionDialog(const FString& Msg); void LogInfoDialog(const FString& Msg); /** Debug logging */ void LogDebugImpl(const FString& Msg); void LogDebugDialog(const FString& Msg); #ifdef LOG_DEBUG_HEAVY_ENABLE #define LOG_DEBUG_HEAVY(message) LogDebug(message) #else #define LOG_DEBUG_HEAVY(message) #endif void LogDebugNode(const FString& Name, class INode* Node); void LogNodeEvent(const MCHAR* Name, INodeEventCallback::NodeKeyTab& Nodes); #ifdef LOG_DEBUG_ENABLE #define LogDebug(Msg) LogDebugImpl(Msg) #else #define LogDebug(Msg) #endif } #include "Windows/HideWindowsPlatformTypes.h"