// Copyright Epic Games, Inc. All Rights Reserved. #include "MuT/CodeGenerator.h" #include "ASTOpMeshTransformWithBoundingMesh.h" #include "Containers/Array.h" #include "Logging/LogCategory.h" #include "Logging/LogMacros.h" #include "Math/IntPoint.h" #include "Math/UnrealMathUtility.h" #include "MuR/ImagePrivate.h" #include "MuR/Layout.h" #include "MuR/MutableTrace.h" #include "MuR/Operations.h" #include "MuR/ParametersPrivate.h" #include "MuR/Platform.h" #include "MuT/ASTOpAddLOD.h" #include "MuT/ASTOpAddExtensionData.h" #include "MuT/ASTOpAddOverlayMaterial.h" #include "MuT/ASTOpConditional.h" #include "MuT/ASTOpConstantBool.h" #include "MuT/ASTOpConstantResource.h" #include "MuT/ASTOpBoolAnd.h" #include "MuT/ASTOpImageCompose.h" #include "MuT/ASTOpImageMipmap.h" #include "MuT/ASTOpImagePixelFormat.h" #include "MuT/ASTOpImageSwizzle.h" #include "MuT/ASTOpImageLayer.h" #include "MuT/ASTOpImageLayerColor.h" #include "MuT/ASTOpImagePatch.h" #include "MuT/ASTOpImageCrop.h" #include "MuT/ASTOpImageBlankLayout.h" #include "MuT/ASTOpImagePlainColor.h" #include "MuT/ASTOpInstanceAdd.h" #include "MuT/ASTOpMeshBindShape.h" #include "MuT/ASTOpMeshClipDeform.h" #include "MuT/ASTOpMeshClipMorphPlane.h" #include "MuT/ASTOpMeshMaskClipMesh.h" #include "MuT/ASTOpMeshMaskClipUVMask.h" #include "MuT/ASTOpMeshRemoveMask.h" #include "MuT/ASTOpMeshDifference.h" #include "MuT/ASTOpMeshMorph.h" #include "MuT/ASTOpMeshOptimizeSkinning.h" #include "MuT/ASTOpMeshExtractLayoutBlocks.h" #include "MuT/ASTOpMeshApplyLayout.h" #include "MuT/ASTOpMeshMerge.h" #include "MuT/ASTOpMeshMaskDiff.h" #include "MuT/ASTOpParameter.h" #include "MuT/ASTOpLayoutRemoveBlocks.h" #include "MuT/ASTOpLayoutFromMesh.h" #include "MuT/ASTOpLayoutMerge.h" #include "MuT/ASTOpLayoutPack.h" #include "MuT/ASTOpReferenceResource.h" #include "MuT/CodeGenerator_SecondPass.h" #include "MuT/CodeOptimiser.h" #include "MuT/CompilerPrivate.h" #include "MuT/ErrorLog.h" #include "MuT/NodeColour.h" #include "MuT/NodeColourConstant.h" #include "MuT/NodeComponent.h" #include "MuT/NodeComponentSwitch.h" #include "MuT/NodeComponentVariation.h" #include "MuT/NodeImage.h" #include "MuT/NodeImageConstant.h" #include "MuT/NodeImageFormat.h" #include "MuT/NodeImageMipmap.h" #include "MuT/NodeImageSwizzle.h" #include "MuT/NodeMatrixConstant.h" #include "MuT/NodeMesh.h" #include "MuT/NodeMeshClipMorphPlane.h" #include "MuT/NodeMeshClipWithMesh.h" #include "MuT/NodeMeshFormat.h" #include "MuT/NodeMeshFragment.h" #include "MuT/NodeMeshMorph.h" #include "MuT/NodeMeshReshape.h" #include "MuT/NodeMeshConstant.h" #include "MuT/NodeModifier.h" #include "MuT/NodeModifierMeshClipDeform.h" #include "MuT/NodeModifierMeshClipMorphPlane.h" #include "MuT/NodeModifierMeshClipWithMesh.h" #include "MuT/NodeModifierMeshClipWithUVMask.h" #include "MuT/NodeModifierMeshTransformInMesh.h" #include "MuT/NodeModifierSurfaceEdit.h" #include "MuT/NodeObject.h" #include "MuT/NodeObjectNew.h" #include "MuT/NodeRange.h" #include "MuT/NodeRangeFromScalar.h" #include "MuT/NodeScalar.h" #include "MuT/NodeScalarConstant.h" #include "MuT/NodeScalarEnumParameter.h" #include "MuT/NodeSurface.h" #include "MuT/NodeSurfaceNew.h" #include "MuT/NodeSurfaceVariation.h" #include "MuT/NodeSurfaceSwitch.h" #include "MuT/TablePrivate.h" #include "Trace/Detail/Channel.h" namespace mu { CodeGenerator::CodeGenerator(CompilerOptions::Private* Options, TFunction& InWaitCallback) : WaitCallback(InWaitCallback), LocalPipe(TEXT("CodeGeneratorPipe")), GenerateMeshConstantPipe(TEXT("GenerateMeshConstantPipe")) { CompilerOptions = Options; // Create the message log ErrorLog = MakeShared(); } void CodeGenerator::GenerateRoot(const Ptr InNode) { MUTABLE_CPUPROFILER_SCOPE(Generate); // First pass FirstPass.Generate(ErrorLog, InNode.get(), CompilerOptions->bIgnoreStates, this); // Second pass SecondPassGenerator SecondPass(&FirstPass, CompilerOptions); bool bSuccess = SecondPass.Generate(ErrorLog, InNode.get()); if (!bSuccess) { return; } // Main pass for each state { MUTABLE_CPUPROFILER_SCOPE(MainPass); int32 CurrentStateIndex = 0; for (const FObjectState& State : FirstPass.States) { MUTABLE_CPUPROFILER_SCOPE(MainPassState); FGenericGenerationOptions Options; Options.State = CurrentStateIndex; FGenericGenerationResult Result; GenerateGeneric(Options, Result, InNode.get()); Ptr StateRoot = Result.Op; States.Emplace(State, StateRoot); ++CurrentStateIndex; } } } void CodeGenerator::GenerateGeneric(const FGenericGenerationOptions& Options, FGenericGenerationResult& OutResult, const Ptr InNode ) { if (!InNode) { return; } // Type-specific generation if (InNode->GetType()->IsA(NodeObject::GetStaticType())) { const NodeObject* ObjectNode = static_cast(InNode.get()); FObjectGenerationOptions ObjectOptions; ObjectOptions.ActiveTags = Options.ActiveTags; ObjectOptions.bIsImage = Options.bIsImage; ObjectOptions.State = Options.State; FObjectGenerationResult ObjectResult; GenerateObject(ObjectOptions, ObjectResult, ObjectNode); OutResult.Op = ObjectResult.Op; return; } else if (InNode->GetType()->IsA(NodeScalar::GetStaticType())) { const NodeScalar* ScalarNode = static_cast(InNode.get()); FScalarGenerationResult ScalarResult; GenerateScalar(ScalarResult, Options, ScalarNode); OutResult.Op = ScalarResult.op; return; } else if (InNode->GetType()->IsA(NodeColour::GetStaticType())) { const NodeColour* ColorNode = static_cast(InNode.get()); FColorGenerationResult Result; GenerateColor(Result, Options, ColorNode); OutResult.Op = Result.op; return; } else if (InNode->GetType()->IsA(NodeProjector::GetStaticType())) { const NodeProjector* projNode = static_cast(InNode.get()); FProjectorGenerationResult ProjResult; GenerateProjector(ProjResult, Options, projNode); OutResult.Op = ProjResult.op; return; } else if (InNode->GetType()->IsA(NodeSurfaceNew::GetStaticType())) { // This no longer happens with the current tools. check(false); return; } else if (InNode->GetType()->IsA(NodeSurfaceVariation::GetStaticType())) { // This happens only if we generate a node graph that has a NodeSurfaceVariation at the root. return; } else if (InNode->GetType()->IsA(NodeSurfaceSwitch::GetStaticType())) { // This happens only if we generate a node graph that has a NodeSurfaceSwitch at the root. return; } else if (InNode->GetType()->IsA(NodeModifier::GetStaticType())) { // This happens only if we generate a node graph that has a modifier at the root. return; } else if (InNode->GetType()->IsA(NodeComponent::GetStaticType())) { const NodeComponent* ComponentNode = static_cast(InNode.get()); FComponentGenerationOptions ComponentOptions(Options, nullptr); GenerateComponent(ComponentOptions, OutResult, ComponentNode); return; } else { // Unsupported node. check(false); } } void CodeGenerator::GenerateObject(const FObjectGenerationOptions& InOptions, FObjectGenerationResult& OutResult, const NodeObject* InUntypedNode) { if (!InUntypedNode) { OutResult = {}; return; } // See if it was already generated FGeneratedObjectCacheKey Key; Key.Node = InUntypedNode; Key.Options = InOptions; FGeneratedObjectsMap::ValueType* Found = GeneratedObjects.Find(Key); if (Found) { OutResult = *Found; return; } // Generate for each different type of node const FNodeType* Type = InUntypedNode->GetType(); if (Type == NodeObjectNew::GetStaticType()) { GenerateObject_New(InOptions, OutResult, static_cast(InUntypedNode)); } else if (Type == NodeObjectGroup::GetStaticType()) { GenerateObject_Group(InOptions, OutResult, static_cast(InUntypedNode)); } else { check(false); } // Cache the result GeneratedObjects.Add(Key, OutResult); } void CodeGenerator::GenerateRange(FRangeGenerationResult& Result, const FGenericGenerationOptions& Options, Ptr Untyped) { if (!Untyped) { Result = FRangeGenerationResult(); return; } // See if it was already generated FGeneratedCacheKey Key; Key.Node = Untyped; Key.Options = Options; { UE::TUniqueLock Lock(GeneratedRanges.Mutex); FGeneratedRangeMap::ValueType* Found = GeneratedRanges.Map.Find(Key); if (Found) { Result = *Found; return; } } // Generate for each different type of node if (Untyped->GetType()==NodeRangeFromScalar::GetStaticType()) { const NodeRangeFromScalar* FromScalar = static_cast(Untyped.get()); Result = FRangeGenerationResult(); Result.rangeName = FromScalar->Name; FScalarGenerationResult ChildResult; GenerateScalar(ChildResult, Options, FromScalar->Size); Result.sizeOp = ChildResult.op; } else { check(false); } // Cache the result { UE::TUniqueLock Lock(GeneratedRanges.Mutex); GeneratedRanges.Map.Add(Key, Result); } } Ptr CodeGenerator::GenerateTableVariableNode(Ptr InNode, const FTableCacheKey& CacheKey, bool bAddNoneOption, const FString& DefaultRowName) { Ptr Result = new NodeScalarEnumParameter; FString ParamName = CacheKey.ParameterName; if (ParamName.Len() == 0) { ParamName = CacheKey.Table->GetName(); } Result->Name = ParamName; Result->DefaultValue = 0; int32 CurrentRow = 0; int32 RowCount = CacheKey.Table->GetPrivate()->Rows.Num(); check(RowCount < MAX_int16) // max FIntValueDesc allows if (bAddNoneOption) { Result->Options.SetNum(RowCount); Result->Options[CurrentRow].Value = -1; Result->Options[CurrentRow].Name = "None"; } else { Result->Options.SetNum(RowCount-1); } // Add the possible values { // See if there is a string column. If there is one, we will use it as names for the // options. Only the first string column will be used. int32 NameCol = -1; int32 NumCols = CacheKey.Table->GetPrivate()->Columns.Num(); for (int32 ColumnIndex=0; ColumnIndexGetPrivate()->Columns[ColumnIndex].Type == ETableColumnType::String) { NameCol = ColumnIndex; } } // Skip "None" option (first row) if it's not required int32 StarCount = bAddNoneOption ? 0 : 1; for (int32 RowIndex = StarCount; RowIndex < RowCount; ++RowIndex) { FString ValueName; if (NameCol > -1) { ValueName = CacheKey.Table->GetPrivate()->Rows[RowIndex].Values[NameCol].String; } Result->Options[CurrentRow].Value = RowIndex; Result->Options[CurrentRow].Name = ValueName; // Set the first row or the selected row as the default one. if(RowIndex == StarCount || ValueName == DefaultRowName) { Result->DefaultValue = RowIndex; } // Set the selected row as default (if exists) if (ValueName == DefaultRowName) { Result->DefaultValue = RowIndex; } ++CurrentRow; } } return Result; } TSharedPtr CodeGenerator::GenerateLayout(Ptr SourceLayout, uint32 MeshIDPrefix) { // This can run in any thread. UE::TUniqueLock Lock(GenerateLayoutConstantState.Mutex); TSharedPtr* CachedLayout = GenerateLayoutConstantState.GeneratedLayouts.Find({ SourceLayout,MeshIDPrefix }); if (CachedLayout) { return *CachedLayout; } TSharedPtr GeneratedLayout = MakeShared(); GeneratedLayout->Size = SourceLayout->Size; GeneratedLayout->MaxSize = SourceLayout->MaxSize; GeneratedLayout->Strategy = SourceLayout->Strategy; GeneratedLayout->ReductionMethod = SourceLayout->ReductionMethod; const int32 BlockCount = SourceLayout->Blocks.Num(); GeneratedLayout->Blocks.SetNum(BlockCount); for (int32 BlockIndex = 0; BlockIndex < BlockCount; ++BlockIndex) { const FSourceLayoutBlock& From = SourceLayout->Blocks[BlockIndex]; FLayoutBlock& To = GeneratedLayout->Blocks[BlockIndex]; To.Min = From.Min; To.Size = From.Size; To.Priority = From.Priority; To.bReduceBothAxes = From.bReduceBothAxes; To.bReduceByTwo = From.bReduceByTwo; // Assign unique ids to each layout block uint64 Id = uint64(MeshIDPrefix) << 32 | uint64(BlockIndex); To.Id = Id; } check(GeneratedLayout->Blocks.IsEmpty() || GeneratedLayout->Blocks[0].Id != FLayoutBlock::InvalidBlockId); GenerateLayoutConstantState.GeneratedLayouts.Add({ SourceLayout,MeshIDPrefix }, GeneratedLayout); return GeneratedLayout; } Ptr CodeGenerator::GenerateImageBlockPatch(Ptr InBlockOp, const NodeModifierSurfaceEdit::FTexture& Patch, TSharedPtr PatchMask, Ptr conditionAd, const FImageGenerationOptions& ImageOptions ) { // Blend operation Ptr FinalOp; { MUTABLE_CPUPROFILER_SCOPE(PatchBlend); Ptr LayerOp = new ASTOpImageLayer(); LayerOp->blendType = Patch.PatchBlendType; LayerOp->base = InBlockOp; // When we patch from edit nodes, we want to apply it to all the channels. // \todo: since we can choose the patch function, maybe we want to be able to select this as well. LayerOp->Flags = Patch.bPatchApplyToAlpha ? OP::ImageLayerArgs::F_APPLY_TO_ALPHA : 0; NodeImage* ImageNode = Patch.PatchImage.get(); Ptr BlendOp; if (ImageNode) { FImageGenerationResult BlendResult; GenerateImage(ImageOptions, BlendResult, ImageNode); BlendOp = BlendResult.op; } else { BlendOp = GenerateMissingImageCode(TEXT("Patch top image"), EImageFormat::RGB_UByte, nullptr, ImageOptions); } BlendOp = GenerateImageFormat(BlendOp, InBlockOp->GetImageDesc().m_format); BlendOp = GenerateImageSize(BlendOp, ImageOptions.RectSize); LayerOp->blend = BlendOp; // Create the rect mask constant Ptr RectConstantOp; { Ptr pNode = new NodeImageConstant(); pNode->SetValue(PatchMask); FImageGenerationOptions ConstantOptions(-1,-1); FImageGenerationResult ConstantResult; GenerateImage(ConstantOptions, ConstantResult, pNode); RectConstantOp = ConstantResult.op; } NodeImage* MaskNode = Patch.PatchMask.get(); Ptr MaskOp; if (MaskNode) { // Combine the block rect mask with the user provided mask. FImageGenerationResult MaskResult; GenerateImage(ImageOptions, MaskResult, MaskNode); MaskOp = MaskResult.op; Ptr PatchCombineOp = new ASTOpImageLayer; PatchCombineOp->base = MaskOp; PatchCombineOp->blend = RectConstantOp; PatchCombineOp->blendType = EBlendType::BT_MULTIPLY; MaskOp = PatchCombineOp; } else { MaskOp = RectConstantOp; } MaskOp = GenerateImageFormat(MaskOp, EImageFormat::L_UByte); MaskOp = GenerateImageSize(MaskOp, ImageOptions.RectSize); LayerOp->mask = MaskOp; FinalOp = LayerOp; } // Condition to enable this patch if (conditionAd) { Ptr conditionalAd; { Ptr op = new ASTOpConditional(); op->type = EOpType::IM_CONDITIONAL; op->no = InBlockOp; op->yes = FinalOp; op->condition = conditionAd; conditionalAd = op; } FinalOp = conditionalAd; } return FinalOp; } //--------------------------------------------------------------------------------------------- void CodeGenerator::GenerateComponent(const FComponentGenerationOptions& InOptions, FGenericGenerationResult& OutResult, const NodeComponent* InUntypedNode) { if (!InUntypedNode) { OutResult = FGenericGenerationResult(); return; } // See if it was already generated FGeneratedComponentCacheKey Key; Key.Node = InUntypedNode; Key.Options = InOptions; FGeneratedComponentMap::ValueType* it = GeneratedComponents.Find(Key); if (it) { OutResult = *it; return; } // Generate for each different type of node const FNodeType* Type = InUntypedNode->GetType(); if (Type == NodeComponentNew::GetStaticType()) { GenerateComponent_New(InOptions, OutResult, static_cast(InUntypedNode)); } else if (Type == NodeComponentEdit::GetStaticType()) { // Nothing to do because it is all preprocessed in the first code generator stage //GenerateComponent_Edit(InOptions, OutResult, static_cast(InUntypedNode)); OutResult.Op = InOptions.BaseInstance; } else if (Type == NodeComponentSwitch::GetStaticType()) { GenerateComponent_Switch(InOptions, OutResult, static_cast(InUntypedNode)); } else if (Type == NodeComponentVariation::GetStaticType()) { GenerateComponent_Variation(InOptions, OutResult, static_cast(InUntypedNode)); } else { check(false); } // Cache the result GeneratedComponents.Add(Key, OutResult); } void CodeGenerator::GenerateComponent_New(const FComponentGenerationOptions& Options, FGenericGenerationResult& Result, const NodeComponentNew* InNode) { TArray LODTasks; LODTasks.Reserve(InNode->LODs.Num()); // Launch tasks for each lod, making sure each LOD depends on the previous one. FLODTask LastLODTask; for (int32 LODIndex = 0; LODIndex < InNode->LODs.Num(); ++LODIndex) { if (const NodeLOD* LODNode = InNode->LODs[LODIndex].get()) { FLODGenerationOptions LODOptions(Options, LODIndex, InNode ); bool bWasEmpty = false; LastLODTask = GenerateLOD(LODOptions, LODNode, LastLODTask); LODTasks.Add(LastLODTask); } } // Launch the task that generates the component // There could be more concurrency here, but it doesn't look like it is relevant yet. FComponentTask ComponentTask = LocalPipe.Launch(TEXT("MutableComponentNew"), [ LODTasks, InNode, Options, this ] () mutable { FGenericGenerationResult Result; // Create the expression for each component in this object Ptr LODsOp = new ASTOpAddLOD(); for (FLODTask& LODTask : LODTasks) { FGenericGenerationResult LODResult = LODTask.GetResult(); LODsOp->lods.Emplace(LODsOp, LODResult.Op); } Ptr LastInstOp = LODsOp; if (Ptr pScalarNode = InNode->OverlayMaterial) { Ptr AddOverlayMaterialOp = new ASTOpAddOverlayMaterial(); AddOverlayMaterialOp->Instance = LastInstOp; // Scalar FScalarGenerationResult ScalarResult; GenerateScalar(ScalarResult, Options, pScalarNode); AddOverlayMaterialOp->OverlayMaterialId = ScalarResult.op; LastInstOp = AddOverlayMaterialOp; } Ptr InstanceOp = new ASTOpInstanceAdd(); InstanceOp->type = EOpType::IN_ADDCOMPONENT; InstanceOp->instance = Options.BaseInstance; InstanceOp->value = LastInstOp; InstanceOp->ExternalId = InNode->Id; Result.Op = InstanceOp; // Add a conditional if this component has conditions for (const FirstPassGenerator::FComponent& Component : FirstPass.Components) { if (Component.Component != InNode) { continue; } if (Component.ComponentCondition || Component.ObjectCondition) { // TODO: This could be done earlier? Ptr ConditionOp = new ASTOpBoolAnd(); ConditionOp->A = Component.ObjectCondition; ConditionOp->B = Component.ComponentCondition; Ptr IfOp = new ASTOpConditional(); IfOp->type = EOpType::IN_CONDITIONAL; IfOp->no = Options.BaseInstance; IfOp->yes = Result.Op; IfOp->condition = ConditionOp; Result.Op = IfOp; } } return Result; }, LODTasks ); // Sync point: we currently don't support task-based generation beyond components so we wait here. // Otherwise we could return the component task to be chained with higher level tasks. if (WaitCallback.IsSet()) { while (!ComponentTask.IsCompleted()) { WaitCallback(); } } Result = ComponentTask.GetResult(); } void CodeGenerator::GenerateComponent_Switch(const FComponentGenerationOptions& Options, FGenericGenerationResult& Result, const NodeComponentSwitch* Node) { MUTABLE_CPUPROFILER_SCOPE(NodeComponentSwitch); if (Node->Options.Num() == 0) { // No options in the switch! Result.Op = Options.BaseInstance; return; } Ptr Op = new ASTOpSwitch(); Op->Type = EOpType::IN_SWITCH; // Variable value if (Node->Parameter) { FScalarGenerationResult ParamResult; GenerateScalar(ParamResult, Options, Node->Parameter.get()); Op->Variable = ParamResult.op; } else { // This argument is required Op->Variable = GenerateMissingScalarCode(TEXT("Switch variable"), 0.0f, Node->GetMessageContext()); } // Options for (int32 OptionIndex = 0; OptionIndex < Node->Options.Num(); ++OptionIndex) { Ptr Branch; if (Node->Options[OptionIndex]) { FGenericGenerationResult BaseResult; GenerateComponent(Options, BaseResult, Node->Options[OptionIndex].get()); Branch = BaseResult.Op; } else { // This argument is not required Branch = Options.BaseInstance; } Op->Cases.Emplace(OptionIndex, Op, Branch); } Result.Op = Op; } void CodeGenerator::GenerateComponent_Variation(const FComponentGenerationOptions& Options, FGenericGenerationResult& Result, const NodeComponentVariation* Node) { Ptr CurrentMeshOp = Options.BaseInstance; // Default case if (Node->DefaultComponent) { FGenericGenerationResult BranchResults; GenerateComponent(Options, BranchResults, Node->DefaultComponent.get()); CurrentMeshOp = BranchResults.Op; } // Process variations in reverse order, since conditionals are built bottom-up. for (int32 VariationIndex = Node->Variations.Num() - 1; VariationIndex >= 0; --VariationIndex) { int32 TagIndex = -1; const FString& Tag = Node->Variations[VariationIndex].Tag; for (int32 i = 0; i < FirstPass.Tags.Num(); ++i) { if (FirstPass.Tags[i].Tag == Tag) { TagIndex = i; } } if (TagIndex < 0) { ErrorLog->Add( FString::Printf(TEXT("Unknown tag found in component variation [%s]."), *Tag), ELMT_WARNING, Node->GetMessageContext(), ELMSB_UNKNOWN_TAG ); continue; } Ptr VariationMeshOp = Options.BaseInstance; if (Node->Variations[VariationIndex].Component) { FGenericGenerationResult BranchResults; GenerateComponent(Options, BranchResults, Node->Variations[VariationIndex].Component.get()); VariationMeshOp = BranchResults.Op; } Ptr Conditional = new ASTOpConditional; Conditional->type = EOpType::IN_CONDITIONAL; Conditional->no = CurrentMeshOp; Conditional->yes = VariationMeshOp; Conditional->condition = FirstPass.Tags[TagIndex].GenericCondition; CurrentMeshOp = Conditional; } Result.Op = CurrentMeshOp; } Ptr CodeGenerator::ApplyTiling(Ptr Source, UE::Math::TIntVector2 Size, EImageFormat Format) { // For now always apply tiling if (CompilerOptions->ImageTiling==0) { return Source; } int32 TileSize = CompilerOptions->ImageTiling; int32 TilesX = FMath::DivideAndRoundUp(Size[0], TileSize); int32 TilesY = FMath::DivideAndRoundUp(Size[1], TileSize); if (TilesX * TilesY <= 2) { return Source; } Ptr BaseImage = new ASTOpImagePlainColor; BaseImage->Size[0] = Size[0]; BaseImage->Size[1] = Size[1]; BaseImage->Format = Format; BaseImage->LODs = 1; Ptr CurrentImage = BaseImage; for (int32 Y = 0; Y < TilesY; ++Y) { for (int32 X = 0; X < TilesX; ++X) { int32 MinX = X * TileSize; int32 MinY = Y * TileSize; int32 TileSizeX = FMath::Min(TileSize, Size[0] - MinX); int32 TileSizeY = FMath::Min(TileSize, Size[1] - MinY); Ptr TileImage = new ASTOpImageCrop(); TileImage->Source = Source; TileImage->Min[0] = MinX; TileImage->Min[1] = MinY; TileImage->Size[0] = TileSizeX; TileImage->Size[1] = TileSizeY; Ptr PatchedImage = new ASTOpImagePatch(); PatchedImage->base = CurrentImage; PatchedImage->patch = TileImage; PatchedImage->location[0] = MinX; PatchedImage->location[1] = MinY; CurrentImage = PatchedImage; } } return CurrentImage; } TSharedPtr CodeGenerator::GenerateImageBlockPatchMask(const NodeModifierSurfaceEdit::FTexture& Patch, FIntPoint GridSize, int32 BlockPixelsX, int32 BlockPixelsY, box RectInCells ) { // Create a patching mask for the block TSharedPtr PatchMask; FIntVector2 SourceTextureSize = { GridSize[0] * BlockPixelsX, GridSize[1] * BlockPixelsY }; FInt32Rect BlockRectInPixels; BlockRectInPixels.Min = { RectInCells.min[0] * BlockPixelsX, RectInCells.min[1] * BlockPixelsY }; BlockRectInPixels.Max = { (RectInCells.min[0] + RectInCells.size[0]) * BlockPixelsX, (RectInCells.min[1] + RectInCells.size[1]) * BlockPixelsY }; for (const FBox2f& PatchRect : Patch.PatchBlocks) { // Does the patch rect intersects the current block at all? FInt32Rect PatchRectInPixels; PatchRectInPixels.Min = { int32(PatchRect.Min[0] * SourceTextureSize[0]), int32(PatchRect.Min[1] * SourceTextureSize[1]) }; PatchRectInPixels.Max = { int32(PatchRect.Max[0] * SourceTextureSize[0]), int32(PatchRect.Max[1] * SourceTextureSize[1]) }; FInt32Rect BlockPatchRect = PatchRectInPixels; BlockPatchRect.Clip(BlockRectInPixels); if (BlockPatchRect.Area() > 0) { FInt32Point BlockSize = BlockRectInPixels.Size(); if (!PatchMask) { PatchMask = MakeShared(BlockSize[0], BlockSize[1], 1, mu::EImageFormat::L_UByte, mu::EInitializationType::Black); } uint8* Pixels = PatchMask->GetMipData(0); FInt32Point BlockPatchOffset = BlockPatchRect.Min - BlockRectInPixels.Min; FInt32Point BlockPatchSize = BlockPatchRect.Size(); for (int32 RowIndex = BlockPatchOffset[1]; RowIndex < BlockPatchOffset[1]+BlockPatchSize[1]; ++RowIndex) { uint8* RowPixels = Pixels + RowIndex * BlockSize[0] + BlockPatchOffset[0]; FMemory::Memset(RowPixels, 255, BlockPatchSize[0]); } } } return PatchMask; } FSurfaceTask CodeGenerator::GenerateSurface( const FSurfaceGenerationOptions& Options, Ptr SurfaceNode, FLODTask PreviousLODTask ) { MUTABLE_CPUPROFILER_SCOPE(GenerateSurface); // Generate the mesh //------------------------------------------------------------------------ // We don't add the mesh here, since it will be added directly at the top of the // component expression in the NodeComponentNew generator with the right merges // and conditions. // But we store it to be used then. // Do we need to generate the mesh? Or was it already generated for state conditions // accepting the current state? TArray TargetSurfaces; TargetSurfaces.Reserve(FirstPass.Surfaces.Num()); for (FirstPassGenerator::FSurface& Surface : FirstPass.Surfaces) { if (Surface.Node != SurfaceNode) { continue; } // Check state conditions const bool bSurfaceValidForThisState = Options.State >= Surface.StateCondition.Num() || Surface.StateCondition[Options.State]; if (!bSurfaceValidForThisState) { continue; } if (Surface.ResultSurfaceTask.IsValid()) { // Reuse the entire surface return Surface.ResultSurfaceTask; } else { // Not already generated, we will generate this TargetSurfaces.Add( &Surface ); } } if (TargetSurfaces.IsEmpty()) { return UE::Tasks::MakeCompletedTask(); } // Gather all modifiers that apply to this surface TArray Modifiers; constexpr bool bModifiersForBeforeOperations = false; // Store the data necessary to apply modifiers for the pre-normal operations stage. // TODO: Should we merge with currently active tags from the InOptions? int32 ComponentId = Options.Component ? Options.Component->Id : -1; GetModifiersFor(ComponentId, SurfaceNode->Tags, bModifiersForBeforeOperations, Modifiers); // This pass on the modifiers is only to detect errors that cannot be detected at the point they are applied. CheckModifiersForSurface(*SurfaceNode, Modifiers, Options.LODIndex); // Generate the mesh FMeshGenerationStaticOptions MeshStaticOptions(ComponentId, Options.LODIndex); MeshStaticOptions.ActiveTags = SurfaceNode->Tags; MeshStaticOptions.State = Options.State; FMeshGenerationDynamicOptions MeshDynamicOptions; MeshDynamicOptions.bLayouts = true; // Normalize UVs if we're going to work with images and layouts. // TODO: This should come from per-layout settings! const bool bNormalizeUVs = false; // !SurfaceNode->Images.IsEmpty(); MeshDynamicOptions.bNormalizeUVs = bNormalizeUVs; // The options depenend on the shared surface being generated, so we need to add the previous lod dependency FMeshOptionsTask MeshOptionsTask = LocalPipe.Launch(TEXT("MutableSurfaceMeshOptions"), [MeshDynamicOptions, SharedSurfaceId=SurfaceNode->SharedSurfaceId, this]() mutable { // This assumes that the lods are processed in order. It checks it this way because some platforms may have empty lods at the top. bool bIsBaseForSharedSurface = SharedSurfaceId != INDEX_NONE; if (bIsBaseForSharedSurface) { UE::TUniqueLock Lock(SharedMeshOptions.Mutex); bIsBaseForSharedSurface = !SharedMeshOptions.Map.Contains(SharedSurfaceId); } // If this is true, we will reuse the surface properties from a higher LOD, se we can skip the generation of material properties and images. const bool bShareSurface = SharedSurfaceId != INDEX_NONE && !bIsBaseForSharedSurface; const FMeshGenerationResult* SharedMeshResults = nullptr; if (bShareSurface) { UE::TUniqueLock Lock(SharedMeshOptions.Mutex); // Do we have the surface we need to share it with? SharedMeshResults = SharedMeshOptions.Map.Find(SharedSurfaceId); check(SharedMeshResults); // Override the layouts with the ones from the surface we share MeshDynamicOptions.OverrideLayouts = SharedMeshResults->GeneratedLayouts; } // Ensure UV islands remain within their main layout block on lower LODs to avoid unexpected reordering // of the layout blocks when reusing a surface between LODs. Used to fix small displacements on vertices // that may cause them to fall on a different block. MeshDynamicOptions.bClampUVIslands = bShareSurface; return MeshDynamicOptions; }, PreviousLODTask ); FMeshTask MeshTask = GenerateMesh(MeshStaticOptions, MeshOptionsTask, SurfaceNode->Mesh); // Apply the modifier for the post-normal operations stage. MeshTask = ApplyMeshModifiers(Modifiers, MeshStaticOptions, MeshOptionsTask, MeshTask, SurfaceNode->SharedSurfaceId, SurfaceNode->GetMessageContext(), nullptr); FSurfaceTask SurfaceTask = LocalPipe.Launch(TEXT("MutableSurface"), [ MeshTask, SurfaceNode, ComponentId, Options, Modifiers, TargetSurfaces, // We need to call some local methods which should be fine sinc4e. this ] () mutable { FMeshGenerationResult MeshResults = MeshTask.GetResult(); // Base mesh is allowed to be missing, aggregate all layouts and operations per layout indices in the // generated mesh, base and extends. TArray SurfaceReferenceLayouts; TArray> SurfaceLayoutOps; int32 MaxLayoutNum = MeshResults.GeneratedLayouts.Num(); for (const FMeshGenerationResult::FExtraLayouts& ExtraLayoutData : MeshResults.ExtraMeshLayouts) { MaxLayoutNum = FMath::Max(MaxLayoutNum, ExtraLayoutData.GeneratedLayouts.Num()); } SurfaceReferenceLayouts.SetNum(MaxLayoutNum); SurfaceLayoutOps.SetNum(MaxLayoutNum); TBitArray<> LayoutFromExtension; LayoutFromExtension.Init(false, MaxLayoutNum); // Scope for access control to shared data bool bIsBaseForSharedSurface = false; bool bShareSurface = false; TArray> SharedResultLayoutOps; { UE::TUniqueLock Lock(SharedMeshOptions.Mutex); bIsBaseForSharedSurface = SurfaceNode->SharedSurfaceId!=INDEX_NONE && !SharedMeshOptions.Map.Contains(SurfaceNode->SharedSurfaceId); // If this is true, we will reuse the surface properties from a higher LOD, se we can skip the generation of material properties and images. bShareSurface = SurfaceNode->SharedSurfaceId != INDEX_NONE && !bIsBaseForSharedSurface; const FMeshGenerationResult* SharedMeshResults = nullptr; if (bShareSurface) { // Do we have the surface we need to share it with? SharedMeshResults = SharedMeshOptions.Map.Find(SurfaceNode->SharedSurfaceId); check(SharedMeshResults); SharedResultLayoutOps = SharedMeshResults->LayoutOps; } // Add layouts form the base mesh. for (int32 LayoutIndex = 0; LayoutIndex < MeshResults.GeneratedLayouts.Num(); ++LayoutIndex) { if (!MeshResults.GeneratedLayouts[LayoutIndex].Layout) { continue; } SurfaceReferenceLayouts[LayoutIndex] = MeshResults.GeneratedLayouts[LayoutIndex]; bool bSharedHasThisLayout = SharedMeshResults && SharedMeshResults->LayoutOps.IsValidIndex(LayoutIndex) && SharedMeshResults->LayoutOps[LayoutIndex]; if (bSharedHasThisLayout) { SurfaceLayoutOps[LayoutIndex] = SharedMeshResults->LayoutOps[LayoutIndex]; } else { Ptr ConstantLayoutOp = new ASTOpConstantResource(); ConstantLayoutOp->Type = EOpType::LA_CONSTANT; ConstantLayoutOp->SetValue( SurfaceReferenceLayouts[LayoutIndex].Layout, CompilerOptions->OptimisationOptions.DiskCacheContext); SurfaceLayoutOps[LayoutIndex] = ConstantLayoutOp; } } } // Add extra layouts. In case there is a missing reference layout, the first visited will // take the role. for (const FMeshGenerationResult::FExtraLayouts& ExtraLayoutsData : MeshResults.ExtraMeshLayouts) { if (!ExtraLayoutsData.MeshFragment) { // No mesh to add, we assume there are no layouts to add either. check(ExtraLayoutsData.GeneratedLayouts.IsEmpty()); continue; } const TArray& ExtraGeneratedLayouts = ExtraLayoutsData.GeneratedLayouts; for (int32 LayoutIndex = 0; LayoutIndex < ExtraGeneratedLayouts.Num(); ++LayoutIndex) { if (!ExtraGeneratedLayouts[LayoutIndex].Layout) { continue; } bool bLayoutSetByThisExtension = false; if (!SurfaceReferenceLayouts[LayoutIndex].Layout) { // This Layout slot is not set by the base surface, set it as reference. SurfaceReferenceLayouts[LayoutIndex] = ExtraGeneratedLayouts[LayoutIndex]; bLayoutSetByThisExtension = true; LayoutFromExtension[LayoutIndex] = bLayoutSetByThisExtension; } if (bShareSurface) { if (!SurfaceLayoutOps[LayoutIndex] && bLayoutSetByThisExtension) { check(SharedResultLayoutOps.IsValidIndex(LayoutIndex)); SurfaceLayoutOps[LayoutIndex] = SharedResultLayoutOps[LayoutIndex]; } } else { Ptr LayoutFragmentConstantOp = new ASTOpConstantResource(); LayoutFragmentConstantOp->Type = EOpType::LA_CONSTANT; LayoutFragmentConstantOp->SetValue( ExtraLayoutsData.GeneratedLayouts[LayoutIndex].Layout, CompilerOptions->OptimisationOptions.DiskCacheContext); Ptr LayoutMergeOp = new ASTOpLayoutMerge(); // Base may be null if the base does not have a mesh with a layout at LayoutIndex. // In that case, when applying the condition this can generate null layouts. LayoutMergeOp->Base = SurfaceLayoutOps[LayoutIndex]; LayoutMergeOp->Added = LayoutFragmentConstantOp; if (ExtraLayoutsData.Condition) { Ptr ConditionalOp = new ASTOpConditional(); ConditionalOp->type = EOpType::LA_CONDITIONAL; ConditionalOp->no = SurfaceLayoutOps[LayoutIndex]; ConditionalOp->yes = LayoutMergeOp; ConditionalOp->condition = ExtraLayoutsData.Condition; SurfaceLayoutOps[LayoutIndex] = ConditionalOp; } else { SurfaceLayoutOps[LayoutIndex] = LayoutMergeOp; } } } } Ptr LastMeshOp = MeshResults.MeshOp; check(SurfaceReferenceLayouts.Num() == SurfaceLayoutOps.Num()); for (int32 LayoutIndex = 0; LayoutIndex < SurfaceReferenceLayouts.Num(); ++LayoutIndex) { if (!SurfaceReferenceLayouts[LayoutIndex].Layout) { continue; } if (SurfaceReferenceLayouts[LayoutIndex].Layout->GetLayoutPackingStrategy() == mu::EPackStrategy::Overlay) { continue; } // Add layout packing instructions if (!bShareSurface) { // Make sure we removed unnecessary blocks Ptr ExtractOp = new ASTOpLayoutFromMesh(); ExtractOp->Mesh = LastMeshOp; check(LayoutIndex < 256); ExtractOp->LayoutIndex = uint8(LayoutIndex); Ptr RemoveOp = new ASTOpLayoutRemoveBlocks(); RemoveOp->Source = SurfaceLayoutOps[LayoutIndex]; RemoveOp->ReferenceLayout = ExtractOp; SurfaceLayoutOps[LayoutIndex] = RemoveOp; // Pack uv blocks Ptr LayoutPackOp = new ASTOpLayoutPack(); LayoutPackOp->Source = SurfaceLayoutOps[LayoutIndex]; SurfaceLayoutOps[LayoutIndex] = LayoutPackOp; } // Create the expression to apply the layout to the mesh { Ptr ApplyLayoutOp = new ASTOpMeshApplyLayout(); ApplyLayoutOp->Mesh = LastMeshOp; ApplyLayoutOp->Layout = SurfaceLayoutOps[LayoutIndex]; ApplyLayoutOp->Channel = (uint16)LayoutIndex; LastMeshOp = ApplyLayoutOp; } } MeshResults.GeneratedLayouts = MoveTemp(SurfaceReferenceLayouts); MeshResults.LayoutOps = MoveTemp(SurfaceLayoutOps); // Store in the surface for later use. for (FirstPassGenerator::FSurface* TargetSurface : TargetSurfaces) { TargetSurface->ResultMeshOp = LastMeshOp; } // Build a series of operations to assemble the surface Ptr LastSurfOp; // Create the expression for each texture, if we are not reusing the surface from another LOD. //------------------------------------------------------------------------ if (!bShareSurface) { for (int32 ImageIndex = 0; ImageIndex < SurfaceNode->Images.Num(); ++ImageIndex) { MUTABLE_CPUPROFILER_SCOPE(SurfaceTexture); // Any image-specific format or mipmapping needs to be applied at the end Ptr mipmapNode; Ptr formatNode; Ptr swizzleNode; bool bFound = false; Ptr pImageNode = SurfaceNode->Images[ImageIndex].Image; while (!bFound && pImageNode) { if (pImageNode->GetType() == NodeImageMipmap::GetStaticType()) { NodeImageMipmap* tm = static_cast(pImageNode.get()); if (!mipmapNode) mipmapNode = tm; pImageNode = tm->Source; } else if (pImageNode->GetType() == NodeImageFormat::GetStaticType()) { NodeImageFormat* tf = static_cast(pImageNode.get()); if (!formatNode) formatNode = tf; pImageNode = tf->Source; } else if (pImageNode->GetType() == NodeImageSwizzle::GetStaticType()) { NodeImageSwizzle* ts = static_cast(pImageNode.get()); if (!ts->Sources.IsEmpty()) { NodeImage* Source = ts->Sources[0].get(); bool bAllSourcesAreTheSame = true; for (int32 SourceIndex = 1; SourceIndex < ts->Sources.Num(); ++SourceIndex) { bAllSourcesAreTheSame = bAllSourcesAreTheSame && (Source == ts->Sources[SourceIndex]); } if (!swizzleNode && bAllSourcesAreTheSame) { swizzleNode = ts; pImageNode = Source; } else { bFound = true; } } else { // break loop if swizzle has no sources. bFound = true; } } else { bFound = true; } } if (bFound) { const NodeSurfaceNew::FImageData& ImageData = SurfaceNode->Images[ImageIndex]; const int32 LayoutIndex = ImageData.LayoutIndex; // If the layout index has been set to negative, it means we should ignore the layout for this image. CompilerOptions::TextureLayoutStrategy ImageLayoutStrategy = (LayoutIndex < 0) ? CompilerOptions::TextureLayoutStrategy::None : CompilerOptions::TextureLayoutStrategy::Pack ; if (ImageLayoutStrategy == CompilerOptions::TextureLayoutStrategy::None) { // Generate the image FImageGenerationOptions ImageOptions(ComponentId, Options.LODIndex); ImageOptions.State = Options.State; ImageOptions.ImageLayoutStrategy = ImageLayoutStrategy; ImageOptions.ActiveTags = SurfaceNode->Tags; ImageOptions.RectSize = { 0, 0 }; // TODO: To tasks FImageGenerationResult Result; GenerateImage(ImageOptions, Result, pImageNode); Ptr imageAd = Result.op; // Placeholder block. Ideally this should be the actual image size constexpr int32 FakeLayoutSize = 256; FIntPoint GridSize(FakeLayoutSize, FakeLayoutSize); FLayoutBlockDesc LayoutBlockDesc; LayoutBlockDesc.BlockPixelsX = 1; LayoutBlockDesc.BlockPixelsY = 1; box< FIntVector2 > RectInCells; RectInCells.min = { 0,0 }; RectInCells.size = { FakeLayoutSize ,FakeLayoutSize }; imageAd = ApplyImageBlockModifiers(Modifiers, ImageOptions, imageAd, ImageData, GridSize, LayoutBlockDesc, RectInCells, SurfaceNode->GetMessageContext()); check(imageAd); if (swizzleNode) { Ptr fop = new ASTOpImageSwizzle(); fop->Format = swizzleNode->NewFormat; fop->Sources[0] = imageAd; fop->Sources[1] = imageAd; fop->Sources[2] = imageAd; fop->Sources[3] = imageAd; fop->SourceChannels[0] = swizzleNode->SourceChannels[0]; fop->SourceChannels[1] = swizzleNode->SourceChannels[1]; fop->SourceChannels[2] = swizzleNode->SourceChannels[2]; fop->SourceChannels[3] = swizzleNode->SourceChannels[3]; check(fop->Format != EImageFormat::None); imageAd = fop; } if (mipmapNode) { Ptr op = new ASTOpImageMipmap(); op->Levels = 0; op->Source = imageAd; op->BlockLevels = 0; op->AddressMode = mipmapNode->Settings.AddressMode; op->FilterType = mipmapNode->Settings.FilterType; imageAd = op; } if (formatNode) { Ptr fop = new ASTOpImagePixelFormat(); fop->Format = formatNode->Format; fop->FormatIfAlpha = formatNode->FormatIfAlpha; fop->Source = imageAd; check(fop->Format != EImageFormat::None); imageAd = fop; } Ptr op = new ASTOpInstanceAdd(); op->type = EOpType::IN_ADDIMAGE; op->instance = LastSurfOp; op->value = imageAd; op->name = SurfaceNode->Images[ImageIndex].Name; LastSurfOp = op; } else if (ImageLayoutStrategy == CompilerOptions::TextureLayoutStrategy::Pack) //-V547 { if (LayoutIndex >= MeshResults.GeneratedLayouts.Num() || LayoutIndex >= MeshResults.LayoutOps.Num()) { ErrorLog->Add("Missing layout in object, or its parent.", ELMT_ERROR, SurfaceNode->GetMessageContext()); } else { TSharedPtr pLayout = MeshResults.GeneratedLayouts[LayoutIndex].Layout; check(pLayout); Ptr op = new ASTOpInstanceAdd(); op->type = EOpType::IN_ADDIMAGE; op->instance = LastSurfOp; // Image //------------------------------------- // Size of a layout block in pixels FIntPoint GridSize = pLayout->GetGridSize(); // Try to guess the layout block description from the first valid block that is generated. FLayoutBlockDesc LayoutBlockDesc; if (formatNode) { LayoutBlockDesc.FinalFormat = formatNode->FormatIfAlpha; if (LayoutBlockDesc.FinalFormat == EImageFormat::None) { LayoutBlockDesc.FinalFormat = formatNode->Format; } } bool bImageSizeWarning = false; // Start with a blank image. It will be completed later with the blockSize, format and mips information Ptr BlankImageOp; Ptr imageAd; { BlankImageOp = new ASTOpImageBlankLayout(); BlankImageOp->Layout = MeshResults.LayoutOps[LayoutIndex]; // The rest ok the op will be completed below BlankImageOp->MipmapCount = 0; imageAd = BlankImageOp; } // Skip the block addition for this image if the layout was from a extension. if (!LayoutFromExtension[LayoutIndex]) { for (int32 BlockIndex = 0; BlockIndex < pLayout->GetBlockCount(); ++BlockIndex) { // Generate the image FImageGenerationOptions ImageOptions(ComponentId, Options.LODIndex); ImageOptions.State = Options.State; ImageOptions.ImageLayoutStrategy = ImageLayoutStrategy; ImageOptions.RectSize = { 0,0 }; ImageOptions.ActiveTags = SurfaceNode->Tags; ImageOptions.LayoutToApply = pLayout; ImageOptions.LayoutBlockId = pLayout->Blocks[BlockIndex].Id; FImageGenerationResult ImageResult; GenerateImage(ImageOptions, ImageResult, pImageNode); Ptr blockAd = ImageResult.op; if (!blockAd) { // The GenerateImage(...) above has failed, skip this block continue; } // Calculate the desc of the generated block. constexpr bool bReturnBestOption = true; FImageDesc BlockDesc = blockAd->GetImageDesc(bReturnBestOption, nullptr); // Block in layout grid units (cells) box< FIntVector2 > RectInCells; RectInCells.min = pLayout->Blocks[BlockIndex].Min; RectInCells.size = pLayout->Blocks[BlockIndex].Size; // Try to update the layout block desc if we don't know it yet. UpdateLayoutBlockDesc(LayoutBlockDesc, BlockDesc, RectInCells.size); // Even if we force the size afterwards, we need some size hint in some cases, like image projections. ImageOptions.RectSize = UE::Math::TIntVector2(BlockDesc.m_size); blockAd = ApplyImageBlockModifiers(Modifiers, ImageOptions, blockAd, ImageData, GridSize, LayoutBlockDesc, RectInCells, SurfaceNode->GetMessageContext()); // Enforce block size and optimizations blockAd = GenerateImageSize(blockAd, FIntVector2(BlockDesc.m_size)); EImageFormat baseFormat = imageAd->GetImageDesc().m_format; // Actually don't do it, it will be propagated from the top format operation. //Ptr blockAd = GenerateImageFormat(blockAd, baseFormat); // Apply tiling to avoid generating chunks of image that are too big. blockAd = ApplyTiling(blockAd, ImageOptions.RectSize, LayoutBlockDesc.FinalFormat); // Compose layout operation Ptr composeOp = new ASTOpImageCompose(); composeOp->Layout = MeshResults.LayoutOps[LayoutIndex]; composeOp->Base = imageAd; composeOp->BlockImage = blockAd; // Set the absolute block index. check(pLayout->Blocks[BlockIndex].Id != FLayoutBlock::InvalidBlockId); composeOp->BlockId = pLayout->Blocks[BlockIndex].Id; imageAd = composeOp; } } check(imageAd); FMeshGenerationStaticOptions ModifierOptions(ComponentId, Options.LODIndex); ModifierOptions.State = Options.State; ModifierOptions.ActiveTags = SurfaceNode->Tags; imageAd = ApplyImageExtendModifiers(Modifiers, ModifierOptions, MeshResults, imageAd, ImageLayoutStrategy, LayoutIndex, ImageData, GridSize, LayoutBlockDesc, SurfaceNode->GetMessageContext()); // Complete the base op BlankImageOp->BlockSize[0] = uint16(LayoutBlockDesc.BlockPixelsX); BlankImageOp->BlockSize[1] = uint16(LayoutBlockDesc.BlockPixelsY); BlankImageOp->Format = GetUncompressedFormat(LayoutBlockDesc.FinalFormat); BlankImageOp->GenerateMipmaps = LayoutBlockDesc.bBlocksHaveMips; BlankImageOp->MipmapCount = 0; if (swizzleNode) { Ptr fop = new ASTOpImageSwizzle(); fop->Format = swizzleNode->NewFormat; for (int32 ChannelIndex = 0; ChannelIndex < swizzleNode->SourceChannels.Num(); ++ChannelIndex) { fop->Sources[ChannelIndex] = imageAd; fop->SourceChannels[ChannelIndex] = swizzleNode->SourceChannels[ChannelIndex]; } check(fop->Format != EImageFormat::None); imageAd = fop; } // Apply mipmap and format if necessary, skip if format is None (possibly because a block was skipped above) bool bNeedsMips = (mipmapNode && LayoutBlockDesc.FinalFormat != EImageFormat::None) || LayoutBlockDesc.bBlocksHaveMips; if (bNeedsMips) { Ptr mop = new ASTOpImageMipmap(); // At the end of the day, we want all the mipmaps. Maybe the code // optimiser will split the process later. mop->Levels = 0; mop->bOnlyTail = false; mop->Source = imageAd; // We have to avoid mips smaller than the image format block size, so // we will devide the layout block by the format block const FImageFormatData& PixelFormatInfo = GetImageFormatData(LayoutBlockDesc.FinalFormat); int32 mipsX = FMath::CeilLogTwo(LayoutBlockDesc.BlockPixelsX / PixelFormatInfo.PixelsPerBlockX); int32 mipsY = FMath::CeilLogTwo(LayoutBlockDesc.BlockPixelsY / PixelFormatInfo.PixelsPerBlockY); mop->BlockLevels = (uint8)FMath::Max(mipsX, mipsY); if (LayoutBlockDesc.BlockPixelsX < PixelFormatInfo.PixelsPerBlockX || LayoutBlockDesc.BlockPixelsY < PixelFormatInfo.PixelsPerBlockY) { // In this case, the mipmap will never be useful for blocks, so we indicate that // it should make the mips at the root of the expression. mop->bOnlyTail = true; } mop->AddressMode = EAddressMode::ClampToEdge; mop->FilterType = EMipmapFilterType::SimpleAverage; if (mipmapNode) { mop->AddressMode = mipmapNode->Settings.AddressMode; mop->FilterType = mipmapNode->Settings.FilterType; } imageAd = mop; } if (formatNode) { Ptr fop = new ASTOpImagePixelFormat(); fop->Format = formatNode->Format; fop->FormatIfAlpha = formatNode->FormatIfAlpha; fop->Source = imageAd; check(fop->Format != EImageFormat::None); imageAd = fop; } op->value = imageAd; // Name op->name = SurfaceNode->Images[ImageIndex].Name; LastSurfOp = op; } } else { // Unimplemented texture layout strategy check(false); } } } // Create the expression for each vector //------------------------------------------------------------------------ for (int32 t = 0; t < SurfaceNode->Vectors.Num(); ++t) { //MUTABLE_CPUPROFILER_SCOPE(SurfaceVector); if (Ptr pVectorNode = SurfaceNode->Vectors[t].Vector) { Ptr op = new ASTOpInstanceAdd(); op->type = EOpType::IN_ADDVECTOR; op->instance = LastSurfOp; // Vector FColorGenerationResult VectorResult; GenerateColor(VectorResult, Options, pVectorNode); op->value = VectorResult.op; // Name op->name = SurfaceNode->Vectors[t].Name; LastSurfOp = op; } } // Create the expression for each scalar //------------------------------------------------------------------------ for (int32 t = 0; t < SurfaceNode->Scalars.Num(); ++t) { // MUTABLE_CPUPROFILER_SCOPE(SurfaceScalar); if (Ptr ScalarNode = SurfaceNode->Scalars[t].Scalar) { Ptr op = new ASTOpInstanceAdd(); op->type = EOpType::IN_ADDSCALAR; op->instance = LastSurfOp; // Scalar FScalarGenerationResult ScalarResult; GenerateScalar(ScalarResult, Options, ScalarNode); op->value = ScalarResult.op; // Name op->name = SurfaceNode->Scalars[t].Name; LastSurfOp = op; } } // Create the expression for each string //------------------------------------------------------------------------ for (int32 t = 0; t < SurfaceNode->Strings.Num(); ++t) { if (Ptr StringNode = SurfaceNode->Strings[t].String) { Ptr op = new ASTOpInstanceAdd(); op->type = EOpType::IN_ADDSTRING; op->instance = LastSurfOp; FStringGenerationResult StringResult; GenerateString(StringResult, Options, StringNode); op->value = StringResult.op; // Name op->name = SurfaceNode->Strings[t].Name; LastSurfOp = op; } } } FSurfaceGenerationResult SurfaceResult; SurfaceResult.SurfaceOp = LastSurfOp; // If we are going to share this surface properties, remember it. if (bIsBaseForSharedSurface) { UE::TUniqueLock Lock(SharedMeshOptions.Mutex); check(!SharedMeshOptions.Map.Contains(SurfaceNode->SharedSurfaceId)); SharedMeshOptions.Map.Add(SurfaceNode->SharedSurfaceId, MeshResults); } return SurfaceResult; } , UE::Tasks::Prerequisites(MeshTask, MeshOptionsTask, PreviousLODTask) ); for (FirstPassGenerator::FSurface* TargetSurface : TargetSurfaces) { TargetSurface->ResultSurfaceTask = SurfaceTask; } return SurfaceTask; } FLODTask CodeGenerator::GenerateLOD(const FLODGenerationOptions& Options, const NodeLOD* InNode, FLODTask PreviousLODTask ) { MUTABLE_CPUPROFILER_SCOPE(GenerateLOD); // Look for all surfaces that belong to this component TArray SurfaceIndices; SurfaceIndices.Reserve(FirstPass.Surfaces.Num()); TArray SurfaceTasks; SurfaceTasks.Reserve(FirstPass.Surfaces.Num()); for (int32 SurfaceIndex = 0; SurfaceIndex < FirstPass.Surfaces.Num(); ++SurfaceIndex) { const FirstPassGenerator::FSurface& SurfaceData = FirstPass.Surfaces[SurfaceIndex]; if (SurfaceData.Component == Options.Component && SurfaceData.LOD == Options.LODIndex) { // Apply state conditions: only generate it if it enabled in this state { bool bEnabledInThisState = true; if (SurfaceData.StateCondition.Num() && Options.State >= 0) { bEnabledInThisState = (Options.State < SurfaceData.StateCondition.Num()) && (SurfaceData.StateCondition[Options.State]); } if (!bEnabledInThisState) { continue; } } FSurfaceGenerationOptions SurfaceOptions(Options); FSurfaceTask SurfaceTask = GenerateSurface(SurfaceOptions, SurfaceData.Node, PreviousLODTask); SurfaceTasks.Add(SurfaceTask); SurfaceIndices.Add(SurfaceIndex); } } TArray Requisites; Requisites.Reserve( SurfaceTasks.Num() + 1); Requisites.Append(SurfaceTasks); if (PreviousLODTask.IsValid()) { Requisites.Add(PreviousLODTask); } FLODTask LODTask = LocalPipe.Launch(TEXT("MutableLOD"), [ SurfaceTasks, SurfaceIndices, this ] () mutable { // Build a series of operations to assemble the component Ptr LastCompOp; Ptr LastMeshOp; // This generates a different ID for each surface in the LOD and the component. It can be used to match it to the // mesh surface. It cannot be 0 because it is a special case for the merge operation. int32 surfaceID = 1; for (int32 SelectedSurfaceIndex=0; SelectedSurfaceIndex SurfaceOp = new ASTOpInstanceAdd(); SurfaceOp->type = EOpType::IN_ADDSURFACE; SurfaceOp->name = SurfaceData.Node->Name; SurfaceOp->instance = LastCompOp; SurfaceOp->value = SurfaceGenerationResult.SurfaceOp; SurfaceOp->id = surfaceID; SurfaceOp->ExternalId = SurfaceData.Node->ExternalId; SurfaceOp->SharedSurfaceId = SurfaceData.Node->SharedSurfaceId; Ptr SurfaceConditionOp = SurfaceData.FinalCondition; { Ptr op = new ASTOpConditional(); op->type = EOpType::IN_CONDITIONAL; op->no = LastCompOp; op->yes = SurfaceOp; op->condition = SurfaceConditionOp; LastCompOp = op; } // Add the mesh with its condition // We add the merge op even for the first mesh, so that we set the surface id. Ptr mergeAd; { Ptr Added = SurfaceData.ResultMeshOp; Ptr MergeOp = new ASTOpMeshMerge(); MergeOp->Base = LastMeshOp; MergeOp->Added = Added; MergeOp->NewSurfaceID = surfaceID; mergeAd = MergeOp; } if (SurfaceConditionOp) { Ptr op = new ASTOpConditional(); op->type = EOpType::ME_CONDITIONAL; op->no = LastMeshOp; op->yes = mergeAd; op->condition = SurfaceConditionOp; LastMeshOp = op; } else { LastMeshOp = mergeAd; } ++surfaceID; } // Add op to optimize the skinning of the resulting mesh { Ptr mop = new ASTOpMeshOptimizeSkinning(); mop->Source = LastMeshOp; LastMeshOp = mop; } // Add the component mesh { Ptr iop = new ASTOpInstanceAdd(); iop->type = EOpType::IN_ADDMESH; iop->instance = LastCompOp; iop->value = LastMeshOp; LastCompOp = iop; } FGenericGenerationResult Result; Result.Op = LastCompOp; return Result; }, Requisites ); return LODTask; } void CodeGenerator::GenerateObject_New(const FObjectGenerationOptions& Options, FObjectGenerationResult& OutResult, const NodeObjectNew* InNode) { MUTABLE_CPUPROFILER_SCOPE(NodeObjectNew); // There is always at least a null parent bool bIsChildObject = Options.ParentObjectNode != nullptr; // Add this object as current parent FObjectGenerationOptions ChildOptions = Options; ChildOptions.ParentObjectNode = InNode; // Parse the child objects first, which will accumulate operations in the patching lists for ( int32 ChildIndex=0; ChildIndex < InNode->Children.Num(); ++ChildIndex) { if ( const NodeObject* pChildNode = InNode->Children[ChildIndex].get() ) { // If there are parent objects, the condition of this object depends on the condition of the parent object FObjectGenerationOptions ThisChildOptions = ChildOptions; if (!ThisChildOptions.CurrentObjectCondition) { // In case there is no group node, we generate a constant true condition // This condition will be overwritten by the group nodes. ThisChildOptions.CurrentObjectCondition = new ASTOpConstantBool(true); } // This op is ignored: everything is stored as patches to apply to the parent when it is compiled. FObjectGenerationResult ThisResult; GenerateObject(ThisChildOptions, ThisResult, pChildNode); OutResult.AdditionalComponents.Append(ThisResult.AdditionalComponents); } } // Create the expression adding all the components Ptr LastCompOp; Ptr PlaceholderOp; if (bIsChildObject) { PlaceholderOp = new ASTOpInstanceAdd; LastCompOp = PlaceholderOp; } // Add the components in this node for ( int32 ComponentIndex=0; ComponentIndex < InNode->Components.Num(); ++ComponentIndex) { const NodeComponent* ComponentNode = InNode->Components[ComponentIndex].get(); if (ComponentNode) { FComponentGenerationOptions ComponentOptions( Options, LastCompOp ); FGenericGenerationResult ComponentResult; GenerateComponent(ComponentOptions, ComponentResult, ComponentNode); LastCompOp = ComponentResult.Op; } } // If we didn't generate anything, make sure we don't use the placeholder. if (LastCompOp == PlaceholderOp) { LastCompOp = nullptr; PlaceholderOp = nullptr; } // Add the components from child objects FObjectGenerationResult::FAdditionalComponentKey ThisKey; ThisKey.ObjectNode = InNode; TArray> MultiAdditionalComponents; OutResult.AdditionalComponents.MultiFind(ThisKey, MultiAdditionalComponents, true); if (LastCompOp && !MultiAdditionalComponents.IsEmpty()) { for (TArray ThisAdditionalComponents : MultiAdditionalComponents) { for (const FObjectGenerationResult::FAdditionalComponentData& Additional : ThisAdditionalComponents) { check(Additional.PlaceholderOp); ASTOp::Replace(Additional.PlaceholderOp, LastCompOp); LastCompOp = Additional.ComponentOp; } } } // Store this chain of components for use in parent objects if necessary if (LastCompOp && bIsChildObject) { FObjectGenerationResult::FAdditionalComponentKey ParentKey; ParentKey.ObjectNode = Options.ParentObjectNode; FObjectGenerationResult::FAdditionalComponentData Data; Data.ComponentOp = LastCompOp; Data.PlaceholderOp = PlaceholderOp; OutResult.AdditionalComponents.FindOrAdd(ParentKey).Add(Data); } // Add an ASTOpAddExtensionData for each connected ExtensionData node for (const NodeObjectNew::FNamedExtensionDataNode& NamedNode : InNode->ExtensionDataNodes) { if (!NamedNode.Node.get()) { // No node connected continue; } // Name must be valid check(NamedNode.Name.Len() > 0); FExtensionDataGenerationResult ChildResult; GenerateExtensionData(ChildResult, Options, NamedNode.Node); if (!ChildResult.Op.get()) { // Failed to generate anything for this node continue; } FConditionalExtensionDataOp& SavedOp = ConditionalExtensionDataOps.AddDefaulted_GetRef(); SavedOp.Condition = Options.CurrentObjectCondition; SavedOp.ExtensionDataOp = ChildResult.Op; SavedOp.ExtensionDataName = NamedNode.Name; } Ptr RootOp = LastCompOp; if (!Options.ParentObjectNode) { for (const FConditionalExtensionDataOp& SavedOp : ConditionalExtensionDataOps) { Ptr ExtensionPinOp = new ASTOpAddExtensionData(); ExtensionPinOp->Instance = ASTChild(ExtensionPinOp, RootOp); ExtensionPinOp->ExtensionData = ASTChild(ExtensionPinOp, SavedOp.ExtensionDataOp); ExtensionPinOp->ExtensionDataName = SavedOp.ExtensionDataName; if (SavedOp.Condition.get()) { Ptr ConditionOp = new ASTOpConditional(); ConditionOp->type = EOpType::IN_CONDITIONAL; ConditionOp->no = RootOp; ConditionOp->yes = ExtensionPinOp; ConditionOp->condition = ASTChild(ConditionOp, SavedOp.Condition); RootOp = ConditionOp; } else { RootOp = ExtensionPinOp; } } } OutResult.Op = RootOp; } void CodeGenerator::GenerateObject_Group(const FObjectGenerationOptions& Options, FObjectGenerationResult& OutResult, const NodeObjectGroup* Node) { TArray UsedNames; // Parse the child objects first, which will accumulate operations in the patching lists for ( int32 ChildIndex=0; ChildIndex < Node->Children.Num(); ++ChildIndex) { if ( const NodeObject* pChildNode = Node->Children[ChildIndex].get() ) { // Look for the child condition in the first pass Ptr ConditionOp; bool bFound = false; for( int32 ObjectIndex = 0; !bFound && ObjectIndexGetName(); if (UsedNames.Contains(ChildName)) { FString Msg = FString::Printf(TEXT("Object group has more than one children with the same name [%s]."), *ChildName ); ErrorLog->Add(Msg, ELMT_WARNING, Node->GetMessageContext()); } else { UsedNames.Add(ChildName); } } } } //--------------------------------------------------------------------------------------------- Ptr CodeGenerator::GenerateMissingBoolCode(const TCHAR* Where, bool Value, const void* ErrorContext ) { // Log a warning FString Msg = FString::Printf(TEXT("Required connection not found: %s"), Where); ErrorLog->Add( Msg, ELMT_ERROR, ErrorContext); // Create a constant node Ptr pNode = new NodeBoolConstant(); pNode->Value = Value; FBoolGenerationResult ChildResult; FGenericGenerationOptions Options; GenerateBool(ChildResult,Options, pNode ); return ChildResult.op; } //--------------------------------------------------------------------------------------------- void CodeGenerator::GetModifiersFor( int32 ComponentId, const TArray& SurfaceTags, bool bModifiersForBeforeOperations, TArray& OutModifiers) const { MUTABLE_CPUPROFILER_SCOPE(GetModifiersFor); if (SurfaceTags.IsEmpty()) { return; } for (const FirstPassGenerator::FModifier& Modifier: FirstPass.Modifiers) { if (!Modifier.Node) { continue; } // Correct stage? if (Modifier.Node->bApplyBeforeNormalOperations != bModifiersForBeforeOperations) { continue; } // Correct component? if (Modifier.Node->RequiredComponentId>=0 && Modifier.Node->RequiredComponentId!=ComponentId) { continue; } // Already there? bool bAlreadyAdded = OutModifiers.FindByPredicate( [&Modifier](const FirstPassGenerator::FModifier& c) {return c.Node == Modifier.Node; }) != nullptr; if (bAlreadyAdded) { continue; } // Matching tags? bool bApply = false; switch (Modifier.Node->MultipleTagsPolicy) { case EMutableMultipleTagPolicy::OnlyOneRequired: { for (const FString& Tag: Modifier.Node->RequiredTags) { if (SurfaceTags.Contains(Tag)) { bApply = true; break; } } break; } case EMutableMultipleTagPolicy::AllRequired: { bApply = true; for (const FString& Tag : Modifier.Node->RequiredTags) { if (!SurfaceTags.Contains(Tag)) { bApply = false; break; } } } } if (bApply) { OutModifiers.Add(Modifier); } } } //--------------------------------------------------------------------------------------------- FMeshTask CodeGenerator::ApplyMeshModifiers( const TArray& Modifiers, const FMeshGenerationStaticOptions& StaticOptions, FMeshOptionsTask Options, FMeshTask BaseTask, int32 SharedSurfaceId, const void* ErrorContext, const NodeMeshConstant* OriginalMeshNode) { FMeshTask LastMeshTask = BaseTask; FMeshTask PreModifiersTask = BaseTask; int32 CurrentLOD = StaticOptions.LODIndex; check(CurrentLOD>=0); // Process mesh extend modifiers (from edit modifiers) int32 EditIndex = 0; for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (StaticOptions.ModifiersToIgnore.Contains(Modifier)) { // Prevent recursion. continue; } if (Modifier.Node->GetType() == NodeModifierSurfaceEdit::GetStaticType()) { const NodeModifierSurfaceEdit* Edit = static_cast(Modifier.Node); bool bAffectsCurrentLOD = Edit->LODs.IsValidIndex(CurrentLOD); if (bAffectsCurrentLOD && Edit->LODs[CurrentLOD].MeshAdd) { Ptr pAdd = Edit->LODs[CurrentLOD].MeshAdd; // Store the data necessary to apply modifiers for the pre-normal operations stage. FMeshGenerationStaticOptions MergedMeshStaticOptions(StaticOptions); MergedMeshStaticOptions.ActiveTags = Edit->EnableTags; // TODO: Append to current? MergedMeshStaticOptions.ModifiersToIgnore.Add(Modifier); FMeshOptionsTask MergedMeshOptionsTask = UE::Tasks::Launch(TEXT("MutableMergedMeshOptions"), [Options, SharedSurfaceId, EditIndex, MergedMeshStaticOptions, this]() mutable { FMeshGenerationDynamicOptions Result = Options.GetResult(); Result.bEnsureAllVerticesHaveLayoutBlock = false; UE::TUniqueLock Lock(SharedMeshOptions.Mutex); // This assumes that the lods are processed in order. It checks it this way because some platforms may have empty lods at the top. const bool bIsBaseForSharedSurface = SharedSurfaceId != INDEX_NONE && !SharedMeshOptions.Map.Contains(SharedSurfaceId); // If this is true, we will reuse the surface properties from a higher LOD, se we can skip the generation of material properties and images. const bool bShareSurface = SharedSurfaceId != INDEX_NONE && !bIsBaseForSharedSurface; const FMeshGenerationResult* SharedMeshResults = nullptr; if (bShareSurface) { // Do we have the surface we need to share it with? SharedMeshResults = SharedMeshOptions.Map.Find(SharedSurfaceId); check(SharedMeshResults); if (SharedMeshResults) { check(SharedMeshResults->ExtraMeshLayouts.IsValidIndex(EditIndex)); Result.OverrideLayouts = SharedMeshResults->ExtraMeshLayouts[EditIndex].GeneratedLayouts; } } return Result; }, Options); FMeshTask AddBaseTask = GenerateMesh(MergedMeshStaticOptions, MergedMeshOptionsTask, pAdd); FMeshTask AddTask = UE::Tasks::Launch(TEXT("MutableMergedMeshAdd"), [AddBaseTask, LastMeshTask, ErrorLog=ErrorLog,Edit,ErrorContext]() mutable { FMeshGenerationResult AddResults = AddBaseTask.GetResult(); FMeshGenerationResult BaseMeshResult = LastMeshTask.GetResult(); // Warn about discrepancies on layout strategy between the added and the base int32 LayoutIndexThatHasBlocks = -1; { if (BaseMeshResult.GeneratedLayouts.Num() != AddResults.GeneratedLayouts.Num()) { // When extending a mesh section the added mesh sectio will use the layout strategy of the base one FString Msg = FString::Printf(TEXT("Extended mesh section layout count is differenta than the mesh being extended.")); ErrorLog->Add(Msg, ELMT_INFO, Edit->GetMessageContext(), ErrorContext); } for (int32 LayoutIndex = 0; LayoutIndex < BaseMeshResult.GeneratedLayouts.Num(); ++LayoutIndex) { if (BaseMeshResult.GeneratedLayouts[LayoutIndex].Layout && BaseMeshResult.GeneratedLayouts[LayoutIndex].Layout->Strategy != EPackStrategy::Overlay) { LayoutIndexThatHasBlocks = LayoutIndex; } if (BaseMeshResult.GeneratedLayouts[LayoutIndex].Layout && AddResults.GeneratedLayouts.IsValidIndex(LayoutIndex) && AddResults.GeneratedLayouts[LayoutIndex].Layout && BaseMeshResult.GeneratedLayouts[LayoutIndex].Layout->Strategy != AddResults.GeneratedLayouts[LayoutIndex].Layout->Strategy) { // When extending a mesh section the added mesh sectio will use the layout strategy of the base one FString Msg = FString::Printf(TEXT("Extended mesh section layout [%d] is using a different strategy than the section being extended. The base strategy will be used."), LayoutIndex); ErrorLog->Add(Msg, ELMT_INFO, Edit->GetMessageContext(), ErrorContext); } } } // Add the operation to extract the relevant layout blocks if necessary // TODO: Handle multiple layouts defining blocks: what to extract? if (LayoutIndexThatHasBlocks >= 0) { Ptr ExtractOp = new ASTOpMeshExtractLayoutBlocks(); ExtractOp->Source = AddResults.MeshOp; ExtractOp->LayoutIndex = LayoutIndexThatHasBlocks; AddResults.MeshOp = ExtractOp; } return AddResults; }, UE::Tasks::Prerequisites(AddBaseTask, LastMeshTask) ); // Apply the modifiers for the post-normal operations stage to the added mesh FMeshGenerationStaticOptions ModifierOptions(StaticOptions); ModifierOptions.ActiveTags = Edit->EnableTags; ModifierOptions.ModifiersToIgnore.AddUnique(Modifier); TArray ChildModifiers; constexpr bool bModifiersForBeforeOperations = false; GetModifiersFor(StaticOptions.ComponentId, ModifierOptions.ActiveTags, bModifiersForBeforeOperations, ChildModifiers); FMeshTask AddWithModifiersTask = ApplyMeshModifiers(ChildModifiers, ModifierOptions, Options, AddTask, SharedSurfaceId, ErrorContext, nullptr); LastMeshTask = UE::Tasks::Launch(TEXT("MutableMeshMergeModifier"), [AddBaseTask, AddWithModifiersTask, LastMeshTask, Modifier, EditIndex]() mutable { FMeshGenerationResult AddResults = AddBaseTask.GetResult(); FMeshGenerationResult AddFinalResults = AddWithModifiersTask.GetResult(); FMeshGenerationResult LastMeshResults = LastMeshTask.GetResult(); FMeshGenerationResult::FExtraLayouts Data; Data.GeneratedLayouts = AddResults.GeneratedLayouts; Data.Condition = Modifier.FinalCondition; Data.MeshFragment = AddFinalResults.MeshOp; if (LastMeshResults.ExtraMeshLayouts.Num() <= EditIndex) { LastMeshResults.ExtraMeshLayouts.SetNum(EditIndex+1); } LastMeshResults.ExtraMeshLayouts[EditIndex] = Data; Ptr MergeOp = new ASTOpMeshMerge; MergeOp->Base = LastMeshResults.MeshOp; MergeOp->Added = AddFinalResults.MeshOp; // will merge the meshes under the same surface MergeOp->NewSurfaceID = 0; // Condition to apply if (Modifier.FinalCondition) { Ptr ConditionalOp = new ASTOpConditional(); ConditionalOp->type = EOpType::ME_CONDITIONAL; ConditionalOp->no = LastMeshResults.MeshOp; ConditionalOp->yes = MergeOp; ConditionalOp->condition = Modifier.FinalCondition; LastMeshResults.MeshOp = ConditionalOp; } else { LastMeshResults.MeshOp = MergeOp; } return LastMeshResults; }, UE::Tasks::Prerequisites(AddWithModifiersTask, LastMeshTask) ); } ++EditIndex; } } // Process mesh remove modifiers (from edit modifiers) for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (StaticOptions.ModifiersToIgnore.Contains(Modifier)) { // Prevent recursion. continue; } if (Modifier.Node->GetType() == NodeModifierSurfaceEdit::GetStaticType()) { const NodeModifierSurfaceEdit* Edit = static_cast(Modifier.Node); bool bAffectsCurrentLOD = Edit->LODs.IsValidIndex(CurrentLOD); // Apply mesh removes from child objects "edit surface" nodes. // "Removes" need to come after "Adds" because some removes may refer to added meshes, // and not the base. // \TODO: Apply base removes first, and then "added meshes" removes here. It may have lower memory footprint during generation. if (bAffectsCurrentLOD && Edit->LODs[CurrentLOD].MeshRemove) { Ptr MeshRemove = Edit->LODs[CurrentLOD].MeshRemove; FMeshGenerationStaticOptions RemoveMeshStaticOptions(StaticOptions); RemoveMeshStaticOptions.ActiveTags = Edit->EnableTags; RemoveMeshStaticOptions.ModifiersToIgnore.Add(Modifier); FMeshGenerationDynamicOptions RemoveMeshDynamicOptions; RemoveMeshDynamicOptions.bLayouts = false; FMeshTask RemoveMeshTask = GenerateMesh(RemoveMeshStaticOptions,UE::Tasks::MakeCompletedTask(RemoveMeshDynamicOptions), MeshRemove); LastMeshTask = UE::Tasks::Launch(TEXT("MutableMeshMergeModifier"), [RemoveMeshTask, LastMeshTask, PreModifiersTask, Edit, Modifier]() mutable { FMeshGenerationResult RemoveResults = RemoveMeshTask.GetResult(); FMeshGenerationResult LastMeshResults = LastMeshTask.GetResult(); FMeshGenerationResult BaseResults = PreModifiersTask.GetResult(); Ptr MaskOp = new ASTOpMeshMaskDiff; // By default, remove from the base Ptr RemoveFrom = BaseResults.BaseMeshOp; MaskOp->Source = RemoveFrom; MaskOp->Fragment = RemoveResults.MeshOp; Ptr RemoveOp = new ASTOpMeshRemoveMask(); RemoveOp->source = LastMeshResults.MeshOp; RemoveOp->FaceCullStrategy = Edit->FaceCullStrategy; RemoveOp->AddRemove(Modifier.FinalCondition, MaskOp); LastMeshResults.MeshOp = RemoveOp; return LastMeshResults; }, UE::Tasks::Prerequisites(RemoveMeshTask, LastMeshTask) ); } } } // Process mesh morph modifiers (from edit modifiers) for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (StaticOptions.ModifiersToIgnore.Contains(Modifier)) { // Prevent recursion. continue; } if (Modifier.Node->GetType() == NodeModifierSurfaceEdit::GetStaticType()) { const NodeModifierSurfaceEdit* Edit = static_cast(Modifier.Node); if (Edit->MeshMorph.IsEmpty()) { continue; } check(OriginalMeshNode); // Request morphed skeletal mesh TSharedPtr< TSharedPtr > ResolveMorphedMesh = MakeShared>(); UE::Tasks::FTask TargetMeshTask; { FMesh* OriginalMesh = OriginalMeshNode->Value.Get(); check(OriginalMesh->IsReference()); uint32 OriginalMeshID = OriginalMesh->GetReferencedMesh(); bool bRunImmediatlyIfPossible = IsInGameThread(); TargetMeshTask = CompilerOptions->OptimisationOptions.ReferencedMeshResourceProvider( OriginalMeshID, Edit->MeshMorph, ResolveMorphedMesh, bRunImmediatlyIfPossible ); } // Factor Ptr FactorOp; if (Edit->MorphFactor) { FScalarGenerationResult ChildResult; GenerateScalar(ChildResult, StaticOptions, Edit->MorphFactor); FactorOp = ChildResult.op; } else { Ptr auxNode = new NodeScalarConstant(); auxNode->Value = 1.0f; FScalarGenerationResult ChildResult; GenerateScalar(ChildResult, StaticOptions, auxNode); FactorOp = ChildResult.op; } Ptr OriginalMeshCopy = OriginalMeshNode; LastMeshTask = UE::Tasks::Launch(TEXT("MutableMeshMorphModifier"), [TargetMeshTask, LastMeshTask, PreModifiersTask, Edit, Modifier, ResolveMorphedMesh, FactorOp, CompilerOptions=CompilerOptions, OriginalMeshCopy]() mutable { FMeshGenerationResult LastMeshResults = LastMeshTask.GetResult(); FMeshGenerationResult BaseResults = PreModifiersTask.GetResult(); TSharedPtr TargetMesh = *ResolveMorphedMesh; if (!TargetMesh) { return LastMeshResults; } // Target mesh Ptr TargetMeshOp = new ASTOpConstantResource; TargetMeshOp->Type = EOpType::ME_CONSTANT; TargetMeshOp->SetValue(TargetMesh->Clone(), CompilerOptions->OptimisationOptions.DiskCacheContext); TargetMeshOp->SourceDataDescriptor = OriginalMeshCopy->SourceDataDescriptor; // Morph generation through mesh diff Ptr DiffOp; { DiffOp = new ASTOpMeshDifference(); DiffOp->Base = BaseResults.BaseMeshOp; DiffOp->Target = TargetMeshOp; // Morphing tex coords here is not supported: // Generating the homogoneous UVs is difficult since we don't have the base // layout yet. DiffOp->bIgnoreTextureCoords = true; } // Morph operation Ptr MorphOp = new ASTOpMeshMorph(); { MorphOp->Base = LastMeshResults.MeshOp; MorphOp->Target = DiffOp; MorphOp->Factor = FactorOp; } // Condition to apply the morph if (Modifier.FinalCondition) { Ptr ConditionalOp = new ASTOpConditional(); ConditionalOp->type = EOpType::ME_CONDITIONAL; ConditionalOp->no = LastMeshResults.MeshOp; ConditionalOp->yes = MorphOp; ConditionalOp->condition = Modifier.FinalCondition; LastMeshResults.MeshOp = ConditionalOp; } else { LastMeshResults.MeshOp = MorphOp; } return LastMeshResults; }, UE::Tasks::Prerequisites(TargetMeshTask, LastMeshTask) ); } } // Process clip-with-mesh modifiers for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (StaticOptions.ModifiersToIgnore.Contains(Modifier)) { // Prevent recursion. continue; } if (Modifier.Node->GetType()== NodeModifierMeshClipWithMesh::GetStaticType()) { const NodeModifierMeshClipWithMesh* TypedClipNode = static_cast(Modifier.Node); FMeshGenerationDynamicOptions RemoveMeshDynamicOptions; RemoveMeshDynamicOptions.bLayouts = false; FMeshGenerationStaticOptions ClipStaticOptions(StaticOptions); ClipStaticOptions.ModifiersToIgnore.AddUnique(Modifier); ClipStaticOptions.ActiveTags.Empty(); FMeshTask ClipMeshTask = GenerateMesh(ClipStaticOptions, UE::Tasks::MakeCompletedTask(RemoveMeshDynamicOptions), TypedClipNode->ClipMesh); LastMeshTask = UE::Tasks::Launch(TEXT("MutableMeshMergeModifier"), [ClipMeshTask, LastMeshTask, PreModifiersTask, Modifier, TypedClipNode, ErrorLog=ErrorLog, ErrorContext]() mutable { FMeshGenerationResult ClipResults = ClipMeshTask.GetResult(); FMeshGenerationResult LastMeshResults = LastMeshTask.GetResult(); FMeshGenerationResult BaseResults = PreModifiersTask.GetResult(); if (!ClipResults.MeshOp) { ErrorLog->Add("Clip mesh has not been generated", ELMT_ERROR, ErrorContext); return LastMeshResults; } Ptr MaskOp = new ASTOpMeshMaskClipMesh(); MaskOp->source = BaseResults.MeshOp; MaskOp->clip = ClipResults.MeshOp; Ptr RemoveOp = new ASTOpMeshRemoveMask(); RemoveOp->source = LastMeshResults.MeshOp; RemoveOp->FaceCullStrategy = TypedClipNode->FaceCullStrategy; RemoveOp->AddRemove(Modifier.FinalCondition, MaskOp); LastMeshResults.MeshOp = RemoveOp; return LastMeshResults; }, UE::Tasks::Prerequisites(ClipMeshTask, LastMeshTask) ); } } // Process clip-with-mask modifiers for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (StaticOptions.ModifiersToIgnore.Contains(Modifier)) { // Prevent recursion. continue; } if (Modifier.Node->GetType() == NodeModifierMeshClipWithUVMask::GetStaticType()) { // Create a constant mesh with the original UVs required by this modifier. // TODO: Optimize, by caching. // TODO: Optimize by formatting and keeping only UVs check(OriginalMeshNode); TSharedPtr< TSharedPtr > ResolveOriginalMesh = MakeShared>(); UE::Tasks::FTask TargetMeshTask; { FMesh* OriginalMesh = OriginalMeshNode->Value.Get(); check(OriginalMesh->IsReference()); uint32 OriginalMeshID = OriginalMesh->GetReferencedMesh(); bool bRunImmediatlyIfPossible = IsInGameThread(); FString NoMorph; TargetMeshTask = CompilerOptions->OptimisationOptions.ReferencedMeshResourceProvider( OriginalMeshID, NoMorph, ResolveOriginalMesh, bRunImmediatlyIfPossible); } const NodeModifierMeshClipWithUVMask* TypedClipNode = static_cast(Modifier.Node); Ptr MaskImage; TSharedPtr Layout; if (TypedClipNode->ClipMask) { FImageGenerationOptions ClipOptions(StaticOptions.ComponentId, StaticOptions.LODIndex); ClipOptions.ImageLayoutStrategy = CompilerOptions::TextureLayoutStrategy::None; ClipOptions.LayoutBlockId = FLayoutBlock::InvalidBlockId; ClipOptions.State = StaticOptions.State; FImageGenerationResult ClipMaskResult; GenerateImage(ClipOptions, ClipMaskResult, TypedClipNode->ClipMask); // It could be IF_L_UBIT, but since this should be optimized out at compile time, leave the most cpu efficient. MaskImage = GenerateImageFormat(ClipMaskResult.op, mu::EImageFormat::L_UByte); } else if (TypedClipNode->ClipLayout) { // Generate the layout with blocks to extract Layout = GenerateLayout(TypedClipNode->ClipLayout, 0); } Ptr OriginalMeshCopy = OriginalMeshNode; LastMeshTask = UE::Tasks::Launch(TEXT("MutableModifier"), [Layout, MaskImage, ResolveOriginalMesh, LastMeshTask, PreModifiersTask, Modifier, TypedClipNode, CompilerOptions = CompilerOptions, ErrorLog = ErrorLog, ErrorContext, OriginalMeshCopy]() mutable { TSharedPtr TargetMesh = *ResolveOriginalMesh; if (!TargetMesh) { return FMeshGenerationResult(); } FMeshGenerationResult LastMeshResults = LastMeshTask.GetResult(); FMeshGenerationResult BaseResults = PreModifiersTask.GetResult(); Ptr UVMeshOp = new ASTOpConstantResource; UVMeshOp->Type = EOpType::ME_CONSTANT; UVMeshOp->SetValue(TargetMesh->Clone(), CompilerOptions->OptimisationOptions.DiskCacheContext); UVMeshOp->SourceDataDescriptor = OriginalMeshCopy->SourceDataDescriptor; Ptr MeshMaskAt = new ASTOpMeshMaskClipUVMask(); MeshMaskAt->Source = BaseResults.BaseMeshOp; MeshMaskAt->UVSource = UVMeshOp; MeshMaskAt->LayoutIndex = TypedClipNode->LayoutIndex; if (TypedClipNode->ClipMask) { MeshMaskAt->MaskImage = MaskImage; if (!MeshMaskAt->MaskImage) { ErrorLog->Add("Clip UV mask has not been generated", ELMT_ERROR, ErrorContext); return LastMeshResults; } } else if (TypedClipNode->ClipLayout) { Ptr LayoutOp = new ASTOpConstantResource(); LayoutOp->Type = EOpType::LA_CONSTANT; LayoutOp->SetValue(Layout, CompilerOptions->OptimisationOptions.DiskCacheContext); MeshMaskAt->MaskLayout = LayoutOp; } else { // No mask or layout specified to clip. Don't clip anything. return LastMeshResults; } if (MeshMaskAt) { Ptr RemoveOp = new ASTOpMeshRemoveMask(); RemoveOp->source = LastMeshResults.MeshOp; RemoveOp->FaceCullStrategy = TypedClipNode->FaceCullStrategy; RemoveOp->AddRemove(Modifier.FinalCondition, MeshMaskAt); LastMeshResults.MeshOp = RemoveOp; } return LastMeshResults; }, UE::Tasks::Prerequisites(TargetMeshTask, LastMeshTask) ); } } // Process clip-morph-plane modifiers for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (StaticOptions.ModifiersToIgnore.Contains(Modifier)) { // Prevent recursion. continue; } if (Modifier.Node->GetType() == NodeModifierMeshClipMorphPlane::GetStaticType()) { const NodeModifierMeshClipMorphPlane* TypedNode = static_cast(Modifier.Node); LastMeshTask = UE::Tasks::Launch(TEXT("MutableModifier"), [LastMeshTask, PreModifiersTask, Modifier, TypedNode]() mutable { FMeshGenerationResult LastMeshResults = LastMeshTask.GetResult(); FMeshGenerationResult BaseResults = PreModifiersTask.GetResult(); Ptr ClipOp = new ASTOpMeshClipMorphPlane(); ClipOp->Source = LastMeshResults.MeshOp; ClipOp->FaceCullStrategy = TypedNode->Parameters.FaceCullStrategy; // Morph to an ellipse { FShape MorphShape; MorphShape.type = (uint8_t)FShape::Type::Ellipse; MorphShape.position = TypedNode->Parameters.Origin; MorphShape.up = TypedNode->Parameters.Normal; // TODO: Move rotation to ellipse rotation reference base instead of passing it directly MorphShape.size = FVector3f(TypedNode->Parameters.Radius1, TypedNode->Parameters.Radius2, TypedNode->Parameters.Rotation); // Generate a "side" vector. // \todo: make generic and move to the vector class { // Generate vector perpendicular to normal for ellipse rotation reference base FVector3f AuxBase(0.f, 1.f, 0.f); if (FMath::Abs(FVector3f::DotProduct(TypedNode->Parameters.Normal, AuxBase)) > 0.95f) { AuxBase = FVector3f(0.f, 0.f, 1.f); } MorphShape.side = FVector3f::CrossProduct(TypedNode->Parameters.Normal, AuxBase); } ClipOp->MorphShape = MorphShape; } // Selection box ClipOp->VertexSelectionType = TypedNode->Parameters.VertexSelectionType; if (ClipOp->VertexSelectionType == EClipVertexSelectionType::Shape) { FShape SelectionShape; SelectionShape.type = (uint8)FShape::Type::AABox; SelectionShape.position = TypedNode->Parameters.SelectionBoxOrigin; SelectionShape.size = TypedNode->Parameters.SelectionBoxRadius; ClipOp->SelectionShape = SelectionShape; } else if (ClipOp->VertexSelectionType == EClipVertexSelectionType::BoneHierarchy) { ClipOp->VertexSelectionBone = TypedNode->Parameters.VertexSelectionBone; ClipOp->VertexSelectionBoneMaxRadius = TypedNode->Parameters.MaxEffectRadius; } ClipOp->Dist = TypedNode->Parameters.DistanceToPlane; ClipOp->Factor = TypedNode->Parameters.LinearityFactor; Ptr ConditionalOp = new ASTOpConditional(); ConditionalOp->type = EOpType::ME_CONDITIONAL; ConditionalOp->no = LastMeshResults.MeshOp; ConditionalOp->yes = ClipOp; ConditionalOp->condition = Modifier.FinalCondition; LastMeshResults.MeshOp = ConditionalOp; return LastMeshResults; }, UE::Tasks::Prerequisites(LastMeshTask) ); } } // Process clip deform modifiers. for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (StaticOptions.ModifiersToIgnore.Contains(Modifier)) { // Prevent recursion. continue; } if (Modifier.Node->GetType() == NodeModifierMeshClipDeform::GetStaticType()) { const NodeModifierMeshClipDeform* TypedClipNode = static_cast(Modifier.Node); FMeshGenerationStaticOptions ClipStaticOptions(StaticOptions); ClipStaticOptions.ActiveTags.Empty(); ClipStaticOptions.ModifiersToIgnore.AddUnique(Modifier); FMeshGenerationDynamicOptions ClipMeshDynamicOptions; ClipMeshDynamicOptions.bLayouts = false; FMeshTask ClipMeshTask = GenerateMesh(ClipStaticOptions, UE::Tasks::MakeCompletedTask(ClipMeshDynamicOptions), TypedClipNode->ClipMesh); LastMeshTask = UE::Tasks::Launch(TEXT("MutableMeshMergeModifier"), [ClipMeshTask, LastMeshTask, Modifier, TypedClipNode, ErrorLog = ErrorLog, ErrorContext]() mutable { FMeshGenerationResult ClipResults = ClipMeshTask.GetResult(); FMeshGenerationResult LastMeshResults = LastMeshTask.GetResult(); Ptr BindOp = new ASTOpMeshBindShape(); BindOp->Mesh = LastMeshResults.MeshOp; BindOp->Shape = ClipResults.MeshOp; BindOp->BindingMethod = static_cast(TypedClipNode->BindingMethod); Ptr ClipOp = new ASTOpMeshClipDeform(); ClipOp->FaceCullStrategy = TypedClipNode->FaceCullStrategy; ClipOp->ClipShape = ClipResults.MeshOp; ClipOp->Mesh = BindOp; if (!ClipOp->ClipShape) { ErrorLog->Add("Clip shape mesh has not been generated", ELMT_ERROR, ErrorContext); return LastMeshResults; } Ptr Op = new ASTOpConditional(); Op->type = EOpType::ME_CONDITIONAL; Op->no = LastMeshResults.MeshOp; Op->yes = ClipOp; Op->condition = Modifier.FinalCondition; LastMeshResults.MeshOp = Op; return LastMeshResults; }, UE::Tasks::Prerequisites(ClipMeshTask, LastMeshTask) ); } } // Process transform mesh within mesh modifiers. for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (StaticOptions.ModifiersToIgnore.Contains(Modifier)) { // Prevent recursion. continue; } if (Modifier.Node->GetType()== NodeModifierMeshTransformInMesh::GetStaticType()) { const NodeModifierMeshTransformInMesh* TypedTransformNode = static_cast(Modifier.Node); // If a matrix node is not connected, the op won't do anything, so let's not create it at all. if (TypedTransformNode->MatrixNode) { if (!TypedTransformNode->MatrixNode || !TypedTransformNode->BoundingMesh) { ErrorLog->Add("Bounding mesh or matrix have not been generated", ELMT_ERROR, ErrorContext); continue; } // Transform matrix. Ptr MatrixOp; FMatrixGenerationResult ChildResult; GenerateMatrix(ChildResult, StaticOptions, TypedTransformNode->MatrixNode); MatrixOp = ChildResult.op; // Bounding mesh FMeshGenerationStaticOptions ClipStaticOptions(StaticOptions); ClipStaticOptions.ActiveTags.Empty(); ClipStaticOptions.ModifiersToIgnore.AddUnique(Modifier); FMeshGenerationDynamicOptions BoundingMeshDynamicOptions; BoundingMeshDynamicOptions.bLayouts = false; FMeshTask BoundingMeshTask = GenerateMesh(ClipStaticOptions, UE::Tasks::MakeCompletedTask(BoundingMeshDynamicOptions), TypedTransformNode->BoundingMesh); LastMeshTask = UE::Tasks::Launch(TEXT("MutableMeshMergeModifier"), [BoundingMeshTask, LastMeshTask, Modifier, MatrixOp, ErrorLog = ErrorLog, ErrorContext]() mutable { FMeshGenerationResult BoundingResults = BoundingMeshTask.GetResult(); FMeshGenerationResult LastMeshResults = LastMeshTask.GetResult(); Ptr TransformOp = new ASTOpMeshTransformWithBoundingMesh(); TransformOp->source = LastMeshResults.MeshOp; TransformOp->matrix = MatrixOp; TransformOp->boundingMesh = BoundingResults.MeshOp; if (TransformOp->boundingMesh) { ASTOp::EClosedMeshTest IsClosed = TransformOp->boundingMesh->IsClosedMesh(); if (IsClosed == ASTOp::EClosedMeshTest::No) { ErrorLog->Add("Mesh used for clipping is not closed.", ELMT_WARNING, ErrorContext); } } // Condition to apply the transform op if (Modifier.FinalCondition) { Ptr ConditionalOp = new ASTOpConditional(); ConditionalOp->type = EOpType::ME_CONDITIONAL; ConditionalOp->no = LastMeshResults.MeshOp; ConditionalOp->yes = TransformOp; ConditionalOp->condition = Modifier.FinalCondition; LastMeshResults.MeshOp = ConditionalOp; } else { LastMeshResults.MeshOp = TransformOp; } return LastMeshResults; }, UE::Tasks::Prerequisites(BoundingMeshTask, LastMeshTask) ); } } } return LastMeshTask; } Ptr CodeGenerator::ApplyImageBlockModifiers( const TArray& Modifiers, const FImageGenerationOptions& Options, Ptr BaseImageOp, const NodeSurfaceNew::FImageData& ImageData, FIntPoint GridSize, const FLayoutBlockDesc& LayoutBlockDesc, box< FIntVector2 > RectInCells, const void* ErrorContext) { Ptr LastImageOp = BaseImageOp; int32 CurrentLOD = Options.LODIndex; check(CurrentLOD >= 0); // Process patch image modifiers (from edit modifiers) for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (Modifier.Node->GetType() == NodeModifierSurfaceEdit::GetStaticType()) { const NodeModifierSurfaceEdit* Edit = static_cast(Modifier.Node); bool bAffectsCurrentLOD = Edit->LODs.IsValidIndex(CurrentLOD); if (!bAffectsCurrentLOD) { continue; } const NodeModifierSurfaceEdit::FTexture* MatchingEdit = Edit->LODs[CurrentLOD].Textures.FindByPredicate( [&](const NodeModifierSurfaceEdit::FTexture& Candidate) { return (Candidate.MaterialParameterName == ImageData.MaterialParameterName); }); if ( !MatchingEdit) { continue; } if (MatchingEdit->PatchImage.get()) { // Does the current block need to be patched? Find out by building a mask. TSharedPtr PatchMask = GenerateImageBlockPatchMask(*MatchingEdit, GridSize, LayoutBlockDesc.BlockPixelsX, LayoutBlockDesc.BlockPixelsY, RectInCells); if (PatchMask) { LastImageOp = GenerateImageBlockPatch(LastImageOp, *MatchingEdit, PatchMask, Modifier.FinalCondition, Options); } } } else { // This modifier doesn't affect the per-block image operations. } } return LastImageOp; } void CodeGenerator::UpdateLayoutBlockDesc(CodeGenerator::FLayoutBlockDesc& Out, FImageDesc BlockDesc, FIntVector2 LayoutCellSize) { if (Out.BlockPixelsX == 0 && LayoutCellSize.X > 0 && LayoutCellSize.Y > 0) { Out.BlockPixelsX = FMath::Max(1, BlockDesc.m_size[0] / LayoutCellSize[0]); Out.BlockPixelsY = FMath::Max(1, BlockDesc.m_size[1] / LayoutCellSize[1]); Out.bBlocksHaveMips = BlockDesc.m_lods > 1; if (Out.FinalFormat==EImageFormat::None) { Out.FinalFormat = BlockDesc.m_format; } } }; Ptr CodeGenerator::ApplyImageExtendModifiers( const TArray& Modifiers, const FMeshGenerationStaticOptions& Options, const FMeshGenerationResult& BaseMeshResults, Ptr BaseImageOp, CompilerOptions::TextureLayoutStrategy ImageLayoutStrategy, int32 LayoutIndex, const NodeSurfaceNew::FImageData& ImageData, FIntPoint GridSize, CodeGenerator::FLayoutBlockDesc& InOutLayoutBlockDesc, const void* ModifiedNodeErrorContext) { Ptr LastImageOp = BaseImageOp; int32 CurrentLOD = Options.LODIndex; check(CurrentLOD >= 0); // Process mesh extend modifiers (from edit modifiers) int32 EditIndex = 0; for (const FirstPassGenerator::FModifier& Modifier : Modifiers) { if (Modifier.Node->GetType() == NodeModifierSurfaceEdit::GetStaticType()) { const NodeModifierSurfaceEdit* Edit = static_cast(Modifier.Node); int32 ThisEditIndex = EditIndex; ++EditIndex; bool bAffectsCurrentLOD = Edit->LODs.IsValidIndex(CurrentLOD); if (!bAffectsCurrentLOD) { continue; } const NodeModifierSurfaceEdit::FTexture* MatchingEdit = Edit->LODs[CurrentLOD].Textures.FindByPredicate( [&](const NodeModifierSurfaceEdit::FTexture& Candidate) { return (Candidate.MaterialParameterName == ImageData.MaterialParameterName); }); if (!MatchingEdit || (MatchingEdit && !MatchingEdit->Extend) ) { if (Edit->LODs[CurrentLOD].MeshAdd) { // When extending a mesh section it is mandatory to provide textures for all section textures handled by Mutable. FString Msg = FString::Printf(TEXT("Required texture [%s] is missing when trying to extend a mesh section."), *ImageData.MaterialParameterName); ErrorLog->Add(Msg, ELMT_INFO, Edit->GetMessageContext(), ModifiedNodeErrorContext); } continue; } if (!BaseMeshResults.ExtraMeshLayouts.IsValidIndex(ThisEditIndex)) { ErrorLog->Add(TEXT("Trying to extend a layout that doesn't exist."), ELMT_WARNING, Edit->GetMessageContext(), ModifiedNodeErrorContext); continue; } const TArray& ExtraLayouts = BaseMeshResults.ExtraMeshLayouts[ThisEditIndex].GeneratedLayouts; if (LayoutIndex >= ExtraLayouts.Num() || !ExtraLayouts[LayoutIndex].Layout) { ErrorLog->Add(TEXT("Trying to extend a layout that doesn't exist."), ELMT_WARNING, Edit->GetMessageContext(), ModifiedNodeErrorContext); continue; } TSharedPtr ExtendLayout = ExtraLayouts[LayoutIndex].Layout; Ptr LastBase = LastImageOp; for (int32 BlockIndex = 0; BlockIndex < ExtendLayout->GetBlockCount(); ++BlockIndex) { // Generate the image block FImageGenerationOptions ImageOptions(Options.ComponentId,Options.LODIndex); ImageOptions.State = Options.State; ImageOptions.ImageLayoutStrategy = ImageLayoutStrategy; ImageOptions.ActiveTags = Edit->EnableTags; // TODO: Merge with current tags? ImageOptions.RectSize = { 0,0 }; ImageOptions.LayoutToApply = ExtendLayout; ImageOptions.LayoutBlockId = ExtendLayout->Blocks[BlockIndex].Id; FImageGenerationResult ExtendResult; GenerateImage(ImageOptions, ExtendResult, MatchingEdit->Extend); Ptr FragmentOp = ExtendResult.op; // Block in layout grid units box< FIntVector2 > RectInCells; RectInCells.min = ExtendLayout->Blocks[BlockIndex].Min; RectInCells.size = ExtendLayout->Blocks[BlockIndex].Size; FImageDesc ExtendDesc = FragmentOp->GetImageDesc(); // If we don't know the size of a layout block in pixels, calculate it UpdateLayoutBlockDesc(InOutLayoutBlockDesc, ExtendDesc, RectInCells.size); // Adjust the format and size of the block to be added // Actually don't do it, it will be propagated from the top format operation. //FragmentOp = GenerateImageFormat(FragmentOp, FinalImageFormat); UE::Math::TIntVector2 ExpectedSize; ExpectedSize[0] = InOutLayoutBlockDesc.BlockPixelsX * RectInCells.size[0]; ExpectedSize[1] = InOutLayoutBlockDesc.BlockPixelsY * RectInCells.size[1]; FragmentOp = GenerateImageSize(FragmentOp, ExpectedSize); // Apply tiling to avoid generating chunks of image that are too big. FragmentOp = ApplyTiling(FragmentOp, ExpectedSize, InOutLayoutBlockDesc.FinalFormat); // Compose operation Ptr ComposeOp = new ASTOpImageCompose(); ComposeOp->Layout = BaseMeshResults.LayoutOps[LayoutIndex]; ComposeOp->Base = LastBase; ComposeOp->BlockImage = FragmentOp; // Set the absolute block index. check(ExtendLayout->Blocks[BlockIndex].Id != FLayoutBlock::InvalidBlockId); ComposeOp->BlockId = ExtendLayout->Blocks[BlockIndex].Id; LastBase = ComposeOp; } // Condition to enable this image extension if (Modifier.FinalCondition) { Ptr Op = new ASTOpConditional(); Op->type = EOpType::IM_CONDITIONAL; Op->no = LastImageOp; Op->yes = LastBase; Op->condition = Modifier.FinalCondition; LastImageOp = Op; } else { LastImageOp = LastBase; } } } return LastImageOp; } void CodeGenerator::CheckModifiersForSurface(const NodeSurfaceNew& Node, const TArray& Modifiers, int32 LODIndex ) { int32 CurrentLOD = LODIndex; check(CurrentLOD >= 0); for (const FirstPassGenerator::FModifier& Mod : Modifiers) { // A mistake in the surface edit modifier usually results in no change visible. Try to detect it. if (Mod.Node->GetType() == NodeModifierSurfaceEdit::GetStaticType()) { const NodeModifierSurfaceEdit* Edit = static_cast(Mod.Node); bool bAffectsCurrentLOD = Edit->LODs.IsValidIndex(CurrentLOD); if (!bAffectsCurrentLOD) { continue; } if (Node.Images.IsEmpty() || Edit->LODs[CurrentLOD].Textures.IsEmpty()) { continue; } bool bAtLeastSomeTexture = false; for (NodeSurfaceNew::FImageData Data : Node.Images) { const NodeModifierSurfaceEdit::FTexture* MatchingEdit = Edit->LODs[CurrentLOD].Textures.FindByPredicate( [&](const NodeModifierSurfaceEdit::FTexture& Candidate) { return (Candidate.MaterialParameterName == Data.MaterialParameterName); }); if (MatchingEdit) { bAtLeastSomeTexture = true; break; } } if (!bAtLeastSomeTexture) { ErrorLog->Add(TEXT("A mesh section modifier applies to a section but no texture matches."), ELMT_WARNING, Edit->GetMessageContext(), Node.GetMessageContext()); } } } } }