Made map builder and exported first pass of maps
This commit is contained in:
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[]
|
||||
{ "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
|
||||
// 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",
|
||||
"AssetRegistry",
|
||||
"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"});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user