Tooling
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
#include "World/WorldMapProjection.h"
|
||||
|
||||
FVector2D FWorldMapProjection::WorldToNormalised(const FVector2D& WorldCm) const
|
||||
{
|
||||
if (!IsValid())
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
const FVector2D FromCentreM = WorldCm / 100.0 - CentreM;
|
||||
return FVector2D(FromCentreM.X / WidthM + 0.5, FromCentreM.Y / HeightM + 0.5);
|
||||
}
|
||||
|
||||
FVector2D FWorldMapProjection::NormalisedToWorldCm(const FVector2D& Normalised) const
|
||||
{
|
||||
if (!IsValid())
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
const FVector2D FromCentreM((Normalised.X - 0.5) * WidthM, (Normalised.Y - 0.5) * HeightM);
|
||||
return (CentreM + FromCentreM) * 100.0;
|
||||
}
|
||||
|
||||
double FWorldMapProjection::WrapU(double U) const
|
||||
{
|
||||
if (!bWrapsX)
|
||||
{
|
||||
return U;
|
||||
}
|
||||
// Not FMath::Fmod: it keeps the sign of the dividend, so -0.02 stays -0.02 and a pan west off the seam
|
||||
// samples outside the art. FMath::Wrap is inclusive of its maximum, which would make 1.0 a second copy of
|
||||
// the seam column. Floor is the one that gives a half-open [0, 1).
|
||||
const double Wrapped = U - FMath::Floor(U);
|
||||
// Floor of a tiny negative can round to exactly 1.0 in double; fold it back so the range stays half-open.
|
||||
return Wrapped >= 1.0 ? 0.0 : Wrapped;
|
||||
}
|
||||
|
||||
double FWorldMapProjection::ShortestDeltaU(double FromU, double ToU) const
|
||||
{
|
||||
double Delta = ToU - FromU;
|
||||
if (bWrapsX)
|
||||
{
|
||||
Delta -= FMath::RoundToDouble(Delta);
|
||||
}
|
||||
return Delta;
|
||||
}
|
||||
|
||||
double FWorldMapProjection::DistanceM(const FVector2D& FromWorldCm, const FVector2D& ToWorldCm) const
|
||||
{
|
||||
if (!IsValid())
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
const FVector2D From = WorldToNormalised(FromWorldCm);
|
||||
const FVector2D To = WorldToNormalised(ToWorldCm);
|
||||
const double DeltaXM = ShortestDeltaU(From.X, To.X) * WidthM;
|
||||
const double DeltaYM = (To.Y - From.Y) * HeightM;
|
||||
return FMath::Sqrt(DeltaXM * DeltaXM + DeltaYM * DeltaYM);
|
||||
}
|
||||
|
||||
FString FWorldMapProjection::ToString() const
|
||||
{
|
||||
return FString::Printf(TEXT("%.2f x %.2f km centred on (%.0f, %.0f) m, elevation %.0f..%.0f m%s"),
|
||||
WidthM / 1000.0, HeightM / 1000.0, CentreM.X, CentreM.Y,
|
||||
ElevationMinM, ElevationMaxM, bWrapsX ? TEXT(", wraps in X") : TEXT(""));
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "WorldMapProjection.generated.h"
|
||||
|
||||
// Where a place in the world is on a picture of the world, and back. // pure
|
||||
//
|
||||
// This is the whole of what a map view needs to know about the ground, and it is four numbers, because
|
||||
// L_World is a *whole planet map imported with no crop* (RawContent/World/Region.json's window is the full
|
||||
// 8192 x 4096 export) laid out as a grid of landscapes straddling the world origin. So the map art and the
|
||||
// world are the same rectangle at two scales, and the transform between them is one multiply and one add per
|
||||
// axis - no render capture, no per-tile bookkeeping, no stitching.
|
||||
//
|
||||
// The axes are the region manifest's: the source image's X runs along world X and its Y along world Y, which
|
||||
// is what Scripts/Authoring/generate_region_tiles.py samples by and therefore what the ground actually is.
|
||||
// On screen that means the map's horizontal axis is world X and *up on the map is world -Y*. That is not the
|
||||
// compass convention and it is deliberately not corrected here: the alternative is a map that disagrees with
|
||||
// the coordinates every other tool in the project prints.
|
||||
//
|
||||
// Normalised coordinates are 0..1 across the art, which is why they are the currency rather than pixels: every
|
||||
// layer is a different render of the same cylinder and they are not all the same resolution.
|
||||
USTRUCT(BlueprintType)
|
||||
struct SALTYCORE_API FWorldMapProjection
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
// The ground the map covers, in metres. 71 400 x 35 700 for L_World.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map")
|
||||
double WidthM = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map")
|
||||
double HeightM = 0.0;
|
||||
|
||||
// Where the centre of the map sits in the world, in metres. The region grid straddles the origin, so this
|
||||
// is 0,0 today; it is a field rather than an assumption because a window cut from somewhere else would not.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map")
|
||||
FVector2D CentreM = FVector2D::ZeroVector;
|
||||
|
||||
// True when the art is a whole cylinder, so its left and right edges are the same meridian. Panning then
|
||||
// crosses the seam instead of stopping at it, and the distance between two points may go the short way
|
||||
// round. False for a window cut out of one, where the edges are edges.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map")
|
||||
bool bWrapsX = false;
|
||||
|
||||
// The height contract, copied from the same manifest, so a readout can say what a place is in metres
|
||||
// without the map owning a second opinion about elevation. Not used by the transform.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map")
|
||||
double ElevationMinM = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map")
|
||||
double ElevationMaxM = 0.0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "World Map")
|
||||
double SeaLevelM = 0.0;
|
||||
|
||||
bool IsValid() const { return WidthM > 0.0 && HeightM > 0.0; }
|
||||
|
||||
FVector2D ExtentM() const { return FVector2D(WidthM, HeightM); }
|
||||
|
||||
/** Metres of ground across the whole map, per axis. The two differ on any map that is not square. */
|
||||
double MetresPerUnitX() const { return WidthM; }
|
||||
double MetresPerUnitY() const { return HeightM; }
|
||||
|
||||
/** World XY in centimetres to 0..1 across the art. Outside the map the result is simply outside 0..1. */
|
||||
FVector2D WorldToNormalised(const FVector2D& WorldCm) const;
|
||||
|
||||
/** The same for a full world location; Z is ignored. */
|
||||
FVector2D WorldToNormalised(const FVector& WorldCm) const { return WorldToNormalised(FVector2D(WorldCm.X, WorldCm.Y)); }
|
||||
|
||||
/** 0..1 across the art back to world XY in centimetres. */
|
||||
FVector2D NormalisedToWorldCm(const FVector2D& Normalised) const;
|
||||
|
||||
/** Metres rather than centimetres, for anything a person reads. */
|
||||
FVector2D NormalisedToWorldM(const FVector2D& Normalised) const { return NormalisedToWorldCm(Normalised) / 100.0; }
|
||||
|
||||
/**
|
||||
* U folded into [0, 1) on a wrapping map, left alone on one that does not wrap. Every pan and every marker
|
||||
* goes through this rather than through fmod at the call site: FMath::Fmod keeps the sign of its argument,
|
||||
* so a westward pan past the seam lands at -0.02 and samples nothing.
|
||||
*/
|
||||
double WrapU(double U) const;
|
||||
|
||||
/** WrapU on the U of a pair, V untouched - V is latitude and a cylinder does not wrap over its poles. */
|
||||
FVector2D WrapNormalised(const FVector2D& Normalised) const { return FVector2D(WrapU(Normalised.X), Normalised.Y); }
|
||||
|
||||
/**
|
||||
* Signed shortest distance from one U to another, in normalised units. On a wrapping map two points either
|
||||
* side of the seam are close, and this is what says so: the result is in [-0.5, 0.5]. Without it a marker
|
||||
* at U 0.99 and a view centred on U 0.01 are 0.98 apart and the marker is drawn off the far edge.
|
||||
*/
|
||||
double ShortestDeltaU(double FromU, double ToU) const;
|
||||
|
||||
/** How far apart two places are on the ground, in metres, taking the short way round a wrapping map. */
|
||||
double DistanceM(const FVector2D& FromWorldCm, const FVector2D& ToWorldCm) const;
|
||||
|
||||
/** A line for a log or a tooltip. Not for a player: this is metres and axis names, not a place name. */
|
||||
FString ToString() const;
|
||||
};
|
||||
Reference in New Issue
Block a user