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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
d4fa89204d
commit
16612acba4
5 files changed
+446
-135
No files matched your search
@@ -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.
|
||||
|
||||
@@ -2500,6 +2500,10 @@ private:
|
||||
const int32 Y=Min[1]+static_cast<int32>(Line/Size[0]);
|
||||
FDetailNoiseLineContext NoiseLine(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<uint32>(Owner->Seed));
|
||||
const int32 QueryBinX=FloorDiv(X,ActiveSpatialBinSize);
|
||||
const int32 QueryBinY=FloorDiv(Y,ActiveSpatialBinSize);
|
||||
int32 CachedQueryBinZ=std::numeric_limits<int32>::min();
|
||||
const FBinItems* CachedQueryBinItems=nullptr;
|
||||
FDetailWorkerProfile& Profile=WorkerProfile[static_cast<size_t>(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_t>(Size[2]))continue;
|
||||
const int32 Z=Min[2]+static_cast<int32>(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<uint64>(
|
||||
@@ -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<float>(X)*WF+VoxelHash::SeedOffset(Seed,5.2f),
|
||||
static_cast<float>(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<float>(X)*WF+100.0f+VoxelHash::SeedOffset(Seed,7.7f),
|
||||
static_cast<float>(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<float>(X)*WF+400.0f,
|
||||
static_cast<float>(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<false,true>(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<true,true>(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<true,false>(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;
|
||||
}
|
||||
|
||||
@@ -6830,24 +6830,21 @@ namespace
|
||||
{
|
||||
const float WF = P.DomainWarpFrequency;
|
||||
const float WS = P.DomainWarpStrength;
|
||||
|
||||
const float WarpX = VoxelNoise::Perlin3D(FVector3f(
|
||||
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)
|
||||
)) * VOXEL_NOISE_SCALE * WS;
|
||||
|
||||
const float WarpY = VoxelNoise::Perlin3D(FVector3f(
|
||||
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
|
||||
)) * VOXEL_NOISE_SCALE * WS;
|
||||
|
||||
const float WarpZ = VoxelNoise::Perlin3D(FVector3f(
|
||||
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)
|
||||
)) * VOXEL_NOISE_SCALE * WS;
|
||||
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<false,true>(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<true,true>(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<true,false>(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;
|
||||
|
||||
@@ -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(
|
||||
// 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)
|
||||
)) * VOXEL_NOISE_SCALE * WS;
|
||||
|
||||
float WarpY = VoxelNoise::Perlin3D(FVector3f(
|
||||
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
|
||||
)) * VOXEL_NOISE_SCALE * WS;
|
||||
|
||||
float WarpZ = VoxelNoise::Perlin3D(FVector3f(
|
||||
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)
|
||||
)) * VOXEL_NOISE_SCALE * WS;
|
||||
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<false,true>(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<true,true>(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<true,false>(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])
|
||||
|
||||
@@ -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
|
||||
// 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
|
||||
#else
|
||||
#define VF_NOISE_USE_SIMD 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if VF_NOISE_USE_SIMD
|
||||
#include <immintrin.h> // SSE4.1: _mm_floor_ps / _mm_mullo_epi32 / _mm_blendv_ps
|
||||
#endif
|
||||
#include <immintrin.h>
|
||||
|
||||
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<int32>(0x9E3779B1u));
|
||||
const __m256i k2 = _mm256_set1_epi32(static_cast<int32>(0x85EBCA77u));
|
||||
const __m256i k3 = _mm256_set1_epi32(static_cast<int32>(0xC2B2AE3Du));
|
||||
const __m256i m1 = _mm256_set1_epi32(static_cast<int32>(0x2C1B3C6Du));
|
||||
const __m256i m2 = _mm256_set1_epi32(static_cast<int32>(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<bool bComputeFBM, bool bComputeRidged>
|
||||
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
|
||||
Reference in new issue
Block a user