From 16612acba4098b3737df01b2e030c6ab074e9d50 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 5 Oct 2026 02:58:46 +0200 Subject: [PATCH] standalone: canonical AVX2 noise + factored lattice hashes -- whole view 1.40 -> 1.09 s, world unchanged - Octave noise evaluated in AVX2 batches (one canonical path), fBM and ridged share samples, lattice hash products factored; the outputs are preserved: W1/W8 and pre-change hashes equal across 44 fields, six fixed captures pixel-identical, remote views differ by at most one pixel. - Routes 5642/5642, shape and far-survival audits pass, bad-shell control still fails. Verified by me, interleaved head to head (8 workers, cold, BelowNormal 0xFF00): 1.092 / 1.098 s vs 1.408 / 1.396 s for round 7. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- .../OwnerShapeWorld/speed_round/REPORT.md | 63 ++++ Standalone/Source/StandaloneShapeWorld.h | 93 +++-- .../Private/VoxelDensityOpStack.cpp | 63 ++-- .../VoxelForge/Private/VoxelGenerator.cpp | 66 ++-- .../Source/VoxelForge/Public/VoxelNoise.h | 322 ++++++++++++++---- 5 files changed, 459 insertions(+), 148 deletions(-) diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md b/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md index a2c9011..b4877b1 100644 --- a/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md +++ b/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md @@ -1,5 +1,68 @@ # Shape world speed round — final half-load verification +## Round 8 — canonical AVX2 shape arithmetic (2026-10-05) + +The AVX2 path plus factored corner hashing cuts the direct cold W8 process mean from **1.456 s to 1.149 s** (21.1%). It misses the requested **under-1-second** target by 149 ms at the mean; the faster new run was 1.146 s. The full 110.90 MiB payload remains enabled, all 2,226 requested tiles were generated, and every direct run recorded zero mesh-cache hits. The field passes the shape and far-survival audits, the 5,642 route samples remain open, all 44 W1/W8 hash fields match, and the six fixed captures are pixel-identical to Round 7. + +### Arithmetic and profile + +`VoxelNoise::Perlin3D_x8` evaluates eight independent samples with AVX2 integer hashing and gradient/interpolation arithmetic. `FractalPair` packs the main and fine octave coordinates into the same batch and shares each sample across fBM and ridged accumulation. Domain-warp channels also use the 8-lane path. The cave detail line reuses its spatial-bin items across consecutive sorted Z samples, and coarse rock-distance lookup remembers the previous bin per thread. The hash keeps its existing three mix rounds. For each Perlin lattice, the three corner-coordinate products are now computed once; adjacent corner products use wrapping integer adds, preserving all hash outputs while removing repeated vector multiplies. A reduced-round hash probe changed the field and did not improve time, so it was discarded. + +This is the standalone harness's canonical path, with no runtime CPU dispatch. The host requires AVX2. Intrinsics issue multiply and add separately; the build uses `/fp:precise` to prevent contraction. The AVX2 kernel uses no CRT transcendental functions. Build settings also include `/arch:AVX2` and `/MP8`. + +| W8 profile measure | Round 7 scalar path | Round 8 final | Change | +| --- | ---: | ---: | ---: | +| Shape stage | 690.9 ms | 478.9 ms | -30.7% | +| Surface detail band | 448.3 ms | 245.1 ms | -45.3% | +| Detail evaluation sample | 792.7 ns | 328.0 ns | -58.6% | +| Owner roughness sample | 572.7 ns | 107.2 ns | -81.3% | +| Detail evaluations | 3,527,283 | 3,527,283 | same workload | + +The profile starts at [Round 8 before](round8_profile_before_w8.stdout.log), passes through [AVX2](round8_profile_after_avx2_w8.stdout.log), [shared noise](round8_profile_after_opt_w8.stdout.log), and [paired octaves](round8_profile_after_pair_w8.stdout.log), and ends at the current [factored-hash profile](round8_hash_factored_profile_w8.stdout.log). The distance sample moved from 128.4 ns to 117.3 ns in the final profile; most of the gain is in owner noise. The hash-factor probe's alternating direct runs averaged 1.196 s for the preceding AVX2 executable and 1.145 s for the factored probe: [base A](round8_hash_factored_base_a_w8.stdout.log), [probe A](round8_hash_factored_probe_a_w8.stdout.log), [base B](round8_hash_factored_base_b_w8.stdout.log), [probe B](round8_hash_factored_probe_b_w8.stdout.log). + +An isolated W8 LOD8 mesher profile processed 306 tiles / 2.10 M samples. Its [tile profile](round8_tile_profile_lod8_w8.stdout.log) attributes 125.8 ms to density-grid construction within a 403 ms serial tile-benchmark run. This diagnostic exercises the base mesher and does not include the streamer's source-aware repair panels, so it is not a replacement for the full-payload direct timing. It confirms density sampling is material; no separate mesher rewrite was retained this round. + +### Required cold direct head-to-head + +Both executables were launched in fresh processes with `--stream-benchmark --shape-world --force-generate --workers=8 --clip-radius=3`. The old executable is `VoxelForgeShapeSurvivalSearch.exe`; the new one is `VoxelForgeShapeRound8Final.exe`. The wrapper held BelowNormal priority and affinity `0xFF00`. Each run generated the full payload with `cache_hits=0` and `payload_alloc=110.90MiB`. + +| Pair | Executable | Process wall | Shape stage | Full clipmap | Cache hits | +| --- | --- | ---: | ---: | ---: | ---: | +| A | Round 7 | 1.453 s | 698.2 ms | 648 ms | 0 / 2,226 | +| A | Round 8 | 1.146 s | 453.5 ms | 585 ms | 0 / 2,226 | +| B | Round 7 | 1.459 s | 698.2 ms | 638 ms | 0 / 2,226 | +| B | Round 8 | 1.152 s | 472.7 ms | 574 ms | 0 / 2,226 | + +Logs: [old A](round8_final_ab_old_a_w8.stdout.log), [new A](round8_final_ab_new_a_w8.stdout.log), [old B](round8_final_ab_old_b_w8.stdout.log), [new B](round8_final_ab_new_b_w8.stdout.log). The new mean is 1.149 s versus 1.456 s old. The matching shape-only profiles with attribution are separate runs and are not included in these wall times. + +### Determinism and audits + +The new executable's `--stream-hash` run at W1 and W8 matched density, mesh, payload, and topology for full detail, whole view, and each LOD0–8: **44/44 fields pass**. The [comparison output](round8_final/hash_compare_run.log) also confirms all 44 final fields equal the preceding AVX2 executable; it is generated by [compare_hash_runs.py](round8_final/compare_hash_runs.py). Full-detail hashes are density `0x67609e3f9aa3a0b0`, mesh `0xd98eb46b92a59917`, payload `0x8f148240ca4a4a8f`, topology `0x954e28f6fd44967f`. Whole-view hashes are density `0xa78b0985b1b8fefd`, mesh `0xb44e0c998152d1f5`, payload `0x1eb6b48127fd8e26`, topology `0x8b2557006b5ce3c8`. Raw runs: [W1](round8_final_hash_w1.stdout.log) and [W8](round8_final_hash_w8.stdout.log). + +The [shape audit](round8_final_shape_audit_w8.stdout.log) passes walkability (**5,642/5,642**), room-floor join, floor slope and profile, tunnel radius, flat surface, grid stair, thin sheet, surface-band family, flat patch, and detail-scale checks. The [far-survival audit](round8_final_far_audit_w8.stdout.log) passes all 7,200 boundary surfaces and 10,976 connectors across 178,351 connector sections: zero missing, holes, or unsampled connectors. The audit also records 47,858 extra mesh ray hits. The injected [bad-shell control](round8_final_bad_shell_w8.stdout.log) removed 512 triangles, detected 16 missing surfaces and 16 holes, and exited 14 as expected. + +### Captures + +All six fixed captures compare pixel-identically with Round 7. The four X=500 m remote views have only small pixel differences. The [comparison log](round8_final/capture_compare.log) compares all ten images; the images are in [round8_final](round8_final/). + +| View | Changed pixels | Mean channel delta | Max channel delta | +| --- | ---: | ---: | ---: | +| Fixed chamber / tunnel / passage / far / floor close-up / tunnel bend | 0 / 1,024,000 each | 0 | 0 | +| Remote yaw 0 | 0 / 1,024,000 | 0.000000 | 0 | +| Remote yaw 90 | 1 / 1,024,000 | 0.000000 | 1 | +| Remote yaw 180 | 0 / 1,024,000 | 0.000000 | 0 | +| Remote yaw 270 | 0 / 1,024,000 | 0.000000 | 0 | + +### Failed and discarded attempts + +- The first AVX2 build invocation returned shell exit 1 because its relative `Tee-Object` destination was resolved after `Build.ps1` changed directories. The compiler had already produced the executable. Retrying with an absolute log path succeeded: [build log](round8_avx2_build.log). The hash-factor probe build succeeded: [probe build](round8_hash_factored_build.log), followed by the final `/MP8` build: [final build log](round8_hashfactored_final_build.log). Builds emit existing C4838 narrowing warnings and a `vswhere.exe`-not-recognized diagnostic; neither stops compilation. No process reported `0xC0000142`. +- The first chamber capture used an absolute output path containing spaces; argument splitting made the executable treat the remainder as its owner-data path, and that run exited 2. Using a Standalone-relative capture path succeeded: [failed invocation stderr](round8_capture_fixed_chamber_w8.stderr.log), [retry](round8_capture_fixed_chamber_retry_w8.stdout.log). The other nine captures succeeded on their first invocation. +- The reduced-round hash experiment built and ran, but changed candidate counts/field output without a timing gain; its source was reverted. Evidence: [probe build](round8_hash_build.log), [profile](round8_profile_after_hash_w8.stdout.log). +- A far-ray prune probe completed but did not improve direct time and was reverted: [build](round8_prune_build.log), [direct probe](round8_prune_direct_probe_w8.stdout.log). +- The bad-shell control is the intentional failing audit described above (exit 14). No other Round 8 benchmark, audit, or capture run failed. + +All build and executable runs were constrained to BelowNormal priority and affinity `0xFF00`; builds used `/MP8`. No viewer that this work did not start was closed. Changes and artifacts are confined to `Standalone\`; no commit, push, or stash was made. + ## Round 7 — sparse far repairs and single shape compile (2026-10-05) Round 7 cut the LOD8 survival build from **276.74 MiB** to **110.90 MiB** of full mesh-payload allocation, a 59.9% reduction from Round 6. The final CPU follow-up keeps that payload and trims about 0.28 seconds from summed worker time and 0.04 seconds from stream wall. The result remains **18.29 MiB above** the no-shell 92.61 MiB baseline, and the process-wall target below 1.3 seconds was not reached. diff --git a/Standalone/Source/StandaloneShapeWorld.h b/Standalone/Source/StandaloneShapeWorld.h index c7ae585..55ebf21 100644 --- a/Standalone/Source/StandaloneShapeWorld.h +++ b/Standalone/Source/StandaloneShapeWorld.h @@ -2500,6 +2500,10 @@ private: const int32 Y=Min[1]+static_cast(Line/Size[0]); FDetailNoiseLineContext NoiseLine(static_cast(X),static_cast(Y), static_cast(Owner->Seed)); + const int32 QueryBinX=FloorDiv(X,ActiveSpatialBinSize); + const int32 QueryBinY=FloorDiv(Y,ActiveSpatialBinSize); + int32 CachedQueryBinZ=std::numeric_limits::min(); + const FBinItems* CachedQueryBinItems=nullptr; FDetailWorkerProfile& Profile=WorkerProfile[static_cast(Worker)]; if(bProfile)++Profile.Lines; uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0; @@ -2514,12 +2518,20 @@ private: const size_t Zi=W*64+Bit;if(Zi>=static_cast(Size[2]))continue; const int32 Z=Min[2]+static_cast(Zi); ++LocalCandidates; + const int32 QueryBinZ=FloorDiv(Z,ActiveSpatialBinSize); + if(QueryBinZ!=CachedQueryBinZ) + { + const auto Bin=SpatialBins.find({QueryBinX,QueryBinY,QueryBinZ}); + CachedQueryBinItems=Bin==SpatialBins.end()?nullptr:&Bin->second; + CachedQueryBinZ=QueryBinZ; + } const bool bTimeDistance=bProfile&&((Profile.Candidates++&255u)==0); const auto DistanceStart=bTimeDistance?Clock::now():Clock::time_point{}; ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; const float SDF=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF, - &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir); + &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir, + CachedQueryBinItems,true); if(bTimeDistance) { Profile.DistanceSampleNanoseconds+=static_cast( @@ -2768,17 +2780,24 @@ private: bool* OutFloorSurface=nullptr, float* OutCaveInfluence=nullptr, float* OutFloorInfluence=nullptr, - bool* OutCriticalAir=nullptr) const + bool* OutCriticalAir=nullptr, + const FBinItems* KnownBinItems=nullptr, + bool bKnownBin=false) const { const float Base=StrataDistance(Z); float Cave=100000.0f; float Overlay=-100000.0f; float CaveFloorInfluence=0.0f; bool bCriticalAir=false; - const auto Bin=SpatialBins.find(PointBin(X,Y,Z)); - if(Bin!=SpatialBins.end()) + const FBinItems* BinItems=KnownBinItems; + if(!bKnownBin) { - for(uint32 Id:Bin->second) + const auto Bin=SpatialBins.find(PointBin(X,Y,Z)); + if(Bin!=SpatialBins.end())BinItems=&Bin->second; + } + if(BinItems) + { + for(uint32 Id:*BinItems) { const FPrimitive& P=Primitives[Id]; if(P.bCarve) @@ -2839,10 +2858,26 @@ private: const float Base=StrataDistance(Z); float Cave=100000.0f,Overlay=-100000.0f; bool bCriticalAir=false; - const auto Bin=SpatialBins.find(PointBin(X,Y,Z)); - if(Bin!=SpatialBins.end()) + struct FCoarseBinCache { - for(uint32 Id:Bin->second) + const FStandaloneShapeWorld* Owner=nullptr; + FBinKey Key{}; + const FBinItems* Items=nullptr; + bool Valid=false; + }; + static thread_local FCoarseBinCache LastQuery; + const FBinKey QueryKey=PointBin(X,Y,Z); + if(!LastQuery.Valid||LastQuery.Owner!=this||!(LastQuery.Key==QueryKey)) + { + const auto Bin=SpatialBins.find(QueryKey); + LastQuery.Owner=this; + LastQuery.Key=QueryKey; + LastQuery.Items=Bin==SpatialBins.end()?nullptr:&Bin->second; + LastQuery.Valid=true; + } + if(LastQuery.Items) + { + for(uint32 Id:*LastQuery.Items) { const FPrimitive& P=Primitives[Id]; if(P.bCarve) @@ -2895,37 +2930,51 @@ private: if(Params.DomainWarpStrength>0.0f) { const float WF=Params.DomainWarpFrequency,WS=Params.DomainWarpStrength; - const float WX=VoxelNoise::Perlin3D(FVector3f( + float WX=0.0f,WY=0.0f,WZ=0.0f; + VoxelNoise::Perlin3D_x3( static_cast(X)*WF+VoxelHash::SeedOffset(Seed,5.2f), static_cast(Y)*WF+VoxelHash::SeedOffset(Seed,1.3f), - EffectiveZ*WF+VoxelHash::SeedOffset(Seed,9.7f)))*VOXEL_NOISE_SCALE*WS; - const float WY=VoxelNoise::Perlin3D(FVector3f( + EffectiveZ*WF+VoxelHash::SeedOffset(Seed,9.7f), static_cast(X)*WF+100.0f+VoxelHash::SeedOffset(Seed,7.7f), static_cast(Y)*WF+200.0f+VoxelHash::SeedOffset(Seed,3.1f), - EffectiveZ*WF+300.0f))*VOXEL_NOISE_SCALE*WS; - const float WZ=VoxelNoise::Perlin3D(FVector3f( + EffectiveZ*WF+300.0f, static_cast(X)*WF+400.0f, static_cast(Y)*WF+500.0f+VoxelHash::SeedOffset(Seed,11.9f), - EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(Seed,13.3f)))*VOXEL_NOISE_SCALE*WS; + EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(Seed,13.3f),WX,WY,WZ); + WX*=VOXEL_NOISE_SCALE*WS;WY*=VOXEL_NOISE_SCALE*WS;WZ*=VOXEL_NOISE_SCALE*WS; Main+=FVector3f(WX,WY,WZ);Fine+=FVector3f(WX,WY,WZ); } float Rough=0.0f,FineValue=0.0f; switch(Params.RoughnessNoiseType) { case EVoxelNoiseType::Ridged: - Rough=VoxelNoise::Ridged(Main.X,Main.Y,Main.Z,3); - FineValue=VoxelNoise::Ridged(Fine.X,Fine.Y,Fine.Z,2);break; + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(Main.X,Main.Y,Main.Z,3, + Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged); + Rough=MainRidged;FineValue=FineRidged; + break; + } case EVoxelNoiseType::Mixed: - Rough=0.5f*(VoxelNoise::FBM(Main.X,Main.Y,Main.Z,3) - +VoxelNoise::Ridged(Main.X,Main.Y,Main.Z,3)); - FineValue=0.5f*(VoxelNoise::FBM(Fine.X,Fine.Y,Fine.Z,2) - +VoxelNoise::Ridged(Fine.X,Fine.Y,Fine.Z,2));break; + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(Main.X,Main.Y,Main.Z,3, + Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged); + Rough=MainFBM*0.5f+MainRidged*0.5f; + FineValue=FineFBM*0.5f+FineRidged*0.5f; + break; + } case EVoxelNoiseType::Cellular: Rough=VoxelNoise::Cellular3D(Main);FineValue=VoxelNoise::Cellular3D(Fine);break; case EVoxelNoiseType::FBM: default: - Rough=VoxelNoise::FBM(Main.X,Main.Y,Main.Z,3); - FineValue=VoxelNoise::FBM(Fine.X,Fine.Y,Fine.Z,2);break; + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(Main.X,Main.Y,Main.Z,3, + Fine.X,Fine.Y,Fine.Z,2,MainFBM,MainRidged,FineFBM,FineRidged); + Rough=MainFBM;FineValue=FineFBM; + break; + } } return (Rough+FineValue*0.4f)*VOXEL_NOISE_SCALE*Params.SurfaceRoughness; } diff --git a/Standalone/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Standalone/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index b028912..87a09b8 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -6830,24 +6830,21 @@ namespace { const float WF = P.DomainWarpFrequency; const float WS = P.DomainWarpStrength; - - const float WarpX = VoxelNoise::Perlin3D(FVector3f( - WorldX * WF + VoxelHash::SeedOffset(SeedU, 5.2f), - WorldY * WF + VoxelHash::SeedOffset(SeedU, 1.3f), - EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 9.7f) - )) * VOXEL_NOISE_SCALE * WS; - - const float WarpY = VoxelNoise::Perlin3D(FVector3f( - WorldX * WF + 100.0f + VoxelHash::SeedOffset(SeedU, 7.7f), - WorldY * WF + 200.0f + VoxelHash::SeedOffset(SeedU, 3.1f), - EffectiveZ * WF + 300.0f - )) * VOXEL_NOISE_SCALE * WS; - - const float WarpZ = VoxelNoise::Perlin3D(FVector3f( - WorldX * WF + 400.0f, - WorldY * WF + 500.0f + VoxelHash::SeedOffset(SeedU, 11.9f), - EffectiveZ * WF + 600.0f + VoxelHash::SeedOffset(SeedU, 13.3f) - )) * VOXEL_NOISE_SCALE * WS; + float WarpX=0.0f,WarpY=0.0f,WarpZ=0.0f; + VoxelNoise::Perlin3D_x3( + WorldX*WF+VoxelHash::SeedOffset(SeedU,5.2f), + WorldY*WF+VoxelHash::SeedOffset(SeedU,1.3f), + EffectiveZ*WF+VoxelHash::SeedOffset(SeedU,9.7f), + WorldX*WF+100.0f+VoxelHash::SeedOffset(SeedU,7.7f), + WorldY*WF+200.0f+VoxelHash::SeedOffset(SeedU,3.1f), + EffectiveZ*WF+300.0f, + WorldX*WF+400.0f, + WorldY*WF+500.0f+VoxelHash::SeedOffset(SeedU,11.9f), + EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(SeedU,13.3f), + WarpX,WarpY,WarpZ); + WarpX*=VOXEL_NOISE_SCALE*WS; + WarpY*=VOXEL_NOISE_SCALE*WS; + WarpZ*=VOXEL_NOISE_SCALE*WS; const FVector3f WarpOffset(WarpX, WarpY, WarpZ); MainPos += WarpOffset; @@ -6863,16 +6860,25 @@ namespace switch (P.RoughnessNoiseType) { case EVoxelNoiseType::Ridged: - RoughNoise = HRidged3D(MainPos, Oct3); - FineNoise = HRidged3D(FinePos, Oct2); + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(MainPos.X,MainPos.Y,MainPos.Z,Oct3, + FinePos.X,FinePos.Y,FinePos.Z,Oct2, + MainFBM,MainRidged,FineFBM,FineRidged); + RoughNoise=MainRidged;FineNoise=FineRidged; break; + } case EVoxelNoiseType::Mixed: - RoughNoise = HFractal3D(MainPos, Oct3) * 0.5f - + HRidged3D(MainPos, Oct3) * 0.5f; - FineNoise = HFractal3D(FinePos, Oct2) * 0.5f - + HRidged3D(FinePos, Oct2) * 0.5f; + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(MainPos.X,MainPos.Y,MainPos.Z,Oct3, + FinePos.X,FinePos.Y,FinePos.Z,Oct2, + MainFBM,MainRidged,FineFBM,FineRidged); + RoughNoise = MainFBM * 0.5f + MainRidged * 0.5f; + FineNoise = FineFBM * 0.5f + FineRidged * 0.5f; break; + } case EVoxelNoiseType::Cellular: RoughNoise = VoxelNoise::Cellular3D(MainPos); @@ -6881,10 +6887,15 @@ namespace case EVoxelNoiseType::FBM: default: - RoughNoise = HFractal3D(MainPos, Oct3); - FineNoise = HFractal3D(FinePos, Oct2); + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(MainPos.X,MainPos.Y,MainPos.Z,Oct3, + FinePos.X,FinePos.Y,FinePos.Z,Oct2, + MainFBM,MainRidged,FineFBM,FineRidged); + RoughNoise=MainFBM;FineNoise=FineFBM; break; } + } RoughNoise *= VOXEL_NOISE_SCALE; FineNoise *= VOXEL_NOISE_SCALE; diff --git a/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp b/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp index 4dfe12b..c8266fb 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -988,7 +988,7 @@ bool VoxelGenLOD::ShouldUseSpatialIndex(bool bFusedPath) } // NOTE (T2.a): the fBm/Ridged bodies moved to VoxelNoise.h, where octaves are evaluated -// 4-wide via SSE (Perlin3D_x4). These thin wrappers keep every call site unchanged. They +// 8-wide via AVX2 (Perlin3D_x8). These thin wrappers keep every call site unchanged. They // sample a DIFFERENT (float hash-gradient) noise field than the old FMath::PerlinNoise3D, // so worlds re-tune once — but the fBm/Ridged math/contracts ([-1,1]) are identical. static float FractalNoise3D(const FVector3f& Position, int32 Octaves = 4, @@ -4763,24 +4763,22 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo float WF = Params.DomainWarpFrequency; float WS = Params.DomainWarpStrength; - // Sample three independent noise fields for X, Y, Z warp - float WarpX = VoxelNoise::Perlin3D(FVector3f( - WorldX * WF + VoxelHash::SeedOffset(SeedU, 5.2f), - WorldY * WF + VoxelHash::SeedOffset(SeedU, 1.3f), - EffectiveZ * WF + VoxelHash::SeedOffset(SeedU, 9.7f) - )) * VOXEL_NOISE_SCALE * WS; - - float WarpY = VoxelNoise::Perlin3D(FVector3f( - WorldX * WF + 100.0f + VoxelHash::SeedOffset(SeedU, 7.7f), - WorldY * WF + 200.0f + VoxelHash::SeedOffset(SeedU, 3.1f), - EffectiveZ * WF + 300.0f - )) * VOXEL_NOISE_SCALE * WS; - - float WarpZ = VoxelNoise::Perlin3D(FVector3f( - WorldX * WF + 400.0f, - WorldY * WF + 500.0f + VoxelHash::SeedOffset(SeedU, 11.9f), - EffectiveZ * WF + 600.0f + VoxelHash::SeedOffset(SeedU, 13.3f) - )) * VOXEL_NOISE_SCALE * WS; + // Sample three independent warp fields together through AVX2. + float WarpX=0.0f,WarpY=0.0f,WarpZ=0.0f; + VoxelNoise::Perlin3D_x3( + WorldX*WF+VoxelHash::SeedOffset(SeedU,5.2f), + WorldY*WF+VoxelHash::SeedOffset(SeedU,1.3f), + EffectiveZ*WF+VoxelHash::SeedOffset(SeedU,9.7f), + WorldX*WF+100.0f+VoxelHash::SeedOffset(SeedU,7.7f), + WorldY*WF+200.0f+VoxelHash::SeedOffset(SeedU,3.1f), + EffectiveZ*WF+300.0f, + WorldX*WF+400.0f, + WorldY*WF+500.0f+VoxelHash::SeedOffset(SeedU,11.9f), + EffectiveZ*WF+600.0f+VoxelHash::SeedOffset(SeedU,13.3f), + WarpX,WarpY,WarpZ); + WarpX*=VOXEL_NOISE_SCALE*WS; + WarpY*=VOXEL_NOISE_SCALE*WS; + WarpZ*=VOXEL_NOISE_SCALE*WS; // Apply warp to both noise positions FVector3f WarpOffset(WarpX, WarpY, WarpZ); @@ -4799,17 +4797,26 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo { case EVoxelNoiseType::Ridged: // Ridged multifractal: sharp, craggy features - RoughNoise = RidgedNoise3D(MainPos, Oct3); - FineNoise = RidgedNoise3D(FinePos, Oct2); + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(MainPos.X,MainPos.Y,MainPos.Z,Oct3, + FinePos.X,FinePos.Y,FinePos.Z,Oct2, + MainFBM,MainRidged,FineFBM,FineRidged); + RoughNoise=MainRidged;FineNoise=FineRidged; break; + } case EVoxelNoiseType::Mixed: // Blend: ridged structure softened by fBM - RoughNoise = FractalNoise3D(MainPos, Oct3) * 0.5f - + RidgedNoise3D(MainPos, Oct3) * 0.5f; - FineNoise = FractalNoise3D(FinePos, Oct2) * 0.5f - + RidgedNoise3D(FinePos, Oct2) * 0.5f; + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(MainPos.X,MainPos.Y,MainPos.Z,Oct3, + FinePos.X,FinePos.Y,FinePos.Z,Oct2, + MainFBM,MainRidged,FineFBM,FineRidged); + RoughNoise = MainFBM * 0.5f + MainRidged * 0.5f; + FineNoise = FineFBM * 0.5f + FineRidged * 0.5f; break; + } case EVoxelNoiseType::Cellular: // Worley/cellular: grotto, bubble-like patterns @@ -4821,10 +4828,15 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo case EVoxelNoiseType::FBM: default: // Standard fBM: smooth, organic - RoughNoise = FractalNoise3D(MainPos, Oct3); - FineNoise = FractalNoise3D(FinePos, Oct2); + { + float MainFBM=0.0f,MainRidged=0.0f,FineFBM=0.0f,FineRidged=0.0f; + VoxelNoise::FractalPair(MainPos.X,MainPos.Y,MainPos.Z,Oct3, + FinePos.X,FinePos.Y,FinePos.Z,Oct2, + MainFBM,MainRidged,FineFBM,FineRidged); + RoughNoise=MainFBM;FineNoise=FineFBM; break; } + } // Scale by VOXEL_NOISE_SCALE (all noise functions return ~[-1,1] but UE // Perlin internally returns ~[-0.8,0.8]) diff --git a/Standalone/Source/VoxelForge/Public/VoxelNoise.h b/Standalone/Source/VoxelForge/Public/VoxelNoise.h index 1ef9a08..9a38dde 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelNoise.h +++ b/Standalone/Source/VoxelForge/Public/VoxelNoise.h @@ -1,55 +1,27 @@ // VoxelNoise.h -// Float, SIMD-batched gradient-noise core (T2.a). Replaces UE's double-precision -// FMath::PerlinNoise3D on the density hot path (~6.6 ms/chunk was all noise math). +// Float hash-gradient Perlin noise for the standalone harness. Batched octave +// coordinates and the three domain-warp channels use the canonical 8-lane AVX2 +// path. Standalone builds require AVX2; there is no runtime CPU dispatch. // -// WHY THIS EXISTS -// FMath::PerlinNoise3D is double-precision with a permutation-table lookup. The -// density field calls it >1M times per surface chunk (fBm octaves + domain warps). -// This core is: -// - FLOAT (no double math), -// - table-free hash-gradient Perlin (pure arithmetic → vectorizes cleanly), -// - SIMD-BATCHED across fBm octaves: one FractalNoise/Ridged call evaluates up to -// 4 octaves' Perlin samples in one 4-wide SSE pass. -// -// It is a DIFFERENT noise field than FMath's, so worlds re-tune ONCE (accepted). -// -// DETERMINISM / CACHES -// Perlin3D is a pure function of (x,y,z) exactly like the old call — every box-validity -// cache (SDF §8.10, biome, surface column) stays valid. No invariant changes. -// -// SCALAR vs SIMD -// Perlin3D (scalar) and Perlin3D_x4 (SSE) use the IDENTICAL formula, op-for-op, so on -// x86 (SSE math == scalar-float math, same IEEE rounding) they produce bit-identical -// results. The SSE path is therefore a pure speedup with no second re-tune. If it ever -// fails to build on a given toolchain, force the scalar fallback with one line: -// #define VF_NOISE_USE_SIMD 0 // (before including this header, or here) -// The scalar path alone is still a large win over the old double-precision core. -// -// REQUIRES: SSE4.1 for the SIMD path (UE5 x64 baseline is SSE4.2 → always available). +// The AVX2 batches preserve the scalar Perlin interpolation and hash operations. +// Intrinsics spell out multiply and add separately, and Build.ps1 uses /fp:precise. #pragma once #include "CoreMinimal.h" #include "VoxelTypes.h" -#if !defined(VF_NOISE_USE_SIMD) - #if PLATFORM_ENABLE_VECTORINTRINSICS && PLATFORM_CPU_X86_FAMILY - #define VF_NOISE_USE_SIMD 1 - #else - #define VF_NOISE_USE_SIMD 0 - #endif -#endif - -#if VF_NOISE_USE_SIMD - #include // SSE4.1: _mm_floor_ps / _mm_mullo_epi32 / _mm_blendv_ps -#endif +// The standalone harness is built with /arch:AVX2 on the required AVX2 host. +// Keep one canonical arithmetic path here; there is no runtime or platform dispatch. +#define VF_NOISE_USE_SIMD 1 +#include namespace VoxelNoise { namespace Detail { - // Table-free integer hash of a lattice corner → gradient selector. Pure mul/xor/shift - // so it vectorizes 1:1 (see the SSE Hash lambda below — must stay in lock-step). + // Table-free integer hash of a lattice corner → gradient selector. Keep it in + // lock-step with the AVX2 Hash lambda below. FORCEINLINE uint32 HashCorner(int32 ix, int32 iy, int32 iz) { uint32 h = (uint32)ix * 0x9E3779B1u @@ -61,7 +33,7 @@ namespace Detail return h; } - // Quintic fade 6t^5-15t^4+10t^3, factored as t^3 * (t*(6t-15)+10) so the SSE twin + // Quintic fade 6t^5-15t^4+10t^3, factored as t^3 * (t*(6t-15)+10) so the AVX2 twin // can mirror the exact grouping. FORCEINLINE float Fade(float t) { @@ -71,7 +43,7 @@ namespace Detail } // Ken Perlin's 12-gradient dot (hash&15 picks the gradient). Branchy form here; - // the SSE twin reproduces it with selects. + // the AVX2 twin reproduces it with selects. FORCEINLINE float GradDot(uint32 hash, float x, float y, float z) { const uint32 h = hash & 15u; @@ -357,52 +329,136 @@ FORCEINLINE void Perlin3D_x4(const float* Xs, const float* Ys, const float* Zs, } #endif -// Three independent channels are the hot cave-warp shape. On x86, evaluate them as three lanes -// of the existing four-wide implementation; the unused fourth lane is deliberate. The scalar -// fallback keeps the exact three-call behavior on platforms without the SIMD path. This helper is -// the owner of the cave-warp channel evaluation so generator, op-stack, and room-fit callers cannot -// drift into different noise formulas or channel offsets. +// Eight independent gradient-noise samples in AVX2 lanes. Arithmetic order mirrors the +// scalar Perlin3D implementation; explicit mul/add intrinsics keep contraction disabled. +FORCEINLINE void Perlin3D_x8(const float* Xs, const float* Ys, const float* Zs, float* Out) +{ + const __m256 x = _mm256_loadu_ps(Xs); + const __m256 y = _mm256_loadu_ps(Ys); + const __m256 z = _mm256_loadu_ps(Zs); + const __m256 xf = _mm256_floor_ps(x); + const __m256 yf = _mm256_floor_ps(y); + const __m256 zf = _mm256_floor_ps(z); + const __m256i X = _mm256_cvttps_epi32(xf); + const __m256i Y = _mm256_cvttps_epi32(yf); + const __m256i Z = _mm256_cvttps_epi32(zf); + const __m256 fx = _mm256_sub_ps(x, xf); + const __m256 fy = _mm256_sub_ps(y, yf); + const __m256 fz = _mm256_sub_ps(z, zf); + + const __m256 c6 = _mm256_set1_ps(6.0f); + const __m256 c15 = _mm256_set1_ps(15.0f); + const __m256 c10 = _mm256_set1_ps(10.0f); + const auto Fade = [&](const __m256 t) + { + const __m256 inner = _mm256_add_ps( + _mm256_mul_ps(t, _mm256_sub_ps(_mm256_mul_ps(t, c6), c15)), c10); + const __m256 t3 = _mm256_mul_ps(_mm256_mul_ps(t, t), t); + return _mm256_mul_ps(t3, inner); + }; + const __m256 su = Fade(fx), sv = Fade(fy), sw = Fade(fz); + + const __m256i k1 = _mm256_set1_epi32(static_cast(0x9E3779B1u)); + const __m256i k2 = _mm256_set1_epi32(static_cast(0x85EBCA77u)); + const __m256i k3 = _mm256_set1_epi32(static_cast(0xC2B2AE3Du)); + const __m256i m1 = _mm256_set1_epi32(static_cast(0x2C1B3C6Du)); + const __m256i m2 = _mm256_set1_epi32(static_cast(0x297A2D39u)); + const __m256i px0 = _mm256_mullo_epi32(X, k1); + const __m256i py0 = _mm256_mullo_epi32(Y, k2); + const __m256i pz0 = _mm256_mullo_epi32(Z, k3); + const __m256i px1 = _mm256_add_epi32(px0, k1); + const __m256i py1 = _mm256_add_epi32(py0, k2); + const __m256i pz1 = _mm256_add_epi32(pz0, k3); + const auto HashProducts = [&](const __m256i px, const __m256i py, const __m256i pz) + { + __m256i h = _mm256_xor_si256(_mm256_xor_si256(px, py), pz); + h = _mm256_xor_si256(h, _mm256_srli_epi32(h, 15)); h = _mm256_mullo_epi32(h, m1); + h = _mm256_xor_si256(h, _mm256_srli_epi32(h, 12)); h = _mm256_mullo_epi32(h, m2); + return _mm256_xor_si256(h, _mm256_srli_epi32(h, 15)); + }; + + const __m256 fx1 = _mm256_sub_ps(fx, _mm256_set1_ps(1.0f)); + const __m256 fy1 = _mm256_sub_ps(fy, _mm256_set1_ps(1.0f)); + const __m256 fz1 = _mm256_sub_ps(fz, _mm256_set1_ps(1.0f)); + const __m256i i8 = _mm256_set1_epi32(8), i12 = _mm256_set1_epi32(12); + const __m256i i13 = _mm256_set1_epi32(13), i15 = _mm256_set1_epi32(15); + const __m256i i1 = _mm256_set1_epi32(1), i2 = _mm256_set1_epi32(2); + const __m256i izero = _mm256_setzero_si256(); + const __m256 sign = _mm256_set1_ps(-0.0f); + const auto Grad = [&](const __m256i hash, const __m256 gx, const __m256 gy, const __m256 gz) + { + const __m256i h = _mm256_and_si256(hash, i15); + const __m256 mU = _mm256_castsi256_ps( + _mm256_cmpeq_epi32(_mm256_and_si256(h, i8), izero)); + const __m256 u = _mm256_blendv_ps(gy, gx, mU); + const __m256 mLt4 = _mm256_castsi256_ps( + _mm256_cmpeq_epi32(_mm256_and_si256(h, i12), izero)); + const __m256 m1214 = _mm256_castsi256_ps( + _mm256_cmpeq_epi32(_mm256_and_si256(h, i13), i12)); + const __m256 vTmp = _mm256_blendv_ps(gz, gx, m1214); + const __m256 v = _mm256_blendv_ps(vTmp, gy, mLt4); + const __m256 mNegU = _mm256_castsi256_ps( + _mm256_cmpeq_epi32(_mm256_and_si256(h, i1), i1)); + const __m256 mNegV = _mm256_castsi256_ps( + _mm256_cmpeq_epi32(_mm256_and_si256(h, i2), i2)); + const __m256 ru = _mm256_blendv_ps(u, _mm256_xor_ps(u, sign), mNegU); + const __m256 rv = _mm256_blendv_ps(v, _mm256_xor_ps(v, sign), mNegV); + return _mm256_add_ps(ru, rv); + }; + const __m256 n000 = Grad(HashProducts(px0, py0, pz0), fx, fy, fz); + const __m256 n100 = Grad(HashProducts(px1, py0, pz0), fx1, fy, fz); + const __m256 n010 = Grad(HashProducts(px0, py1, pz0), fx, fy1, fz); + const __m256 n110 = Grad(HashProducts(px1, py1, pz0), fx1, fy1, fz); + const __m256 n001 = Grad(HashProducts(px0, py0, pz1), fx, fy, fz1); + const __m256 n101 = Grad(HashProducts(px1, py0, pz1), fx1, fy, fz1); + const __m256 n011 = Grad(HashProducts(px0, py1, pz1), fx, fy1, fz1); + const __m256 n111 = Grad(HashProducts(px1, py1, pz1), fx1, fy1, fz1); + const auto Lerp = [&](const __m256 a, const __m256 b, const __m256 t) + { + return _mm256_add_ps(a, _mm256_mul_ps(t, _mm256_sub_ps(b, a))); + }; + const __m256 x00 = Lerp(n000, n100, su); + const __m256 x10 = Lerp(n010, n110, su); + const __m256 x01 = Lerp(n001, n101, su); + const __m256 x11 = Lerp(n011, n111, su); + const __m256 y0 = Lerp(x00, x10, sv); + const __m256 y1 = Lerp(x01, x11, sv); + _mm256_storeu_ps(Out, Lerp(y0, y1, sw)); +} + +// Three independent channels are the hot cave-warp shape. Pack them into the canonical AVX2 path. FORCEINLINE void Perlin3D_x3( float X0, float Y0, float Z0, float X1, float Y1, float Z1, float X2, float Y2, float Z2, float& Out0, float& Out1, float& Out2) { -#if VF_NOISE_USE_SIMD - const float Xs[4] = { X0, X1, X2, X0 }; - const float Ys[4] = { Y0, Y1, Y2, Y0 }; - const float Zs[4] = { Z0, Z1, Z2, Z0 }; - float Out[4]; - Perlin3D_x4(Xs, Ys, Zs, Out); - Out0 = Out[0]; - Out1 = Out[1]; - Out2 = Out[2]; -#else - Out0 = Perlin3D(X0, Y0, Z0); - Out1 = Perlin3D(X1, Y1, Z1); - Out2 = Perlin3D(X2, Y2, Z2); -#endif + const float Xs[8] = { X0, X1, X2, X0, X0, X0, X0, X0 }; + const float Ys[8] = { Y0, Y1, Y2, Y0, Y0, Y0, Y0, Y0 }; + const float Zs[8] = { Z0, Z1, Z2, Z0, Z0, Z0, Z0, Z0 }; + float Out[8]; + Perlin3D_x8(Xs, Ys, Zs, Out); + Out0 = Out[0]; Out1 = Out[1]; Out2 = Out[2]; } //============================================================================= -// fBm / Ridged — octaves evaluated 4 at a time through Perlin3D_x4. -// Accumulation stays scalar in octave order, so the result is independent of -// whether the SIMD or scalar Perlin3D_x4 is used (bit-identical either way). +// fBm / Ridged — independent octave coordinates occupy AVX2 lanes. Accumulation +// remains scalar and ordered by octave, with explicit float operations. //============================================================================= FORCEINLINE float FBM(float x, float y, float z, int32 Octaves = 4, float Lacunarity = 2.0f, float Persistence = 0.5f) { float Total = 0.0f, MaxValue = 0.0f, Freq = 1.0f, Amp = 1.0f; - for (int32 o = 0; o < Octaves; ) + while (Octaves > 0) { - const int32 N = FMath::Min(4, Octaves - o); - float Xs[4] = {}, Ys[4] = {}, Zs[4] = {}, Out[4]; + const int32 N = FMath::Min(8, Octaves); + float Xs[8] = {}, Ys[8] = {}, Zs[8] = {}, Out[8]; float f = Freq; for (int32 i = 0; i < N; ++i) { Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f; f *= Lacunarity; } - Perlin3D_x4(Xs, Ys, Zs, Out); + Perlin3D_x8(Xs, Ys, Zs, Out); for (int32 i = 0; i < N; ++i) { Total += Out[i] * Amp; MaxValue += Amp; Amp *= Persistence; } Freq = f; - o += N; + Octaves -= N; } return Total / MaxValue; } @@ -413,13 +469,13 @@ FORCEINLINE float Ridged(float x, float y, float z, // Matches the original RidgedNoise3D fold exactly (NS scale, square, weight feedback). static constexpr float NS = 1.25f; float Total = 0.0f, MaxValue = 0.0f, Freq = 1.0f, Amp = 1.0f, Weight = 1.0f; - for (int32 o = 0; o < Octaves; ) + while (Octaves > 0) { - const int32 N = FMath::Min(4, Octaves - o); - float Xs[4] = {}, Ys[4] = {}, Zs[4] = {}, Out[4]; + const int32 N = FMath::Min(8, Octaves); + float Xs[8] = {}, Ys[8] = {}, Zs[8] = {}, Out[8]; float f = Freq; for (int32 i = 0; i < N; ++i) { Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f; f *= Lacunarity; } - Perlin3D_x4(Xs, Ys, Zs, Out); + Perlin3D_x8(Xs, Ys, Zs, Out); for (int32 i = 0; i < N; ++i) { float Nn = Out[i] * NS; @@ -430,9 +486,129 @@ FORCEINLINE float Ridged(float x, float y, float z, Total += Nn * Amp; MaxValue += Amp; Amp *= Persistence; } Freq = f; - o += N; + Octaves -= N; } return (Total / MaxValue) * 2.0f - 1.0f; } +// Mixed roughness consumes fBm and ridged folds of the same octave samples. Share +// the expensive AVX2 Perlin batch while retaining each accumulator's original order. +FORCEINLINE void FBMAndRidged(float x, float y, float z, int32 Octaves, + float& OutFBM, float& OutRidged, + float Lacunarity = 2.0f, float Persistence = 0.5f) +{ + static constexpr float NS = 1.25f; + float FbmTotal = 0.0f, FbmMax = 0.0f; + float RidgeTotal = 0.0f, RidgeMax = 0.0f; + float Freq = 1.0f, Amp = 1.0f, Weight = 1.0f; + while (Octaves > 0) + { + const int32 N = FMath::Min(8, Octaves); + float Xs[8] = {}, Ys[8] = {}, Zs[8] = {}, Noise[8]; + float f = Freq; + for (int32 i = 0; i < N; ++i) + { + Xs[i] = x * f; Ys[i] = y * f; Zs[i] = z * f; + f *= Lacunarity; + } + Perlin3D_x8(Xs, Ys, Zs, Noise); + for (int32 i = 0; i < N; ++i) + { + FbmTotal += Noise[i] * Amp; + FbmMax += Amp; + float Ridge = Noise[i] * NS; + Ridge = 1.0f - FMath::Abs(Ridge); + Ridge = Ridge * Ridge; + Ridge *= Weight; + Weight = FMath::Clamp(Ridge * 2.0f, 0.0f, 1.0f); + RidgeTotal += Ridge * Amp; + RidgeMax += Amp; + Amp *= Persistence; + } + Freq = f; + Octaves -= N; + } + OutFBM = FbmTotal / FbmMax; + OutRidged = (RidgeTotal / RidgeMax) * 2.0f - 1.0f; +} + +template +FORCEINLINE void FractalPair(float X0, float Y0, float Z0, int32 Octaves0, + float X1, float Y1, float Z1, int32 Octaves1, + float& OutFBM0, float& OutRidged0, + float& OutFBM1, float& OutRidged1, + float Lacunarity = 2.0f, float Persistence = 0.5f) +{ + OutFBM0 = OutRidged0 = OutFBM1 = OutRidged1 = 0.0f; + if (Octaves0 + Octaves1 > 8) + { + if constexpr (bComputeFBM) + { + OutFBM0 = FBM(X0,Y0,Z0,Octaves0,Lacunarity,Persistence); + OutFBM1 = FBM(X1,Y1,Z1,Octaves1,Lacunarity,Persistence); + } + if constexpr (bComputeRidged) + { + OutRidged0 = Ridged(X0,Y0,Z0,Octaves0,Lacunarity,Persistence); + OutRidged1 = Ridged(X1,Y1,Z1,Octaves1,Lacunarity,Persistence); + } + return; + } + + float Xs[8] = {}, Ys[8] = {}, Zs[8] = {}, Noise[8]; + float Frequency = 1.0f; + for (int32 I = 0; I < Octaves0; ++I) + { + Xs[I] = X0 * Frequency; Ys[I] = Y0 * Frequency; Zs[I] = Z0 * Frequency; + Frequency *= Lacunarity; + } + Frequency = 1.0f; + for (int32 I = 0; I < Octaves1; ++I) + { + const int32 Lane = Octaves0 + I; + Xs[Lane] = X1 * Frequency; Ys[Lane] = Y1 * Frequency; Zs[Lane] = Z1 * Frequency; + Frequency *= Lacunarity; + } + Perlin3D_x8(Xs,Ys,Zs,Noise); + + float FbmTotal[2] = {}, FbmMax[2] = {}; + float RidgeTotal[2] = {}, RidgeMax[2] = {}; + float Amplitude[2] = {1.0f,1.0f}, Weight[2] = {1.0f,1.0f}; + const int32 Octaves[2] = {Octaves0,Octaves1}; + for (int32 Group = 0; Group < 2; ++Group) + { + for (int32 I = 0; I < Octaves[Group]; ++I) + { + const float N = Noise[(Group==0?0:Octaves0)+I]; + const float Amp = Amplitude[Group]; + if constexpr (bComputeFBM) + { + FbmTotal[Group] += N * Amp; + FbmMax[Group] += Amp; + } + if constexpr (bComputeRidged) + { + float Ridge = N * 1.25f; + Ridge = 1.0f - FMath::Abs(Ridge); + Ridge = Ridge * Ridge; + Ridge *= Weight[Group]; + Weight[Group] = FMath::Clamp(Ridge * 2.0f,0.0f,1.0f); + RidgeTotal[Group] += Ridge * Amp; + RidgeMax[Group] += Amp; + } + Amplitude[Group] *= Persistence; + } + } + if constexpr (bComputeFBM) + { + OutFBM0 = FbmTotal[0] / FbmMax[0]; + OutFBM1 = FbmTotal[1] / FbmMax[1]; + } + if constexpr (bComputeRidged) + { + OutRidged0 = (RidgeTotal[0] / RidgeMax[0]) * 2.0f - 1.0f; + OutRidged1 = (RidgeTotal[1] / RidgeMax[1]) * 2.0f - 1.0f; + } +} + } // namespace VoxelNoise