Made map builder and exported first pass of maps
This commit is contained in:
248
BuildScripts/BakeWorldMap.bat
Normal file
248
BuildScripts/BakeWorldMap.bat
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
REM Bakes the world map of the world partition map named at the end of each command below: the whole region as
|
||||||
|
REM one asset, and the same region cut into a grid of pieces as one asset each. Seventeen textures come out of a
|
||||||
|
REM full run, and the map UI in the game switches between them as it zooms: the whole region while the player is
|
||||||
|
REM looking at all of it, and the piece they are looking at once they are close enough for the whole map's pixel
|
||||||
|
REM to be a blur.
|
||||||
|
REM
|
||||||
|
REM The whole map is one texture over the whole region: on this map that is twelve thousand metres at 16384
|
||||||
|
REM pixels, which is 0.73 metres of world per pixel of picture, a map to open and see where one is. The pieces
|
||||||
|
REM are the same region cut into a 4 x 4 grid, so three thousand metres a side, and each piece is baked at 8192
|
||||||
|
REM pixels, which is 0.37 metres of world per pixel: half the world per pixel of the whole map, which is the
|
||||||
|
REM detail the zoomed in view is for. Both layers capture 2048 pixels of their own per tile, so a tile
|
||||||
|
REM of a piece costs the same to render as a tile of the whole map and the difference between the layers is only
|
||||||
|
REM how much world stands behind those pixels.
|
||||||
|
REM
|
||||||
|
REM The two layers are not a level of detail of each other, they are seventeen textures and the UI picks one, so
|
||||||
|
REM they have to agree about where the world is to the pixel: they are baked from the same region by the same
|
||||||
|
REM builder, and the piece a run bakes is the only thing about that the command lines below change. Nothing in
|
||||||
|
REM the game reads one of the seventeen; they are pictures, and where the world is inside one of them is a
|
||||||
|
REM question the settings actor answers separately.
|
||||||
|
REM
|
||||||
|
REM What the map covers is the WorldMapBounds of the OrthoMapCaptureActor in the map, which is the intended place
|
||||||
|
REM to set it: open the map, select the actor, type the bounds in and save the map. A commandlet reads the map
|
||||||
|
REM from disk, so a region that was never saved is not in what it reads.
|
||||||
|
REM
|
||||||
|
REM On this map the island stands on the origin and measures about 375 by 390 metres, centred within about 45
|
||||||
|
REM metres of it, so the region is the twelve kilometre square Min=(-600000,-600000,-100000)
|
||||||
|
REM Max=(600000,600000,100000). The island is then about a thirtieth of the width of the picture and most of the
|
||||||
|
REM rest is sea: ten of the whole map's sixty four tiles have anything in them, which is the island and what
|
||||||
|
REM stands around it near the origin and a second group of tiles away to the west of it. What every run prints
|
||||||
|
REM about its empty tiles is how that is checked. Z is not part of what the map covers, it is the height the
|
||||||
|
REM captures look down from, and the Z in the actor's own box is where a run that passes no region gets one from.
|
||||||
|
REM
|
||||||
|
REM WorldMapBounds is also what the game reads when it turns a position on the map back into a world position, so
|
||||||
|
REM the actor's numbers are the numbers this bake used, and the two cannot be allowed to drift apart. All six of
|
||||||
|
REM them have to be on the actor before the run below, which passes no region of its own.
|
||||||
|
REM
|
||||||
|
REM Four flags exist for running without the editor. All apply to this run only and all override the actor:
|
||||||
|
REM
|
||||||
|
REM -WorldMapBounds=minX,minY,maxX,maxY states the region outright, keeping the actor's Z. It wins over the
|
||||||
|
REM actor's own bounds, so it is the way to try a region out before
|
||||||
|
REM committing it to the map. The whole map run below does not pass it:
|
||||||
|
REM what gets baked is what the actor says.
|
||||||
|
REM -FitBounds takes the region from the partition instead. Do not use it on this
|
||||||
|
REM map. A world partition reports the bounds of its own sparse octree
|
||||||
|
REM grid rather than the bounds of its content, and an actor in this
|
||||||
|
REM map has a world sized bound of its own, which pushes the octree up
|
||||||
|
REM to a level 30 cell either side of the origin. It answers with a box
|
||||||
|
REM of +/-13743895347200 units, and a tile of that at eight tiles is
|
||||||
|
REM 3435973836800 units across, far more than a cell can hold, so
|
||||||
|
REM IsWorldMapGridValid refuses the bake and says so. The button of the
|
||||||
|
REM same name on the actor's details panel has the same problem, so set
|
||||||
|
REM the region by hand.
|
||||||
|
REM -WorldMapPiece=column,row bakes one piece of the region instead of the whole of it. The
|
||||||
|
REM column counts along +X from the region's Min end and the row counts
|
||||||
|
REM along +Y from the same corner, so (0, 0) is the far corner of the
|
||||||
|
REM map and the last piece is at the near one.
|
||||||
|
REM -WorldMapPieceGrid=n says how many pieces a side that index is being read in: four for
|
||||||
|
REM the 4 x 4 grid this script bakes. It has to be given with
|
||||||
|
REM -WorldMapPiece, because the same index in another grid of the same
|
||||||
|
REM region is a different square of the world, and it is only warned
|
||||||
|
REM about when it arrives without one.
|
||||||
|
REM
|
||||||
|
REM A piece run refuses -WorldMapBounds and -FitBounds rather than combining them with a piece, because the two
|
||||||
|
REM say different things about the same region: one states the region the pieces are cut out of, and the other
|
||||||
|
REM states the piece itself. A region that has been stated can be cut into any grid at all, so there is no one
|
||||||
|
REM right answer to what a piece of it means. Set the region on the actor first and then ask for a piece of it.
|
||||||
|
REM A piece index outside the grid is refused too, and so is a piece of a region that is not a whole number of
|
||||||
|
REM pieces across, which is a piece whose edges are not where the game thinks they are.
|
||||||
|
REM
|
||||||
|
REM -MapTextureSize and -TilesPerAxis shrink the map for a trial run: 512 pixels in 2 x 2 tiles walks the whole
|
||||||
|
REM grid in four steps, and still covers the same region, because a region read off the actor is re-aligned to
|
||||||
|
REM whatever grid the run actually walks. Given with -WorldMapPiece the same two shrink one piece instead, and
|
||||||
|
REM that re-alignment is skipped: a piece's box is a window cut out of the region the pieces tile, so it is the
|
||||||
|
REM tiles that follow the piece and not the other way round, and aligning a window to its own tile grid would
|
||||||
|
REM quietly bake a region that is not one of the pieces. The whole map run below passes neither, so it bakes at
|
||||||
|
REM the resolution and tile count set on the actor, which for this map are 16384 in eight tiles; the piece runs
|
||||||
|
REM pass both, because 8192 in four tiles is what a piece is.
|
||||||
|
REM
|
||||||
|
REM The grid, and what the pieces are called. A piece goes beside the whole map in the same folder, named after it
|
||||||
|
REM with the grid and the piece's own place on the end, so this map's seventeen assets are
|
||||||
|
REM WorldMap_Persistent_GameWorld and WorldMap_Persistent_GameWorld_Piece4x4_0_0 through _Piece4x4_3_3. Column 0
|
||||||
|
REM is the end of the region at its Min and row 0 is the same corner, which puts this map's grid like this:
|
||||||
|
REM
|
||||||
|
REM row 3: (0,3) (1,3) (2,3) (3,3) every piece 3000 x 3000 metres
|
||||||
|
REM row 2: (0,2) (1,2) (2,2) (3,2) the region Min=(-600000,-600000) Max=(600000,600000)
|
||||||
|
REM row 1: (0,1) (1,1) (2,1) (3,1) +X runs along a row, +Y up a column
|
||||||
|
REM row 0: (0,0) (1,0) (2,0) (3,0) the origin is the corner the four middle pieces share
|
||||||
|
REM +--------------------------+
|
||||||
|
REM col 0 col 1 col 2 col 3
|
||||||
|
REM
|
||||||
|
REM The island stands on the origin, so it falls across the four middle pieces (1,1) (2,1) (1,2) and (2,2), and
|
||||||
|
REM Min=(-300000,-300000) Max=(300000,300000) covers those four and no others. The other twelve pieces are open
|
||||||
|
REM sea: a bake of one of those that reports a tile with the island in it has the region wrong, and one of the
|
||||||
|
REM four that reports nothing at all has it wrong the other way.
|
||||||
|
REM
|
||||||
|
REM Where the baked map goes is set on the actor too, by OutputPackageFolder, and the asset in it is named after
|
||||||
|
REM the map it was baked from: this map bakes to /Game/Maps/Generated/WorldMap_Persistent_GameWorld, and baking
|
||||||
|
REM it again replaces that same asset rather than making a second one. A piece is named the same way with its
|
||||||
|
REM grid, column and row on the end, so the sixteen of them sit beside the whole map in the same folder and
|
||||||
|
REM re-baking one piece is a change to one asset. That folder and WorldMapWarmupFrames are read off the actor as
|
||||||
|
REM well and have to be set there, so a bake of a map whose actor has no output folder is refused before it
|
||||||
|
REM renders anything.
|
||||||
|
REM
|
||||||
|
REM -DebugCapture also writes every tile it captures to Intermediate\WorldMap as a bitmap, and the stitched map as
|
||||||
|
REM a PNG, and both are named after the asset the run is baking and after the tile's own place in the grid that
|
||||||
|
REM run walked:
|
||||||
|
REM
|
||||||
|
REM WorldMap_Persistent_GameWorld-Tile_<column>_<row>.bmp the whole map, one file a tile
|
||||||
|
REM WorldMap_Persistent_GameWorld.png the whole map, stitched
|
||||||
|
REM WorldMap_Persistent_GameWorld_Piece4x4_<c>_<r>-Tile_<c>_<r>.bmp one piece, one file a tile
|
||||||
|
REM WorldMap_Persistent_GameWorld_Piece4x4_<c>_<r>.png one piece, stitched
|
||||||
|
REM
|
||||||
|
REM Those two are the pair to look at when a bake comes out wrong: the tiles say whether each square captured
|
||||||
|
REM what it should have, and the stitched map says whether they were placed in the right squares. The asset name
|
||||||
|
REM in front of the tile index is what keeps the tiles of one piece from overwriting another's, which is in turn
|
||||||
|
REM what makes the strip of tiles along a shared edge worth comparing: a seam between two pieces that is in the
|
||||||
|
REM world reads as one line in each of them, and a seam the bake made reads as a difference.
|
||||||
|
REM
|
||||||
|
REM It is not on by default because it is a great deal of bitmap. A tile of this bake is 2048 pixels square at
|
||||||
|
REM four bytes a pixel, which is sixteen megabytes, so the sixty four of the whole map are most of a gigabyte and
|
||||||
|
REM the sixteen of one piece are a quarter of that. Add it to a command line below, or to the end of a manual
|
||||||
|
REM run, when there is something to look at, and to one piece at a time when it is a seam being looked at.
|
||||||
|
REM
|
||||||
|
REM A full run of this script is seventeen commandlet runs: sixty four tiles for the whole map and sixteen for
|
||||||
|
REM each of the sixteen pieces, which is three hundred and twenty tiles in all. On the machine this was written on
|
||||||
|
REM all seventeen took eight and a half minutes, first log line to last. Almost none of that is capture: the whole
|
||||||
|
REM map walks its sixty four tiles in seventeen seconds and a piece its sixteen in three to five, so the great
|
||||||
|
REM majority of every run is the editor starting, reading the map and building the texture. Building is where the
|
||||||
|
REM memory goes rather than the time: the whole map run peaks at eleven and a half gigabytes of physical memory,
|
||||||
|
REM against the twelve gigabytes its sixteen thousand pixel texture reports needing while it is built, and a piece
|
||||||
|
REM run peaks at five and a half against the three its eight thousand pixel one does. The runs are one after
|
||||||
|
REM another rather than at once for the same reason a bake is one at a time: two of them at once would be two
|
||||||
|
REM editors with a renderer each, holding two of these textures at the same time.
|
||||||
|
REM
|
||||||
|
REM What comes out is seventeen virtual textures, which is what lets a renderer hold a map of this size at all: a
|
||||||
|
REM sixteen thousand pixel one that covers the whole region and sixteen eight thousand pixel ones that each cover a
|
||||||
|
REM sixteenth of its area. They are small on disk because the region is all but empty. This map's own bake wrote the
|
||||||
|
REM whole map as one and a half megabytes, the six pieces that have anything in them as between a third of a
|
||||||
|
REM megabyte and two, and the ten that hold nothing but sea as fourteen kilobytes each, eight megabytes for the
|
||||||
|
REM seventeen together. Where a map this size is large is on the card rather than on disk: what showing one needs is
|
||||||
|
REM a virtual texture pool big enough for the part of it that is on screen, and this project's pool section gives it
|
||||||
|
REM sixty four megabytes by default and, with bPoolAutoGrowInEditor off, does not grow while the editor runs. Sixty
|
||||||
|
REM four megabytes is a great many of the pages these are cut into, and one of them on a screen is a small part of
|
||||||
|
REM the pool; the entry to raise, if a UI ever shows more of them at once than the pool holds, is
|
||||||
|
REM DefaultSizeInMegabyte in that section of Config\DefaultEngine.ini.
|
||||||
|
REM
|
||||||
|
REM The tiles that come back empty are counted out loud by every run, and that count is the quickest way to see a
|
||||||
|
REM region that no longer covers the world: a region that has shrunk onto the world reports few or none of them
|
||||||
|
REM empty, and one that has grown past the world reports all of them. On this map a whole map bake reports fifty
|
||||||
|
REM four of its sixty four tiles empty, so ten of them have something in them, and the sixteen pieces report
|
||||||
|
REM between ten and sixteen of their sixteen: ten of the pieces are all sea and come back entirely empty, the two
|
||||||
|
REM west of the island report ten, the two the island's own corner is in report thirteen and the two east of those
|
||||||
|
REM report fifteen. Those counts are of the world rather than of the bake, so they move when the map does; what is
|
||||||
|
REM worth reading them for is a count that has gone to all empty or all full, which is the region and not the bake.
|
||||||
|
|
||||||
|
REM -AllowCommandletRendering is not optional. A builder that captures is refused without it, because a
|
||||||
|
REM commandlet has no renderer unless it is told to start one.
|
||||||
|
REM
|
||||||
|
REM The per corner coverage check is only as talkative as its own log category, which does not print its Verbose
|
||||||
|
REM lines by default. A corner that lands outside its own tile is an Error and stops the walk, so a run that
|
||||||
|
REM reaches the end of the grid has passed every corner of it.
|
||||||
|
REM
|
||||||
|
REM The editor commandlet has to be the same configuration the project modules were built in, or it will load
|
||||||
|
REM modules that do not contain this builder. DebugGame is what a local build produces; uncomment the
|
||||||
|
REM Development line to bake with a development build instead, which is the faster of the two.
|
||||||
|
REM
|
||||||
|
REM Every run below is logged to a file of its own under Intermediate\WorldMap as well as to the window: the whole
|
||||||
|
REM map to whole-map.log and a piece to piece-<column>-<row>.log, which is what makes a bake of seventeen runs
|
||||||
|
REM readable after the fact. -abslog takes an absolute path, which is what %PROJECT_ROOT% below is for: the logs
|
||||||
|
REM land in the project's own intermediate folder whatever directory this file is run from. It also moves that
|
||||||
|
REM run's log out of the editor's Saved\Logs, and the file is written as the run goes rather than at its end, so
|
||||||
|
REM one can be read, or tailed, while its bake is still walking the grid.
|
||||||
|
|
||||||
|
SET EDITOR_CMD=C:/Program Files/Epic Games/UE_5.8/Engine/Binaries/Win64/UnrealEditor-Win64-DebugGame-Cmd.exe
|
||||||
|
REM SET EDITOR_CMD=C:/Program Files/Epic Games/UE_5.8/Engine/Binaries/Win64/UnrealEditor-Cmd.exe
|
||||||
|
|
||||||
|
SET PROJECT=C:/Projects/ProjectEleri/ProjectEleri.uproject
|
||||||
|
|
||||||
|
REM The project's own folder, taken from the project file above so that this file can be run from anywhere: the
|
||||||
|
REM per run logs and the trial run both hang off it.
|
||||||
|
FOR %%I IN ("%PROJECT%") DO SET PROJECT_ROOT=%%~dpI
|
||||||
|
|
||||||
|
REM What a run of this file bakes. The whole map is baked at whatever resolution and tile count the settings actor
|
||||||
|
REM carries, which for this map are 16384 in eight tiles; a piece is baked at the two numbers below, because 8192
|
||||||
|
REM pixels over a quarter of the width is what a piece is.
|
||||||
|
SET MAP=/Game/Maps/Persistent_GameWorld
|
||||||
|
SET BUILDER=-run=WorldPartitionBuilderCommandlet -Builder=/Script/ProjectEleriEditor.WorldMapBuilder -AllowCommandletRendering -SCCProvider=None -unattended -Verbose
|
||||||
|
SET PIECE_SIZE=8192
|
||||||
|
SET PIECE_TILES=4
|
||||||
|
SET PIECE_GRID=8
|
||||||
|
SET PIECE_ROOT=%PROJECT_ROOT%Intermediate\WorldMap
|
||||||
|
|
||||||
|
REM Whether to bake the whole map as well as the pieces. Set to 0 to bake only the sixteen pieces, which is what
|
||||||
|
REM a run that changed the piece numbers and nothing else wants.
|
||||||
|
SET WHOLE_MAP=1
|
||||||
|
|
||||||
|
REM Bake one piece instead of all sixteen: its column and row as -WorldMapPiece takes them, or empty for all of
|
||||||
|
REM them. ONLY_PIECE=1,1 is the piece the island's own corner is in, and is the one to bake when the piece layer
|
||||||
|
REM is being tried out rather than built.
|
||||||
|
SET ONLY_PIECE=
|
||||||
|
|
||||||
|
IF NOT EXIST "%PIECE_ROOT%" MKDIR "%PIECE_ROOT%"
|
||||||
|
|
||||||
|
REM The geometry trial of one piece: four tiles of 512 pixels over the whole square a real piece covers, with a
|
||||||
|
REM bitmap of each to look at. It bakes one asset rather than seventeen and its log says which region that was,
|
||||||
|
REM so it is the cheap first thing to run when the piece arithmetic in the builder changes.
|
||||||
|
REM "%EDITOR_CMD%" "%PROJECT%" %BUILDER% %MAP% -WorldMapPiece=0,0 -WorldMapPieceGrid=%PIECE_GRID% -MapTextureSize=512 -TilesPerAxis=2 -DebugCapture -log -stdout
|
||||||
|
|
||||||
|
IF "%WHOLE_MAP%"=="1" (
|
||||||
|
ECHO "Baking the whole map..."
|
||||||
|
"%EDITOR_CMD%" "%PROJECT%" %BUILDER% %MAP% -abslog="%PIECE_ROOT%\whole-map.log" -log -stdout
|
||||||
|
IF ERRORLEVEL 1 GOTO :BAKE_FAILED
|
||||||
|
)
|
||||||
|
|
||||||
|
ECHO "Baking the sixteen pieces..."
|
||||||
|
SET /A PIECE_LAST=%PIECE_GRID% - 1
|
||||||
|
FOR /L %%R IN (0,1,%PIECE_LAST%) DO (
|
||||||
|
FOR /L %%C IN (0,1,%PIECE_LAST%) DO (
|
||||||
|
CALL :bake_piece %%C %%R
|
||||||
|
IF ERRORLEVEL 1 GOTO :BAKE_FAILED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ECHO "DONE! The whole map and the sixteen pieces of it are in the output folder set on the settings actor."
|
||||||
|
PAUSE
|
||||||
|
EXIT /B 0
|
||||||
|
|
||||||
|
:BAKE_FAILED
|
||||||
|
ECHO "A run failed, so the pieces after it were not baked. The run that failed says why in the log above."
|
||||||
|
ECHO "Fix it and run this file again: the runs that succeeded are saved already, and both the failed run and the"
|
||||||
|
ECHO "skipped ones replace what is there rather than making second assets. To bake one of them on its own, set"
|
||||||
|
ECHO "ONLY_PIECE at the top of this file to its column and row and set WHOLE_MAP to 0."
|
||||||
|
PAUSE
|
||||||
|
EXIT /B 1
|
||||||
|
|
||||||
|
REM One piece: the region comes from the actor and the piece from the two arguments, which are its column and its
|
||||||
|
REM row. A piece that is not the one ONLY_PIECE asks for is skipped without a word, which is what makes that
|
||||||
|
REM setting a way to bake one piece through the same loop as all sixteen.
|
||||||
|
:bake_piece
|
||||||
|
IF NOT "%ONLY_PIECE%"=="" IF NOT "%ONLY_PIECE%"=="%1,%2" EXIT /B 0
|
||||||
|
|
||||||
|
ECHO "Baking piece %1,%2..."
|
||||||
|
"%EDITOR_CMD%" "%PROJECT%" %BUILDER% %MAP% -WorldMapPiece=%1,%2 -WorldMapPieceGrid=%PIECE_GRID% -MapTextureSize=%PIECE_SIZE% -TilesPerAxis=%PIECE_TILES% -abslog="%PIECE_ROOT%\piece-%1-%2.log" -log -stdout
|
||||||
|
|
||||||
|
IF ERRORLEVEL 1 EXIT /B 1
|
||||||
|
EXIT /B 0
|
||||||
BIN
Content/Blueprints/BP_OrthoMapCaptureActor.uasset
LFS
Normal file
BIN
Content/Blueprints/BP_OrthoMapCaptureActor.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_0.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_0.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_1.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_1.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_2.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_2.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_3.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_3.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_4.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_4.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_5.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_5.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_6.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_6.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_7.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_7.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_0.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_0.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_1.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_1.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_2.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_2.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_3.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_3.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_4.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_4.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_5.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_5.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_6.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_6.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_7.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_7.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_0.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_0.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_1.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_1.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_2.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_2.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_3.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_3.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_4.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_4.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_5.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_5.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_6.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_6.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_7.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_7.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_0.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_0.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_1.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_1.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_2.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_2.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_3.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_3.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_4.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_4.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_5.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_5.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_6.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_6.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_7.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_7.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_0.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_0.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_1.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_1.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_2.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_2.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_3.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_3.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_4.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_4.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_5.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_5.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_6.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_6.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_7.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_7.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_0.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_0.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_1.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_1.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_2.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_2.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_3.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_3.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_4.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_4.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_5.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_5.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_6.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_6.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_7.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_7.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_0.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_0.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_1.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_1.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_2.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_2.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_3.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_3.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_4.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_4.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_5.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_5.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_6.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_6.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_7.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_7.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_0.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_0.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_1.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_1.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_2.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_2.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_3.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_3.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_4.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_4.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_5.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_5.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_6.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_6.uasset
LFS
Normal file
Binary file not shown.
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_7.uasset
LFS
Normal file
BIN
Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_7.uasset
LFS
Normal file
Binary file not shown.
Binary file not shown.
BIN
Content/Textures/Map/RT_MapCapture.uasset
LFS
Normal file
BIN
Content/Textures/Map/RT_MapCapture.uasset
LFS
Normal file
Binary file not shown.
Binary file not shown.
865
Source/ProjectEleri/GameObjects/OrthoMapCaptureActor.cpp
Normal file
865
Source/ProjectEleri/GameObjects/OrthoMapCaptureActor.cpp
Normal file
@@ -0,0 +1,865 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#include "OrthoMapCaptureActor.h"
|
||||||
|
|
||||||
|
#include "Components/SceneCaptureComponent2D.h"
|
||||||
|
#include "Engine/Scene.h"
|
||||||
|
#include "Engine/TextureRenderTarget2D.h"
|
||||||
|
#include "EngineDefines.h"
|
||||||
|
#include "Math/OrthoMatrix.h"
|
||||||
|
#include "Math/RotationMatrix.h"
|
||||||
|
#include "Math/TranslationMatrix.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "SceneView.h"
|
||||||
|
#include "WorldPartition/WorldPartition.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/** The axis swap every view matrix carries, and the one thing that is easy to leave out of one built by hand:
|
||||||
|
* a camera's own frame is forward, right, up, while the view space a projection matrix is written against is
|
||||||
|
* right, up, forward. This is the same matrix FGameplayStatics::CalculateViewProjectionMatricesFromMinimalView
|
||||||
|
* builds, and without it a capture and any mapping through the same matrices both come out a quarter turn
|
||||||
|
* sideways, with the world axis the camera looks along ending up across the picture instead of down it. */
|
||||||
|
const FMatrix ViewSpaceSwizzle(
|
||||||
|
FPlane(0, 0, 1, 0),
|
||||||
|
FPlane(1, 0, 0, 0),
|
||||||
|
FPlane(0, 1, 0, 0),
|
||||||
|
FPlane(0, 0, 0, 1));
|
||||||
|
|
||||||
|
/** The rotation every world map capture is taken through: straight down, with the camera's up direction along
|
||||||
|
* -Y. It is the rotation the engine's own world partition minimap captures through, and keeping it one named
|
||||||
|
* value is what stops SetCaptureRegion and ComputeRegionMatrices pointing two different ways. */
|
||||||
|
const FRotator WorldMapCaptureRotation(-90.0, 0.0, -90.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
AOrthoMapCaptureActor::AOrthoMapCaptureActor()
|
||||||
|
{
|
||||||
|
// Nothing here needs a tick. The capture renders when it is moved or when it is asked to, and it is
|
||||||
|
// carried out by the renderer rather than by this actor's frame.
|
||||||
|
PrimaryActorTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
|
||||||
|
SetRootComponent(SceneRoot);
|
||||||
|
// Movable rather than static: the whole point of this actor is that it gets dragged and turned, and
|
||||||
|
// a static root refuses to move in the editor.
|
||||||
|
SceneRoot->SetMobility(EComponentMobility::Movable);
|
||||||
|
|
||||||
|
CaptureComponent = CreateDefaultSubobject<USceneCaptureComponent2D>(TEXT("CaptureComponent"));
|
||||||
|
CaptureComponent->SetupAttachment(SceneRoot);
|
||||||
|
CaptureComponent->SetMobility(EComponentMobility::Movable);
|
||||||
|
|
||||||
|
// Defaults of the mirrored settings. ApplySettings is what copies them onto the component.
|
||||||
|
RenderTarget = nullptr;
|
||||||
|
CaptureSourceOverride = SCS_BaseColor;
|
||||||
|
OrthoWidth = 4096.0f;
|
||||||
|
bAutoCalculateOrthoPlanes = true;
|
||||||
|
bCaptureEveryFrame = false;
|
||||||
|
bCaptureOnMovement = true;
|
||||||
|
bUnlitCapture = true;
|
||||||
|
bFlipV = false;
|
||||||
|
bFlipU = false;
|
||||||
|
|
||||||
|
// World map defaults: no region chosen yet, 16k of map cut into 8 tiles of 2048 pixels, which is 64
|
||||||
|
// captures. The region is the one setting that cannot be guessed, and FitBoundsToWorldPartition is what
|
||||||
|
// fills it in from the world partition.
|
||||||
|
WorldMapBounds = FBox(ForceInit);
|
||||||
|
MapTextureSize = 16384;
|
||||||
|
TilesPerAxis = 8;
|
||||||
|
OutputPackageFolder = TEXT("/Game/Maps/Generated");
|
||||||
|
WorldMapWarmupFrames = 5;
|
||||||
|
|
||||||
|
// The plain settings go on in the constructor, so that the component is consistent even before the
|
||||||
|
// first OnConstruction. The show flags deliberately do not: SetShowFlagSettings rebuilds them
|
||||||
|
// against the archetype, which is not a question worth asking about an object that is still being
|
||||||
|
// constructed.
|
||||||
|
CaptureComponent->ProjectionType = ECameraProjectionMode::Orthographic;
|
||||||
|
CaptureComponent->OrthoWidth = OrthoWidth;
|
||||||
|
CaptureComponent->bAutoCalculateOrthoPlanes = bAutoCalculateOrthoPlanes;
|
||||||
|
CaptureComponent->CaptureSource = CaptureSourceOverride;
|
||||||
|
CaptureComponent->bCaptureEveryFrame = 0;
|
||||||
|
CaptureComponent->bCaptureOnMovement = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::ApplySettings()
|
||||||
|
{
|
||||||
|
if (!CaptureComponent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orthographic every time, and not only when it already is: a perspective capture that was mapped
|
||||||
|
// as if it were orthographic would put every position somewhere it does not belong, and the mapping
|
||||||
|
// below has no way to tell that it happened.
|
||||||
|
CaptureComponent->ProjectionType = ECameraProjectionMode::Orthographic;
|
||||||
|
CaptureComponent->OrthoWidth = FMath::Max(1.0f, OrthoWidth);
|
||||||
|
CaptureComponent->bAutoCalculateOrthoPlanes = bAutoCalculateOrthoPlanes;
|
||||||
|
|
||||||
|
// CaptureSource is not a property of the component a caller can set from the editor or from
|
||||||
|
// Blueprint, so this forward is the only way in.
|
||||||
|
CaptureComponent->CaptureSource = CaptureSourceOverride;
|
||||||
|
|
||||||
|
CaptureComponent->TextureTarget = RenderTarget;
|
||||||
|
CaptureComponent->bCaptureEveryFrame = bCaptureEveryFrame ? 1 : 0;
|
||||||
|
CaptureComponent->bCaptureOnMovement = bCaptureOnMovement ? 1 : 0;
|
||||||
|
|
||||||
|
ApplyCaptureShowFlags();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::ApplyCaptureShowFlags() const
|
||||||
|
{
|
||||||
|
if (!CaptureComponent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Written through the serialized setting list and not onto ShowFlags directly. ShowFlags is rebuilt
|
||||||
|
// from the archetype by every PostLoad (USceneCaptureComponent::UpdateShowFlags), so a direct write
|
||||||
|
// survives until the level is loaded again and then quietly stops applying, which is a capture that
|
||||||
|
// looks right in the editor and wrong in a build.
|
||||||
|
TArray<FEngineShowFlagsSetting> ShowFlagSettings;
|
||||||
|
|
||||||
|
if (bUnlitCapture)
|
||||||
|
{
|
||||||
|
// The features that give a captured region a mood instead of a shape. Left on, the map comes out
|
||||||
|
// shaded by whatever stands above it and tinted by the sky. The names have to match the show
|
||||||
|
// flag identifiers exactly, because a name that does not resolve is skipped without a word.
|
||||||
|
const TCHAR* FlagsToDisable[] =
|
||||||
|
{
|
||||||
|
TEXT("Lighting"),
|
||||||
|
TEXT("DynamicShadows"),
|
||||||
|
TEXT("SkyLighting"),
|
||||||
|
TEXT("Atmosphere"),
|
||||||
|
TEXT("Fog"),
|
||||||
|
TEXT("VolumetricFog"),
|
||||||
|
TEXT("PostProcessing"),
|
||||||
|
TEXT("AntiAliasing"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const TCHAR* FlagName : FlagsToDisable)
|
||||||
|
{
|
||||||
|
FEngineShowFlagsSetting& Setting = ShowFlagSettings.AddDefaulted_GetRef();
|
||||||
|
Setting.ShowFlagName = FlagName;
|
||||||
|
Setting.Enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty list is how the component is put back onto its own defaults.
|
||||||
|
CaptureComponent->SetShowFlagSettings(ShowFlagSettings);
|
||||||
|
|
||||||
|
// The vignette is applied per view, so with a capture it darkens the middle of the captured region
|
||||||
|
// rather than the edge of a screen, and a set of captures laid out next to each other reads as a
|
||||||
|
// grid of dark circles. The engine's own minimap capture turns it off for the same reason.
|
||||||
|
CaptureComponent->PostProcessSettings.bOverride_VignetteIntensity = 1;
|
||||||
|
CaptureComponent->PostProcessSettings.VignetteIntensity = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::OnConstruction(const FTransform& Transform)
|
||||||
|
{
|
||||||
|
Super::OnConstruction(Transform);
|
||||||
|
|
||||||
|
// An instance placed in a level is built from a template before its own saved properties are read
|
||||||
|
// back, so this is where an edit made on that template reaches the component. It also runs on every
|
||||||
|
// property edit in the editor, which is what keeps the copies of these settings from drifting apart.
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
// A level placed instance is put together before the game starts, and a spawned one is not built
|
||||||
|
// through OnConstruction at all, so this is the one point both paths share.
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
void AOrthoMapCaptureActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||||
|
{
|
||||||
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||||
|
|
||||||
|
// Every property on this actor is really a setting of the capture component, and the details panel
|
||||||
|
// shows the component's own copies of them as well. An edit that is not pushed down leaves the two
|
||||||
|
// describing different things.
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool AOrthoMapCaptureActor::GetTargetSize(int32& OutSizeX, int32& OutSizeY) const
|
||||||
|
{
|
||||||
|
OutSizeX = 0;
|
||||||
|
OutSizeY = 0;
|
||||||
|
|
||||||
|
if (!RenderTarget || RenderTarget->SizeX <= 0 || RenderTarget->SizeY <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutSizeX = RenderTarget->SizeX;
|
||||||
|
OutSizeY = RenderTarget->SizeY;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AOrthoMapCaptureActor::ComputeMapUV(const FVector& WorldLocation, FVector2D& OutUV) const
|
||||||
|
{
|
||||||
|
OutUV = FVector2D::ZeroVector;
|
||||||
|
|
||||||
|
if (!CaptureComponent)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 SizeX = 0;
|
||||||
|
int32 SizeY = 0;
|
||||||
|
if (!GetTargetSize(SizeX, SizeY))
|
||||||
|
{
|
||||||
|
// No warning here: this is the per frame path for anything that maps along with the capture, and
|
||||||
|
// a missing render target is reported where a person asks for a mapping instead.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The camera the renderer will build this capture's view from, asked for rather than assembled here
|
||||||
|
// out of the transform and OrthoWidth. Anything derived from the component this way stays in step
|
||||||
|
// with the picture, including the ortho clip planes and any offset the engine applies to them.
|
||||||
|
FMinimalViewInfo ViewInfo;
|
||||||
|
CaptureComponent->GetCameraView(0.0f, ViewInfo);
|
||||||
|
|
||||||
|
ViewInfo.ProjectionMode = ECameraProjectionMode::Orthographic;
|
||||||
|
ViewInfo.OrthoWidth = FMath::Max(1.0f, CaptureComponent->OrthoWidth);
|
||||||
|
// The render target is the capture's viewport, so it, and not the camera, is what fixes the aspect
|
||||||
|
// ratio: the height of the captured rectangle is OrthoWidth * SizeY / SizeX.
|
||||||
|
ViewInfo.AspectRatio = static_cast<float>(SizeX) / static_cast<float>(SizeY);
|
||||||
|
ViewInfo.bConstrainAspectRatio = false;
|
||||||
|
|
||||||
|
// A capture that renders through a projection matrix of its own has to be mapped through that same
|
||||||
|
// matrix, or the mapping and the picture would describe two different regions.
|
||||||
|
const FMatrix ProjectionMatrix = CaptureComponent->bUseCustomProjectionMatrix
|
||||||
|
? CaptureComponent->CustomProjectionMatrix
|
||||||
|
: ViewInfo.CalculateProjectionMatrix();
|
||||||
|
|
||||||
|
const FMatrix ViewMatrix = FTranslationMatrix(-ViewInfo.Location) * FInverseRotationMatrix(ViewInfo.Rotation) * ViewSpaceSwizzle;
|
||||||
|
const FMatrix ViewProjectionMatrix = ViewMatrix * ProjectionMatrix;
|
||||||
|
|
||||||
|
// The whole image is the viewport here, so the pixel a position projects to is also the pixel it
|
||||||
|
// occupies in the render target.
|
||||||
|
const FIntRect ViewRect(0, 0, SizeX, SizeY);
|
||||||
|
|
||||||
|
// Asking for positions outside the view is on: landing outside the captured rectangle is an ordinary
|
||||||
|
// question with an ordinary answer (a UV outside 0-1) and not a failure.
|
||||||
|
FVector2D ScreenPosition;
|
||||||
|
FSceneView::ProjectWorldToScreen(WorldLocation, ViewRect, ViewProjectionMatrix, ScreenPosition, /*bShouldCalcOutsideViewPosition=*/true);
|
||||||
|
|
||||||
|
OutUV.X = ScreenPosition.X / static_cast<double>(SizeX);
|
||||||
|
OutUV.Y = ScreenPosition.Y / static_cast<double>(SizeY);
|
||||||
|
|
||||||
|
// ProjectWorldToScreen puts the view's up direction at a smaller pixel Y, and pixel Y counts down
|
||||||
|
// from the top of the image, so V 0 is the top row of the render target already. The flip is only
|
||||||
|
// there for the consumers that keep V the other way round.
|
||||||
|
if (bFlipV)
|
||||||
|
{
|
||||||
|
OutUV.Y = 1.0 - OutUV.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D AOrthoMapCaptureActor::WorldToMapUV(FVector WorldLocation) const
|
||||||
|
{
|
||||||
|
FVector2D UV;
|
||||||
|
ComputeMapUV(WorldLocation, UV);
|
||||||
|
return UV;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AOrthoMapCaptureActor::WorldToMapUVChecked(FVector WorldLocation, FVector2D& OutUV, bool& bIsInsideCapture) const
|
||||||
|
{
|
||||||
|
const bool bHasUV = ComputeMapUV(WorldLocation, OutUV);
|
||||||
|
|
||||||
|
// Computed from the UV rather than taken from the projection, which reports whether a position is in
|
||||||
|
// front of the camera, not whether it is inside the captured rectangle.
|
||||||
|
bIsInsideCapture = bHasUV
|
||||||
|
&& OutUV.X >= 0.0 && OutUV.X <= 1.0
|
||||||
|
&& OutUV.Y >= 0.0 && OutUV.Y <= 1.0;
|
||||||
|
|
||||||
|
return bHasUV;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AOrthoMapCaptureActor::IsWorldPointInCapture(FVector WorldLocation) const
|
||||||
|
{
|
||||||
|
FVector2D UV;
|
||||||
|
bool bIsInsideCapture = false;
|
||||||
|
WorldToMapUVChecked(WorldLocation, UV, bIsInsideCapture);
|
||||||
|
return bIsInsideCapture;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D AOrthoMapCaptureActor::GetCaptureWorldSize() const
|
||||||
|
{
|
||||||
|
if (!CaptureComponent)
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Width = FMath::Max(1.0f, CaptureComponent->OrthoWidth);
|
||||||
|
|
||||||
|
// Without a target there is no aspect ratio to read, so the height is reported as equal to the width.
|
||||||
|
// That is a guess, which is why the mapping itself refuses to work in this state.
|
||||||
|
double AspectRatio = 1.0;
|
||||||
|
if (RenderTarget && RenderTarget->SizeX > 0 && RenderTarget->SizeY > 0)
|
||||||
|
{
|
||||||
|
AspectRatio = static_cast<double>(RenderTarget->SizeX) / static_cast<double>(RenderTarget->SizeY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FVector2D(Width, Width / AspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
//----- World map -----
|
||||||
|
//
|
||||||
|
// The world map is this same capture repeated over a grid of tiles and stitched back together, and what
|
||||||
|
// makes that work is that the grid's geometry lives in exactly one place: ComputeRegionMatrices, the
|
||||||
|
// projection and view matrix pair a region is captured through. The baker aims the capture at a tile by
|
||||||
|
// handing that tile's box to SetCaptureRegion, and the mapping below projects positions through the same
|
||||||
|
// pair, so a tile's pixels and the UVs that describe them are two readings of one camera rather than two
|
||||||
|
// implementations that are meant to agree.
|
||||||
|
//
|
||||||
|
// Nothing in this section reads the component's transform or OrthoWidth. Those describe the free roaming
|
||||||
|
// capture the rest of the actor is for, and they are deliberately not what a map is built from.
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/** Where a world position lands in an image drawn through these matrices: the same arithmetic
|
||||||
|
* FSceneView::ProjectWorldToScreen does, with the whole render target as the viewport. Going through the
|
||||||
|
* matrices rather than working the answer out from the rotation is what makes a UV computed here the UV
|
||||||
|
* the picture actually has. */
|
||||||
|
FVector2D ProjectRegionUV(const FMatrix& ViewMatrix, const FMatrix& ProjectionMatrix, const FVector& WorldLocation)
|
||||||
|
{
|
||||||
|
const FVector4 ClipPosition = (ViewMatrix * ProjectionMatrix).TransformFVector4(FVector4(WorldLocation, 1.0));
|
||||||
|
|
||||||
|
// An orthographic projection does not divide by W, so W is 1 and the clip position is already the
|
||||||
|
// position on the image in the -1 to 1 space a viewport maps from. Y is turned round here because
|
||||||
|
// projection space counts upwards while a texture's V counts down from its top row.
|
||||||
|
const double W = FMath::IsNearlyZero(ClipPosition.W) ? 1.0 : ClipPosition.W;
|
||||||
|
|
||||||
|
return FVector2D(
|
||||||
|
ClipPosition.X / W * 0.5 + 0.5,
|
||||||
|
0.5 - ClipPosition.Y / W * 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::ComputeRegionMatrices(const FBox& WorldRegion, FMatrix& OutViewMatrix, FMatrix& OutProjectionMatrix) const
|
||||||
|
{
|
||||||
|
const FVector Center = WorldRegion.GetCenter();
|
||||||
|
const FVector Extent = WorldRegion.GetExtent();
|
||||||
|
|
||||||
|
// Half extents, because that is what FReversedZOrthoMatrix takes: this is the same call the engine's own
|
||||||
|
// orthographic camera path makes, with OrthoWidth / 2 for the width. A square region therefore fills a
|
||||||
|
// square image with no distortion, which is the whole reason the world map region is required to be
|
||||||
|
// square rather than merely tidy.
|
||||||
|
const float HalfWidth = static_cast<float>(FMath::Max(1.0, FMath::Abs(Extent.X)));
|
||||||
|
const float HalfHeight = static_cast<float>(FMath::Max(1.0, FMath::Abs(Extent.Y)));
|
||||||
|
|
||||||
|
// The near and far planes cannot be taken from the region: a map has to see everything from the camera
|
||||||
|
// down to the ground, and the camera deliberately sits above the region rather than at a usable near
|
||||||
|
// plane. This is the engine's own minimap answer to the same problem, and it leaves the visible depth
|
||||||
|
// range at half of UE_FLOAT_HUGE_DISTANCE either side of the camera, which is five kilometres of world
|
||||||
|
// below a camera that is only ever a few kilometres up.
|
||||||
|
const double ZOffset = UE_FLOAT_HUGE_DISTANCE / 2.0;
|
||||||
|
|
||||||
|
OutProjectionMatrix = FReversedZOrthoMatrix(
|
||||||
|
HalfWidth,
|
||||||
|
HalfHeight,
|
||||||
|
0.5f / ZOffset,
|
||||||
|
ZOffset);
|
||||||
|
|
||||||
|
const FVector CameraLocation(Center.X, Center.Y, WorldRegion.Max.Z);
|
||||||
|
|
||||||
|
OutViewMatrix = FTranslationMatrix(-CameraLocation) * FInverseRotationMatrix(WorldMapCaptureRotation) * ViewSpaceSwizzle;
|
||||||
|
}
|
||||||
|
|
||||||
|
double AOrthoMapCaptureActor::GetTileWorldSize() const
|
||||||
|
{
|
||||||
|
// The region the map covers is square, so either extent answers the question. X is the one the grid is
|
||||||
|
// anchored to, which is also the axis a world partition's cell indices run along.
|
||||||
|
return WorldMapBounds.GetExtent().X * 2.0 / static_cast<double>(FMath::Max(1, TilesPerAxis));
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 AOrthoMapCaptureActor::GetTilePixelSize() const
|
||||||
|
{
|
||||||
|
return MapTextureSize / FMath::Max(1, TilesPerAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D AOrthoMapCaptureActor::WorldToBakedMapUV(FVector WorldLocation) const
|
||||||
|
{
|
||||||
|
FMatrix ViewMatrix;
|
||||||
|
FMatrix ProjectionMatrix;
|
||||||
|
ComputeRegionMatrices(WorldMapBounds, ViewMatrix, ProjectionMatrix);
|
||||||
|
|
||||||
|
FVector2D MapUV = ProjectRegionUV(ViewMatrix, ProjectionMatrix, WorldLocation);
|
||||||
|
|
||||||
|
// The same two flags the bake honours, applied at the same point in the chain, so a UV reported here and
|
||||||
|
// a pixel written by the bake describe the same place in the same picture.
|
||||||
|
if (bFlipU)
|
||||||
|
{
|
||||||
|
MapUV.X = 1.0 - MapUV.X;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bFlipV)
|
||||||
|
{
|
||||||
|
MapUV.Y = 1.0 - MapUV.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapUV;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector AOrthoMapCaptureActor::BakedMapUVToWorld(FVector2D MapUV, double Height) const
|
||||||
|
{
|
||||||
|
const FVector Min = WorldMapBounds.Min;
|
||||||
|
const FVector Max = WorldMapBounds.Max;
|
||||||
|
|
||||||
|
// Three samples of the very mapping the forward direction uses, inverted rather than the algebra being
|
||||||
|
// written out a second time. That is what keeps the pair exact, flips and all: whatever
|
||||||
|
// WorldToBakedMapUV does, including the two flips and whichever world axes the region's picture turns out
|
||||||
|
// to run along, comes back out here.
|
||||||
|
const FVector2D UVAtMin = WorldToBakedMapUV(FVector(Min.X, Min.Y, Min.Z));
|
||||||
|
const FVector2D UVAtMaxX = WorldToBakedMapUV(FVector(Max.X, Min.Y, Min.Z));
|
||||||
|
const FVector2D UVAtMaxY = WorldToBakedMapUV(FVector(Min.X, Max.Y, Min.Z));
|
||||||
|
|
||||||
|
const FVector2D AlongX = UVAtMaxX - UVAtMin;
|
||||||
|
const FVector2D AlongY = UVAtMaxY - UVAtMin;
|
||||||
|
const double Determinant = AlongX.X * AlongY.Y - AlongX.Y * AlongY.X;
|
||||||
|
|
||||||
|
if (FMath::IsNearlyZero(Determinant))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("%s: the world map mapping is degenerate, so a map position cannot be turned back into a world position. Check WorldMapBounds and MapTextureSize."),
|
||||||
|
*GetActorNameOrLabel());
|
||||||
|
|
||||||
|
return FVector(Min.X, Min.Y, Min.Z + Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector2D Delta = MapUV - UVAtMin;
|
||||||
|
const double FractionX = (Delta.X * AlongY.Y - Delta.Y * AlongY.X) / Determinant;
|
||||||
|
const double FractionY = (AlongX.X * Delta.Y - AlongX.Y * Delta.X) / Determinant;
|
||||||
|
|
||||||
|
return FVector(
|
||||||
|
Min.X + FractionX * (Max.X - Min.X),
|
||||||
|
Min.Y + FractionY * (Max.Y - Min.Y),
|
||||||
|
Min.Z + Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::SetRenderTarget(UTextureRenderTarget2D* InRenderTarget)
|
||||||
|
{
|
||||||
|
RenderTarget = InRenderTarget;
|
||||||
|
|
||||||
|
ApplySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AOrthoMapCaptureActor::IsWorldMapGridValid(FText& OutReason) const
|
||||||
|
{
|
||||||
|
auto Reject = [&OutReason](const FText& Reason)
|
||||||
|
{
|
||||||
|
OutReason = Reason;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (TilesPerAxis < 1)
|
||||||
|
{
|
||||||
|
return Reject(NSLOCTEXT("EleriMapCapture", "WorldMapNoTiles",
|
||||||
|
"TilesPerAxis is zero or negative, so the world map would be cut into no tiles at all."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MapTextureSize < 1)
|
||||||
|
{
|
||||||
|
return Reject(NSLOCTEXT("EleriMapCapture", "WorldMapNoPixels",
|
||||||
|
"MapTextureSize is zero or negative, so the world map has no pixels to bake into."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MapTextureSize % TilesPerAxis != 0)
|
||||||
|
{
|
||||||
|
return Reject(FText::Format(
|
||||||
|
NSLOCTEXT("EleriMapCapture", "WorldMapTilePixelsNotWhole",
|
||||||
|
"MapTextureSize ({0}) does not divide evenly by TilesPerAxis ({1}), so a tile would be a fraction of a pixel wide and the tiles could not be stitched back together without a seam."),
|
||||||
|
FText::AsNumber(MapTextureSize),
|
||||||
|
FText::AsNumber(TilesPerAxis)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 MaximumSize = UTexture::GetMaximumDimensionOfNonVT();
|
||||||
|
if (MapTextureSize > MaximumSize)
|
||||||
|
{
|
||||||
|
return Reject(FText::Format(
|
||||||
|
NSLOCTEXT("EleriMapCapture", "WorldMapTooWide",
|
||||||
|
"MapTextureSize ({0}) is wider than the {1} a texture can be without being a virtual texture, and a world map is one stitched texture. Bake at {1} or less."),
|
||||||
|
FText::AsNumber(MapTextureSize),
|
||||||
|
FText::AsNumber(MaximumSize)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WorldMapBounds.IsValid)
|
||||||
|
{
|
||||||
|
return Reject(NSLOCTEXT("EleriMapCapture", "WorldMapNoBounds",
|
||||||
|
"WorldMapBounds has no extent, so there is no region for the world map to cover. FitBoundsToWorldPartition seeds it from the world partition."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector Extent = WorldMapBounds.GetExtent();
|
||||||
|
if (Extent.X <= 0.0 || Extent.Y <= 0.0)
|
||||||
|
{
|
||||||
|
return Reject(NSLOCTEXT("EleriMapCapture", "WorldMapFlatBounds",
|
||||||
|
"WorldMapBounds is flat along one of its axes, so there is no ground for the world map to cover."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Width = Extent.X * 2.0;
|
||||||
|
const double Height = Extent.Y * 2.0;
|
||||||
|
if (!FMath::IsNearlyEqual(Width, Height, FMath::Max(Width, Height) * 1.e-4))
|
||||||
|
{
|
||||||
|
return Reject(FText::Format(
|
||||||
|
NSLOCTEXT("EleriMapCapture", "WorldMapNotSquare",
|
||||||
|
"WorldMapBounds is {0} by {1} world units. It has to be square: the map texture is square and so is each capture that fills it, so a region twice as wide as it is tall would either be squashed into the map or baked with a third of the tiles blank."),
|
||||||
|
FText::AsNumber(FMath::RoundToInt(Width)),
|
||||||
|
FText::AsNumber(FMath::RoundToInt(Height))));
|
||||||
|
}
|
||||||
|
|
||||||
|
const double TileSize = Width / static_cast<double>(TilesPerAxis);
|
||||||
|
|
||||||
|
if (TileSize > MAX_int32)
|
||||||
|
{
|
||||||
|
return Reject(FText::Format(
|
||||||
|
NSLOCTEXT("EleriMapCapture", "WorldMapTileTooLarge",
|
||||||
|
"A tile would be {0} world units across, which is more than the baker's cell size can hold. Use a larger TilesPerAxis."),
|
||||||
|
FText::AsNumber(FMath::RoundToInt(TileSize))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The bake walks a grid of whole sized cells, so a tile edge that is not a whole number of units cannot
|
||||||
|
// land on a cell edge. A millionth of a unit is far below the precision a world position carries, so
|
||||||
|
// anything within that of whole is whole as far as this is concerned.
|
||||||
|
if (!FMath::IsNearlyEqual(TileSize, FMath::RoundToDouble(TileSize), 1.e-6))
|
||||||
|
{
|
||||||
|
return Reject(FText::Format(
|
||||||
|
NSLOCTEXT("EleriMapCapture", "WorldMapTileSizeNotWhole",
|
||||||
|
"WorldMapBounds divided by TilesPerAxis gives a tile {0} world units wide, which is not a whole number of units. The tiles are walked as whole sized cells, so its edges would not fall on cell edges."),
|
||||||
|
FText::AsNumber(TileSize)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const double OffsetX = WorldMapBounds.Min.X / TileSize;
|
||||||
|
const double OffsetY = WorldMapBounds.Min.Y / TileSize;
|
||||||
|
if (!FMath::IsNearlyEqual(OffsetX, FMath::RoundToDouble(OffsetX), 1.e-6)
|
||||||
|
|| !FMath::IsNearlyEqual(OffsetY, FMath::RoundToDouble(OffsetY), 1.e-6))
|
||||||
|
{
|
||||||
|
return Reject(FText::Format(
|
||||||
|
NSLOCTEXT("EleriMapCapture", "WorldMapUnaligned",
|
||||||
|
"WorldMapBounds starts at {0}, which is not on a tile boundary: its min X and min Y both have to be whole multiples of the tile size ({1}). FitBoundsToWorldPartition puts it on the grid."),
|
||||||
|
FText::AsCultureInvariant(WorldMapBounds.Min.ToCompactString()),
|
||||||
|
FText::AsNumber(FMath::RoundToInt(TileSize))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where the bake puts its result is part of whether these settings can be baked: a grid that cannot be
|
||||||
|
// saved is sixty four captures thrown away at the end of the run, and a bake that saves to the wrong
|
||||||
|
// place is worse than one that refuses to start.
|
||||||
|
if (OutputPackageFolder.IsEmpty())
|
||||||
|
{
|
||||||
|
return Reject(NSLOCTEXT("EleriMapCapture", "WorldMapNoOutputFolder",
|
||||||
|
"OutputPackageFolder is empty, so the world map bake would have nowhere to put the map it built. It is the folder the baked asset is saved into, as a long package path such as /Game/Maps/Generated."));
|
||||||
|
}
|
||||||
|
|
||||||
|
FText FolderReason;
|
||||||
|
if (!FPackageName::IsValidLongPackageName(OutputPackageFolder, /*bIncludeReadOnlyRoots=*/true, &FolderReason))
|
||||||
|
{
|
||||||
|
return Reject(FText::Format(
|
||||||
|
NSLOCTEXT("EleriMapCapture", "WorldMapOutputFolderInvalid",
|
||||||
|
"OutputPackageFolder ({0}) is not a long package path a baked map could be saved into: {1}"),
|
||||||
|
FText::AsCultureInvariant(OutputPackageFolder),
|
||||||
|
FolderReason));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::CaptureNow()
|
||||||
|
{
|
||||||
|
if (!CaptureComponent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RenderTarget)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("%s: no render target is assigned, so the capture has nowhere to render."), *GetActorNameOrLabel());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CaptureComponent->CaptureScene();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::RefreshCapture()
|
||||||
|
{
|
||||||
|
// Modified first, so that this lands in the undo history like any other edit made from the details
|
||||||
|
// panel, rather than being an action that cannot be taken back.
|
||||||
|
Modify();
|
||||||
|
|
||||||
|
ApplySettings();
|
||||||
|
CaptureNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
FBox AOrthoMapCaptureActor::GetAlignedWorldMapBounds(const FBox& SourceBounds) const
|
||||||
|
{
|
||||||
|
if (!SourceBounds.IsValid)
|
||||||
|
{
|
||||||
|
return WorldMapBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The longer side decides the square, and the square is then rounded up to a whole number of tiles so a
|
||||||
|
// tile comes out a whole number of units wide. Rounding up rather than to nearest is what keeps the result
|
||||||
|
// a superset of the region it has to cover.
|
||||||
|
const FVector SourceExtent = SourceBounds.GetExtent();
|
||||||
|
const double LongestSide = FMath::Max(SourceExtent.X, SourceExtent.Y) * 2.0;
|
||||||
|
const int32 Tiles = FMath::Max(1, TilesPerAxis);
|
||||||
|
const double TileSize = FMath::Max(1.0, FMath::CeilToDouble(LongestSide / static_cast<double>(Tiles)));
|
||||||
|
const double AlignedSide = TileSize * static_cast<double>(Tiles);
|
||||||
|
|
||||||
|
// Centred on the source and then snapped outwards onto the grid, so the square still contains the source
|
||||||
|
// and its edges land on tile boundaries. Up to one tile of margin is the price of whole tile edges, and a
|
||||||
|
// margin of empty world is a much smaller cost than a map that misses its own corner.
|
||||||
|
const FVector SourceCenter = SourceBounds.GetCenter();
|
||||||
|
const double MinX = FMath::FloorToDouble((SourceCenter.X - AlignedSide * 0.5) / TileSize) * TileSize;
|
||||||
|
const double MinY = FMath::FloorToDouble((SourceCenter.Y - AlignedSide * 0.5) / TileSize) * TileSize;
|
||||||
|
|
||||||
|
return FBox(
|
||||||
|
FVector(MinX, MinY, SourceBounds.Min.Z),
|
||||||
|
FVector(MinX + AlignedSide, MinY + AlignedSide, SourceBounds.Max.Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::SetCaptureRegion(const FBox& WorldRegion)
|
||||||
|
{
|
||||||
|
if (!CaptureComponent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMatrix ViewMatrix;
|
||||||
|
FMatrix ProjectionMatrix;
|
||||||
|
ComputeRegionMatrices(WorldRegion, ViewMatrix, ProjectionMatrix);
|
||||||
|
|
||||||
|
// The transform and the matrix have to describe the same camera, which is why the rotation used here is
|
||||||
|
// the very one ComputeRegionMatrices built the view matrix from: one named value, two uses, and no way for
|
||||||
|
// the capture and the mapping to drift apart.
|
||||||
|
CaptureComponent->SetWorldLocationAndRotation(
|
||||||
|
FVector(WorldRegion.GetCenter().X, WorldRegion.GetCenter().Y, WorldRegion.Max.Z),
|
||||||
|
WorldMapCaptureRotation);
|
||||||
|
|
||||||
|
CaptureComponent->bUseCustomProjectionMatrix = true;
|
||||||
|
CaptureComponent->CustomProjectionMatrix = ProjectionMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AOrthoMapCaptureActor::IsCapturingRegion() const
|
||||||
|
{
|
||||||
|
return CaptureComponent && CaptureComponent->bUseCustomProjectionMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::ReportMapping()
|
||||||
|
{
|
||||||
|
if (!CaptureComponent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 SizeX = 0;
|
||||||
|
int32 SizeY = 0;
|
||||||
|
if (!GetTargetSize(SizeX, SizeY))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("%s: no render target is assigned, so there is no image for world positions to map into."), *GetActorNameOrLabel());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector2D WorldSize = GetCaptureWorldSize();
|
||||||
|
const FTransform ComponentTransform = CaptureComponent->GetComponentTransform();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("%s: a %d x %d image covers %.1f x %.1f world units, centred on %s facing %s."),
|
||||||
|
*GetActorNameOrLabel(), SizeX, SizeY, WorldSize.X, WorldSize.Y,
|
||||||
|
*ComponentTransform.GetLocation().ToCompactString(), *ComponentTransform.Rotator().ToCompactString());
|
||||||
|
|
||||||
|
// The middle and the four edges of the captured rectangle. They are placed in the component's own
|
||||||
|
// frame rather than in world axes, so the report holds whatever way the actor has been turned.
|
||||||
|
struct FMappingProbe
|
||||||
|
{
|
||||||
|
const TCHAR* Label;
|
||||||
|
FVector LocalOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FMappingProbe Probes[] =
|
||||||
|
{
|
||||||
|
{ TEXT("centre"), FVector::ZeroVector },
|
||||||
|
{ TEXT("local +Y (right)"), FVector(0.0, WorldSize.X * 0.5, 0.0) },
|
||||||
|
{ TEXT("local -Y (left)"), FVector(0.0, -WorldSize.X * 0.5, 0.0) },
|
||||||
|
{ TEXT("local +Z (up)"), FVector(0.0, 0.0, WorldSize.Y * 0.5) },
|
||||||
|
{ TEXT("local -Z (down)"), FVector(0.0, 0.0, -WorldSize.Y * 0.5) },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const FMappingProbe& Probe : Probes)
|
||||||
|
{
|
||||||
|
const FVector WorldLocation = ComponentTransform.TransformPositionNoScale(Probe.LocalOffset);
|
||||||
|
|
||||||
|
FVector2D UV;
|
||||||
|
bool bIsInsideCapture = false;
|
||||||
|
WorldToMapUVChecked(WorldLocation, UV, bIsInsideCapture);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("%s: %s -> UV (%.4f, %.4f)"), *GetActorNameOrLabel(), Probe.Label, UV.X, UV.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the numbers above should read as, so this can be judged without a reference to compare against.
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("%s: a correct mapping puts the centre at (0.5000, 0.5000), the two local Y probes on the U 0 and U 1 edges at V 0.5, and the two local Z probes on the V 0 and V 1 edges at U 0.5."),
|
||||||
|
*GetActorNameOrLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::FitBoundsToWorldPartition()
|
||||||
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
UWorldPartition* WorldPartition = World ? World->GetWorldPartition() : nullptr;
|
||||||
|
if (!WorldPartition)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("%s: the world is not partitioned, so it has no partition bounds to fit the world map to. Set WorldMapBounds by hand."),
|
||||||
|
*GetActorNameOrLabel());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FBox PartitionBounds = WorldPartition->GetEditorWorldBounds();
|
||||||
|
if (!PartitionBounds.IsValid)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("%s: the world partition reports no editor world bounds, so there is nothing for the world map to be fitted to."),
|
||||||
|
*GetActorNameOrLabel());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Modify();
|
||||||
|
WorldMapBounds = GetAlignedWorldMapBounds(PartitionBounds);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("%s: world map bounds fitted to the partition bounds (min %s, max %s)."),
|
||||||
|
*GetActorNameOrLabel(),
|
||||||
|
*PartitionBounds.Min.ToCompactString(), *PartitionBounds.Max.ToCompactString());
|
||||||
|
|
||||||
|
ReportWorldMapMapping();
|
||||||
|
#else
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("%s: the world partition's editor bounds only exist in the editor, so the world map bounds cannot be fitted to them here."),
|
||||||
|
*GetActorNameOrLabel());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void AOrthoMapCaptureActor::ReportWorldMapMapping()
|
||||||
|
{
|
||||||
|
FText Reason;
|
||||||
|
|
||||||
|
if (!IsWorldMapGridValid(Reason))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error, TEXT("%s: the world map settings cannot be baked: %s"), *GetActorNameOrLabel(), *Reason.ToString());
|
||||||
|
|
||||||
|
// The suggestion is only worth printing when there is a region to suggest a fix for. An unset box is
|
||||||
|
// the state of every fresh actor, and FitBoundsToWorldPartition is the answer to it.
|
||||||
|
if (WorldMapBounds.IsValid && WorldMapBounds.GetExtent().X > 0.0 && WorldMapBounds.GetExtent().Y > 0.0)
|
||||||
|
{
|
||||||
|
const FBox Aligned = GetAlignedWorldMapBounds(WorldMapBounds);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("%s: the same region on the tile grid would be min %s, max %s."),
|
||||||
|
*GetActorNameOrLabel(), *Aligned.Min.ToCompactString(), *Aligned.Max.ToCompactString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("%s: world map: %d x %d pixels as %d x %d tiles of %d pixels, each tile %.0f world units, over %s."),
|
||||||
|
*GetActorNameOrLabel(),
|
||||||
|
MapTextureSize, MapTextureSize,
|
||||||
|
TilesPerAxis, TilesPerAxis,
|
||||||
|
GetTilePixelSize(), GetTileWorldSize(),
|
||||||
|
*WorldMapBounds.ToString());
|
||||||
|
|
||||||
|
// The corners of the region, named by the world axes they are made of, so that which way round the picture
|
||||||
|
// comes out can be read off the UVs: the pair that moves U is the world axis the map's columns count.
|
||||||
|
struct FWorldMapProbe
|
||||||
|
{
|
||||||
|
const TCHAR* Label;
|
||||||
|
FVector Location;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FVector Min = WorldMapBounds.Min;
|
||||||
|
const FVector Max = WorldMapBounds.Max;
|
||||||
|
|
||||||
|
const FWorldMapProbe Probes[] =
|
||||||
|
{
|
||||||
|
{ TEXT("min X, min Y"), FVector(Min.X, Min.Y, Min.Z) },
|
||||||
|
{ TEXT("max X, min Y"), FVector(Max.X, Min.Y, Min.Z) },
|
||||||
|
{ TEXT("min X, max Y"), FVector(Min.X, Max.Y, Min.Z) },
|
||||||
|
{ TEXT("max X, max Y"), FVector(Max.X, Max.Y, Min.Z) },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const FWorldMapProbe& Probe : Probes)
|
||||||
|
{
|
||||||
|
const FVector2D MapUV = WorldToBakedMapUV(Probe.Location);
|
||||||
|
const FVector RoundTrip = BakedMapUVToWorld(MapUV, Probe.Location.Z - Min.Z);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("%s: %s -> UV (%.4f, %.4f), pixel (%d, %d), back to (%.1f, %.1f)"),
|
||||||
|
*GetActorNameOrLabel(), Probe.Label, MapUV.X, MapUV.Y,
|
||||||
|
FMath::RoundToInt(MapUV.X * MapTextureSize), FMath::RoundToInt(MapUV.Y * MapTextureSize),
|
||||||
|
RoundTrip.X, RoundTrip.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("%s: a correct world map puts the four corners on the four corners of the pixel grid, and each one reads back as the world position it came from."),
|
||||||
|
*GetActorNameOrLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
EDataValidationResult AOrthoMapCaptureActor::IsDataValid(FDataValidationContext& Context) const
|
||||||
|
{
|
||||||
|
EDataValidationResult Result = Super::IsDataValid(Context);
|
||||||
|
|
||||||
|
if (!RenderTarget)
|
||||||
|
{
|
||||||
|
Context.AddError(NSLOCTEXT("EleriMapCapture", "TargetMissing",
|
||||||
|
"No render target is assigned, so the capture has nowhere to render and WorldToMapUV has no image to map into. Assign one, and make it square unless the captured region is meant to be oblong."));
|
||||||
|
Result = EDataValidationResult::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OrthoWidth <= 0.0f)
|
||||||
|
{
|
||||||
|
Context.AddError(NSLOCTEXT("EleriMapCapture", "OrthoWidthNotPositive",
|
||||||
|
"OrthoWidth is zero or negative, so the captured region has no width and the whole map collapses onto a line."));
|
||||||
|
Result = EDataValidationResult::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RenderTarget && RenderTarget->SizeX > 0 && RenderTarget->SizeY > 0 && RenderTarget->SizeX != RenderTarget->SizeY)
|
||||||
|
{
|
||||||
|
Context.AddWarning(FText::Format(
|
||||||
|
NSLOCTEXT("EleriMapCapture", "TargetNotSquare",
|
||||||
|
"The render target is {0} x {1}, so the captured region is {2} x {3} world units rather than square. That is a legitimate shape, it just is not the one a map usually wants, and anything that assumes the region is square will be off by the difference."),
|
||||||
|
FText::AsNumber(RenderTarget->SizeX),
|
||||||
|
FText::AsNumber(RenderTarget->SizeY),
|
||||||
|
FText::AsNumber(FMath::RoundToInt(OrthoWidth)),
|
||||||
|
FText::AsNumber(FMath::RoundToInt(GetCaptureWorldSize().Y))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The world map settings are the tiled bake's input, and a set of them that cannot produce a coherent map
|
||||||
|
// is worth saying out loud rather than finding out halfway through a bake. An unset box is left alone: it is
|
||||||
|
// the state of every fresh actor, and the feature is only in use once a region has been chosen.
|
||||||
|
if (WorldMapBounds.IsValid)
|
||||||
|
{
|
||||||
|
FText Reason;
|
||||||
|
if (!IsWorldMapGridValid(Reason))
|
||||||
|
{
|
||||||
|
Context.AddError(FText::Format(
|
||||||
|
NSLOCTEXT("EleriMapCapture", "WorldMapGridInvalid",
|
||||||
|
"The tiled world map cannot be baked with these settings: {0}"),
|
||||||
|
Reason));
|
||||||
|
Result = EDataValidationResult::Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A capture with the post process turned off cannot show a source that reads the post processed picture: it
|
||||||
|
// comes out clear, which reads as a map of an empty world rather than as a misconfigured capture. The bake
|
||||||
|
// settles this on its own, but an actor set up this way shows a blank preview and is worth saying so. The
|
||||||
|
// three sources the renderer treats as needing the scene colour are the ones in question.
|
||||||
|
if (bUnlitCapture
|
||||||
|
&& (CaptureSourceOverride == SCS_FinalColorLDR
|
||||||
|
|| CaptureSourceOverride == SCS_FinalColorHDR
|
||||||
|
|| CaptureSourceOverride == SCS_FinalToneCurveHDR))
|
||||||
|
{
|
||||||
|
Context.AddError(NSLOCTEXT("EleriMapCapture", "SourceNeedsPostProcess",
|
||||||
|
"bUnlitCapture is on, which turns the post process off, but the capture source reads the picture the post process produces. Nothing comes out of that combination and the capture is blank. Set the capture source to Base Color, or turn bUnlitCapture off."));
|
||||||
|
Result = EDataValidationResult::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
281
Source/ProjectEleri/GameObjects/OrthoMapCaptureActor.h
Normal file
281
Source/ProjectEleri/GameObjects/OrthoMapCaptureActor.h
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Engine/EngineTypes.h"
|
||||||
|
#include "Misc/DataValidation.h"
|
||||||
|
|
||||||
|
#include "OrthoMapCaptureActor.generated.h"
|
||||||
|
|
||||||
|
class USceneCaptureComponent2D;
|
||||||
|
class UTextureRenderTarget2D;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orthographic scene capture of a rectangular slice of the world, together with the conversion between
|
||||||
|
* world positions and the 0-1 UV space of the image that slice is rendered into.
|
||||||
|
*
|
||||||
|
* The actor is meant to be dragged around by hand: WorldToMapUV reads the capture component's live
|
||||||
|
* transform, so the rectangle it maps is wherever the actor has been moved and turned to, with nothing
|
||||||
|
* to re-synchronise afterwards.
|
||||||
|
*
|
||||||
|
* The settings mirrored on this actor are the ones that drive the capture. ApplySettings pushes them
|
||||||
|
* down onto the component, which is why the component's own copies of them are not worth editing: the
|
||||||
|
* next push overwrites them.
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class PROJECTELERI_API AOrthoMapCaptureActor : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
AOrthoMapCaptureActor();
|
||||||
|
|
||||||
|
/** Pushes the mirrored settings down onto the capture. This is also the moment a level placed
|
||||||
|
* instance picks up an edit made on its archetype. No capture is started here on purpose:
|
||||||
|
* construction runs while a level is still being built, and the renderer is not in a state to
|
||||||
|
* draw a scene capture out of it. */
|
||||||
|
virtual void OnConstruction(const FTransform& Transform) override;
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||||
|
virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//----- Components -----
|
||||||
|
|
||||||
|
/** Plain scene component as the root, so the actor keeps a transform even with the capture off. */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Map Capture")
|
||||||
|
USceneComponent* SceneRoot;
|
||||||
|
|
||||||
|
/** The capture itself. Its location and rotation are what WorldToMapUV measures from, and its
|
||||||
|
* forward axis is the direction it looks down. */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Map Capture")
|
||||||
|
USceneCaptureComponent2D* CaptureComponent;
|
||||||
|
|
||||||
|
//----- Capture settings -----
|
||||||
|
|
||||||
|
/** Image the capture renders into. Its resolution is also the aspect ratio the mapping uses, so the
|
||||||
|
* mapping is only trustworthy once this is assigned. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture")
|
||||||
|
UTextureRenderTarget2D* RenderTarget;
|
||||||
|
|
||||||
|
/** The component's own CaptureSource is not exposed to the details panel or to Blueprint by the
|
||||||
|
* engine, so it is mirrored here and pushed down by ApplySettings.
|
||||||
|
*
|
||||||
|
* The base colour rather than the finished picture, because the finished picture is what a post process
|
||||||
|
* produces and this actor turns the post process off to make a map: a source that reads it then reads
|
||||||
|
* nothing at all and the capture comes out clear, whatever the world in front of it looks like. The base
|
||||||
|
* colour comes out of the g-buffer and does not care. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture")
|
||||||
|
TEnumAsByte<ESceneCaptureSource> CaptureSourceOverride;
|
||||||
|
|
||||||
|
/** Width of the captured region in world units. The height follows from the render target's aspect
|
||||||
|
* ratio rather than being set here, which is why GetCaptureWorldSize reports both. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture", meta = (ClampMin = "1.0", UIMin = "1.0"))
|
||||||
|
float OrthoWidth;
|
||||||
|
|
||||||
|
/** Lets the engine place the near and far clip planes from OrthoWidth instead of leaving them where
|
||||||
|
* they are. Worth keeping on for a capture that is moved around. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture")
|
||||||
|
bool bAutoCalculateOrthoPlanes;
|
||||||
|
|
||||||
|
/** Capture every frame. Off by default, because a map of a region only changes when the region
|
||||||
|
* moves. Turning it on makes the capture cost a rendering pass per frame. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture")
|
||||||
|
bool bCaptureEveryFrame;
|
||||||
|
|
||||||
|
/** Capture when the actor moves. This is what makes dragging the actor in the editor show the region
|
||||||
|
* it is covering. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture")
|
||||||
|
bool bCaptureOnMovement;
|
||||||
|
|
||||||
|
/** Strips lighting, the atmosphere, fog and the post process out of the capture, so the result reads
|
||||||
|
* as a map rather than as a photograph of the level. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture")
|
||||||
|
bool bUnlitCapture;
|
||||||
|
|
||||||
|
/** Flips the V that WorldToMapUV returns, for the consumers that expect V to run bottom to top. It
|
||||||
|
* is off by default because V is a texture coordinate: V 0 is the top row of the render target. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture")
|
||||||
|
bool bFlipV;
|
||||||
|
|
||||||
|
/** Flips the U that the mapping returns, the horizontal counterpart of bFlipV. The tiled world map bake
|
||||||
|
* honours both flags as well, so the picture and the UVs cannot end up describing two different
|
||||||
|
* images. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture")
|
||||||
|
bool bFlipU;
|
||||||
|
|
||||||
|
//----- World map -----
|
||||||
|
|
||||||
|
/** Region the baked world map covers. It has to be square, because the map texture is square and so is
|
||||||
|
* each of the captures that fill it, and its min X and Y have to sit on tile boundaries, because the
|
||||||
|
* bake walks a grid of whole sized cells. FitBoundsToWorldPartition sets both of those from the world
|
||||||
|
* partition, and IsWorldMapGridValid says when an edited box has broken either. Z only sets the height
|
||||||
|
* the captures look down from. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture|World Map")
|
||||||
|
FBox WorldMapBounds;
|
||||||
|
|
||||||
|
/** Resolution of the whole baked map in pixels. 16384 is the widest a texture can be without being a
|
||||||
|
* virtual texture, which is the ceiling for an asset that is one stitched image. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture|World Map", meta = (ClampMin = "64", ClampMax = "16384", UIMin = "1024", UIMax = "16384"))
|
||||||
|
int32 MapTextureSize;
|
||||||
|
|
||||||
|
/** The map is captured as this many tiles per axis, so a bake is TilesPerAxis squared captures of
|
||||||
|
* MapTextureSize / TilesPerAxis pixels each. Finer tiles keep each capture's render target and each
|
||||||
|
* loaded cell small; coarser tiles are fewer passes to stitch. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture|World Map", meta = (ClampMin = "1", ClampMax = "64", UIMin = "1", UIMax = "16"))
|
||||||
|
int32 TilesPerAxis;
|
||||||
|
|
||||||
|
/** Folder the baked map is saved into, as a long package path. The asset is named after the map it was
|
||||||
|
* baked from, so re-baking a map replaces its own map and nothing else. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture|World Map")
|
||||||
|
FString OutputPackageFolder;
|
||||||
|
|
||||||
|
/** Frames each tile is captured for before the one that counts. A tile whose cells are still streaming in
|
||||||
|
* renders as nothing until they have arrived, so a capture taken the instant a cell is loaded can come
|
||||||
|
* back empty; a few frames of ticking is what gives the streaming a chance to finish, and it is also most
|
||||||
|
* of what a bake spends its time on. Five is the number the engine's own world partition minimap captures
|
||||||
|
* with. Zero is allowed, and is a bake that trusts the cell to be fully resident already. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Map Capture|World Map", meta = (ClampMin = "0", UIMin = "0", UIMax = "20"))
|
||||||
|
int32 WorldMapWarmupFrames;
|
||||||
|
|
||||||
|
//----- Mapping -----
|
||||||
|
|
||||||
|
/** Where a world position lands in the captured image, as UV. Values outside 0-1 are positions
|
||||||
|
* outside the captured rectangle, and are returned as they are rather than clamped: which of the
|
||||||
|
* two a caller wants is the caller's business. Returns (0,0) when there is no render target to map
|
||||||
|
* into, since there is no image for a position to land in. */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Map Capture")
|
||||||
|
FVector2D WorldToMapUV(FVector WorldLocation) const;
|
||||||
|
|
||||||
|
/** WorldToMapUV with the two things a caller usually needs to decide with, kept apart: whether the
|
||||||
|
* UV could be worked out at all (a render target is assigned) and whether the position is inside
|
||||||
|
* the captured rectangle. The return value reports the first, bIsInsideCapture the second. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Map Capture")
|
||||||
|
bool WorldToMapUVChecked(FVector WorldLocation, FVector2D& OutUV, bool& bIsInsideCapture) const;
|
||||||
|
|
||||||
|
/** Whether a world position is inside the captured rectangle, on its plane or not: the capture is
|
||||||
|
* orthographic, so only the two axes across the camera's view matter. */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Map Capture")
|
||||||
|
bool IsWorldPointInCapture(FVector WorldLocation) const;
|
||||||
|
|
||||||
|
/** Size of the region the capture covers, in world units: X across the image and Y down it. Y is
|
||||||
|
* OrthoWidth scaled by the render target's aspect ratio, which is why an unassigned target makes
|
||||||
|
* this a square guess rather than an answer. */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Map Capture")
|
||||||
|
FVector2D GetCaptureWorldSize() const;
|
||||||
|
|
||||||
|
//----- World map mapping -----
|
||||||
|
|
||||||
|
/** Where a world position lands in the baked world map, as a UV over the whole map. The UV comes from
|
||||||
|
* the same top down camera the tiled bake captures through, so it describes the picture that is
|
||||||
|
* actually baked rather than an approximation of it, and bFlipU and bFlipV are applied here as they
|
||||||
|
* are there. Different from WorldToMapUV on purpose: that one measures the live capture, this one
|
||||||
|
* measures the map, and they answer for different images. */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Map Capture|World Map")
|
||||||
|
FVector2D WorldToBakedMapUV(FVector WorldLocation) const;
|
||||||
|
|
||||||
|
/** The inverse of WorldToBakedMapUV. Height is measured up from the bottom of WorldMapBounds, so a
|
||||||
|
* height of zero is the map's own plane: a map position is a place on the ground, and only the caller
|
||||||
|
* knows how high above it the thing being placed belongs. */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Map Capture|World Map")
|
||||||
|
FVector BakedMapUVToWorld(FVector2D MapUV, double Height) const;
|
||||||
|
|
||||||
|
/** World size of one tile, which is also the side of the square region a single capture covers. */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Map Capture|World Map")
|
||||||
|
double GetTileWorldSize() const;
|
||||||
|
|
||||||
|
/** Pixel size of one tile: MapTextureSize / TilesPerAxis. A map is TilesPerAxis tiles of this size
|
||||||
|
* across, and the two divide evenly or the settings are rejected. */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Map Capture|World Map")
|
||||||
|
int32 GetTilePixelSize() const;
|
||||||
|
|
||||||
|
/** Whether the world map settings describe a grid that can be baked, and when they do not, why not. The
|
||||||
|
* tiled bake asks this before it does anything, so settings that cannot produce a coherent map are
|
||||||
|
* reported rather than quietly baked into something misaligned. Not a UFUNCTION because Blueprint has
|
||||||
|
* no tidy way to hand a reason back alongside an answer. */
|
||||||
|
bool IsWorldMapGridValid(FText& OutReason) const;
|
||||||
|
|
||||||
|
/** The square, tile aligned box that contains SourceBounds, for the current tile settings: the region a
|
||||||
|
* world map of that world should cover. FitBoundsToWorldPartition writes it onto WorldMapBounds, and
|
||||||
|
* the validator reports it as the fix when an edited box is off the grid. Pure arithmetic, so it is
|
||||||
|
* kept apart from the world partition it is usually applied to. */
|
||||||
|
FBox GetAlignedWorldMapBounds(const FBox& SourceBounds) const;
|
||||||
|
|
||||||
|
//----- Capture control -----
|
||||||
|
|
||||||
|
/** Assigns the render target and pushes the settings down, which is the whole job in one call for
|
||||||
|
* the callers that only have a target to hand over. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Map Capture")
|
||||||
|
void SetRenderTarget(UTextureRenderTarget2D* InRenderTarget);
|
||||||
|
|
||||||
|
/** Renders the capture once, whatever bCaptureEveryFrame and bCaptureOnMovement say. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Map Capture")
|
||||||
|
void CaptureNow();
|
||||||
|
|
||||||
|
/** Applies the mirrored settings and then renders once. Editor facing: this is the button that makes
|
||||||
|
* an edited actor show its picture without having to nudge it. */
|
||||||
|
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Map Capture")
|
||||||
|
void RefreshCapture();
|
||||||
|
|
||||||
|
/** Logs where the centre and the four edges of the captured rectangle land in UV, so that the
|
||||||
|
* mapping can be checked against the picture on the render target without a scene to drive it. */
|
||||||
|
UFUNCTION(BlueprintCallable, CallInEditor, Category = "Map Capture")
|
||||||
|
void ReportMapping();
|
||||||
|
|
||||||
|
/** Points the capture at exactly this world box by giving it a projection matrix of its own, whatever
|
||||||
|
* the actor's own transform happens to be. This is how the tiled world map bake aims the camera at one
|
||||||
|
* tile, and it is why the bake and the world map mapping agree: both go through this same matrix
|
||||||
|
* rather than through OrthoWidth and the target's aspect ratio. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Map Capture")
|
||||||
|
void SetCaptureRegion(const FBox& WorldRegion);
|
||||||
|
|
||||||
|
/** Whether the capture is being aimed by a projection matrix of its own rather than by the actor's
|
||||||
|
* transform. */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Map Capture")
|
||||||
|
bool IsCapturingRegion() const;
|
||||||
|
|
||||||
|
/** Seeds WorldMapBounds from the world partition's own bounds, squared up and put on tile boundaries, so
|
||||||
|
* the map covers everything the partition holds and no more than a tile of margin around it. Editor
|
||||||
|
* facing, because that is where the partition's editor bounds exist. */
|
||||||
|
UFUNCTION(CallInEditor, Category = "Map Capture|World Map")
|
||||||
|
void FitBoundsToWorldPartition();
|
||||||
|
|
||||||
|
/** Logs the world map settings, the corners of the region they cover, and where each corner lands in the
|
||||||
|
* baked map, so that "is this going to line up?" can be answered without a bake. Each corner is also
|
||||||
|
* put back through BakedMapUVToWorld, which is the check that the two directions are inverses. */
|
||||||
|
UFUNCTION(CallInEditor, Category = "Map Capture|World Map")
|
||||||
|
void ReportWorldMapMapping();
|
||||||
|
|
||||||
|
/** Copies the mirrored settings onto the capture component. Safe to call at any point in the object's
|
||||||
|
* life once it is constructed. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Map Capture")
|
||||||
|
void ApplySettings();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
/** Overridden to apply the settings to an instance that arrived with saved properties instead of
|
||||||
|
* through construction, which is the path a level placed actor takes. Protected because that is
|
||||||
|
* what it is on AActor, and nothing outside the actor has a reason to start one. */
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
/** Sets the component's show flags, which is not the same as writing to its ShowFlags member: that
|
||||||
|
* member is rebuilt from the archetype every time the component is loaded or edited, so anything
|
||||||
|
* written to it directly is lost at the next load. The serialized setting list survives that. */
|
||||||
|
void ApplyCaptureShowFlags() const;
|
||||||
|
|
||||||
|
/** Resolution of the render target in pixels, or false and two zeroes when there is none to read. */
|
||||||
|
bool GetTargetSize(int32& OutSizeX, int32& OutSizeY) const;
|
||||||
|
|
||||||
|
/** Projects a world position through the same camera the capture renders with, and reports whether
|
||||||
|
* it could: the maths needs a render target, because the target is what fixes the aspect ratio. */
|
||||||
|
bool ComputeMapUV(const FVector& WorldLocation, FVector2D& OutUV) const;
|
||||||
|
|
||||||
|
/** The view and projection matrices a capture of WorldRegion is taken through. The projection covers the
|
||||||
|
* region exactly, which is what makes a tile's UVs run 0-1 across that tile, and the view matrix is the
|
||||||
|
* straight down camera the region is captured from. Kept in one place because everything about the
|
||||||
|
* world map, the bake and the mapping alike, is a consequence of these two matrices. */
|
||||||
|
void ComputeRegionMatrices(const FBox& WorldRegion, FMatrix& OutViewMatrix, FMatrix& OutProjectionMatrix) const;
|
||||||
|
};
|
||||||
@@ -39,6 +39,12 @@ public class ProjectEleri : ModuleRules
|
|||||||
PrivateDependencyModuleNames.AddRange(new string[]
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
{ "OnlineSubsystem", "OnlineSubsystemUtils", "Landscape", "LTween" });
|
{ "OnlineSubsystem", "OnlineSubsystemUtils", "Landscape", "LTween" });
|
||||||
|
|
||||||
|
// The game's own headers live in folders at the module root rather than under Public/, and an editor tool
|
||||||
|
// that drives a scene capture needs the capture actor: its world map settings are what the tiled bake
|
||||||
|
// reads. Declaring the module root as a public include root is what lets a module that depends on this
|
||||||
|
// one include them by their folder.
|
||||||
|
PublicIncludePaths.Add(ModuleDirectory);
|
||||||
|
|
||||||
// Uncomment if you are using Slate UI
|
// Uncomment if you are using Slate UI
|
||||||
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||||
|
|
||||||
|
|||||||
1163
Source/ProjectEleriEditor/Private/OrthoMap/WorldMapBuilder.cpp
Normal file
1163
Source/ProjectEleriEditor/Private/OrthoMap/WorldMapBuilder.cpp
Normal file
File diff suppressed because it is too large
Load Diff
217
Source/ProjectEleriEditor/Private/OrthoMap/WorldMapBuilder.h
Normal file
217
Source/ProjectEleriEditor/Private/OrthoMap/WorldMapBuilder.h
Normal 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;
|
||||||
|
};
|
||||||
@@ -22,6 +22,10 @@ public class ProjectEleriEditor : ModuleRules
|
|||||||
"ToolMenus",
|
"ToolMenus",
|
||||||
"AssetRegistry",
|
"AssetRegistry",
|
||||||
"ContentBrowser",
|
"ContentBrowser",
|
||||||
|
// The world map builder drives a scene capture and reads the render target back on the CPU, which
|
||||||
|
// is where the render thread and the pixel formats it copies between come in.
|
||||||
|
"RenderCore",
|
||||||
|
"RHI",
|
||||||
"ProjectEleri"});
|
"ProjectEleri"});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user