wip(standalone): GPU field step 2 -- far-field shapes/strata/relief in shared C++/HLSL, GPU field+mesh

Seven LOD2-8 tiles: RX 7900 XTX field+meshing 0.335 ms vs CPU generateTile 15.81 ms; repeat runs
bit-identical; CPU W1/W8 44/44 hashes unchanged. Not yet a real gain: rock/air signs still come from
CPU masks for every sample. Viewer does not draw GPU tiles yet; no whole-view timing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-06 08:02:02 +02:00
1 parent d4b4377d30
commit b61ccff549
11 files changed
+2361 -9

No files matched your search

+192 -6
View File
@@ -6,6 +6,7 @@
#include "VoxelNoise.h"
#include "VoxelStrateManager.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include "VoxelForgeFieldShared.h"
#include <algorithm>
#include <array>
@@ -326,6 +327,7 @@ public:
// Route validation is world-wide. Detail regions reuse the same authored route data and
// do not need to repeat the 5,642-point world audit for every entering tile group.
if(bValidatePassageRoutes)ValidatePassageRoutes();
RebuildSharedFieldPrimitives();
Built=true;
if(bIncludeFarWorld)BuildFarWorld();
const uint64 WordBytes=static_cast<uint64>(RockWords.size()+CandidateWords.size())*sizeof(uint64);
@@ -432,6 +434,7 @@ public:
Stats.PrimitiveCount=Primitives.size();
if(Primitives.empty())
{Error="owner shape extraction produced no rooms, tunnels, or passages";return false;}
RebuildSharedFieldPrimitives();
Built=true;
return true;
}
@@ -461,6 +464,128 @@ public:
OutIndices.erase(std::unique(OutIndices.begin(),OutIndices.end()),OutIndices.end());
}
bool ExportFarFieldTileInput(const FIntVector& Origin,int32 Cells,int32 Step,
VoxelForgeCore::FarFieldTileInput& Out,std::string& Error) const
{
if(!FarBuilt||Cells<=0||Cells>32||Step<=0)
{Error="far field export requires a built far world and a bounded coarse tile";return false;}
const int32 Dim=Cells+3;
const size_t SampleCount=static_cast<size_t>(Dim)*Dim*Dim;
std::vector<std::vector<uint32>> PerSample(SampleCount);
std::vector<uint8> Used(Primitives.size(),0);
Out={};Out.origin={Origin.X,Origin.Y,Origin.Z};Out.cells=Cells;Out.step=Step;Out.sampleDim=Dim;
if(LayerRanges.empty())
{Out.strataLo=-100000.0f;Out.strataHi=100000.0f;}
else
{Out.strataLo=static_cast<float>(LayerRanges[0].Lo);Out.strataHi=static_cast<float>(LayerRanges[0].Hi);}
Out.strataBlend=StrataCaveBlendRadius;
if(Owner&&Owner->Manager)
{
const double TileZ0=Origin.Z,TileZ1=Origin.Z+static_cast<double>(Cells)*Step;
for(const FStrateSlot& Slot:Owner->Manager->GetLayout())
{
const FStrateGenerationParams Params=Owner->Manager->GetGenerationParams(
FIntVector(0,0,(Slot.TopChunkZ+Slot.BottomChunkZ)/2));
const float HalfWidth=static_cast<float>(std::max(8.0,
static_cast<double>(Params.BoundarySealThickness)+8.0));
const float Boundaries[2]={static_cast<float>(Slot.BottomChunkZ)*CHUNK_SIZE,
static_cast<float>(Slot.TopChunkZ+1)*CHUNK_SIZE};
for(const float Z:Boundaries)
if(Z+HalfWidth>=TileZ0&&Z-HalfWidth<=TileZ1)
Out.stratumBoundaries.push_back({Z,HalfWidth});
}
std::sort(Out.stratumBoundaries.begin(),Out.stratumBoundaries.end(),
[](const auto& A,const auto& B){return A.z<B.z;});
Out.stratumBoundaries.erase(std::unique(Out.stratumBoundaries.begin(),
Out.stratumBoundaries.end(),[](const auto& A,const auto& B)
{return std::abs(A.z-B.z)<0.01f;}),Out.stratumBoundaries.end());
}
Out.sampleShapeRanges.resize(SampleCount);
Out.rockAirSigns.assign((SampleCount+31u)/32u,0u);
Out.protectedAir.assign((SampleCount+31u)/32u,0u);
size_t SampleIndex=0;
for(int32 Z=-1;Z<=Cells+1;++Z)
for(int32 Y=-1;Y<=Cells+1;++Y)
for(int32 X=-1;X<=Cells+1;++X,++SampleIndex)
{
const int32 WX=Origin.X+X*Step,WY=Origin.Y+Y*Step,WZ=Origin.Z+Z*Step;
if(WX>=Min[0]&&WX<=Max[0]&&WY>=Min[1]&&WY<=Max[1]&&WZ>=Min[2]&&WZ<=Max[2])
{Error="tile lattice intersects the near analytic/detail region; this export is far-field only";return false;}
const float Density=DensityAtLattice(WX,WY,WZ);
if(Density>=0.0f)Out.rockAirSigns[SampleIndex>>5]|=1u<<(SampleIndex&31u);
if(IsInsideCriticalAir(WX,WY,WZ))
{
Out.protectedAir[SampleIndex>>5]|=1u<<(SampleIndex&31u);
++Out.protectedSampleCount;
}
const auto Found=SpatialBins.find(PointBin(WX,WY,WZ));
if(Found==SpatialBins.end())continue;
auto& Ids=PerSample[SampleIndex];
Ids.assign(Found->second.begin(),Found->second.end());
for(const uint32 Id:Ids)if(Id<Used.size())Used[Id]=1;
}
std::vector<uint32> Remap(Primitives.size(),std::numeric_limits<uint32>::max());
Out.shapes.reserve(std::count(Used.begin(),Used.end(),uint8{1}));
for(uint32 Id=0;Id<Used.size();++Id)
{
if(!Used[Id])continue;
Remap[Id]=static_cast<uint32>(Out.shapes.size());
const VF_FieldPrimitive& S=SharedFieldPrimitives[Id];
VoxelForgeCore::FarFieldPrimitiveGpu G{};
G.flags=S.flags;
for(int32 A=0;A<3;++A)
{
G.minVoxel[A]=S.minVoxel[A];G.maxVoxel[A]=S.maxVoxel[A];
G.center[A]=static_cast<float>(S.center[A]);
G.capsuleStart[A]=static_cast<float>(S.capsuleStart[A]);
G.capsuleDelta[A]=static_cast<float>(S.capsuleDelta[A]);
G.roundedHalfExtent[A]=static_cast<float>(S.roundedHalfExtent[A]);
}
for(int32 I=0;I<9;++I)G.q[I]=static_cast<float>(S.q[I]);
G.minRadius=static_cast<float>(S.minRadius);
G.capsuleAxisLengthSquared=static_cast<float>(S.capsuleAxisLengthSquared);
G.capsuleAxisLength=static_cast<float>(S.capsuleAxisLength);
G.capsuleStartRadius=static_cast<float>(S.capsuleStartRadius);
G.capsuleEndRadius=static_cast<float>(S.capsuleEndRadius);
G.profileStartT=static_cast<float>(S.profileStartT);G.profileEndT=static_cast<float>(S.profileEndT);
G.profileStartRadius=static_cast<float>(S.profileStartRadius);G.profileEndRadius=static_cast<float>(S.profileEndRadius);
G.radiusSlopeStart=static_cast<float>(S.radiusSlopeStart);G.radiusSlopeEnd=static_cast<float>(S.radiusSlopeEnd);
G.rounding=static_cast<float>(S.rounding);
G.blendRadius=static_cast<float>(S.blendRadius);G.floorBlendRadius=static_cast<float>(S.floorBlendRadius);
G.floorCenterZ=static_cast<float>(S.floorCenterZ);G.floorSlopeX=static_cast<float>(S.floorSlopeX);
G.floorSlopeY=static_cast<float>(S.floorSlopeY);
G.floorCurveOriginX=static_cast<float>(S.floorCurveOriginX);G.floorCurveOriginY=static_cast<float>(S.floorCurveOriginY);
G.floorCurveDeltaX=static_cast<float>(S.floorCurveDeltaX);G.floorCurveDeltaY=static_cast<float>(S.floorCurveDeltaY);
G.floorCurveLength=static_cast<float>(S.floorCurveLength);
G.floorCurveStartZ=static_cast<float>(S.floorCurveStartZ);G.floorCurveEndZ=static_cast<float>(S.floorCurveEndZ);
G.floorCurveTangentStart=static_cast<float>(S.floorCurveTangentStart);
G.floorCurveTangentEnd=static_cast<float>(S.floorCurveTangentEnd);
G.floorCurveStartBlend=static_cast<float>(S.floorCurveStartBlend);
G.floorCurveEndBlend=static_cast<float>(S.floorCurveEndBlend);
Out.shapes.push_back(G);
}
uint64 CandidateCount=0;
for(const auto& Ids:PerSample)CandidateCount+=Ids.size();
if(CandidateCount>std::numeric_limits<uint32>::max())
{Error="far tile candidate list exceeds the GPU 32-bit offset limit";return false;}
Out.sampleShapeIds.reserve(static_cast<size_t>(CandidateCount));
for(size_t I=0;I<PerSample.size();++I)
{
const auto& Ids=PerSample[I];
Out.sampleShapeRanges[I]={static_cast<uint32>(Out.sampleShapeIds.size()),
static_cast<uint32>(Ids.size())};
for(const uint32 Id:Ids)
{
if(Id>=Remap.size()||Remap[Id]==std::numeric_limits<uint32>::max())
{Error="far tile shape remap is inconsistent";return false;}
Out.sampleShapeIds.push_back(Remap[Id]);
}
}
return true;
}
bool DebugCriticalAirAt(double X,double Y,double Z) const { return IsInsideCriticalAir(X,Y,Z); }
bool DebugSolidOverlayAt(double X,double Y,double Z) const { return IsInsideSolidOverlay(X,Y,Z); }
@@ -505,6 +630,7 @@ public:
if(FarBuilt)return;
auto Stage=std::chrono::steady_clock::now();
BuildFarParentShapes();
RebuildSharedFieldPrimitives();
Stats.FarPrimitiveSeconds=std::chrono::duration<double>(std::chrono::steady_clock::now()-Stage).count();
Stage=std::chrono::steady_clock::now();
BuildSpatialBins(SpatialBinSize,false);
@@ -1407,6 +1533,64 @@ private:
static std::array<double,3> Cross(const std::array<double,3>& A,const std::array<double,3>& B)
{return {A[1]*B[2]-A[2]*B[1],A[2]*B[0]-A[0]*B[2],A[0]*B[1]-A[1]*B[0]};}
static VF_FieldPrimitive MakeSharedFieldPrimitive(const FPrimitive& P)
{
VF_FieldPrimitive Out{};
if(P.bCapsule)Out.flags|=VF_FIELD_PRIMITIVE_CAPSULE;
if(P.bSmoothCapsuleRadius)Out.flags|=VF_FIELD_PRIMITIVE_SMOOTH_CAPSULE;
if(P.bRoundedBox)Out.flags|=VF_FIELD_PRIMITIVE_ROUNDED_BOX;
if(P.bHasFloorClip)Out.flags|=VF_FIELD_PRIMITIVE_HAS_FLOOR;
if(P.bCurvedFloorClip)Out.flags|=VF_FIELD_PRIMITIVE_CURVED_FLOOR;
if(P.bCarve)Out.flags|=VF_FIELD_PRIMITIVE_CARVE;
if(P.bCritical)Out.flags|=VF_FIELD_PRIMITIVE_CRITICAL;
for(int32 A=0;A<3;++A)
{
Out.minVoxel[A]=P.Min[A];Out.maxVoxel[A]=P.Max[A];
Out.center[A]=P.CenterSub[A]/16.0;
Out.capsuleStart[A]=P.CapsuleStartWorld[A];
Out.capsuleDelta[A]=P.CapsuleAxisDelta[A];
Out.roundedHalfExtent[A]=P.RoundedHalfExtent[A];
}
for(int32 I=0;I<3;++I)for(int32 J=0;J<3;++J)
Out.q[I*3+J]=static_cast<double>(P.Q[I][J])/QScale;
Out.minRadius=P.MinRadius;
Out.capsuleAxisLengthSquared=P.CapsuleAxisLengthSquared;
Out.capsuleAxisLength=P.CapsuleAxisLength;
Out.capsuleStartRadius=P.CapsuleStartRadius;
Out.capsuleEndRadius=P.CapsuleEndRadius;
Out.profileStartT=P.CapsuleProfileStartT;
Out.profileEndT=P.CapsuleProfileEndT;
Out.profileStartRadius=P.CapsuleProfileStartRadius;
Out.profileEndRadius=P.CapsuleProfileEndRadius;
Out.radiusSlopeStart=P.CapsuleRadiusSlopeStart;
Out.radiusSlopeEnd=P.CapsuleRadiusSlopeEnd;
Out.rounding=P.Rounding;
Out.blendRadius=P.BlendRadius;
Out.floorBlendRadius=P.FloorBlendRadius;
Out.floorCenterZ=P.FloorCenterZ;
Out.floorSlopeX=P.FloorSlopeX;
Out.floorSlopeY=P.FloorSlopeY;
Out.floorCurveOriginX=P.FloorCurveOriginX;
Out.floorCurveOriginY=P.FloorCurveOriginY;
Out.floorCurveDeltaX=P.FloorCurveDeltaX;
Out.floorCurveDeltaY=P.FloorCurveDeltaY;
Out.floorCurveLength=P.FloorCurveLength;
Out.floorCurveStartZ=P.FloorCurveStartZ;
Out.floorCurveEndZ=P.FloorCurveEndZ;
Out.floorCurveTangentStart=P.FloorCurveTangentStart;
Out.floorCurveTangentEnd=P.FloorCurveTangentEnd;
Out.floorCurveStartBlend=P.FloorCurveStartBlend;
Out.floorCurveEndBlend=P.FloorCurveEndBlend;
return Out;
}
void RebuildSharedFieldPrimitives()
{
SharedFieldPrimitives.clear();SharedFieldPrimitives.reserve(Primitives.size());
for(const FPrimitive& P:Primitives)
SharedFieldPrimitives.push_back(MakeSharedFieldPrimitive(P));
}
void AddEllipsoid(const FVector& Center,const std::array<double,3>& Radii,
const std::array<double,3>& Axis0={1,0,0},
const std::array<double,3>& Axis1={0,1,0},
@@ -3633,6 +3817,7 @@ private:
for(uint32 Id:*QueryItems)
{
const FPrimitive& P=Primitives[Id];
const VF_FieldPrimitive& Shared=SharedFieldPrimitives[Id];
if(P.bCarve)
{
if(P.bCritical&&X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
@@ -3642,22 +3827,22 @@ private:
if(X<P.Min[0]-DetailBand||X>P.Max[0]+DetailBand
||Y<P.Min[1]-DetailBand||Y>P.Max[1]+DetailBand
||Z<P.Min[2]-DetailBand||Z>P.Max[2]+DetailBand)continue;
float Distance=ShapeDistance(P,X,Y,Z);
float Distance=VF_FieldPrimitiveDistance(Shared,X,Y,Z);
if(P.bHasFloorClip)
{
const double Floor=FloorAt(P,X,Y);
Distance=SmoothMaxDistance(Distance,static_cast<float>(Floor-Z),
const double Floor=VF_FieldPrimitiveFloorAt(Shared,X,Y);
Distance=VF_FieldSmoothMax(Distance,static_cast<float>(Floor-Z),
static_cast<float>(P.FloorBlendRadius));
}
Cave=SmoothMinDistance(Cave,Distance,static_cast<float>(P.BlendRadius));
Cave=VF_FieldSmoothMin(Cave,Distance,static_cast<float>(P.BlendRadius));
}
else if(X>=P.Min[0]&&X<=P.Max[0]&&Y>=P.Min[1]&&Y<=P.Max[1]
&&Z>=P.Min[2]&&Z<=P.Max[2])
Overlay=std::max(Overlay,-ShapeDistance(P,X,Y,Z));
Overlay=std::max(Overlay,-VF_FieldPrimitiveDistance(Shared,X,Y,Z));
}
}
if(OutCriticalAir)*OutCriticalAir=bCriticalAir;
return SmoothMinDistance(Base,std::max(Cave,Overlay),StrataCaveBlendRadius);
return VF_FieldSmoothMin(Base,std::max(Cave,Overlay),StrataCaveBlendRadius);
}
struct FOwnerNoiseLineContext
@@ -4094,6 +4279,7 @@ private:
std::vector<FInterval> LayerRanges;
std::vector<FLayerField> LayerFields;
std::vector<FPrimitive> Primitives;
std::vector<VF_FieldPrimitive> SharedFieldPrimitives;
std::vector<FFarConnectorSegment> FarConnectorSegments;
std::unordered_map<FBinKey,std::vector<uint32>,FBinHash> FarConnectorSpatialBins;
std::vector<FEditBrush> EditBrushes;