#pragma once #include "CoreMinimal.h" #include "DescentUnits.h" /** * The small math helpers the design uses everywhere (DESIGN_UE.md section 0): v2's clamp / lerp / smooth / damp / * smin, the datum and the one depth function, metre <-> centimetre conversions, the seeded mulberry32 random and * the seeded 3D simplex noise. Header-only, no UObject, safe to call from worker threads. * * Only what sections 0 and 1 define lives here. A system that needs more math puts it in its own header, so this * file never changes after the core task and never conflicts in a merge. */ namespace Descent { // ------------------------------------------------------------------------------------------------------ // Units. DescentUnits.h has MetresToUU / UUToMetres; one Unreal unit is one centimetre, so the names below // are the same conversion under the name people think in when they talk about a mesh or a collision shape. // ------------------------------------------------------------------------------------------------------ FORCEINLINE constexpr double MetresToCm(double Metres) { return Metres * UUPerMetre; } FORCEINLINE constexpr double CmToMetres(double Cm) { return Cm / UUPerMetre; } FORCEINLINE FVector MetresToCm(const FVector& Metres) { return Metres * UUPerMetre; } FORCEINLINE FVector CmToMetres(const FVector& Cm) { return Cm / UUPerMetre; } // ------------------------------------------------------------------------------------------------------ // v2 helpers (01_core.js), ported verbatim. // ------------------------------------------------------------------------------------------------------ template FORCEINLINE constexpr T Clamp(T Value, T Lo, T Hi) { return Value < Lo ? Lo : (Value > Hi ? Hi : Value); } FORCEINLINE constexpr double Lerp(double A, double B, double T) { return A + (B - A) * T; } /** Smoothstep with free edges: 0 at E0, 1 at E1, 3t^2 - 2t^3 between. v2 `smooth`. */ FORCEINLINE double Smooth(double E0, double E1, double X) { const double T = Clamp((X - E0) / (E1 - E0), 0.0, 1.0); return T * T * (3.0 - 2.0 * T); } /** Frame-rate independent exponential approach: `lerp(a, b, 1 - exp(-k dt))`. v2 `damp`. */ FORCEINLINE double Damp(double A, double B, double K, double Dt) { return Lerp(A, B, 1.0 - FMath::Exp(-K * Dt)); } /** Polynomial smooth minimum: `h = max(k - |a - b|, 0) / k; min(a, b) - h^2 k / 4`. v2 `smin` (03_world.js). */ FORCEINLINE double SMin(double A, double B, double K) { const double H = FMath::Max(K - FMath::Abs(A - B), 0.0) / K; return FMath::Min(A, B) - H * H * K * 0.25; } // ------------------------------------------------------------------------------------------------------ // Datum and depth. Depth = DATUM - Z, computed ONLY by DepthOfZ. Every readout that needs a depth // (HUD, dashboards, temperature, horror, tests) goes through it; nothing writes `1780 - z` by hand. // ------------------------------------------------------------------------------------------------------ /** Z (metres) at which depth is zero. The Throat's rim is Z = +40, i.e. depth 1,740 m. */ constexpr double DatumM = 1780.0; /** Depth in metres of a point at altitude Z metres. */ FORCEINLINE constexpr double DepthOfZ(double ZMetres) { return DatumM - ZMetres; } /** The altitude (metres) of a given depth. The inverse of DepthOfZ. */ FORCEINLINE constexpr double ZOfDepth(double DepthMetres) { return DatumM - DepthMetres; } /** Depth in metres of a point given in Unreal units (an actor location). */ FORCEINLINE double DepthOfWorldUU(const FVector& WorldUU) { return DepthOfZ(UUToMetres(WorldUU.Z)); } // ------------------------------------------------------------------------------------------------------ // Seeded random: v2 mulberry32. Gameplay uses this so tests are repeatable; FMath::Rand is for cosmetics. // ------------------------------------------------------------------------------------------------------ /** Bit-exact port of v2 `mulberry32(seed)`: Next() returns the same doubles in [0, 1) the browser build did. */ struct FRng { uint32 State; explicit FRng(uint32 Seed = 1337u) : State(Seed) {} /** [0, 1) */ double Next() { State += 0x6D2B79F5u; uint32 T = (State ^ (State >> 15)) * (1u | State); T = (T + (T ^ (T >> 7)) * (61u | T)) ^ T; return static_cast(T ^ (T >> 14)) / 4294967296.0; } /** [A, B) (v2 `rr`) */ double Range(double A, double B) { return A + (B - A) * Next(); } /** 0 .. N-1; returns 0 for N <= 0 */ int32 Int(int32 N) { return N > 0 ? static_cast(Next() * N) : 0; } }; // ------------------------------------------------------------------------------------------------------ // Seeded 3D simplex noise (Gustavson), v2 makeNoise3. Output is about -1..1. Immutable after construction, // so one instance can be shared by every worker thread. // ------------------------------------------------------------------------------------------------------ class FNoise3 { public: explicit FNoise3(uint32 Seed) { FRng R(Seed); uint8 P[256]; for (int32 I = 0; I < 256; ++I) { P[I] = static_cast(I); } for (int32 I = 255; I > 0; --I) { const int32 J = static_cast(R.Next() * (I + 1)); const uint8 T = P[I]; P[I] = P[J]; P[J] = T; } for (int32 I = 0; I < 512; ++I) { Perm[I] = P[I & 255]; PermMod12[I] = static_cast(Perm[I] % 12); } } double operator()(double X, double Y, double Z) const { static constexpr int8 G[36] = { 1,1,0,-1,1,0,1,-1,0,-1,-1,0,1,0,1,-1,0,1,1,0,-1,-1,0,-1,0,1,1,0,-1,1,0,1,-1,0,-1,-1 }; constexpr double F3 = 1.0 / 3.0; constexpr double G3 = 1.0 / 6.0; const double S = (X + Y + Z) * F3; const int32 I = static_cast(FMath::FloorToDouble(X + S)); const int32 J = static_cast(FMath::FloorToDouble(Y + S)); const int32 K = static_cast(FMath::FloorToDouble(Z + S)); const double T = (I + J + K) * G3; const double X0 = X - I + T, Y0 = Y - J + T, Z0 = Z - K + T; int32 I1, J1, K1, I2, J2, K2; if (X0 >= Y0) { if (Y0 >= Z0) { I1 = 1; J1 = 0; K1 = 0; I2 = 1; J2 = 1; K2 = 0; } else if (X0 >= Z0) { I1 = 1; J1 = 0; K1 = 0; I2 = 1; J2 = 0; K2 = 1; } else { I1 = 0; J1 = 0; K1 = 1; I2 = 1; J2 = 0; K2 = 1; } } else { if (Y0 < Z0) { I1 = 0; J1 = 0; K1 = 1; I2 = 0; J2 = 1; K2 = 1; } else if (X0 < Z0) { I1 = 0; J1 = 1; K1 = 0; I2 = 0; J2 = 1; K2 = 1; } else { I1 = 0; J1 = 1; K1 = 0; I2 = 1; J2 = 1; K2 = 0; } } const double X1 = X0 - I1 + G3, Y1 = Y0 - J1 + G3, Z1 = Z0 - K1 + G3; const double X2 = X0 - I2 + 2.0 * G3, Y2 = Y0 - J2 + 2.0 * G3, Z2 = Z0 - K2 + 2.0 * G3; const double X3 = X0 - 1.0 + 3.0 * G3, Y3 = Y0 - 1.0 + 3.0 * G3, Z3 = Z0 - 1.0 + 3.0 * G3; const int32 II = I & 255, JJ = J & 255, KK = K & 255; double N = 0.0; double Tt; int32 Gi; Tt = 0.6 - X0 * X0 - Y0 * Y0 - Z0 * Z0; if (Tt > 0.0) { Gi = PermMod12[II + Perm[JJ + Perm[KK]]] * 3; Tt *= Tt; N += Tt * Tt * (G[Gi] * X0 + G[Gi + 1] * Y0 + G[Gi + 2] * Z0); } Tt = 0.6 - X1 * X1 - Y1 * Y1 - Z1 * Z1; if (Tt > 0.0) { Gi = PermMod12[II + I1 + Perm[JJ + J1 + Perm[KK + K1]]] * 3; Tt *= Tt; N += Tt * Tt * (G[Gi] * X1 + G[Gi + 1] * Y1 + G[Gi + 2] * Z1); } Tt = 0.6 - X2 * X2 - Y2 * Y2 - Z2 * Z2; if (Tt > 0.0) { Gi = PermMod12[II + I2 + Perm[JJ + J2 + Perm[KK + K2]]] * 3; Tt *= Tt; N += Tt * Tt * (G[Gi] * X2 + G[Gi + 1] * Y2 + G[Gi + 2] * Z2); } Tt = 0.6 - X3 * X3 - Y3 * Y3 - Z3 * Z3; if (Tt > 0.0) { Gi = PermMod12[II + 1 + Perm[JJ + 1 + Perm[KK + 1]]] * 3; Tt *= Tt; N += Tt * Tt * (G[Gi] * X3 + G[Gi + 1] * Y3 + G[Gi + 2] * Z3); } return 32.0 * N; } private: uint8 Perm[512]; uint8 PermMod12[512]; }; }