// Copyright Epic Games, Inc. All Rights Reserved. #include "LandscapeImportHelper.h" #include "LandscapeEditorModule.h" #include "LandscapeDataAccess.h" #include "LandscapeConfigHelper.h" #include "Modules/ModuleManager.h" #include "HAL/FileManager.h" #include "Misc/Paths.h" #include "LandscapeEditorUtils.h" #define LOCTEXT_NAMESPACE "LandscapeImportHelper" bool FLandscapeImportHelper::ExtractCoordinates(const FString& BaseFilename, FIntPoint& OutCoord, FString& OutBaseFilePattern) { //We expect file name in form: _x_y int32 XPos = BaseFilename.Find(TEXT("_x"), ESearchCase::IgnoreCase, ESearchDir::FromEnd); int32 YPos = BaseFilename.Find(TEXT("_y"), ESearchCase::IgnoreCase, ESearchDir::FromEnd); if (XPos != INDEX_NONE && YPos != INDEX_NONE && XPos < YPos) { FString XCoord = BaseFilename.Mid(XPos + 2, YPos - (XPos + 2)); FString YCoord = BaseFilename.Mid(YPos + 2, BaseFilename.Len() - (YPos + 2)); if (XCoord.IsNumeric() && YCoord.IsNumeric()) { OutBaseFilePattern = BaseFilename.Mid(0, XPos); TTypeFromString::FromString(OutCoord.X, *XCoord); TTypeFromString::FromString(OutCoord.Y, *YCoord); return true; } } return false; } void FLandscapeImportHelper::GetMatchingFiles(const FString& FilePathPattern, TArray& OutFileToImport) { FString BaseFilePathPattern = FPaths::GetBaseFilename(FilePathPattern); IFileManager::Get().IterateDirectoryRecursively(*FPaths::GetPath(FilePathPattern), [&OutFileToImport, &FilePathPattern, &BaseFilePathPattern](const TCHAR* FilenameOrDirectory, bool bIsDirectory) { if (!bIsDirectory) { FString Filename(FilenameOrDirectory); FString BaseFilename = FPaths::GetBaseFilename(Filename); bool bIsValidFile = false; // The file name must match either exactly (e.g. MyHeightmap.png) : if (BaseFilename == BaseFilePathPattern) { bIsValidFile = true; } // Or it must exactly match the pattern (e.g. MyHeightmap_x0_y0.png ok, MyHeightmap0.png not ok) : else { FIntPoint Coord; FString BaseFilePattern; bool bIsValidPatternFileName = FLandscapeImportHelper::ExtractCoordinates(BaseFilename, Coord, BaseFilePattern); bIsValidFile = bIsValidPatternFileName && (BaseFilePattern == BaseFilePathPattern); } if (bIsValidFile) { OutFileToImport.Add(Filename); } } return true; }); } template ELandscapeImportResult GetImportDataInternal(const FLandscapeImportDescriptor& ImportDescriptor, int32 DescriptorIndex, FName LayerName, T DefaultValue, TArray& OutData, FText& OutMessage) { if (DescriptorIndex < 0 || DescriptorIndex >= ImportDescriptor.ImportResolutions.Num()) { OutMessage = LOCTEXT("Import_InvalidDescriptorIndex", "Invalid Descriptor Index"); return ELandscapeImportResult::Error; } if (!ImportDescriptor.FileDescriptors.Num() || ImportDescriptor.ImportResolutions.Num() != ImportDescriptor.FileResolutions.Num()) { OutMessage = LOCTEXT("Import_InvalidDescriptor", "Invalid Descriptor"); return ELandscapeImportResult::Error; } int64 TotalWidth = ImportDescriptor.ImportResolutions[DescriptorIndex].Width; // convert from uint32 int64 TotalHeight = ImportDescriptor.ImportResolutions[DescriptorIndex].Height; if (TotalWidth <= 0 || TotalHeight <= 0) { OutMessage = LOCTEXT("Import_InvalidImportResolution", "Import Resolution is not valid"); return ELandscapeImportResult::Error; } if (TotalWidth > MAX_int64 / TotalHeight) // Total Pixels should fit in an int64 { OutMessage = LOCTEXT("Import_ImageTooLarge", "Landscape image is too large"); return ELandscapeImportResult::Error; } int64 TotalPixels = TotalWidth * TotalHeight; OutData.Reset(); OutData.SetNumZeroed(TotalPixels); // Initialize All to default value so that non-covered regions have data TArray StrideData; StrideData.SetNumUninitialized(TotalWidth); for (int64 X = 0; X < TotalWidth; ++X) { StrideData[X] = DefaultValue; } for (int64 Y = 0; Y < TotalHeight; ++Y) { FMemory::Memcpy(&OutData[Y * TotalWidth], StrideData.GetData(), sizeof(T) * TotalWidth); } ELandscapeImportResult Result = ELandscapeImportResult::Success; // Import Regions ILandscapeEditorModule& LandscapeEditorModule = FModuleManager::GetModuleChecked("LandscapeEditor"); const ILandscapeFileFormat* FileFormat = LandscapeEditorModule.GetFormatByExtension(*FPaths::GetExtension(ImportDescriptor.FileDescriptors[0].FilePath, true)); check(FileFormat); int64 FileWidth = ImportDescriptor.FileResolutions[DescriptorIndex].Width; // convert from uint32 int64 FileHeight = ImportDescriptor.FileResolutions[DescriptorIndex].Height; for (const FLandscapeImportFileDescriptor& FileDescriptor : ImportDescriptor.FileDescriptors) { FLandscapeImportData ImportData = FileFormat->Import(*FileDescriptor.FilePath, LayerName, ImportDescriptor.FileResolutions[DescriptorIndex]); OutMessage = ImportData.ErrorMessage; Result = ImportData.ResultCode; if (ImportData.ResultCode == ELandscapeImportResult::Error) { break; } // NOTE: this assumes the same file resolution for all descriptors.. int64 StartX = FileDescriptor.Coord.X * FileWidth; int64 StartY = FileDescriptor.Coord.Y * FileHeight; for (int64 Y = 0; Y < FileHeight; ++Y) { int64 DestY = StartY + Y; FMemory::Memcpy(&OutData[DestY * TotalWidth + StartX], &ImportData.Data[Y * FileWidth], FileWidth * sizeof(T)); } } return Result; } template ELandscapeImportResult GetImportDescriptorInternal(const FString& FilePath, bool bSingleFile, bool bFlipYAxis, FName LayerName, FLandscapeImportDescriptor& OutImportDescriptor, FText& OutMessage) { OutImportDescriptor.Reset(); if (FilePath.IsEmpty()) { OutMessage = LOCTEXT("Import_InvalidPath", "Invalid file"); return ELandscapeImportResult::Error; } FIntPoint OutCoord{}; FIntPoint MinCoord(INT32_MAX, INT32_MAX); // All coords should be rebased to the min FIntPoint MaxCoord(INT32_MIN, INT32_MIN); FString OutFileImportPattern; FString FilePathPattern; TArray OutFilesToImport; // If we are handling multiple files handle the case where Filepath is in the _xN_yM.extention format or if it's a pattern already if (!bSingleFile) { if (FLandscapeImportHelper::ExtractCoordinates(FPaths::GetBaseFilename(FilePath), OutCoord, OutFileImportPattern)) { FilePathPattern = FPaths::GetPath(FilePath) / OutFileImportPattern; } else { FilePathPattern = FPaths::GetBaseFilename(FilePath, false); } FLandscapeImportHelper::GetMatchingFiles(FilePathPattern, OutFilesToImport); if (OutFilesToImport.IsEmpty()) { return ELandscapeImportResult::Error; } if (OutFilesToImport.Contains(FilePath)) { // If one of the files found has a name that exactly matches yet we have on or more numbered files matching the pattern, then we have an ambiguity and warn the user about it : if (OutFilesToImport.Num() > 1) { OutMessage = FText::Format(LOCTEXT("Import_AmbiguousImportFile", "Ambiguous import files found :\nThere's a single '{0}.{3}' file and {1} {1}|plural(one=file,other=files) whose {1}|plural(one=name,other=names) {1}|plural(one=matches,other=match) the proper input pattern (ex: '{2}_x0_y0.{3}').\nPlease rename the single file."), FText::FromString(FPaths::GetBaseFilename(FilePath)), OutFilesToImport.Num() - 1, FText::FromString(FPaths::GetBaseFilename(FilePathPattern)), FText::FromString(FPaths::GetExtension(FilePath))); return ELandscapeImportResult::Error; } else { // We have a single file found and it's exactly matching the expected file name, consider we're in single file mode : check(OutFilesToImport.Num() == 1); bSingleFile = true; } } } else { OutFilesToImport.Add(FilePath); } TArray ImportResolutions; ILandscapeEditorModule& LandscapeEditorModule = FModuleManager::GetModuleChecked("LandscapeEditor"); const ILandscapeFileFormat* FileFormat = nullptr; for (const FString& ImportFilename : OutFilesToImport) { const bool bFirst = FileFormat == nullptr; const ILandscapeFileFormat* CurrentFileFormat = LandscapeEditorModule.GetFormatByExtension(*FPaths::GetExtension(ImportFilename, true)); if (FileFormat != nullptr && FileFormat != CurrentFileFormat) { OutMessage = LOCTEXT("Import_MismatchFileType", "Not all files have the same file type"); return ELandscapeImportResult::Error; } FileFormat = CurrentFileFormat; if (FileFormat) { FLandscapeFileInfo FileInfo = FileFormat->Validate(*ImportFilename); if (FileInfo.ResultCode == ELandscapeImportResult::Error) { OutMessage = FileInfo.ErrorMessage; return FileInfo.ResultCode; } FString OutLocalFileImportPattern; if (!bSingleFile && !FLandscapeImportHelper::ExtractCoordinates(FPaths::GetBaseFilename(ImportFilename), OutCoord, OutLocalFileImportPattern)) { OutMessage = FText::Format(LOCTEXT("Import_InvalidFilename", "File '{0}' doesn't have the proper pattern(ex: '{1}_x0_y0.{2})'"),FText::FromString(FPaths::GetBaseFilename(ImportFilename)), FText::FromString(OutFileImportPattern), FText::FromString(FPaths::GetExtension(FilePath))); return ELandscapeImportResult::Error; } MinCoord.X = FMath::Min(OutCoord.X, MinCoord.X); MinCoord.Y = FMath::Min(OutCoord.Y, MinCoord.Y); MaxCoord.X = FMath::Max(OutCoord.X, MaxCoord.X); MaxCoord.Y = FMath::Max(OutCoord.Y, MaxCoord.Y); FLandscapeImportFileDescriptor FileDescriptor(ImportFilename, OutCoord); OutImportDescriptor.FileDescriptors.Add(FileDescriptor); if (bFirst) { // Resolutions will need to match for all files (keep the first one to compare) OutImportDescriptor.FileResolutions = MoveTemp(FileInfo.PossibleResolutions); if (FileInfo.DataScale.IsSet()) { OutImportDescriptor.Scale = FileInfo.DataScale.GetValue(); OutImportDescriptor.Scale.Z *= LANDSCAPE_INV_ZSCALE; } } else { if (OutImportDescriptor.FileResolutions != FileInfo.PossibleResolutions) { OutMessage = LOCTEXT("Import_MismatchResolution", "Not all files have the same resolution"); return ELandscapeImportResult::Error; } else if (FileInfo.DataScale.IsSet()) { FVector CurrentScale = FileInfo.DataScale.GetValue(); CurrentScale.Z *= LANDSCAPE_INV_ZSCALE; if (!OutImportDescriptor.Scale.Equals(CurrentScale)) { OutMessage = LOCTEXT("Import_MismatchScale", "Not all files have the same data scale"); return ELandscapeImportResult::Error; } } } } else { OutMessage = LOCTEXT("Import_UnknownFileType", "File type not recognized"); return ELandscapeImportResult::Error; } } check(OutImportDescriptor.FileDescriptors.Num()); // Rebase with MinCoord for (FLandscapeImportFileDescriptor& FileDescriptor : OutImportDescriptor.FileDescriptors) { FileDescriptor.Coord -= MinCoord; } MaxCoord -= MinCoord; // Flip Coordinates on Y axis if (bFlipYAxis) { for (FLandscapeImportFileDescriptor& FileDescriptor : OutImportDescriptor.FileDescriptors) { FileDescriptor.Coord.Y = MaxCoord.Y - FileDescriptor.Coord.Y; } } // Compute Import Total Size for (const FLandscapeFileResolution& Resolution : OutImportDescriptor.FileResolutions) { OutImportDescriptor.ImportResolutions.Add(FLandscapeImportResolution((MaxCoord.X+1)*Resolution.Width, (MaxCoord.Y+1)*Resolution.Height)); } return ELandscapeImportResult::Success; } template void TransformImportDataInternal(const TArray& InData, TArray& OutData, const FLandscapeImportResolution& CurrentResolution, const FLandscapeImportResolution& RequiredResolution, ELandscapeImportTransformType TransformType, FIntPoint Offset) { check(InData.Num() == CurrentResolution.Width * CurrentResolution.Height); if (TransformType == ELandscapeImportTransformType::Resample) { const FIntRect SrcRegion(0, 0, CurrentResolution.Width - 1, CurrentResolution.Height - 1); const FIntRect DestRegion(0, 0, RequiredResolution.Width - 1, RequiredResolution.Height - 1); FLandscapeConfigHelper::ResampleData(InData, OutData, SrcRegion, DestRegion); } else if(TransformType == ELandscapeImportTransformType::ExpandCentered || TransformType == ELandscapeImportTransformType::ExpandOffset) { int32 OffsetX = 0; int32 OffsetY = 0; if (TransformType == ELandscapeImportTransformType::ExpandCentered) { OffsetX = (int32)(RequiredResolution.Width - CurrentResolution.Width) / 2; OffsetY = (int32)(RequiredResolution.Height - CurrentResolution.Height) / 2; } else if (TransformType == ELandscapeImportTransformType::ExpandOffset) { OffsetX = Offset.X; OffsetY = Offset.Y; } const FIntRect SrcRegion(0, 0, CurrentResolution.Width - 1, CurrentResolution.Height - 1); const FIntRect DestRegion(-OffsetX, -OffsetY, RequiredResolution.Width - OffsetX - 1, RequiredResolution.Height - OffsetY - 1); FLandscapeConfigHelper::ExpandData(InData, OutData, SrcRegion, DestRegion, OffsetX != 0 || OffsetY != 0); } else { OutData = InData; } } ELandscapeImportResult FLandscapeImportHelper::GetHeightmapImportDescriptor(const FString& FilePath, bool bSingleFile, bool bFlipYAxis, FLandscapeImportDescriptor& OutImportDescriptor, FText& OutMessage) { return GetImportDescriptorInternal(FilePath, bSingleFile, bFlipYAxis, NAME_None, OutImportDescriptor, OutMessage); } ELandscapeImportResult FLandscapeImportHelper::GetWeightmapImportDescriptor(const FString& FilePath, bool bSingleFile, bool bFlipYAxis, FName LayerName, FLandscapeImportDescriptor& OutImportDescriptor, FText& OutMessage) { return GetImportDescriptorInternal(FilePath, bSingleFile, bFlipYAxis, LayerName, OutImportDescriptor, OutMessage); } ELandscapeImportResult FLandscapeImportHelper::GetHeightmapImportData(const FLandscapeImportDescriptor& ImportDescriptor, int32 DescriptorIndex, TArray& OutData, FText& OutMessage) { return GetImportDataInternal(ImportDescriptor, DescriptorIndex, NAME_None, static_cast(LandscapeDataAccess::MidValue), OutData, OutMessage); } ELandscapeImportResult FLandscapeImportHelper::GetWeightmapImportData(const FLandscapeImportDescriptor& ImportDescriptor, int32 DescriptorIndex, FName LayerName, TArray& OutData, FText& OutMessage) { return GetImportDataInternal(ImportDescriptor, DescriptorIndex, LayerName, 0, OutData, OutMessage); } void FLandscapeImportHelper::TransformWeightmapImportData(const TArray& InData, TArray& OutData, const FLandscapeImportResolution& CurrentResolution, const FLandscapeImportResolution& RequiredResolution, ELandscapeImportTransformType TransformType, FIntPoint Offset) { TransformImportDataInternal(InData, OutData, CurrentResolution, RequiredResolution, TransformType, Offset); } void FLandscapeImportHelper::TransformHeightmapImportData(const TArray& InData, TArray& OutData, const FLandscapeImportResolution& CurrentResolution, const FLandscapeImportResolution& RequiredResolution, ELandscapeImportTransformType TransformType, FIntPoint Offset) { TransformImportDataInternal(InData, OutData, CurrentResolution, RequiredResolution, TransformType, Offset); } void FLandscapeImportHelper::ChooseBestComponentSizeForImport(int32 Width, int32 Height, int32& InOutQuadsPerSection, int32& InOutSectionsPerComponent, FIntPoint& OutComponentCount) { bool bValidSubsectionSizeParam = false; bool bValidQuadsPerSectionParam = false; check(Width > 0 && Height > 0); const int32 MaxComponents = LandscapeEditorUtils::GetMaxSizeInComponents(); bool bFoundMatch = false; // Try to find a section size and number of sections that exactly matches the dimensions of the heightfield for (int32 SectionSizesIdx = UE_ARRAY_COUNT(FLandscapeConfig::SubsectionSizeQuadsValues) - 1; SectionSizesIdx >= 0; SectionSizesIdx--) { for (int32 NumSectionsIdx = 0; NumSectionsIdx < UE_ARRAY_COUNT(FLandscapeConfig::NumSectionValues); NumSectionsIdx++) { int32 ss = FLandscapeConfig::SubsectionSizeQuadsValues[SectionSizesIdx]; int32 ns = FLandscapeConfig::NumSectionValues[NumSectionsIdx]; // Check if the passed in values are found in the array of valid values bValidSubsectionSizeParam |= (InOutSectionsPerComponent == ns); bValidQuadsPerSectionParam |= (InOutQuadsPerSection == ss); if (((Width - 1) % (ss * ns)) == 0 && ((Width - 1) / (ss * ns)) <= MaxComponents && ((Height - 1) % (ss * ns)) == 0 && ((Height - 1) / (ss * ns)) <= MaxComponents) { bFoundMatch = true; InOutQuadsPerSection = ss; InOutSectionsPerComponent = ns; OutComponentCount.X = (Width - 1) / (ss * ns); OutComponentCount.Y = (Height - 1) / (ss * ns); break; } } if (bFoundMatch) { break; } } if (!bFoundMatch) { if (!bValidSubsectionSizeParam) { InOutSectionsPerComponent = FLandscapeConfig::NumSectionValues[0]; } if (!bValidQuadsPerSectionParam) { InOutQuadsPerSection = FLandscapeConfig::SubsectionSizeQuadsValues[0]; } // if there was no exact match, try increasing the section size until we encompass the whole heightmap const int32 CurrentSectionSize = InOutQuadsPerSection; const int32 CurrentNumSections = InOutSectionsPerComponent; for (int32 SectionSizesIdx = 0; SectionSizesIdx < UE_ARRAY_COUNT(FLandscapeConfig::SubsectionSizeQuadsValues); SectionSizesIdx++) { if (FLandscapeConfig::SubsectionSizeQuadsValues[SectionSizesIdx] < CurrentSectionSize) { continue; } const int32 ComponentsX = FMath::DivideAndRoundUp((Width - 1), FLandscapeConfig::SubsectionSizeQuadsValues[SectionSizesIdx] * CurrentNumSections); const int32 ComponentsY = FMath::DivideAndRoundUp((Height - 1), FLandscapeConfig::SubsectionSizeQuadsValues[SectionSizesIdx] * CurrentNumSections); if (ComponentsX <= 32 && ComponentsY <= 32) { bFoundMatch = true; InOutQuadsPerSection = FLandscapeConfig::SubsectionSizeQuadsValues[SectionSizesIdx]; OutComponentCount.X = ComponentsX; OutComponentCount.Y = ComponentsY; break; } } } if (!bFoundMatch) { // if the heightmap is very large, fall back to using the largest values we support const int32 MaxSectionSize = FLandscapeConfig::SubsectionSizeQuadsValues[UE_ARRAY_COUNT(FLandscapeConfig::SubsectionSizeQuadsValues) - 1]; const int32 MaxNumSubSections = FLandscapeConfig::NumSectionValues[UE_ARRAY_COUNT(FLandscapeConfig::NumSectionValues) - 1]; const int32 ComponentsX = FMath::DivideAndRoundUp((Width - 1), MaxSectionSize * MaxNumSubSections); const int32 ComponentsY = FMath::DivideAndRoundUp((Height - 1), MaxSectionSize * MaxNumSubSections); bFoundMatch = true; InOutQuadsPerSection = MaxSectionSize; InOutSectionsPerComponent = MaxNumSubSections; OutComponentCount.X = ComponentsX; OutComponentCount.Y = ComponentsY; } check(bFoundMatch); } #undef LOCTEXT_NAMESPACE