fix(determinism): own sin/cos on the world path -- same world on every CPU, 3.5% faster
On Win64, FMath::Sin/Cos resolve to CRT sinf/cosf (GenericPlatformMath.h:521/530; no platform override), and the UCRT picks FMA3 or non-FMA3 implementations at startup by CPU. /fp:precise closed only the compiler half, so the same build and seed could build a slightly different world on two Windows PCs, and on Linux. - VoxelMath::DetSin/DetCos/DetSinCos (VoxelTypes.h): fixed double-precision period reduction to a quarter period, then fixed float minimax polynomials; no CRT, FMath or CPU-dependent intrinsic. Measured max error vs sinf 5.96e-8 over the used range (|arg| <= 6.19). - Every world-deciding FMath::Sin/Cos replaced (generator, op stacks, cave morphology, strate manager, content placement, strate measure); none remain in runtime code. - Pinned test VoxelForge.Determinism.DetSinCos (golden bit patterns + error bound). - Diagnostic start-up switch -voxel.CrtFma3=0 (Windows) forces the CRT's non-FMA3 path. - Parse-once console switches now initialise in StartupModule on the game thread; workers only read them (was an unsynchronised first parse inside GetDensityAt). - Corrected the CRC comment that claimed a CRC32 cannot give a false positive. Proof (Codex round; orchestrator re-checked): - Before the fix, FMA3 on vs off was identical on this Ryzen 7 5800X3D: the hole exists but was not observed in this world. After the fix, the canonical exports are byte-identical with FMA3 on and off. - The world changed once, as expected. New canonical export SHA-256 CBB8AE18...906F (geometry B9EF84AC; was B3E5F4C3...377); worms region 9242C093 (was 311CA42C), walk OBJ 20CCF7BD.... Triangle counts unchanged in every tile. Order-independent nearest-vertex distance, worms region: max 8.7 micrometres (the reported "11.4 m" was a line-order artefact of a line-by-line OBJ diff); export region max 3e-5 m. - Capability exact: player-fit 20,830, walk-reachable 10,909, connected. TilePostReachProof, sealed-proof, cross-platform and strate-crossing tests pass. - Performance, headless game path, S/M/S/M vs the committed-tip DLL: static worker 27.41 s vs 28.44 s (-3.6%), moving 59.59 s vs 61.74 s (-3.5%); LOD0 p50/p95 not worse. From Sol's code review (DESIGN-SOL-2026-09-15-CODE.md, section 4.1), branch sol/deterministic-math. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y
This commit is contained in:
1 parent
cda082fa10
commit
10fd0b6e77
16 files changed
+393
-85
No files matched your search
@@ -163,15 +163,10 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters)
|
||||
// que ça rendait cette mesure caduque. **C'est faux, et il a fallu vérifier plutôt que
|
||||
// supposer.**
|
||||
//
|
||||
// Il reste `FMath::Sin` / `FMath::Cos`, présents partout dans le chemin de densité (lignes de
|
||||
// strates, nervures, placement des salles, rotations). **`sinf`/`cosf` ne sont PAS spécifiés
|
||||
// par IEEE-754** : le CRT de MSVC et la libm de la glibc ont parfaitement le droit de rendre
|
||||
// des résultats différents (typiquement ≤ 1 ULP, mais différents). `FPSemantics` a donc fermé
|
||||
// la moitié COMPILATEUR de §C9 et laissé ouverte la moitié BIBLIOTHÈQUE.
|
||||
//
|
||||
// FPSemantics = Precise removed the float-MODEL difference, but sinf/cosf are not IEEE-754
|
||||
// specified, so MSVC's CRT and glibc's libm may still differ. The compiler half of C9 is closed;
|
||||
// the library half is not.
|
||||
// The world path now owns its sine/cosine range reduction and polynomial in VoxelMath. Before
|
||||
// that replacement, `sinf`/`cosf` were not specified by IEEE-754: MSVC's CRT and glibc's libm
|
||||
// could return different results (typically <= 1 ULP). `FPSemantics` closed the compiler half
|
||||
// of §C9; DetSin/DetCos close the library half as well.
|
||||
//
|
||||
// D'où trois bandes au lieu d'une : une bande unique à 1e-4 est **100× trop large** pour un
|
||||
// écart de libm (~1e-6 en absolu sur des densités de magnitude ~10), donc elle sur-estime
|
||||
@@ -223,37 +218,30 @@ bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters)
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("CROSS-PLATFORM DIGEST (seed %d, %d samples, step %d/%d)\n")
|
||||
TEXT(" SHAPE digest : 0x%016llX <- must match across Windows/Linux. This is the world.\n")
|
||||
TEXT(" FIELD digest : 0x%016llX <- bit-for-bit. May differ; see NearIso below.\n")
|
||||
TEXT(" FIELD digest : 0x%016llX <- expected bit-for-bit after DetSin/DetCos.\n")
|
||||
TEXT(" solid %d / air %d / NaN %d"),
|
||||
World.Settings->Seed, NumSamples, XYStep, ZStep,
|
||||
ShapeDigest, FieldDigest, NumSolid, NumSamples - NumSolid, NumNaN));
|
||||
|
||||
// Le PROFIL de proximité à l'isosurface, plutôt qu'un seul seuil binaire.
|
||||
// Le PROFIL de proximité à l'isosurface reste utile pour visualiser la marge géométrique,
|
||||
// mais il ne mesure plus une divergence CRT : the world trig path is deterministic now.
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("NearIso profile over %d samples: %d within 1e-4, %d within 1e-5, %d within 1e-6. ")
|
||||
TEXT("Only the LAST number is the cross-platform risk: FPSemantics = Precise removed the ")
|
||||
TEXT("float-MODEL difference, so what remains is that sinf/cosf are not IEEE-754 specified ")
|
||||
TEXT("and MSVC's CRT may differ from glibc's libm by ~1 ULP. On densities of magnitude ~10 ")
|
||||
TEXT("that is ~1e-6 absolute, which is why the wide band over-states the risk ~100x."),
|
||||
TEXT("This is a geometric margin profile; DetSin/DetCos removes the prior CRT/libm source ")
|
||||
TEXT("of cross-platform variation."),
|
||||
NumSamples, NumNearWide, NumNearMid, NumNearTight));
|
||||
|
||||
if (NumNearTight > 0)
|
||||
{
|
||||
AddWarning(FString::Printf(
|
||||
TEXT("%d of %d samples sit within 1e-6 of the isosurface -- tight enough that a libm ")
|
||||
TEXT("difference between MSVC and glibc could flip their SIGN, i.e. one voxel solid for ")
|
||||
TEXT("a Windows host and air for a Linux client. FMath::Sin/Cos are used throughout the ")
|
||||
TEXT("density path (layer lines, ribs, room placement, rotations), so this is the ")
|
||||
TEXT("REMAINING half of AUDIT C9 -- the compiler half is fixed, the library half is not. ")
|
||||
TEXT("If this must be zero, the fix is a deterministic in-house sin/cos in the density ")
|
||||
TEXT("path (one more world re-tune), not another build flag."),
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%d of %d samples sit within 1e-6 of the isosurface. The proximity is worth ")
|
||||
TEXT("recording, but it is no longer a CRT/libm cross-platform risk because the production ")
|
||||
TEXT("world path uses deterministic VoxelMath::DetSin/DetCos."),
|
||||
NumNearTight, NumSamples));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddInfo(TEXT("No sample sits within 1e-6 of the isosurface, so no sampled voxel is close ")
|
||||
TEXT("enough for a libm difference to flip its side. Evidence, not proof: it covers ")
|
||||
TEXT("this grid, not every voxel of a world."));
|
||||
AddInfo(TEXT("No sample sits within 1e-6 of the isosurface on this grid."));
|
||||
}
|
||||
|
||||
TestEqual(TEXT("no sample produced NaN"), NumNaN, 0);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Pinned contract for VoxelMath::DetSin/DetCos/DetSinCos.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FDetSinCosGolden
|
||||
{
|
||||
float Input;
|
||||
uint32 SinBits;
|
||||
uint32 CosBits;
|
||||
};
|
||||
|
||||
uint32 FloatBits(float Value)
|
||||
{
|
||||
uint32 Bits = 0;
|
||||
FMemory::Memcpy(&Bits, &Value, sizeof(Bits));
|
||||
return Bits;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeDetSinCosTest,
|
||||
"VoxelForge.Determinism.DetSinCos",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeDetSinCosTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
|
||||
const float Pi = 3.1415926535897932384626433832795f;
|
||||
const float TwoPi = 6.283185307179586476925286766559f;
|
||||
// The expected values are deliberately bit patterns rather than tolerances. This corpus is
|
||||
// the pinned implementation contract; update it only with an intentional world re-tune.
|
||||
static const FDetSinCosGolden Corpus[] =
|
||||
{
|
||||
{ 0.0f, 0x00000000u, 0x3F800000u },
|
||||
{-0.0f, 0x80000000u, 0x3F800000u },
|
||||
{ Pi, 0xB3BBBD2Eu, 0xBF800000u },
|
||||
{-Pi, 0x33BBBD2Eu, 0xBF800000u },
|
||||
{ TwoPi, 0x343BBD2Eu, 0x3F800000u },
|
||||
{-TwoPi, 0xB43BBD2Eu, 0x3F800000u },
|
||||
{ 0.001f, 0x3A83126Du, 0x3F7FFFF8u },
|
||||
{-0.001f, 0xBA83126Du, 0x3F7FFFF8u },
|
||||
{ 123456.789f, 0xBF7FA83Du, 0x3D53E806u },
|
||||
{-123456.789f, 0x3F7FA83Du, 0x3D53E806u },
|
||||
{ 1000000.25f, 0xBDDBDDEBu, 0x3F7E853Du },
|
||||
{-1000000.25f, 0x3DDBDDEBu, 0x3F7E853Du },
|
||||
};
|
||||
|
||||
for (const FDetSinCosGolden& Sample : Corpus)
|
||||
{
|
||||
float ActualSin = 0.0f;
|
||||
float ActualCos = 0.0f;
|
||||
VoxelMath::DetSinCos(ActualSin, ActualCos, Sample.Input);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("DetSinCos input %.9g -> sin=0x%08X cos=0x%08X"),
|
||||
Sample.Input, FloatBits(ActualSin), FloatBits(ActualCos)));
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("sin bits for %.9g"), Sample.Input),
|
||||
FloatBits(ActualSin), Sample.SinBits);
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("cos bits for %.9g"), Sample.Input),
|
||||
FloatBits(ActualCos), Sample.CosBits);
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("DetSin agrees with paired sin for %.9g"), Sample.Input),
|
||||
FloatBits(VoxelMath::DetSin(Sample.Input)), FloatBits(ActualSin));
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("DetCos agrees with paired cos for %.9g"), Sample.Input),
|
||||
FloatBits(VoxelMath::DetCos(Sample.Input)), FloatBits(ActualCos));
|
||||
}
|
||||
|
||||
// The canonical export measured |argument| <= 6.19255877. Sweep a small margin around that
|
||||
// envelope; the large corpus above separately exercises the double range reduction.
|
||||
constexpr float MeasuredWorldArgumentMax = 6.25f;
|
||||
constexpr int32 SweepSamples = 200001;
|
||||
float MaxSinError = 0.0f;
|
||||
float MaxCosError = 0.0f;
|
||||
for (int32 Index = 0; Index < SweepSamples; ++Index)
|
||||
{
|
||||
const float Alpha = -MeasuredWorldArgumentMax
|
||||
+ (2.0f * MeasuredWorldArgumentMax)
|
||||
* static_cast<float>(Index) / static_cast<float>(SweepSamples - 1);
|
||||
float ActualSin = 0.0f;
|
||||
float ActualCos = 0.0f;
|
||||
VoxelMath::DetSinCos(ActualSin, ActualCos, Alpha);
|
||||
MaxSinError = FMath::Max(MaxSinError, FMath::Abs(ActualSin - ::sinf(Alpha)));
|
||||
MaxCosError = FMath::Max(MaxCosError, FMath::Abs(ActualCos - ::cosf(Alpha)));
|
||||
}
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("DetSinCos max abs error vs sinf/cosf over [-%.1f,+%.1f], %d samples: "
|
||||
"sin %.9g, cos %.9g"),
|
||||
MeasuredWorldArgumentMax, MeasuredWorldArgumentMax, SweepSamples,
|
||||
MaxSinError, MaxCosError));
|
||||
TestTrue(TEXT("DetSin max error over measured world range <= 2e-6"), MaxSinError <= 2.0e-6f);
|
||||
TestTrue(TEXT("DetCos max error over measured world range <= 2e-6"), MaxCosError <= 2.0e-6f);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -963,8 +963,10 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
for (int32 RingSample = 0; RingSample < RingSampleCount; ++RingSample)
|
||||
{
|
||||
const float Angle = 2.0f * PI * (float)RingSample / (float)RingSampleCount;
|
||||
const float SampleX = Passage.LowerPoint.X + FMath::Cos(Angle) * RingRadius;
|
||||
const float SampleY = Passage.LowerPoint.Y + FMath::Sin(Angle) * RingRadius;
|
||||
float SinAngle = 0.0f, CosAngle = 0.0f;
|
||||
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
|
||||
const float SampleX = Passage.LowerPoint.X + CosAngle * RingRadius;
|
||||
const float SampleY = Passage.LowerPoint.Y + SinAngle * RingRadius;
|
||||
const float Density = World.Generator->GetDensityAt(
|
||||
SampleX,
|
||||
SampleY,
|
||||
@@ -1115,7 +1117,7 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
|
||||
LandingGradient = MaxGradient;
|
||||
WorstLandingFloorGradient = FMath::Max(WorstLandingFloorGradient, MaxGradient);
|
||||
const float NormalZ = 1.0f / FMath::Sqrt(1.0f + MaxGradient * MaxGradient);
|
||||
bFloorPassed = NormalZ + KINDA_SMALL_NUMBER >= FMath::Cos(
|
||||
bFloorPassed = NormalZ + KINDA_SMALL_NUMBER >= VoxelMath::DetCos(
|
||||
FMath::DegreesToRadians(LandingWalkableAngleDegrees));
|
||||
}
|
||||
|
||||
|
||||
@@ -990,7 +990,7 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
|
||||
return false;
|
||||
}
|
||||
|
||||
const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians(
|
||||
const float MinimumWalkableNormalZ = VoxelMath::DetCos(FMath::DegreesToRadians(
|
||||
CharacterFitSettings.PlayerWalkableFloorAngleDegrees));
|
||||
const int32 SupportFootprintColumns = VF_CountSupportFootprintColumns(
|
||||
CharacterFitSettings.PlayerCapsuleRadiusVoxels);
|
||||
|
||||
@@ -2928,7 +2928,7 @@ namespace
|
||||
const int32 MaxDownwardSearchCells = FMath::CeilToInt(
|
||||
static_cast<float>(MaxStepHeightVoxelsReal));
|
||||
const float MaxStepHeightVoxels = static_cast<float>(MaxStepHeightVoxelsReal);
|
||||
const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians(
|
||||
const float MinimumWalkableNormalZ = VoxelMath::DetCos(FMath::DegreesToRadians(
|
||||
Settings.PlayerWalkableFloorAngleDegrees));
|
||||
|
||||
TArray<float> SupportHeights;
|
||||
@@ -3263,7 +3263,9 @@ namespace
|
||||
const float DirectionAngle = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(Site.Hash ^ 0xCAFEBABEu)) * 2.0f * PI;
|
||||
const float StretchDistance = Site.RadiusXY * 0.7f;
|
||||
const FVector Direction(FMath::Cos(DirectionAngle), FMath::Sin(DirectionAngle), 0.0f);
|
||||
float SinDirection = 0.0f, CosDirection = 0.0f;
|
||||
VoxelMath::DetSinCos(SinDirection, CosDirection, DirectionAngle);
|
||||
const FVector Direction(CosDirection, SinDirection, 0.0f);
|
||||
RoomSDF = VoxelSDF::Capsule(
|
||||
Position,
|
||||
Site.Center + Direction * StretchDistance,
|
||||
@@ -6823,7 +6825,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
{
|
||||
const float T = static_cast<float>(ControlIndex)
|
||||
/ static_cast<float>(WanderSegments);
|
||||
const float Envelope = FMath::Sin(T * PI);
|
||||
const float Envelope = VoxelMath::DetSin(T * PI);
|
||||
FVector WorldControlPoint = FMath::Lerp(WorldEndA, WorldEndB, T);
|
||||
const float RadiusBase = FMath::Lerp(RadA, RadB, T);
|
||||
float ControlRadius = RadiusBase;
|
||||
@@ -6840,7 +6842,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
// serpentine route without moving either mouth.
|
||||
const float Side = RawSide * 0.65f + PreviousSide * 0.35f;
|
||||
const float WindWave = bNeedsWind
|
||||
? FMath::Sin(static_cast<float>(WindWaves) * PI * T)
|
||||
? VoxelMath::DetSin(static_cast<float>(WindWaves) * PI * T)
|
||||
* WindAmplitude * Envelope
|
||||
* ((LedgeChoiceHash & 1u) != 0u ? 1.0f : -1.0f)
|
||||
: 0.0f;
|
||||
@@ -7364,7 +7366,9 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
CR.ShapeType = 2;
|
||||
const float DirAngle = VoxelHash::ToFloat01(VoxelHash::Mix(CR.Hash ^ 0xCAFEBABEu)) * 2.0f * PI;
|
||||
const float StretchDist = CR.RadiusXY * 0.7f;
|
||||
const FVector Dir(FMath::Cos(DirAngle), FMath::Sin(DirAngle), 0.0f);
|
||||
float SinDir = 0.0f, CosDir = 0.0f;
|
||||
VoxelMath::DetSinCos(SinDir, CosDir, DirAngle);
|
||||
const FVector Dir(CosDir, SinDir, 0.0f);
|
||||
CR.ShapeA = CR.Center + Dir * StretchDist;
|
||||
CR.ShapeB = CR.Center - Dir * StretchDist;
|
||||
CR.ShapeR = FMath::Min(CR.RadiusXY * 0.6f, CR.RadiusZ);
|
||||
@@ -7484,8 +7488,8 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
const float Angle = VoxelHash::ToFloat01(AH4) * PI;
|
||||
const float HalfSpan = CR.RadiusXY
|
||||
* (0.5f + VoxelHash::ToFloat01(VoxelHash::Mix(AH4)) * 0.35f);
|
||||
const float CosA = FMath::Cos(Angle);
|
||||
const float SinA = FMath::Sin(Angle);
|
||||
float SinA = 0.0f, CosA = 0.0f;
|
||||
VoxelMath::DetSinCos(SinA, CosA, Angle);
|
||||
FCachedArch& Arch = CR.Arches[i];
|
||||
Arch.bActive = true;
|
||||
Arch.EndpointA = FVector(ArcCX - CosA * HalfSpan,
|
||||
@@ -7517,8 +7521,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
Pinch.CenterX = PnX;
|
||||
Pinch.CenterY = PnY;
|
||||
Pinch.CenterZ = PnZ;
|
||||
Pinch.CosAngle = FMath::Cos(PnAngle);
|
||||
Pinch.SinAngle = FMath::Sin(PnAngle);
|
||||
VoxelMath::DetSinCos(Pinch.SinAngle, Pinch.CosAngle, PnAngle);
|
||||
Pinch.MaxExtent = FMath::Max(
|
||||
FeatureParams.PinchLength, FeatureParams.PinchStrength) + 5.0f;
|
||||
Pinch.HalfLength = FeatureParams.PinchLength * 0.5f;
|
||||
|
||||
@@ -501,9 +501,9 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
for (int32 e = 0; e < Entries.Num(); ++e)
|
||||
{
|
||||
CosMaxSlope[e] = (Entries[e].Profile.MaxSlopeAngle < 89.99f)
|
||||
? FMath::Cos(FMath::DegreesToRadians(Entries[e].Profile.MaxSlopeAngle)) : -1.0f;
|
||||
? VoxelMath::DetCos(FMath::DegreesToRadians(Entries[e].Profile.MaxSlopeAngle)) : -1.0f;
|
||||
CosMinSlope[e] = (Entries[e].Profile.MinSlopeAngle > 0.01f)
|
||||
? FMath::Cos(FMath::DegreesToRadians(Entries[e].Profile.MinSlopeAngle)) : -1.0f;
|
||||
? VoxelMath::DetCos(FMath::DegreesToRadians(Entries[e].Profile.MinSlopeAngle)) : -1.0f;
|
||||
}
|
||||
|
||||
// Per-COLUMN biome cache: ResolveBiomeSampleAt's noise-heavy cell classification is box-validated
|
||||
@@ -647,8 +647,10 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const float Ang = VoxelHash::ToFloat01(IH) * 2.0f * PI;
|
||||
const float RadVox = FMath::Lerp(Comp.RadiusMinVox, Comp.RadiusMaxVox,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(IH ^ 0x77u)));
|
||||
const float SatVX = VX + FMath::Cos(Ang) * RadVox; // voxel, actor-local
|
||||
const float SatVY = VY + FMath::Sin(Ang) * RadVox;
|
||||
float SinAng = 0.0f, CosAng = 0.0f;
|
||||
VoxelMath::DetSinCos(SinAng, CosAng, Ang);
|
||||
const float SatVX = VX + CosAng * RadVox; // voxel, actor-local
|
||||
const float SatVY = VY + SinAng * RadVox;
|
||||
|
||||
// Optional per-satellite gating (relief/moisture/biome-border at ITS own XY).
|
||||
if (Comp.Profile.Conditions.Num() > 0 &&
|
||||
@@ -693,8 +695,10 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr
|
||||
const float SAng = VoxelHash::ToFloat01(JH) * 2.0f * PI;
|
||||
const float SRadVox = FMath::Lerp(Sub.RadiusMinVox, Sub.RadiusMaxVox,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(JH ^ 0x77u)));
|
||||
const float SubVX = SatVX + FMath::Cos(SAng) * SRadVox;
|
||||
const float SubVY = SatVY + FMath::Sin(SAng) * SRadVox;
|
||||
float SinSAng = 0.0f, CosSAng = 0.0f;
|
||||
VoxelMath::DetSinCos(SinSAng, CosSAng, SAng);
|
||||
const float SubVX = SatVX + CosSAng * SRadVox;
|
||||
const float SubVY = SatVY + SinSAng * SRadVox;
|
||||
|
||||
if (Sub.Profile.Conditions.Num() > 0 &&
|
||||
!Gen->EvaluateTerrainConditions(Sub.Profile.Conditions, SubVX, SubVY, Ctx.BiomeCtx)) continue;
|
||||
@@ -1230,9 +1234,9 @@ bool UVoxelContentManager::SpawnFromProfile(const FPlacementProfile& P, uint32 H
|
||||
|
||||
// Surface-tilt gates (acos(|N.Z|); guarded so defaults cost no trig).
|
||||
if (P.MaxSlopeAngle < 89.99f &&
|
||||
FMath::Abs(N.Z) < FMath::Cos(FMath::DegreesToRadians(P.MaxSlopeAngle))) return false;
|
||||
FMath::Abs(N.Z) < VoxelMath::DetCos(FMath::DegreesToRadians(P.MaxSlopeAngle))) return false;
|
||||
if (P.MinSlopeAngle > 0.01f &&
|
||||
FMath::Abs(N.Z) > FMath::Cos(FMath::DegreesToRadians(P.MinSlopeAngle))) return false;
|
||||
FMath::Abs(N.Z) > VoxelMath::DetCos(FMath::DegreesToRadians(P.MinSlopeAngle))) return false;
|
||||
|
||||
const FVector LocalPos(LocalX, LocalY, ZC * VOXEL_SIZE);
|
||||
if (P.bRequireWaterRelative && Ctx.bHasWater)
|
||||
|
||||
@@ -7504,7 +7504,7 @@ namespace
|
||||
if (!(DistFromSurface < LineRange)) { return; }
|
||||
|
||||
const float LinePhase = WorldZ * (2.0f * PI) / LP.LayerLineSpacing;
|
||||
float LineValue = FMath::Sin(LinePhase);
|
||||
float LineValue = VoxelMath::DetSin(LinePhase);
|
||||
|
||||
LineValue = FMath::Max(LineValue, 0.0f);
|
||||
LineValue = LineValue * LineValue * LineValue; // affûtage cubique
|
||||
@@ -7606,7 +7606,7 @@ namespace
|
||||
if (!(DistFromSurface < RibRange)) { return; }
|
||||
|
||||
const float RibPhase = WorldZ * (2.0f * PI) / LP.RibbingSpacing + PI * 0.5f;
|
||||
float RibValue = FMath::Sin(RibPhase);
|
||||
float RibValue = VoxelMath::DetSin(RibPhase);
|
||||
|
||||
RibValue = FMath::Max(RibValue, 0.0f);
|
||||
RibValue = RibValue * RibValue; // profil de bosse arrondi
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
|
||||
#include "VoxelForgeModule.h"
|
||||
#include "VoxelStackSampler.h"
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Parse.h"
|
||||
|
||||
#if PLATFORM_WINDOWS
|
||||
#include <math.h>
|
||||
#endif
|
||||
|
||||
// This macro registers our module with Unreal
|
||||
// "VoxelForge" must match the module name in VoxelForge.Build.cs and .uplugin
|
||||
@@ -11,7 +19,30 @@ IMPLEMENT_MODULE(FVoxelForgeModule, VoxelForge)
|
||||
void FVoxelForgeModule::StartupModule()
|
||||
{
|
||||
// Called when plugin loads
|
||||
// We don't need to do anything here for now
|
||||
// Keep this diagnostic before any plugin-owned generation can begin. The UCRT may select
|
||||
// its FMA3 math implementations during process startup; this switch lets the canonical export
|
||||
// compare that path with the non-FMA3 implementation on the same machine.
|
||||
#if PLATFORM_WINDOWS
|
||||
const int32 Fma3Before = _get_FMA3_enable();
|
||||
int32 RequestedFma3 = 0;
|
||||
const bool bHasFma3Override = FParse::Value(
|
||||
FCommandLine::Get(), TEXT("voxel.CrtFma3="), RequestedFma3);
|
||||
if (bHasFma3Override)
|
||||
{
|
||||
_set_FMA3_enable(RequestedFma3 != 0 ? 1 : 0);
|
||||
}
|
||||
const int32 Fma3After = _get_FMA3_enable();
|
||||
UE_LOG(LogTemp, Log,
|
||||
TEXT("VoxelForge CRT FMA3 diagnostic: before=%d override=%s requested=%d after=%d"),
|
||||
Fma3Before,
|
||||
bHasFma3Override ? TEXT("yes") : TEXT("no"),
|
||||
RequestedFma3,
|
||||
Fma3After);
|
||||
#endif
|
||||
// These parsers used to be entered lazily by worker-side density/mesher calls. Resolve the
|
||||
// command line once while the module is starting, before any generation task is submitted.
|
||||
VoxelGenLOD::InitializeConsoleSwitches();
|
||||
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
||||
UE_LOG(LogTemp, Log, TEXT("VoxelForge module started!"));
|
||||
}
|
||||
|
||||
|
||||
@@ -857,9 +857,18 @@ void VoxelGenLOD::SetThreadSampleStep(int32 Value)
|
||||
SampleStep = Value;
|
||||
}
|
||||
|
||||
void VoxelGenLOD::InitializeConsoleSwitches()
|
||||
{
|
||||
VF_ParseFastIsFiniteSwitch();
|
||||
VF_ParseFusedEvaluatorSwitch();
|
||||
VF_ParseWormBlockSkipSwitch();
|
||||
VF_ParseTileCacheWindowSwitch();
|
||||
VF_ParseSpatialIndexSwitch();
|
||||
VF_ParseChunkRoutingCacheSwitch();
|
||||
}
|
||||
|
||||
bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath)
|
||||
{
|
||||
VF_ParseTileCacheWindowSwitch();
|
||||
if (GVoxelForgeTileCacheWindow == 0)
|
||||
{
|
||||
return false;
|
||||
@@ -900,7 +909,6 @@ void VoxelGenLOD::SetThreadTileContext(
|
||||
|
||||
bool VoxelGenLOD::ShouldUseSpatialIndex(bool bFusedPath)
|
||||
{
|
||||
VF_ParseSpatialIndexSwitch();
|
||||
if (GVoxelForgeSpatialIndex >= 0)
|
||||
{
|
||||
return GVoxelForgeSpatialIndex != 0;
|
||||
@@ -1126,7 +1134,9 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
||||
const float zc = FMath::Lerp(InnerBot + 8.0f, InnerTop - 8.0f,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0xB1u)));
|
||||
const float ang = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0xB2u)) * PI;
|
||||
const float dxu = FMath::Cos(ang), dyu = FMath::Sin(ang);
|
||||
float sinAng = 0.0f, cosAng = 0.0f;
|
||||
VoxelMath::DetSinCos(sinAng, cosAng, ang);
|
||||
const float dxu = cosAng, dyu = sinAng;
|
||||
const float bx = (nx + 0.5f) * Sp, by = (ny + 0.5f) * Sp;
|
||||
const float half = Sp * 0.6f;
|
||||
Bridges.Add({ FVector(bx - dxu * half, by - dyu * half, zc),
|
||||
@@ -1175,7 +1185,9 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
||||
const uint32 h = VoxelHash::Cell(nx, ny, Seed ^ 0x52470001u);
|
||||
if (VoxelHash::ToFloat01(h) > D.RidgeDensity) continue;
|
||||
const float ang = VoxelHash::ToFloat01(VoxelHash::Mix(h ^ 0x9001u)) * PI;
|
||||
const float dxu = FMath::Cos(ang), dyu = FMath::Sin(ang);
|
||||
float sinAng = 0.0f, cosAng = 0.0f;
|
||||
VoxelMath::DetSinCos(sinAng, cosAng, ang);
|
||||
const float dxu = cosAng, dyu = sinAng;
|
||||
const float bx = (nx + 0.5f) * Sp, by = (ny + 0.5f) * Sp;
|
||||
const float half = Sp * 0.45f;
|
||||
Ridges.Add({ bx - dxu * half, by - dyu * half, bx + dxu * half, by + dyu * half });
|
||||
@@ -1374,7 +1386,9 @@ static bool VF_AnyBridgeCanTouchLattice(
|
||||
InnerBot + 8.0f, InnerTop - 8.0f,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u)));
|
||||
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB2u)) * PI;
|
||||
const float DXU = FMath::Cos(Angle), DYU = FMath::Sin(Angle);
|
||||
float SinAngle = 0.0f, CosAngle = 0.0f;
|
||||
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
|
||||
const float DXU = CosAngle, DYU = SinAngle;
|
||||
const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
|
||||
const float Half = Spacing * 0.6f;
|
||||
const float HorizontalSq = VF_DistanceSquaredToXYSegment(
|
||||
@@ -1453,7 +1467,9 @@ static bool VF_AnyRidgeCanTouchLattice(
|
||||
continue;
|
||||
}
|
||||
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9001u)) * PI;
|
||||
const float DXU = FMath::Cos(Angle), DYU = FMath::Sin(Angle);
|
||||
float SinAngle = 0.0f, CosAngle = 0.0f;
|
||||
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
|
||||
const float DXU = CosAngle, DYU = SinAngle;
|
||||
const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
|
||||
const float Half = Spacing * 0.45f;
|
||||
if (VF_DistanceSquaredToXYSegment(
|
||||
@@ -1554,7 +1570,9 @@ static bool VF_AnyBridgeCanTouchBox(
|
||||
InnerBot + 8.0f, InnerTop - 8.0f,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB1u)));
|
||||
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xB2u)) * PI;
|
||||
const float DX = FMath::Cos(Angle), DY = FMath::Sin(Angle);
|
||||
float SinAngle = 0.0f, CosAngle = 0.0f;
|
||||
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
|
||||
const float DX = CosAngle, DY = SinAngle;
|
||||
const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
|
||||
const float Half = Spacing * 0.6f;
|
||||
const FVector A(BX - DX * Half, BY - DY * Half, ZC);
|
||||
@@ -1609,7 +1627,9 @@ static bool VF_AnyRidgeCanTouchBox(
|
||||
continue;
|
||||
}
|
||||
const float Angle = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9001u)) * PI;
|
||||
const float DX = FMath::Cos(Angle), DY = FMath::Sin(Angle);
|
||||
float SinAngle = 0.0f, CosAngle = 0.0f;
|
||||
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
|
||||
const float DX = CosAngle, DY = SinAngle;
|
||||
const float BX = (NX + 0.5f) * Spacing, BY = (NY + 0.5f) * Spacing;
|
||||
const float Half = Spacing * 0.45f;
|
||||
const FVector A(BX - DX * Half, BY - DY * Half, InnerBot);
|
||||
@@ -2776,7 +2796,6 @@ UVoxelGenerator::UVoxelGenerator()
|
||||
|
||||
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
||||
{
|
||||
VF_ParseFastIsFiniteSwitch();
|
||||
// Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas
|
||||
// déréférencer l'asset de settings.
|
||||
Seed = Settings ? Settings->GetEffectiveWorldSeed() : 0;
|
||||
@@ -2817,10 +2836,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
VoxelGenLOD::bTilePostReachBypass = PreviousBypass;
|
||||
}
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt);
|
||||
VF_ParseFastIsFiniteSwitch();
|
||||
VF_ParseFusedEvaluatorSwitch();
|
||||
VF_ParseWormBlockSkipSwitch();
|
||||
VF_ParseChunkRoutingCacheSwitch();
|
||||
VoxelDensityProfile::FScopedTimer DensityProfileTimer(
|
||||
VoxelDensityProfile::EBucket::GetDensityAt);
|
||||
VoxelDensityProfile::FScopedTimer DensityPrologueTimer(
|
||||
@@ -3164,9 +3179,10 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
CP_Tunnel = StrateManager->GetGenerationParams(ChunkCoord);
|
||||
// AUDIT §C2 — l'empreinte est calculée ICI, une fois par chunk, au seul endroit où
|
||||
// les params changent. `FStrateGenerationParams` est du POD pur (aucun TArray /
|
||||
// FString / pointeur), donc une CRC mémoire ne peut pas donner de FAUX POSITIF ; au
|
||||
// pire un octet de padding donne un faux MANQUE, c'est-à-dire une reconstruction de
|
||||
// cache. On se trompe du côté du CPU, jamais du côté d'une salle fausse.
|
||||
// FString / pointeur), donc une CRC mémoire peut donner un FAUX POSITIF par
|
||||
// collision CRC32 ; au pire un octet de padding donne aussi un faux MANQUE, c'est-
|
||||
// à-dire une reconstruction de cache. Le risque pratique est une collision, jamais
|
||||
// une salle fausse créée par la seule différence de padding.
|
||||
CP_TunnelFP = FCrc::MemCrc32(&CP_Tunnel, sizeof(CP_Tunnel));
|
||||
break;
|
||||
}
|
||||
@@ -4705,7 +4721,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
{
|
||||
// Sine wave along Z: peaks at each line position
|
||||
float LinePhase = WorldZ * (2.0f * PI) / Params.LayerLineSpacing;
|
||||
float LineValue = FMath::Sin(LinePhase);
|
||||
float LineValue = VoxelMath::DetSin(LinePhase);
|
||||
|
||||
// Sharpen to thin grooves: only carve where sine > 0, then cube it.
|
||||
// sin → max(sin, 0) → pow(_, 3) turns broad sine humps into thin spikes
|
||||
@@ -4735,7 +4751,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
||||
{
|
||||
// Sine wave along Z, offset by half-period from layer lines
|
||||
float RibPhase = WorldZ * (2.0f * PI) / Params.RibbingSpacing + PI * 0.5f;
|
||||
float RibValue = FMath::Sin(RibPhase);
|
||||
float RibValue = VoxelMath::DetSin(RibPhase);
|
||||
|
||||
// Half-wave rectify (only positive → ribs, not grooves) then smooth
|
||||
RibValue = FMath::Max(RibValue, 0.0f);
|
||||
@@ -5720,7 +5736,7 @@ float UVoxelGenerator::ComputeSurfaceTerrainZ(float WorldX, float WorldY,
|
||||
if (Params.LayerLineDepth > 0.0f && Params.LayerLineSpacing > 0.0f)
|
||||
{
|
||||
const float Phase = Terrain * (2.0f * PI / Params.LayerLineSpacing);
|
||||
Terrain -= FMath::Sin(Phase) * Params.LayerLineDepth;
|
||||
Terrain -= VoxelMath::DetSin(Phase) * Params.LayerLineDepth;
|
||||
}
|
||||
|
||||
// Beach: flatten terrain toward the water line within BeachWidth. (Water level is
|
||||
|
||||
@@ -229,7 +229,7 @@ namespace
|
||||
if (P.LayerLineDepth <= 0.0f || P.LayerLineSpacing <= 0.0f) { return; }
|
||||
|
||||
const float Phase = InOut.Height * (2.0f * PI / P.LayerLineSpacing);
|
||||
InOut.Height -= FMath::Sin(Phase) * P.LayerLineDepth;
|
||||
InOut.Height -= VoxelMath::DetSin(Phase) * P.LayerLineDepth;
|
||||
}
|
||||
|
||||
// `sin` ∈ [-1,1] ⇒ borne exacte.
|
||||
|
||||
@@ -82,6 +82,12 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void UVoxelMarchingCubesMesher::InitializeConsoleSwitches()
|
||||
{
|
||||
VF_ParseOperatorBlockSwitch();
|
||||
VF_ParseTilePostReachSwitches();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// MAIN ALGORITHM
|
||||
//=============================================================================
|
||||
@@ -94,8 +100,6 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
int64* OutDensitySampleCount)
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
|
||||
VF_ParseOperatorBlockSwitch();
|
||||
VF_ParseTilePostReachSwitches();
|
||||
FVoxelMeshData MeshData;
|
||||
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
|
||||
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
|
||||
|
||||
@@ -1827,8 +1827,10 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
// surface shaft, while this passage opens a separate room in the strate network.
|
||||
const float Angle = PassageRandom01(PassageSaltAngle) * (2.0f * PI);
|
||||
const float Distance = PassageRandomRange(PlacementLo, PlacementHi, PassageSaltDistance);
|
||||
const float PX = FMath::Cos(Angle) * Distance;
|
||||
const float PY = FMath::Sin(Angle) * Distance;
|
||||
float SinAngle = 0.0f, CosAngle = 0.0f;
|
||||
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
|
||||
const float PX = CosAngle * Distance;
|
||||
const float PY = SinAngle * Distance;
|
||||
|
||||
++TotalPassages;
|
||||
|
||||
@@ -1990,7 +1992,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
Passage.ControlRadii.Reserve(Segments + 1);
|
||||
|
||||
// WIDTH profile: mouth radius at the ends, mid radius in the centre (taper/bulge).
|
||||
auto RadiusAt = [&](float t) { return FMath::Lerp(Cfg.MouthRadius, Cfg.MidRadius, FMath::Sin(t * PI)); };
|
||||
auto RadiusAt = [&](float t) { return FMath::Lerp(Cfg.MouthRadius, Cfg.MidRadius, VoxelMath::DetSin(t * PI)); };
|
||||
|
||||
// Per-passage shape seeds.
|
||||
const float WormFreq = PassageRandomRange(0.8f, 1.8f, PassageSaltWormFreq); // (vertical wobble only)
|
||||
@@ -2005,7 +2007,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
{
|
||||
const float T = (float)s / (float)Segments;
|
||||
float Z = FMath::Lerp(TopZ, BottomZ, T);
|
||||
const float Env = FMath::Sin(T * PI); // 0 at both ends → mouths stay anchored
|
||||
const float Env = VoxelMath::DetSin(T * PI); // 0 at both ends → mouths stay anchored
|
||||
float OX = 0.0f, OY = 0.0f;
|
||||
|
||||
switch (Cfg.Style)
|
||||
@@ -2018,8 +2020,10 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
case EVoxelPassageStyle::Spiral:
|
||||
{
|
||||
const float Ang = PhaseA + T * Cfg.SpiralTurns * 2.0f * PI;
|
||||
OX = FMath::Cos(Ang) * Cfg.SpiralRadius * Env;
|
||||
OY = FMath::Sin(Ang) * Cfg.SpiralRadius * Env;
|
||||
float SinAng = 0.0f, CosAng = 0.0f;
|
||||
VoxelMath::DetSinCos(SinAng, CosAng, Ang);
|
||||
OX = CosAng * Cfg.SpiralRadius * Env;
|
||||
OY = SinAng * Cfg.SpiralRadius * Env;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2029,8 +2033,10 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
const int32 Steps = FMath::Clamp(Cfg.CascadeSteps, 1, 16);
|
||||
const int32 Idx = FMath::Min((int32)(T * Steps), Steps - 1);
|
||||
const float SA = PhaseA + (float)Idx * 2.39996f; // golden-angle spread
|
||||
OX = FMath::Cos(SA) * Cfg.CascadeLedge * Env;
|
||||
OY = FMath::Sin(SA) * Cfg.CascadeLedge * Env;
|
||||
float SinSA = 0.0f, CosSA = 0.0f;
|
||||
VoxelMath::DetSinCos(SinSA, CosSA, SA);
|
||||
OX = CosSA * Cfg.CascadeLedge * Env;
|
||||
OY = SinSA * Cfg.CascadeLedge * Env;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2042,7 +2048,7 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
// 2D organic wander — it curls and meanders "here and there" rather than
|
||||
// oscillating along one line (zig-zag) or orbiting the axis (spiral).
|
||||
// Flat-top envelope keeps full motion along the length but anchors the mouths.
|
||||
const float WormEnv = FMath::Clamp(FMath::Sin(T * PI) * 3.0f, 0.0f, 1.0f);
|
||||
const float WormEnv = FMath::Clamp(VoxelMath::DetSin(T * PI) * 3.0f, 0.0f, 1.0f);
|
||||
OX = PassageFBM(T * BendFreq, NSeedX) * VOXEL_NOISE_SCALE * Cfg.Wander * WormEnv;
|
||||
OY = PassageFBM(T * BendFreq, NSeedY + 53.0f) * VOXEL_NOISE_SCALE * Cfg.Wander * WormEnv;
|
||||
break;
|
||||
@@ -2084,7 +2090,9 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
FVector Direction(Delta.X, Delta.Y, 0.0f);
|
||||
if (!Direction.Normalize())
|
||||
{
|
||||
Direction = FVector(FMath::Cos(Angle), FMath::Sin(Angle), 0.0f);
|
||||
float SinAngle = 0.0f, CosAngle = 0.0f;
|
||||
VoxelMath::DetSinCos(SinAngle, CosAngle, Angle);
|
||||
Direction = FVector(CosAngle, SinAngle, 0.0f);
|
||||
if (!Direction.Normalize())
|
||||
{
|
||||
Direction = FVector(1.0f, 0.0f, 0.0f);
|
||||
@@ -2138,8 +2146,9 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
// This is deliberately a construction rule, not a seed-dependent observation.
|
||||
const float SwitchbackAngle =
|
||||
PassageRandom01(PassageSaltUpperDoor) * 2.0f * PI;
|
||||
FVector2D SwitchbackDirection(
|
||||
FMath::Cos(SwitchbackAngle), FMath::Sin(SwitchbackAngle));
|
||||
float SinSwitchback = 0.0f, CosSwitchback = 0.0f;
|
||||
VoxelMath::DetSinCos(SinSwitchback, CosSwitchback, SwitchbackAngle);
|
||||
FVector2D SwitchbackDirection(CosSwitchback, SinSwitchback);
|
||||
if (!SwitchbackDirection.Normalize())
|
||||
{
|
||||
SwitchbackDirection = FVector2D(1.0f, 0.0f);
|
||||
|
||||
@@ -1405,7 +1405,7 @@ namespace VoxelStrateMeasurePrivate
|
||||
const int32 MaxDownwardSearchCells = static_cast<int32>(
|
||||
FMath::CeilToDouble(MaxStepHeightVoxelsReal));
|
||||
const float MaxStepHeightVoxels = static_cast<float>(MaxStepHeightVoxelsReal);
|
||||
const float MinimumWalkableNormalZ = FMath::Cos(FMath::DegreesToRadians(
|
||||
const float MinimumWalkableNormalZ = VoxelMath::DetCos(FMath::DegreesToRadians(
|
||||
Settings.PlayerWalkableFloorAngleDegrees));
|
||||
|
||||
TArray<FIntPoint> SupportOffsets;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "VoxelTypes.h"
|
||||
#include "VoxelStrateTypes.h"
|
||||
#include "VoxelBiomeTypes.h"
|
||||
@@ -73,6 +74,8 @@ namespace VoxelGenLOD
|
||||
VOXELFORGE_API void SetThreadOctaveBias(int32 Value);
|
||||
VOXELFORGE_API int32 GetThreadSampleStep();
|
||||
VOXELFORGE_API void SetThreadSampleStep(int32 Value);
|
||||
// Called by the module's game-thread startup before any worker can enter GetDensityAt.
|
||||
VOXELFORGE_API void InitializeConsoleSwitches();
|
||||
VOXELFORGE_API bool IsTileCacheWindowEnabled(bool bFusedPath = true);
|
||||
VOXELFORGE_API bool GetThreadTileCacheWindow(
|
||||
FIntVector& OutOriginVoxels, int32& OutStep, int32& OutCellsPerAxis);
|
||||
|
||||
@@ -44,6 +44,10 @@ class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Parse command-line switches on the module's game-thread startup. GenerateMesh is a worker
|
||||
// entry point and therefore only reads the resulting values.
|
||||
static void InitializeConsoleSwitches();
|
||||
|
||||
/**
|
||||
* Génère le mesh d'une TUILE de clipmap (voir FVoxelTileKey).
|
||||
*
|
||||
|
||||
@@ -51,6 +51,148 @@ namespace VoxelMath
|
||||
{
|
||||
return GFastIsFinite != 0 ? IsFiniteFast(Value) : FMath::IsFinite(Value);
|
||||
}
|
||||
|
||||
// Deterministic trigonometry for world decisions.
|
||||
//
|
||||
// The implementation deliberately owns both the range reduction and the polynomial. It
|
||||
// does not call the CRT, FMath, or a platform vector intrinsic: the same finite float is
|
||||
// reduced by the same constants and evaluated by the same scalar arithmetic on every host.
|
||||
// The reduction is done in double so world-coordinate phases do not lose all low bits before
|
||||
// the period is removed. The polynomial itself is float arithmetic, matching the values
|
||||
// consumed by the density field and keeping the hot path small.
|
||||
FORCEINLINE void DetSinCos(float& OutSin, float& OutCos, float Value)
|
||||
{
|
||||
if (!IsFiniteFast(Value))
|
||||
{
|
||||
// Non-finite values are outside the world contract. Preserve the input rather than
|
||||
// entering an undefined integer conversion during range reduction.
|
||||
OutSin = Value;
|
||||
OutCos = Value;
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr double TwoPi = 6.283185307179586476925286766559;
|
||||
constexpr double HalfPi = 1.57079632679489661923132169164;
|
||||
|
||||
// Round the number of complete periods toward the nearest integer. All measured world
|
||||
// phases are many orders of magnitude below INT64_MAX. For a pathological finite input
|
||||
// whose period count does not fit in int64, use the exact integer significand from the
|
||||
// float representation and reduce it by fixed binary doubling instead of converting an
|
||||
// out-of-range quotient.
|
||||
const double PeriodsReal = static_cast<double>(Value) / TwoPi;
|
||||
constexpr double MaxSafeInt64 = 9223372036854774784.0;
|
||||
double Reduced = 0.0;
|
||||
if (PeriodsReal >= MaxSafeInt64 || PeriodsReal <= -MaxSafeInt64)
|
||||
{
|
||||
uint32 ValueBits = 0;
|
||||
FMemory::Memcpy(&ValueBits, &Value, sizeof(ValueBits));
|
||||
const uint32 MagnitudeBits = ValueBits & 0x7FFFFFFFu;
|
||||
const uint32 ExponentBits = (MagnitudeBits >> 23) & 0xFFu;
|
||||
const uint32 Significand = (MagnitudeBits & 0x007FFFFFu) | 0x00800000u;
|
||||
const int32 BinaryExponent = static_cast<int32>(ExponentBits) - 127 - 23;
|
||||
|
||||
double ReducedInteger = 0.0;
|
||||
for (int32 Bit = 23; Bit >= 0; --Bit)
|
||||
{
|
||||
ReducedInteger *= 2.0;
|
||||
ReducedInteger += static_cast<double>((Significand >> Bit) & 1u);
|
||||
if (ReducedInteger >= TwoPi)
|
||||
{
|
||||
ReducedInteger -= TwoPi;
|
||||
}
|
||||
}
|
||||
for (int32 Shift = 0; Shift < BinaryExponent; ++Shift)
|
||||
{
|
||||
ReducedInteger *= 2.0;
|
||||
if (ReducedInteger >= TwoPi)
|
||||
{
|
||||
ReducedInteger -= TwoPi;
|
||||
}
|
||||
}
|
||||
Reduced = (ValueBits & 0x80000000u) != 0u ? -ReducedInteger : ReducedInteger;
|
||||
}
|
||||
else
|
||||
{
|
||||
int64 Periods = 0;
|
||||
if (PeriodsReal >= 0.0)
|
||||
{
|
||||
Periods = static_cast<int64>(PeriodsReal + 0.5);
|
||||
}
|
||||
else
|
||||
{
|
||||
Periods = static_cast<int64>(PeriodsReal - 0.5);
|
||||
}
|
||||
|
||||
Reduced = static_cast<double>(Value)
|
||||
- static_cast<double>(Periods) * TwoPi;
|
||||
}
|
||||
|
||||
// Reduce the remaining angle to [-pi/4, pi/4]. Both reduction branches above keep
|
||||
// Reduced bounded by one period, so this quadrant conversion is always in int32 range.
|
||||
const double QuadrantReal = Reduced / HalfPi;
|
||||
int32 Quadrant = 0;
|
||||
if (QuadrantReal >= 0.0)
|
||||
{
|
||||
Quadrant = static_cast<int32>(QuadrantReal + 0.5);
|
||||
}
|
||||
else
|
||||
{
|
||||
Quadrant = static_cast<int32>(QuadrantReal - 0.5);
|
||||
}
|
||||
Reduced -= static_cast<double>(Quadrant) * HalfPi;
|
||||
const float Y = static_cast<float>(Reduced);
|
||||
const float Y2 = Y * Y;
|
||||
|
||||
// Fixed minimax polynomials, the same degree as UE's scalar SinCos implementation. The
|
||||
// quarter-period reduction gives them a smaller interval than UE's [-pi/2, pi/2] path.
|
||||
const float SinP = (((((-2.3889859e-08f * Y2 + 2.7525562e-06f) * Y2
|
||||
- 0.00019840874f) * Y2 + 0.0083333310f) * Y2
|
||||
- 0.16666667f) * Y2 + 1.0f) * Y;
|
||||
const float CosP = (((((-2.6051615e-07f * Y2 + 2.4760495e-05f) * Y2
|
||||
- 0.0013888378f) * Y2 + 0.041666638f) * Y2
|
||||
- 0.5f) * Y2 + 1.0f);
|
||||
|
||||
int32 QuadrantMod = Quadrant % 4;
|
||||
if (QuadrantMod < 0)
|
||||
{
|
||||
QuadrantMod += 4;
|
||||
}
|
||||
switch (QuadrantMod)
|
||||
{
|
||||
case 0:
|
||||
OutSin = SinP;
|
||||
OutCos = CosP;
|
||||
break;
|
||||
case 1:
|
||||
OutSin = CosP;
|
||||
OutCos = -SinP;
|
||||
break;
|
||||
case 2:
|
||||
OutSin = -SinP;
|
||||
OutCos = -CosP;
|
||||
break;
|
||||
default:
|
||||
OutSin = -CosP;
|
||||
OutCos = SinP;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
FORCEINLINE float DetSin(float Value)
|
||||
{
|
||||
float SinValue = 0.0f;
|
||||
float CosValue = 0.0f;
|
||||
DetSinCos(SinValue, CosValue, Value);
|
||||
return SinValue;
|
||||
}
|
||||
|
||||
FORCEINLINE float DetCos(float Value)
|
||||
{
|
||||
float SinValue = 0.0f;
|
||||
float CosValue = 0.0f;
|
||||
DetSinCos(SinValue, CosValue, Value);
|
||||
return CosValue;
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
|
||||
Reference in new issue
Block a user