Made map builder and exported first pass of maps

This commit is contained in:
2026-09-25 14:42:07 +02:00
parent a97ce5de43
commit aa4c87d88a
76 changed files with 2990 additions and 2 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,217 @@
// 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 <project> -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=<column>,<row> and -WorldMapPieceGrid=<n>, 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<AOrthoMapCaptureActor> 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<UPackage> DestinationPackage;
UPROPERTY()
TObjectPtr<UTexture2D> 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;
};