// Copyright Epic Games, Inc. All Rights Reserved. #include "BakeMultiMeshAttributeMapsTool.h" #include "InteractiveToolManager.h" #include "ToolBuilderUtil.h" #include "DynamicMesh/DynamicMesh3.h" #include "DynamicMesh/DynamicMeshAttributeSet.h" #include "DynamicMesh/MeshTransforms.h" #include "MeshDescriptionToDynamicMesh.h" #include "Sampling/MeshNormalMapEvaluator.h" #include "Sampling/MeshPropertyMapEvaluator.h" #include "Sampling/MeshResampleImageEvaluator.h" #include "ImageUtils.h" #include "AssetUtils/Texture2DUtil.h" #include "EngineAnalytics.h" #include "TargetInterfaces/MaterialProvider.h" #include "TargetInterfaces/MeshDescriptionProvider.h" #include "TargetInterfaces/PrimitiveComponentBackedTarget.h" #include "TargetInterfaces/StaticMeshBackedTarget.h" #include "ToolTargetManager.h" #include "ModelingToolTargetUtil.h" // required to pass UStaticMesh asset so we can save at same location #include "Engine/Classes/Engine/StaticMesh.h" using namespace UE::Geometry; #define LOCTEXT_NAMESPACE "UBakeMultiMeshAttributeMapsTool" /* * ToolBuilder */ const FToolTargetTypeRequirements& UBakeMultiMeshAttributeMapsToolBuilder::GetTargetRequirements() const { static FToolTargetTypeRequirements TypeRequirements({ UMeshDescriptionProvider::StaticClass(), UPrimitiveComponentBackedTarget::StaticClass(), UStaticMeshBackedTarget::StaticClass(), // FMeshSceneAdapter currently only supports StaticMesh targets UMaterialProvider::StaticClass() }); return TypeRequirements; } bool UBakeMultiMeshAttributeMapsToolBuilder::CanBuildTool(const FToolBuilderState& SceneState) const { const int32 NumTargets = SceneState.TargetManager->CountSelectedAndTargetable(SceneState, GetTargetRequirements()); return (NumTargets > 1); } UMultiSelectionMeshEditingTool* UBakeMultiMeshAttributeMapsToolBuilder::CreateNewTool(const FToolBuilderState& SceneState) const { return NewObject(SceneState.ToolManager); } /** * MeshSceneAdapter bake detail sampler for baking N detail meshes to 1 target mesh. */ class FMeshBakerMeshSceneSampler : public IMeshBakerDetailSampler { public: using FDetailTextureMap = TMap; public: /** * Constructor. * * Input FMeshSceneAdapter's spatial evaluation cache is assumed to be built. */ FMeshBakerMeshSceneSampler(FMeshSceneAdapter* Scene) : MeshScene(Scene) { } // Begin IMeshBakerDetailSampler interface virtual void ProcessMeshes(TFunctionRef ProcessFn) const override { auto ProcessChildMesh = [ProcessFn](const FActorAdapter*, const FActorChildMesh* ChildMesh) { if (ChildMesh) { ProcessFn(ChildMesh->MeshSpatial); } }; MeshScene->ProcessActorChildMeshes(ProcessChildMesh); } virtual int32 GetTriangleCount(const void* Mesh) const override { const IMeshSpatialWrapper* Spatial = static_cast(Mesh); return Spatial->GetTriangleCount(); } virtual void SetTextureMap(const void* Mesh, const FBakeDetailTexture& Map) override { DetailTextureMaps[Mesh] = Map; } virtual void SetNormalMap(const void* Mesh, const FBakeDetailTexture& Map) override { DetailNormalMaps[Mesh] = Map; } virtual const FBakeDetailTexture* GetTextureMap(const void* Mesh) const override { return DetailTextureMaps.Find(Mesh); } virtual const FBakeDetailTexture* GetNormalMap(const void* Mesh) const override { return DetailNormalMaps.Find(Mesh); } virtual bool SupportsIdentityCorrespondence() const override { return false; } virtual bool SupportsNearestPointCorrespondence() const override { return false; } virtual bool SupportsRaycastCorrespondence() const override { return true; } virtual const void* FindNearestHitTriangle( const FRay3d& Ray, double& NearestT, int& TriId, FVector3d& TriBaryCoords, const IMeshSpatial::FQueryOptions& Options = IMeshSpatial::FQueryOptions()) const override { // TODO: Pass-through max distance to FMeshSceneAdapter query FMeshSceneRayHit HitResult; const bool bHit = MeshScene->FindNearestRayIntersection(Ray, HitResult); const void* HitMesh = nullptr; // Use TNumericLimits::Max() for consistency with MeshAABBTree3. NearestT = (Options.MaxDistance < TNumericLimits::Max()) ? Options.MaxDistance : TNumericLimits::Max(); if (bHit && HitResult.RayDistance < Options.MaxDistance) { TriId = HitResult.HitMeshTriIndex; NearestT = HitResult.RayDistance; TriBaryCoords = HitResult.HitMeshBaryCoords; HitMesh = HitResult.HitMeshSpatialWrapper; } return HitMesh; } virtual bool TestAnyHitTriangle( const FRay3d& Ray, const IMeshSpatial::FQueryOptions& Options = IMeshSpatial::FQueryOptions()) const override { // TODO: Add proper test any hit triangle support (for Occlusion) // TODO: Pass through max distance to FMeshSceneAdapter query double NearestT = TNumericLimits::Max(); int TriId = IndexConstants::InvalidID; FVector3d TriBaryCoords; return FindNearestHitTriangle(Ray, NearestT, TriId, TriBaryCoords, Options) != nullptr; } virtual FAxisAlignedBox3d GetBounds() const override { return MeshScene->GetBoundingBox(); } virtual bool IsTriangle(const void* Mesh, const int TriId) const override { const IMeshSpatialWrapper* Spatial = static_cast(Mesh); return Spatial->IsTriangle(TriId); } virtual FIndex3i GetTriangle(const void* Mesh, const int TriId) const override { const IMeshSpatialWrapper* Spatial = static_cast(Mesh); return Spatial->GetTriangle(TriId); } virtual FVector3d GetTriNormal(const void* Mesh, const int TriId) const override { // TODO checkSlow(false); return FVector3d::Zero(); } virtual int32 GetMaterialID(const void* Mesh, const int TriId) const override { // TODO checkSlow(false); return IndexConstants::InvalidID; } virtual bool HasNormals(const void* Mesh) const override { const IMeshSpatialWrapper* Spatial = static_cast(Mesh); return Spatial->HasNormals(); } virtual bool HasUVs(const void* Mesh, const int UVLayer = 0) const override { const IMeshSpatialWrapper* Spatial = static_cast(Mesh); return Spatial->HasUVs(UVLayer); } virtual bool HasTangents(const void* Mesh) const override { // TODO checkSlow(false); return false; } virtual bool HasColors(const void* Mesh) const override { // TODO checkSlow(false); return false; } virtual FVector3d TriBaryInterpolatePoint( const void* Mesh, const int32 TriId, const FVector3d& BaryCoords) const override { const IMeshSpatialWrapper* Spatial = static_cast(Mesh); return Spatial->TriBaryInterpolatePoint(TriId, BaryCoords); } virtual bool TriBaryInterpolateNormal( const void* Mesh, const int32 TriId, const FVector3d& BaryCoords, FVector3f& NormalOut) const override { const IMeshSpatialWrapper* Spatial = static_cast(Mesh); return Spatial->TriBaryInterpolateNormal(TriId, BaryCoords, NormalOut); } virtual bool TriBaryInterpolateUV( const void* Mesh, const int32 TriId, const FVector3d& BaryCoords, const int UVLayer, FVector2f& UVOut ) const override { const IMeshSpatialWrapper* Spatial = static_cast(Mesh); return Spatial->TriBaryInterpolateUV(TriId, BaryCoords, UVLayer, UVOut); } virtual bool TriBaryInterpolateColor( const void* Mesh, const int32 TriId, const FVector3d& BaryCoords, FVector4f& ColorOut) const override { // TODO checkSlow(false); return false; } virtual bool TriBaryInterpolateTangents( const void* Mesh, const int32 TriId, const FVector3d& BaryCoords, FVector3d& TangentX, FVector3d& TangentY) const override { // TODO checkSlow(false); return false; } virtual void GetCurvature( const void* Mesh, FMeshVertexCurvatureCache& CurvatureCache) const override { // TODO checkSlow(false); } // End IMeshBakerDetailSampler interface /** Initialize the mesh to color textures map */ void SetTextureMaps(const FDetailTextureMap& Map) { DetailTextureMaps = Map; } /** Initialize the mesh to normal textures map */ void SetNormalMaps(const FDetailTextureMap& Map) { DetailNormalMaps = Map; } protected: FMeshSceneAdapter* MeshScene = nullptr; FDetailTextureMap DetailTextureMaps; FDetailTextureMap DetailNormalMaps; }; /* * Operators */ class FMultiMeshMapBakerOp : public TGenericDataOperator { public: // General bake settings FMeshSceneAdapter* DetailMeshScene = nullptr; UE::Geometry::FDynamicMesh3* BaseMesh; TSharedPtr BaseMeshTangents; TUniquePtr Baker; UBakeMultiMeshAttributeMapsTool::FBakeSettings BakeSettings; // Detail bake data TArray>> CachedColorImages; UBakeMultiMeshAttributeMapsTool::FTextureImageMap CachedMeshToColorImageMap; // Begin TGenericDataOperator interface virtual void CalculateResult(FProgressCancel* Progress) override { Baker = MakeUnique(); Baker->CancelF = [Progress]() { return Progress && Progress->Cancelled(); }; Baker->SetTargetMesh(BaseMesh); Baker->SetTargetMeshUVLayer(BakeSettings.TargetUVLayer); Baker->SetDimensions(BakeSettings.Dimensions); Baker->SetProjectionDistance(BakeSettings.ProjectionDistance); Baker->SetSamplesPerPixel(BakeSettings.SamplesPerPixel); Baker->SetTargetMeshTangents(BaseMeshTangents); FMeshBakerMeshSceneSampler DetailSampler(DetailMeshScene); Baker->SetDetailSampler(&DetailSampler); for (const EBakeMapType MapType : ENUM_EBAKEMAPTYPE_ALL) { switch (BakeSettings.BakeMapTypes & MapType) { case EBakeMapType::TangentSpaceNormal: { TSharedPtr NormalEval = MakeShared(); Baker->AddEvaluator(NormalEval); break; } case EBakeMapType::Texture: { TSharedPtr TextureEval = MakeShared(); DetailSampler.SetTextureMaps(CachedMeshToColorImageMap); Baker->AddEvaluator(TextureEval); break; } default: break; } } Baker->Bake(); SetResult(MoveTemp(Baker)); } // End TGenericDataOperator interface }; /* * Tool */ void UBakeMultiMeshAttributeMapsTool::Setup() { TRACE_CPUPROFILER_EVENT_SCOPE(UBakeMultiMeshAttributeMapsTool::Setup); Super::Setup(); // Initialize base mesh const UE::Geometry::FTransform3d BaseToWorld = UE::ToolTarget::GetLocalToWorldTransform(Targets[0]); PreviewMesh->ProcessMesh([this, BaseToWorld](const FDynamicMesh3& Mesh) { TargetMesh.Copy(Mesh); TargetMeshTangents = MakeShared(&TargetMesh); TargetMeshTangents->CopyTriVertexTangents(Mesh); // FMeshSceneAdapter operates in world space, so ensure our mesh transformed to world. MeshTransforms::ApplyTransform(TargetMesh, BaseToWorld); TargetSpatial.SetMesh(&TargetMesh, true); }); // Initialize detail sampler const int NumTargets = Targets.Num(); TArray DetailComponents; for (int Idx = 1; Idx < NumTargets; ++Idx) { if (UPrimitiveComponent* Component = UE::ToolTarget::GetTargetComponent(Targets[Idx])) { DetailComponents.Add(Component); } } DetailMeshScene.AddComponents(DetailComponents); DetailMeshScene.Build(FMeshSceneAdapterBuildOptions()); DetailMeshScene.BuildSpatialEvaluationCache(); UToolTarget* Target = Targets[0]; // Setup tool property sets Settings = NewObject(this); Settings->RestoreProperties(this); AddToolPropertySource(Settings); Settings->WatchProperty(Settings->MapTypes, [this](int32) { OpState |= EBakeOpState::Evaluate; UpdateOnModeChange(); }); Settings->WatchProperty(Settings->MapPreview, [this](FString) { UpdateVisualization(); GetToolManager()->PostInvalidation(); }); Settings->WatchProperty(Settings->Resolution, [this](EBakeTextureResolution) { OpState |= EBakeOpState::Evaluate; }); Settings->WatchProperty(Settings->BitDepth, [this](EBakeTextureBitDepth) { OpState |= EBakeOpState::Evaluate; }); Settings->WatchProperty(Settings->SamplesPerPixel, [this](EBakeTextureSamplesPerPixel) { OpState |= EBakeOpState::Evaluate; }); InputMeshSettings = NewObject(this); InputMeshSettings->RestoreProperties(this); AddToolPropertySource(InputMeshSettings); InputMeshSettings->TargetStaticMesh = GetStaticMeshTarget(Target); UpdateUVLayerNames(InputMeshSettings->TargetUVLayer, InputMeshSettings->TargetUVLayerNamesList, TargetMesh); InputMeshSettings->WatchProperty(InputMeshSettings->TargetUVLayer, [this](FString) { OpState |= EBakeOpState::Evaluate; }); InputMeshSettings->WatchProperty(InputMeshSettings->ProjectionDistance, [this](float) { OpState |= EBakeOpState::Evaluate; }); ResultSettings = NewObject(this); ResultSettings->RestoreProperties(this); AddToolPropertySource(ResultSettings); SetToolPropertySourceEnabled(ResultSettings, true); // Pre-populate detail mesh data TArray DetailColorTextures; for (int Idx = 1; Idx < NumTargets; ++Idx) { UToolTarget* DetailTarget = Targets[Idx]; // Hide each of our detail targets since this baker operates solely in world space // which will occlude the preview of the target mesh. UE::ToolTarget::HideSourceObject(DetailTarget); const UPrimitiveComponent* Component = UE::ToolTarget::GetTargetComponent(Targets[Idx]); UTexture2D* DetailColorTexture = nullptr; ProcessComponentTextures(Component, [&DetailColorTexture](const int NumMaterials, const int MaterialID, const TArray& Textures) { // TODO: Support multiple materialIDs per detail mesh if (MaterialID == 0) { const int SelectedTextureIndex = SelectColorTextureToBake(Textures); if (SelectedTextureIndex >= 0) { DetailColorTexture = Cast(Textures[SelectedTextureIndex]); } } }); FBakeMultiMeshDetailProperties SourceMeshProp; SourceMeshProp.SourceMesh = GetStaticMeshTarget(DetailTarget); SourceMeshProp.SourceTexture = DetailColorTexture; InputMeshSettings->SourceMeshes.Add(SourceMeshProp); InputMeshSettings->WatchProperty(InputMeshSettings->SourceMeshes[Idx-1].SourceTexture, [this](UTexture2D*) { OpState |= EBakeOpState::Evaluate; }); InputMeshSettings->WatchProperty(InputMeshSettings->SourceMeshes[Idx-1].SourceTextureUVLayer, [this](int) { OpState |= EBakeOpState::Evaluate; }); } UpdateOnModeChange(); OpState |= EBakeOpState::Evaluate; SetToolDisplayName(LOCTEXT("ToolName", "Bake Textures")); GetToolManager()->DisplayMessage( LOCTEXT("OnStartTool", "Bake Maps. Select Bake Mesh (LowPoly) first, then select Detail Meshes (HiPoly) to bake. Texture Assets will be created on Accept. "), EToolMessageLevel::UserNotification); PostSetup(); } bool UBakeMultiMeshAttributeMapsTool::CanAccept() const { const bool bValidOp = (OpState & EBakeOpState::Invalid) != EBakeOpState::Invalid; bool bCanAccept = bValidOp && Compute ? Compute->HaveValidResult() : false; if (bCanAccept) { // Allow Accept if all non-None types have valid results. for (const TTuple>& Result : ResultSettings->Result) { bCanAccept = bCanAccept && Result.Get<1>(); } } return bCanAccept; } TUniquePtr> UBakeMultiMeshAttributeMapsTool::MakeNewOperator() { TUniquePtr Op = MakeUnique(); Op->DetailMeshScene = &DetailMeshScene; Op->BaseMesh = &TargetMesh; Op->BakeSettings = CachedBakeSettings; constexpr EBakeMapType RequiresTangents = EBakeMapType::TangentSpaceNormal | EBakeMapType::BentNormal; if (static_cast(CachedBakeSettings.BakeMapTypes & RequiresTangents)) { Op->BaseMeshTangents = TargetMeshTangents; } if (static_cast(CachedBakeSettings.BakeMapTypes & EBakeMapType::Texture)) { Op->CachedColorImages = CachedColorImages; Op->CachedMeshToColorImageMap = CachedMeshToColorImagesMap; } return Op; } void UBakeMultiMeshAttributeMapsTool::Shutdown(EToolShutdownType ShutdownType) { TRACE_CPUPROFILER_EVENT_SCOPE(UBakeMultiMeshAttributeMapsTool::Shutdown); Super::Shutdown(ShutdownType); Settings->SaveProperties(this); InputMeshSettings->SaveProperties(this); if (Compute) { Compute->Shutdown(); } // Restore visibility of detail targets const int NumTargets = Targets.Num(); for (int Idx = 1; Idx < NumTargets; ++Idx) { UE::ToolTarget::ShowSourceObject(Targets[Idx]); } if (ShutdownType == EToolShutdownType::Accept) { IStaticMeshBackedTarget* StaticMeshTarget = Cast(Targets[0]); UObject* SourceAsset = StaticMeshTarget ? StaticMeshTarget->GetStaticMesh() : nullptr; const UPrimitiveComponent* SourceComponent = UE::ToolTarget::GetTargetComponent(Targets[0]); CreateTextureAssets(ResultSettings->Result, SourceComponent->GetWorld(), SourceAsset); } } void UBakeMultiMeshAttributeMapsTool::UpdateResult() { if (OpState == EBakeOpState::Clean) { return; } // clear warning (ugh) GetToolManager()->DisplayMessage(FText(), EToolMessageLevel::UserWarning); const int32 ImageSize = static_cast(Settings->Resolution); const FImageDimensions Dimensions(ImageSize, ImageSize); FBakeSettings BakeSettings; BakeSettings.Dimensions = Dimensions; BakeSettings.BitDepth = Settings->BitDepth; BakeSettings.TargetUVLayer = InputMeshSettings->TargetUVLayerNamesList.IndexOfByKey(InputMeshSettings->TargetUVLayer); BakeSettings.ProjectionDistance = InputMeshSettings->ProjectionDistance; BakeSettings.bProjectionInWorldSpace = true; // Always world space BakeSettings.SamplesPerPixel = static_cast(Settings->SamplesPerPixel); // Record the original map types and process the raw bitfield which may add // additional targets. BakeSettings.SourceBakeMapTypes = static_cast(Settings->MapTypes); BakeSettings.BakeMapTypes = GetMapTypes(Settings->MapTypes); // update bake cache settings if (!(CachedBakeSettings == BakeSettings)) { CachedBakeSettings = BakeSettings; CachedDetailSettings = FBakeMultiMeshDetailSettings(); } // Clear our invalid bitflag to check again for valid inputs. OpState &= ~EBakeOpState::Invalid; // Update map type settings OpState |= UpdateResult_DetailMeshes(); if ((bool)(CachedBakeSettings.BakeMapTypes & EBakeMapType::TangentSpaceNormal)) { OpState |= UpdateResult_Normal(CachedBakeSettings.Dimensions); } // Early exit if op input parameters are invalid. if (static_cast(OpState & EBakeOpState::Invalid)) { InvalidateResults(); return; } // This should be the only point of compute invalidation to // minimize synchronization issues. InvalidateCompute(); } EBakeOpState UBakeMultiMeshAttributeMapsTool::UpdateResult_DetailMeshes() { FBakeMultiMeshDetailSettings NewSettings; // Iterate through our detail properties to build our detail mesh data. const int32 NumDetail = InputMeshSettings->SourceMeshes.Num(); CachedColorImages.SetNum(NumDetail); CachedColorUVLayers.SetNum(NumDetail); TMap ActorToDataMap; for (int Idx = 0; Idx < NumDetail; ++Idx) { UActorComponent* ActorComponent = UE::ToolTarget::GetTargetComponent(Targets[Idx+1]); ActorToDataMap.Emplace(ActorComponent, Idx); // Color map data if (static_cast(CachedBakeSettings.BakeMapTypes & EBakeMapType::Texture)) { UTexture2D* ColorMapSourceTexture = InputMeshSettings->SourceMeshes[Idx].SourceTexture; const int ColorMapUVLayer = InputMeshSettings->SourceMeshes[Idx].SourceTextureUVLayer; if (!ColorMapSourceTexture) { GetToolManager()->DisplayMessage(LOCTEXT("InvalidTextureWarning", "The Source Texture is not valid"), EToolMessageLevel::UserWarning); return EBakeOpState::Invalid; } TSharedPtr, ESPMode::ThreadSafe> ColorTextureImage = MakeShared, ESPMode::ThreadSafe>(); if (!UE::AssetUtils::ReadTexture(ColorMapSourceTexture, *ColorTextureImage, bPreferPlatformData)) { GetToolManager()->DisplayMessage(LOCTEXT("CannotReadTextureWarning", "Cannot read from the source texture"), EToolMessageLevel::UserWarning); return EBakeOpState::Invalid; } CachedColorImages[Idx] = ColorTextureImage; CachedColorUVLayers[Idx] = ColorMapUVLayer; } } // Iterate through mesh scene adapter and build mesh to data maps. CachedMeshToColorImagesMap.Empty(); auto BuildMeshToDataMaps = [this, ActorToDataMap](const FActorAdapter*, const FActorChildMesh* ChildMesh) { if (ChildMesh) { if (const int* DataIndex = ActorToDataMap.Find(ChildMesh->SourceComponent)) { if (static_cast(CachedBakeSettings.BakeMapTypes & EBakeMapType::Texture)) { CachedMeshToColorImagesMap.Emplace( ChildMesh->MeshSpatial, FTextureImageData(CachedColorImages[*DataIndex].Get(), CachedColorUVLayers[*DataIndex])); } } } }; DetailMeshScene.ProcessActorChildMeshes(BuildMeshToDataMaps); // This method will always force a re-evaluation. return EBakeOpState::Evaluate; } void UBakeMultiMeshAttributeMapsTool::UpdateVisualization() { PreviewMesh->SetOverrideRenderMaterial(PreviewMaterial); // Populate Settings->Result from CachedMaps for (const TTuple>& Map : CachedMaps) { if (ResultSettings->Result.Contains(Map.Get<0>())) { ResultSettings->Result[Map.Get<0>()] = Map.Get<1>(); } } UpdatePreview(Settings->MapPreview, Settings->MapPreviewNamesMap); } void UBakeMultiMeshAttributeMapsTool::UpdateOnModeChange() { OnMapTypesUpdated( static_cast(Settings->MapTypes), ResultSettings->Result, Settings->MapPreview, Settings->MapPreviewNamesList, Settings->MapPreviewNamesMap); } void UBakeMultiMeshAttributeMapsTool::InvalidateResults() { for (TTuple>& Result : ResultSettings->Result) { Result.Get<1>() = nullptr; } } void UBakeMultiMeshAttributeMapsTool::GatherAnalytics(FBakeAnalytics::FMeshSettings& Data) { if (FEngineAnalytics::IsAvailable()) { Data.NumTargetMeshTris = TargetMesh.TriangleCount(); Data.NumDetailMesh = 0; Data.NumDetailMeshTris = 0; DetailMeshScene.ProcessActorChildMeshes([&Data](const FActorAdapter* ActorAdapter, const FActorChildMesh* ChildMesh) { if (ChildMesh) { ++Data.NumDetailMesh; Data.NumDetailMeshTris += ChildMesh->MeshSpatial->GetTriangleCount(); } }); } } #undef LOCTEXT_NAMESPACE