// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "WorldPartition/WorldPartitionBuilder.h" #include "WorldMapBuilder.generated.h" class AOrthoMapCaptureActor; class UTexture2D; /** Which way round the world's two horizontal axes end up in the baked picture. * * None of this is worked out from the capture rotation. The builder asks the settings actor's own * WorldToBakedMapUV where a step along each world axis lands, because that mapping is what the game will use * to turn a world position into a map position, and a bake that arranged its tiles any other way would * disagree with it. A negative move means that axis is mirrored in the picture, which the stitch has to undo * while it copies the tile's own pixels in. */ struct FWorldMapOrientation { /** True when the picture's U runs along world X, false when it runs along world Y. */ bool bUFollowsX = true; /** True when the picture's V runs along world Y, false when it runs along world X. */ bool bVFollowsY = true; /** True when U grows towards smaller world X or Y, which is what the actor's bFlipU does to the mapping. */ bool bMirrorU = false; /** True when V grows towards smaller world Y or X, which is what the actor's bFlipV does to the mapping. */ bool bMirrorV = false; }; /** * Bakes one world map texture for a world partition world, by capturing the region a tile at a time and * stitching the tiles back into a single asset. * * Run it as a commandlet: * * UnrealEditor-Cmd.exe -run=WorldPartitionBuilderCommandlet * -Builder=/Script/ProjectEleriEditor.WorldMapBuilder -AllowCommandletRendering -SCCProvider=None * /Game/Maps/Persistent_GameWorld -log * * The region, the resolution and the flips all live on the AOrthoMapCaptureActor in the map rather than on this * builder, so that the picture that gets baked and the mapping the game reads map positions back through are * the same numbers read twice rather than two settings that have to be kept in step by hand. */ UCLASS(MinimalAPI) class UWorldMapBuilder : public UWorldPartitionBuilder { GENERATED_BODY() public: /** Nothing can be captured without a renderer, and the engine insists a commandlet be told so. */ virtual bool RequiresCommandletRendering() const override { return true; } /** One capture per tile: the framework loads each tile's own region and unloads it again afterwards, which * is what keeps a map of a world partition world inside a sane memory budget rather than loading all of * it at once. */ virtual ELoadingMode GetLoadingMode() const override { return ELoadingMode::IterativeCells2D; } protected: virtual bool PreRun(UWorld* World, FPackageSourceControlHelper& PackageHelper) override; virtual bool RunInternal(UWorld* World, const FCellInfo& InCellInfo, FPackageSourceControlHelper& PackageHelper) override; virtual bool PostRun(UWorld* World, FPackageSourceControlHelper& PackageHelper, const bool bInRunSuccess) override; virtual bool ShouldSkipCell(const FWorldBuilderCellCoord& CellCoord) const override; private: /** Finds the settings actor and works the grid out from it, including handing the framework the cell size * and bounds that make a cell index a tile index. Reports why not when the settings cannot be baked. */ bool ReadSettings(UWorld* World, FText& OutReason); /** Cuts the region the settings actor holds into the piece grid a piece run asks for, and points the actor at * the one piece this run is to bake. Reads -WorldMapPiece=, and -WorldMapPieceGrid=, and * refuses anything about the pair, or about the region they are cut from, that would not give pieces tiling * that region exactly. Reports why not. */ bool DerivePieceRegion(const FString& PieceIndexArgument, FText& OutReason); /** Asks the mapping which way round the world axes land in the baked picture. Fails when both picture * directions turn out to follow the same world axis, which would put two tiles in one square of the map. */ bool DeriveOrientation(const AOrthoMapCaptureActor& Settings); /** The tile's position in the map's own grid of tiles: X is the tile index along the picture's U and Y the * index along its V, whichever world axes those turn out to be. */ FIntPoint GetTileIndices(int32 CellX, int32 CellY) const; /** Where a tile's pixels belong in the baked texture, flips and all. */ FIntPoint GetTilePixelOrigin(const FIntPoint& TileIndices) const; /** Checks that the tile's four world corners land inside this tile's own square of the map, which is what * says the grid and the mapping agree, before anything expensive is rendered. */ bool CheckTileCoverage(const FBox& TileBounds, const FIntPoint& PixelOrigin) const; /** The world region one tile covers, worked out from the grid rather than from the cell the framework * reported, so that what is captured can be checked against what the settings say before it is captured. * Z is the map's own Z range, which only sets the height the capture looks down from. */ FBox GetTileWorldBounds(int64 CellX, int64 CellY) const; /** Opens the package the baked map is saved into, creates the texture inside it or takes over the one a * previous bake left there, and locks that texture's only mip so the tiles have somewhere to be stitched * as they arrive. Until this has succeeded there is nothing to bake into. */ bool OpenDestinationTexture(UWorld* World, FText& OutReason); /** The texture every capture renders into, made on the first tile of the run and reused for the rest of it. * The capture helper re-initialises whatever texture it is handed, and a map's worth of dead sixteen * megabyte textures is a gigabyte the transient package cannot give back in the middle of a bake. */ UTexture2D* GetTileTexture(); /** Copies one captured tile's pixels into the square of the map that tile owns, undoing whichever of the * two flips the settings asked for while it copies. */ bool StitchTile(UTexture2D* InTileTexture, const FIntPoint& PixelOrigin); /** Counts the pixels of a captured tile that have anything in them, which is how a tile that came back * empty is told apart from one that simply holds nothing but sky. */ static int32 CountVisiblePixels(UTexture2D* TileTexture); /** Writes a locked BGRA8 buffer out as an image under the project's intermediate files, which is what * -DebugCapture is for: a bitmap per tile, and a PNG for the whole stitched map, since sixteen thousand * pixels square is a gigabyte of bitmap and a few tens of megabytes of PNG. */ void SaveDebugImage(const FString& InFileName, const uint8* Pixels, int32 Width, int32 Height, bool bAsPng) const; /** Gives the baked texture the settings a world map asset needs, saves the package, and reports the file * to the framework. The last thing a successful bake does. */ bool SaveBakedMap(FPackageSourceControlHelper& PackageHelper); /** Puts the actor's own settings back after a run that overrode them. Harmless to call when nothing was * overridden. */ void RestoreOverriddenSettings(); /** The actor the settings come from, kept referenced so that a collection while the builder runs cannot * leave this dangling. The numbers below are what a run actually uses. */ UPROPERTY() TObjectPtr SettingsActor; FBox MapBounds = FBox(ForceInit); int64 GridOriginX = 0; int64 GridOriginY = 0; int32 TilesPerAxis = 1; int32 MapTextureSize = 0; int32 TilePixels = 0; double TileWorldSize = 0.0; FWorldMapOrientation Orientation; /** Tiles walked so far and when the walk started, so that a run can say how it went at the end. */ int32 WalkedTileCount = 0; double RunStartTime = 0.0; /** The actor's own settings before any -MapTextureSize, -TilesPerAxis or region alignment override, and * whether they were changed. An override stays in place for the whole run, because the mapping the * coverage checks go through reads the actor's bounds, so putting them back happens in PostRun. */ bool bOverrodeSettings = false; int32 OriginalMapTextureSize = 0; int32 OriginalTilesPerAxis = 0; FBox OriginalWorldMapBounds = FBox(ForceInit); //----- A bake of one piece of the region ----- /** Whether this run bakes one piece of the region rather than the whole of it. A piece is a window onto the * region the actor holds, so its own box takes that region's place on the actor for the length of the run * and everything below reads it as the region without knowing the difference. */ bool bPieceRun = false; /** How many pieces a side the region is cut into, and which of them this run bakes. Four a side is the * default because that is how the in game map pages the island: the island stands where the middle four * pieces meet, and a UI that pages those four together shows all of it. * * The pieces are numbered by world axes rather than by the picture: the column runs along +X and the row * along +Y from the region's own min corner, so a piece's index says where it is in the world and does not * move when the map's own flips do. */ int32 PieceColumn = 0; int32 PieceRow = 0; int32 PieceGrid = 4; /** The region the pieces are cut out of. Kept because the piece's box takes that region's place on the actor * for the length of the run, and a log line that could not say what a piece is a piece of would be a log * line about a region nobody asked for. */ FBox PieceParentBounds = FBox(ForceInit); //----- The asset being baked ----- /** Name of the map the settings were read from, which is what the baked asset is named after. */ FString MapName; /** Where the baked map goes, held on the builder for the length of the run: the package, the texture * inside it, and the pixels of that texture's only mip. The mip is locked once for the whole walk rather * than once per tile, so the lock opens in PreRun and closes in PostRun whatever happens in between: a * lock left open would leave the texture unwritable for anyone who opened it next. */ UPROPERTY() TObjectPtr DestinationPackage; UPROPERTY() TObjectPtr DestinationTexture; uint8* DestinationPixels = nullptr; FString DestinationAssetName; FString DestinationPackageName; FString DestinationPackageFileName; /** The texture a capture renders into, held across the whole walk rather than made per tile. It is a * scratch buffer of one tile's pixels and nothing else: it is never saved, never registered, and lives in * the transient package. A plain pointer rather than a TObjectPtr because the capture helper takes the * texture by reference and may hand a different one back, which is a raw pointer the reference binds to and * a TObjectPtr is not. */ UPROPERTY() UTexture2D* TileTexture = nullptr; /** Whether -DebugCapture was given, in which case every tile and the stitched map are written out as * images as well. A bake costs a capture per tile either way; this only adds files to look at. */ bool bDebugCapture = false; /** Tiles that came back with nothing visible in them. A map is allowed to be mostly empty, but a tile * that rendered nothing at all is usually a setting rather than a stretch of landscape, and this count * is the one number that says which kind of bake this was. */ int32 EmptyTileCount = 0; };