wip(L27, side branch): passages cross open voids inside shared natural rock columns
Unsupported passage runs > 12.5 m become a solid relief-bearing column (ceiling/island to ground, flared base) with the helix inside, for every recipe; FI anchor uses the same path. All 4 styles x both recipes: 0/0/0, 0 open-air, naturalness pass; no-long-bridge audit (L26 fails it). Deterministic, Clang = GCC. Look approved; cost +35% SW / +19% FI blocks merge (L28). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
1 parent
37a223046c
commit
07b3fe2852
14 files changed
+772
-201
No files matched your search
@@ -281,7 +281,7 @@ namespace floating_island_detail
|
||||
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
||||
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
||||
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
||||
if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 7
|
||||
if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8
|
||||
|| (recipe.geometryVersion == 1
|
||||
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
||||
|| recipe.unitsPerVoxel != Q
|
||||
@@ -753,7 +753,8 @@ namespace floating_island_detail
|
||||
const double rootBlend = std::clamp(radius * 0.12, 2.0 * Q, 5.0 * Q);
|
||||
for (std::int32_t root = 0; root < rootCount; ++root)
|
||||
sdf = SmoothMin(sdf, RootLobeDensity(recipe, island, root, x, y, z), rootBlend);
|
||||
if (recipe.geometryVersion >= 5 && island.ordinal == recipe.anchorIslandOrdinal
|
||||
if (recipe.geometryVersion >= 5 && recipe.geometryVersion < 8
|
||||
&& island.ordinal == recipe.anchorIslandOrdinal
|
||||
&& recipe.anchorRootBottomZSubvoxels < island.bottomZSubvoxels)
|
||||
{
|
||||
// The central buttress must meet the island crust underside. IslandBottom
|
||||
@@ -944,14 +945,14 @@ namespace floating_island_detail
|
||||
FloatingIslandAnchorV1& island)
|
||||
{
|
||||
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
||||
const bool isAnchor=recipe.geometryVersion>=5
|
||||
const bool isAnchorRoot=recipe.geometryVersion>=5&&recipe.geometryVersion<8
|
||||
&&island.ordinal==recipe.anchorIslandOrdinal;
|
||||
const std::int64_t reach = std::max(island.radiusSubvoxels,
|
||||
isAnchor?recipe.anchorRootRadiusSubvoxels:std::int64_t{0})
|
||||
isAnchorRoot?recipe.anchorRootRadiusSubvoxels:std::int64_t{0})
|
||||
+ recipe.contourWarpSubvoxels + pad;
|
||||
island.influenceMinimumSubvoxels = {island.centerSubvoxels[0] - reach,
|
||||
island.centerSubvoxels[1] - reach,
|
||||
(isAnchor?std::min(island.bottomZSubvoxels,recipe.anchorRootBottomZSubvoxels)
|
||||
(isAnchorRoot?std::min(island.bottomZSubvoxels,recipe.anchorRootBottomZSubvoxels)
|
||||
:island.bottomZSubvoxels)-pad};
|
||||
island.influenceMaximumSubvoxels = {island.centerSubvoxels[0] + reach,
|
||||
island.centerSubvoxels[1] + reach, island.topZSubvoxels + pad};
|
||||
@@ -1026,14 +1027,15 @@ namespace floating_island_detail
|
||||
{ error = "floating-island anchor has invalid coordinates or an inverted bound"; return false; }
|
||||
}
|
||||
const std::int64_t pad = IslandBlendMargin(recipe) + recipe.roughnessSubvoxels;
|
||||
const bool hasAnchorRoot=recipe.geometryVersion>=5&&recipe.geometryVersion<8&&isAnchor;
|
||||
const std::int64_t reach = std::max(island.radiusSubvoxels,
|
||||
isAnchor?recipe.anchorRootRadiusSubvoxels:std::int64_t{0})
|
||||
hasAnchorRoot?recipe.anchorRootRadiusSubvoxels:std::int64_t{0})
|
||||
+ recipe.contourWarpSubvoxels + pad;
|
||||
if (island.influenceMinimumSubvoxels[0] > island.centerSubvoxels[0] - reach
|
||||
|| island.influenceMaximumSubvoxels[0] < island.centerSubvoxels[0] + reach
|
||||
|| island.influenceMinimumSubvoxels[1] > island.centerSubvoxels[1] - reach
|
||||
|| island.influenceMaximumSubvoxels[1] < island.centerSubvoxels[1] + reach
|
||||
|| island.influenceMinimumSubvoxels[2] > (isAnchor
|
||||
|| island.influenceMinimumSubvoxels[2] > (hasAnchorRoot
|
||||
?std::min(island.bottomZSubvoxels,recipe.anchorRootBottomZSubvoxels)
|
||||
:island.bottomZSubvoxels) - pad
|
||||
|| island.influenceMaximumSubvoxels[2] < island.topZSubvoxels + pad
|
||||
|
||||
@@ -41,27 +41,31 @@ public:
|
||||
double SealThickness=0.0;
|
||||
};
|
||||
|
||||
const std::vector<FStandalonePassageBridgeRange>& GetPassageBridgeRanges() const
|
||||
{return PassageBridgeRanges;}
|
||||
const std::vector<FStandalonePassageVoidCrossing>& GetPassageVoidCrossings() const
|
||||
{return PassageVoidCrossings;}
|
||||
|
||||
bool IsResolvedPassageBridgeSample(std::size_t PassageIndex,std::size_t SegmentIndex,
|
||||
bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex,
|
||||
const VoxelForgeCore::Vec3d& Position) const
|
||||
{
|
||||
if(IsPassageBridgeSegment(PassageIndex,SegmentIndex)
|
||||
&&PassageBridgeSignedDistance(PassageIndex,SegmentIndex,
|
||||
static_cast<float>(Position.x),static_cast<float>(Position.y),
|
||||
static_cast<float>(Position.z))<=0.0)return true;
|
||||
for(const auto& range:PassageBridgeRanges)
|
||||
(void)SegmentIndex;
|
||||
for(const auto& range:PassageVoidCrossings)
|
||||
{
|
||||
if(range.passageIndex!=PassageIndex)continue;
|
||||
for(std::size_t segment=range.firstSegment;segment<=range.lastSegment;++segment)
|
||||
if(segment!=SegmentIndex&&PassageBridgeSignedDistance(PassageIndex,segment,
|
||||
static_cast<float>(Position.x),static_cast<float>(Position.y),
|
||||
static_cast<float>(Position.z))<=0.0)return true;
|
||||
if(!range.needsColumn||range.passageIndex!=PassageIndex)continue;
|
||||
if(PassageColumnSignedDistance(range,static_cast<float>(Position.x),
|
||||
static_cast<float>(Position.y),static_cast<float>(Position.z))<=0.0)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsResolvedPassageShortSpanSample(std::size_t PassageIndex,std::size_t SegmentIndex,
|
||||
const VoxelForgeCore::Vec3d& Position) const
|
||||
{
|
||||
return IsPassageShortSpanSegment(PassageIndex,SegmentIndex)
|
||||
&&PassageBridgeSignedDistance(PassageIndex,SegmentIndex,
|
||||
static_cast<float>(Position.x),static_cast<float>(Position.y),
|
||||
static_cast<float>(Position.z))<=0.0;
|
||||
}
|
||||
|
||||
double GetCoreVoxelSizeCm() const
|
||||
{
|
||||
return Owner == nullptr || Owner->CoreVoxelSizeMicrons == 0
|
||||
@@ -197,6 +201,13 @@ public:
|
||||
-passage.controlPoints.front().radiusSubvoxels+playerCenterAboveFloorQ;
|
||||
const double lastFloorQ=passage.controlPoints.back().positionSubvoxels[2]
|
||||
-passage.controlPoints.back().radiusSubvoxels+playerCenterAboveFloorQ;
|
||||
for(const auto& crossing:PassageVoidCrossings)
|
||||
{
|
||||
if(!crossing.needsColumn||crossing.passageIndex!=passageIndex
|
||||
||qz>crossing.topZSubvoxels||qz<crossing.bottomZSubvoxels)continue;
|
||||
const double columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z);
|
||||
composed=std::min(composed,static_cast<float>(columnDistance/Q));
|
||||
}
|
||||
if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels
|
||||
||qz<lastFloorQ-2.0*passage.mouthRadiusSubvoxels)
|
||||
{
|
||||
@@ -297,7 +308,7 @@ public:
|
||||
const double wallReliefWeight=1.0-0.80*verticalWallAlignment;
|
||||
const double roughSignedDistance=bestSignedDistance
|
||||
+static_cast<double>(std::clamp(relief,-1.0f,1.0f))*0.24*qPerMetre*wallReliefWeight;
|
||||
if(IsPassageBridgeSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
||||
if(IsPassageShortSpanSegment(passageIndex,static_cast<std::size_t>(bestIndex)))
|
||||
{
|
||||
const double bridgeDistance=PassageBridgeSignedDistance(passageIndex,
|
||||
static_cast<std::size_t>(bestIndex),X,Y,Z);
|
||||
@@ -317,8 +328,9 @@ public:
|
||||
const double radiusB=segmentB.radiusSubvoxels;
|
||||
// Keep the tread within the probe band and below the capsule's
|
||||
// lowest sampled point.
|
||||
const double floorOffset=IsPassageBridgeSegment(passageIndex,
|
||||
const double floorOffset=IsPassageShortSpanSegment(passageIndex,
|
||||
static_cast<std::size_t>(index))
|
||||
||IsPassageColumnSegment(passageIndex,static_cast<std::size_t>(index))
|
||||
?PassageBridgeDeckOffsetMetres(passageIndex,static_cast<std::size_t>(index))
|
||||
:0.20;
|
||||
const double horizontalLengthSq=dx*dx+dy*dy;
|
||||
@@ -387,11 +399,14 @@ public:
|
||||
considerSupportSegment(index);
|
||||
// Keep route treads inside the native rock envelope. Carving may reopen the
|
||||
// floor above this point, but a support cannot create a new surface in air.
|
||||
const bool bridgeFloorSupport=hasSupportFloor
|
||||
&&IsPassageBridgeSegment(passageIndex,
|
||||
const bool passageColumnFloorSupport=hasSupportFloor
|
||||
&&IsPassageColumnSegment(passageIndex,
|
||||
static_cast<std::size_t>(bestSupportSegmentIndex));
|
||||
const bool shortSpanFloorSupport=hasSupportFloor
|
||||
&&IsPassageShortSpanSegment(passageIndex,
|
||||
static_cast<std::size_t>(bestSupportSegmentIndex));
|
||||
if(hasSupportFloor&&bestPlayerCapsuleSignedDistance>=0.05*qPerMetre
|
||||
&&(density<0.0f||bridgeFloorSupport))
|
||||
&&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport))
|
||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||
}
|
||||
return composed;
|
||||
@@ -416,9 +431,8 @@ public:
|
||||
const double radius=static_cast<double>(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q;
|
||||
const double floorThicknessVoxels=Owner&&Owner->CoreVoxelSizeMicrons>0
|
||||
?1000000.0/Owner->CoreVoxelSizeMicrons:4.0;
|
||||
// A resolved passage bridge extends to a 3.5 m half-width and almost 5 m
|
||||
// below the bore floor. Keep those cells shape-dependent for classification
|
||||
// and streaming even before its bridge ranges have been compiled.
|
||||
// Short support spans can extend to a 3.5 m half-width and almost 5 m
|
||||
// below the bore floor. Keep those cells shape-dependent for classification.
|
||||
const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels);
|
||||
const double footDepth=std::max(floorThicknessVoxels,bridgeFootDepthVoxels);
|
||||
const double low[3]={
|
||||
@@ -435,6 +449,19 @@ public:
|
||||
if(overlap)return true;
|
||||
}
|
||||
}
|
||||
for(const auto& crossing:PassageVoidCrossings)
|
||||
{
|
||||
if(!crossing.needsColumn)continue;
|
||||
const double radius=static_cast<double>(crossing.maximumRadiusSubvoxels)/Q;
|
||||
const double low[3]={crossing.centerXSubvoxels/Q-radius,
|
||||
crossing.centerYSubvoxels/Q-radius,crossing.bottomZSubvoxels/Q};
|
||||
const double high[3]={crossing.centerXSubvoxels/Q+radius,
|
||||
crossing.centerYSubvoxels/Q+radius,crossing.topZSubvoxels/Q};
|
||||
bool overlap=true;
|
||||
for(int axis=0;axis<3;++axis)
|
||||
overlap&=high[axis]>=minimum[axis]&&low[axis]<=maximum[axis];
|
||||
if(overlap)return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -785,8 +812,10 @@ public:
|
||||
if(RoundTunnelsEnabled)
|
||||
BuildRoundTunnelPrimitives();
|
||||
BuildSpatialBins(LineBinSize);
|
||||
BuildPassageBridgePlan();
|
||||
Stats.LocalLineBinSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
|
||||
const auto SharedStage=Clock::now();
|
||||
RebuildSharedFieldPrimitives();
|
||||
Stats.SharedFieldSeconds=std::chrono::duration<double>(Clock::now()-SharedStage).count();
|
||||
Stats.ShapeSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
||||
Stats.PrimitiveCount=Primitives.size();
|
||||
if(Primitives.empty())
|
||||
@@ -797,6 +826,10 @@ public:
|
||||
RockWords.assign(static_cast<size_t>(Size[0])*Size[1]*WordsPerZ,0);
|
||||
SurfaceDistanceQ=std::make_unique_for_overwrite<int16[]>(static_cast<size_t>(NodeCount));
|
||||
BuildPackedRockIntervals(Stats.RockIntervalLines);
|
||||
// Native density queries for non-heightfield strates use the exact interval
|
||||
// evaluator. Build those native intervals before scanning passage support;
|
||||
// the scan itself still completes before any resolved column is composed.
|
||||
BuildPassageVoidCrossingPlan();
|
||||
BuildCandidateBands(Stats.CandidateAxisLines,Stats.CandidateMergeLines,
|
||||
Stats.CandidateMergeSeconds);
|
||||
if(!BuildGpuVisualDetailNodeSets(InGpuVisualDetailTiles,InCpuVisualDetailTiles,Error))
|
||||
@@ -820,9 +853,6 @@ public:
|
||||
return false;
|
||||
}
|
||||
Stats.PassageValidationSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
|
||||
Substage=Clock::now();
|
||||
RebuildSharedFieldPrimitives();
|
||||
Stats.SharedFieldSeconds=std::chrono::duration<double>(Clock::now()-Substage).count();
|
||||
Built=true;
|
||||
if(bIncludeFarWorld)BuildFarWorld();
|
||||
const uint64 WordBytes=static_cast<uint64>(RockWords.size()+CandidateWords.size())*sizeof(uint64);
|
||||
@@ -1693,23 +1723,27 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
bool IsPassageBridgeSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const
|
||||
bool IsPassageColumnSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const
|
||||
{
|
||||
return std::any_of(PassageBridgeRanges.begin(),PassageBridgeRanges.end(),
|
||||
[PassageIndex,SegmentIndex](const auto& range)
|
||||
{return range.passageIndex==PassageIndex
|
||||
&&SegmentIndex>=range.firstSegment&&SegmentIndex<=range.lastSegment;});
|
||||
return std::any_of(PassageVoidCrossings.begin(),PassageVoidCrossings.end(),
|
||||
[PassageIndex,SegmentIndex](const auto& crossing)
|
||||
{return crossing.needsColumn&&crossing.passageIndex==PassageIndex
|
||||
&&SegmentIndex>=crossing.firstSegment&&SegmentIndex<=crossing.lastSegment;});
|
||||
}
|
||||
|
||||
bool IsPassageShortSpanSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const
|
||||
{
|
||||
return std::any_of(PassageVoidCrossings.begin(),PassageVoidCrossings.end(),
|
||||
[PassageIndex,SegmentIndex](const auto& crossing)
|
||||
{return !crossing.needsColumn&&crossing.passageIndex==PassageIndex
|
||||
&&SegmentIndex>=crossing.firstSegment&&SegmentIndex<=crossing.lastSegment;});
|
||||
}
|
||||
|
||||
double PassageBridgeDeckOffsetMetres(std::size_t PassageIndex,
|
||||
std::size_t SegmentIndex) const
|
||||
{
|
||||
for(const auto& range:PassageBridgeRanges)
|
||||
{
|
||||
if(range.passageIndex!=PassageIndex||SegmentIndex>range.lastSegment)continue;
|
||||
if(SegmentIndex>=range.firstSegment)return 0.13;
|
||||
}
|
||||
return 0.20;
|
||||
(void)PassageIndex;(void)SegmentIndex;
|
||||
return 0.13;
|
||||
}
|
||||
|
||||
double PassageBridgeSignedDistance(std::size_t PassageIndex,std::size_t SegmentIndex,
|
||||
@@ -1760,6 +1794,109 @@ private:
|
||||
return outside+inside;
|
||||
}
|
||||
|
||||
double PassageColumnSignedDistance(const FStandalonePassageVoidCrossing& crossing,
|
||||
float X,float Y,float Z) const
|
||||
{
|
||||
if(crossing.maximumRadiusSubvoxels<=0||crossing.topZSubvoxels
|
||||
<=crossing.bottomZSubvoxels)return std::numeric_limits<double>::infinity();
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,
|
||||
ResolvedLayout?ResolvedLayout->coreVoxelSizeMicrometres:250000u);
|
||||
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,
|
||||
qz=static_cast<double>(Z)*Q;
|
||||
const double dx=qx-crossing.centerXSubvoxels;
|
||||
const double dy=qy-crossing.centerYSubvoxels;
|
||||
const double maximumRadius=static_cast<double>(crossing.maximumRadiusSubvoxels);
|
||||
const double horizontalSquared=dx*dx+dy*dy;
|
||||
if(horizontalSquared>maximumRadius*maximumRadius)
|
||||
return DeterministicSqrt(horizontalSquared)-maximumRadius;
|
||||
if(qz>crossing.topZSubvoxels)
|
||||
return qz-crossing.topZSubvoxels;
|
||||
if(qz<crossing.bottomZSubvoxels)
|
||||
return crossing.bottomZSubvoxels-qz;
|
||||
|
||||
// The lower-strate route is already walkable in its native field and binds
|
||||
// directly to the passage mouth. Preserve that player capsule as the column
|
||||
// flares into the ground, so the pillar joins the route instead of sealing it.
|
||||
if(ResolvedLayout&&crossing.passageIndex<ResolvedLayout->passages.size()
|
||||
&&qz<crossing.bottomZSubvoxels+18.0*qPerMetre)
|
||||
{
|
||||
const auto& passage=ResolvedLayout->passages[crossing.passageIndex];
|
||||
const auto lower=std::find_if(ResolvedLayout->strates.begin(),
|
||||
ResolvedLayout->strates.end(),[&](const auto& value)
|
||||
{return value.strateIndex==passage.lowerStrateIndex;});
|
||||
if(lower!=ResolvedLayout->strates.end())
|
||||
{
|
||||
const double capsuleRadius=0.37*qPerMetre;
|
||||
const double capsuleHalfHeight=0.86*qPerMetre;
|
||||
const double capsuleRadiusSquared=capsuleRadius*capsuleRadius;
|
||||
const VoxelForgeCore::ResolvedLayoutAnchorV2* previous=nullptr;
|
||||
for(const auto& anchor:lower->anchors)
|
||||
{
|
||||
if(anchor.kind!="route_point"||anchor.values.size()<3)continue;
|
||||
if(previous)
|
||||
{
|
||||
const double ax=previous->values[0],ay=previous->values[1];
|
||||
const double dx=static_cast<double>(anchor.values[0])-ax;
|
||||
const double dy=static_cast<double>(anchor.values[1])-ay;
|
||||
const double lengthSquared=dx*dx+dy*dy;
|
||||
const double t=lengthSquared>0.0?std::clamp(
|
||||
((qx-ax)*dx+(qy-ay)*dy)/lengthSquared,0.0,1.0):0.0;
|
||||
const double routeX=ax+t*dx,routeY=ay+t*dy;
|
||||
const double routeZ=previous->values[2]+t*(
|
||||
static_cast<double>(anchor.values[2])-previous->values[2]);
|
||||
const double routeDx=qx-routeX,routeDy=qy-routeY;
|
||||
if(routeDx*routeDx+routeDy*routeDy<=capsuleRadiusSquared
|
||||
&&std::abs(qz-routeZ)<=capsuleHalfHeight)
|
||||
return maximumRadius+qPerMetre;
|
||||
}
|
||||
previous=&anchor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const double height=static_cast<double>(crossing.topZSubvoxels
|
||||
-crossing.bottomZSubvoxels);
|
||||
const double t=std::clamp((crossing.topZSubvoxels-qz)/height,0.0,1.0);
|
||||
const auto smoothStep=[](double value)
|
||||
{
|
||||
value=std::clamp(value,0.0,1.0);
|
||||
return value*value*(3.0-2.0*value);
|
||||
};
|
||||
const float z=static_cast<float>(Z);
|
||||
const float scaledZ=z*0.12f;
|
||||
const float driftX=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x434f4c58u);
|
||||
const float driftY=VF_FieldSurfaceRelief(0.0f,0.0f,scaledZ,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x434f4c59u);
|
||||
const double axisX=crossing.centerXSubvoxels
|
||||
+std::clamp(static_cast<double>(driftX),-1.0,1.0)*0.35*qPerMetre;
|
||||
const double axisY=crossing.centerYSubvoxels
|
||||
+std::clamp(static_cast<double>(driftY),-1.0,1.0)*0.35*qPerMetre;
|
||||
const double centerDx=qx-axisX,centerDy=qy-axisY;
|
||||
const double radialDistance=DeterministicSqrt(centerDx*centerDx+centerDy*centerDy);
|
||||
const float broadRelief=VF_FieldSurfaceRelief(X*0.12f,Y*0.12f,Z*0.12f,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x434f4c52u);
|
||||
const float surfaceRelief=VF_FieldSurfaceRelief(X,Y,Z,
|
||||
static_cast<std::uint32_t>(Owner?Owner->Seed:0)^0x434f4c53u);
|
||||
const double baseFlare=6.0*qPerMetre*smoothStep((t-0.82)/0.18);
|
||||
const double ceilingFlare=2.0*qPerMetre*(1.0-smoothStep(t/0.075));
|
||||
const double irregularity=1.25*qPerMetre
|
||||
+std::clamp(static_cast<double>(broadRelief),-1.0,1.0)*1.10*qPerMetre
|
||||
+std::clamp(static_cast<double>(surfaceRelief),-1.0,1.0)*0.35*qPerMetre;
|
||||
const double radius=static_cast<double>(crossing.minimumRadiusSubvoxels)
|
||||
+std::max(0.15*qPerMetre,irregularity)+baseFlare+ceilingFlare;
|
||||
const double radialDistanceField=radialDistance-radius;
|
||||
const double topDistance=qz-crossing.topZSubvoxels;
|
||||
const double bottomDistance=crossing.bottomZSubvoxels-qz;
|
||||
const double verticalDistance=std::max(topDistance,bottomDistance);
|
||||
const double outside=DeterministicSqrt(
|
||||
std::max(radialDistanceField,0.0)*std::max(radialDistanceField,0.0)
|
||||
+std::max(verticalDistance,0.0)*std::max(verticalDistance,0.0));
|
||||
const double inside=std::min(std::max(radialDistanceField,verticalDistance),0.0);
|
||||
return outside+inside;
|
||||
}
|
||||
|
||||
float NativePassageDensityAt(float X,float Y,float Z) const
|
||||
{
|
||||
float density=0.0f;
|
||||
@@ -1775,33 +1912,25 @@ private:
|
||||
return ApplyResolvedStrateBoundaryBand(density,X,Y,Z);
|
||||
}
|
||||
|
||||
void BuildPassageBridgePlan()
|
||||
void BuildPassageVoidCrossingPlan()
|
||||
{
|
||||
PassageBridgeRanges.clear();
|
||||
PassageVoidCrossings.clear();
|
||||
if(!Owner||!ResolvedLayout||ResolvedLayout->passages.empty()
|
||||
||!Owner->PassageBridgeCache)return;
|
||||
auto& cache=*Owner->PassageBridgeCache;
|
||||
||!Owner->PassageVoidCrossingCache)return;
|
||||
auto& cache=*Owner->PassageVoidCrossingCache;
|
||||
std::lock_guard<std::mutex> lock(cache.mutex);
|
||||
const auto cached=cache.byLayout.find(ResolvedLayout->stableIdentity);
|
||||
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v1";
|
||||
const auto cached=cache.byLayout.find(cacheKey);
|
||||
if(cached!=cache.byLayout.end())
|
||||
{PassageBridgeRanges=cached->second;return;}
|
||||
{PassageVoidCrossings=cached->second;return;}
|
||||
|
||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,
|
||||
ResolvedLayout->coreVoxelSizeMicrometres);
|
||||
const auto inSurfaceWorldStratum=[&](double zSubvoxels)
|
||||
{
|
||||
const double zMicrometres=zSubvoxels
|
||||
*ResolvedLayout->coreVoxelSizeMicrometres/Q;
|
||||
return std::any_of(ResolvedLayout->strates.begin(),ResolvedLayout->strates.end(),
|
||||
[zMicrometres](const auto& stratum)
|
||||
{return stratum.generatorKind=="SurfaceWorld"
|
||||
&&zMicrometres>=stratum.interiorBottomMicrometres
|
||||
&&zMicrometres<stratum.interiorTopMicrometres;});
|
||||
};
|
||||
constexpr std::array<std::array<double,2>,5> supportOffsets{{
|
||||
{{0.0,0.0}},{{0.45,0.0}},{{-0.45,0.0}},{{0.0,0.45}},{{0.0,-0.45}}}};
|
||||
constexpr std::array<double,2> supportDepthsMetres{{0.125,0.25}};
|
||||
constexpr double shortSpanMaximumMetres=12.5; // 2.5 x the 5 m legacy support thickness.
|
||||
struct FPathSample
|
||||
{
|
||||
std::size_t segment=0;
|
||||
@@ -1818,14 +1947,10 @@ private:
|
||||
for(const double depth:supportDepthsMetres)
|
||||
{
|
||||
const double sampleZ=floorQ-depth*qPerMetre;
|
||||
// Bridge spans address the measured SurfaceWorld void. Other native
|
||||
// strata retain their own route/cave composition and are excluded here.
|
||||
if(!inSurfaceWorldStratum(sampleZ)){hasSolid=true;break;}
|
||||
float density=1.0f;
|
||||
if(!NativeSurfaceDensityAt(
|
||||
const float density=NativePassageDensityAt(
|
||||
static_cast<float>((point[0]+offset[0]*qPerMetre)/Q),
|
||||
static_cast<float>((point[1]+offset[1]*qPerMetre)/Q),
|
||||
static_cast<float>(sampleZ/Q),density))continue;
|
||||
static_cast<float>(sampleZ/Q));
|
||||
if(density<0.0f){hasSolid=true;break;}
|
||||
}
|
||||
supported+=hasSolid;
|
||||
@@ -1874,9 +1999,10 @@ private:
|
||||
const std::size_t gapBegin=cursor;
|
||||
while(cursor<samples.size()&&!samples[cursor].nativeSupported)++cursor;
|
||||
const std::size_t gapEnd=cursor-1;
|
||||
if(gapBegin==0||cursor>=samples.size())continue;
|
||||
const std::size_t startSample=gapBegin>0?gapBegin-1:gapBegin;
|
||||
const std::size_t endSample=cursor<samples.size()?cursor:samples.size()-1;
|
||||
double gapLength=0.0;
|
||||
for(std::size_t index=gapBegin;index<=gapEnd;++index)
|
||||
for(std::size_t index=startSample+1;index<=endSample;++index)
|
||||
{
|
||||
const auto& a=samples[index-1].point;
|
||||
const auto& b=samples[index].point;
|
||||
@@ -1884,25 +2010,105 @@ private:
|
||||
gapLength+=DeterministicSqrt(dx*dx+dy*dy+dz*dz)/qPerMetre;
|
||||
}
|
||||
if(gapLength<1.0)continue;
|
||||
FStandalonePassageBridgeRange range;
|
||||
range.passageIndex=passageIndex;
|
||||
range.firstSegment=samples[gapBegin-1].segment;
|
||||
range.lastSegment=samples[cursor].segment;
|
||||
range.unsupportedSamples=gapEnd-gapBegin+1;
|
||||
range.unsupportedLengthMetres=gapLength;
|
||||
range.startPoint=samples[gapBegin-1].point;
|
||||
range.endPoint=samples[cursor].point;
|
||||
PassageBridgeRanges.push_back(range);
|
||||
std::printf("[PassageRockBridgePlan] id=%s first_segment=%zu last_segment=%zu unsupported_samples=%llu gap_length_m=%.3f connected_start=1 connected_end=1 width_m=6.000 thickness_m=3.000..5.000 status=PLANNED\n",
|
||||
passage.stableId.c_str(),range.firstSegment,range.lastSegment,
|
||||
static_cast<unsigned long long>(range.unsupportedSamples),gapLength);
|
||||
FStandalonePassageVoidCrossing crossing;
|
||||
crossing.passageIndex=passageIndex;
|
||||
crossing.firstSegment=samples[startSample].segment;
|
||||
crossing.lastSegment=samples[endSample].segment;
|
||||
crossing.unsupportedSamples=gapEnd-gapBegin+1;
|
||||
crossing.unsupportedLengthMetres=gapLength;
|
||||
crossing.startPoint=samples[startSample].point;
|
||||
crossing.endPoint=samples[endSample].point;
|
||||
crossing.connectedAtStart=gapBegin>0&&samples[startSample].nativeSupported;
|
||||
crossing.connectedAtEnd=cursor<samples.size()&&samples[endSample].nativeSupported;
|
||||
crossing.needsColumn=gapLength>shortSpanMaximumMetres;
|
||||
if(crossing.needsColumn&&passage.controlPoints.size()>1)
|
||||
crossing.lastSegment=std::min(crossing.lastSegment+1,
|
||||
passage.controlPoints.size()-2);
|
||||
if(crossing.needsColumn)
|
||||
{
|
||||
std::int64_t sumX=0,sumY=0;
|
||||
std::uint64_t centerSamples=0;
|
||||
for(std::size_t index=gapBegin;index<=gapEnd;++index)
|
||||
{
|
||||
sumX+=static_cast<std::int64_t>(std::llround(samples[index].point[0]));
|
||||
sumY+=static_cast<std::int64_t>(std::llround(samples[index].point[1]));
|
||||
++centerSamples;
|
||||
}
|
||||
crossing.centerXSubvoxels=centerSamples>0
|
||||
?static_cast<std::int64_t>(std::llround(static_cast<double>(sumX)/centerSamples)):0;
|
||||
crossing.centerYSubvoxels=centerSamples>0
|
||||
?static_cast<std::int64_t>(std::llround(static_cast<double>(sumY)/centerSamples)):0;
|
||||
for(const auto& stratum:ResolvedLayout->strates)
|
||||
if(stratum.strateIndex==passage.upperStrateIndex
|
||||
&&stratum.generatorKind=="FloatingIslands")
|
||||
for(const auto& anchor:stratum.anchors)
|
||||
if(anchor.kind=="passage_anchor"&&anchor.values.size()>=2)
|
||||
{
|
||||
crossing.centerXSubvoxels=anchor.values[0];
|
||||
crossing.centerYSubvoxels=anchor.values[1];
|
||||
break;
|
||||
}
|
||||
double maximumOffsetSubvoxels=0.0;
|
||||
double maximumBoreSubvoxels=0.0;
|
||||
for(std::size_t index=startSample;index<=endSample;++index)
|
||||
{
|
||||
const auto& point=samples[index].point;
|
||||
const double dx=point[0]-crossing.centerXSubvoxels;
|
||||
const double dy=point[1]-crossing.centerYSubvoxels;
|
||||
maximumOffsetSubvoxels=std::max(maximumOffsetSubvoxels,
|
||||
DeterministicSqrt(dx*dx+dy*dy));
|
||||
maximumBoreSubvoxels=std::max(maximumBoreSubvoxels,point[3]);
|
||||
}
|
||||
const double maximumOffsetMetres=maximumOffsetSubvoxels/qPerMetre;
|
||||
const double maximumBoreMetres=maximumBoreSubvoxels/qPerMetre;
|
||||
const double minimumRadiusMetres=maximumOffsetMetres+maximumBoreMetres+2.0;
|
||||
const double nominalRadiusMetres=minimumRadiusMetres+1.25;
|
||||
crossing.maximumRouteOffsetMetres=maximumOffsetMetres;
|
||||
crossing.maximumBoreRadiusMetres=maximumBoreMetres;
|
||||
crossing.minimumRadiusSubvoxels=static_cast<std::int64_t>(std::ceil(
|
||||
minimumRadiusMetres*qPerMetre));
|
||||
crossing.nominalRadiusSubvoxels=static_cast<std::int64_t>(std::llround(
|
||||
nominalRadiusMetres*qPerMetre));
|
||||
crossing.maximumRadiusSubvoxels=crossing.minimumRadiusSubvoxels
|
||||
+static_cast<std::int64_t>(std::ceil(11.25*qPerMetre));
|
||||
const double startZ=crossing.startPoint[2];
|
||||
const double endZ=crossing.endPoint[2];
|
||||
crossing.topZSubvoxels=static_cast<std::int64_t>(std::llround(
|
||||
std::max(startZ,endZ)+5.0*qPerMetre));
|
||||
crossing.bottomZSubvoxels=static_cast<std::int64_t>(std::llround(
|
||||
std::min(startZ,endZ)-6.0*qPerMetre));
|
||||
}
|
||||
PassageVoidCrossings.push_back(crossing);
|
||||
if(crossing.needsColumn)
|
||||
{
|
||||
const double coreRadius=crossing.nominalRadiusSubvoxels/qPerMetre;
|
||||
const double minimumWall=coreRadius-crossing.maximumRouteOffsetMetres
|
||||
-crossing.maximumBoreRadiusMetres;
|
||||
std::printf("[PassageVoidColumnPlan] id=%s segments=%zu..%zu unsupported_samples=%llu span_m=%.3f threshold_m=%.3f connected=%d/%d axis_m=(%.3f,%.3f) z_m=%.3f..%.3f route_radius_m=%.3f bore_radius_m=%.3f min_radius_m=%.3f nominal_radius_m=%.3f max_radius_m=%.3f nominal_wall_m=%.3f flare_m=6.000 status=%s\n",
|
||||
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
||||
static_cast<unsigned long long>(crossing.unsupportedSamples),gapLength,
|
||||
shortSpanMaximumMetres,static_cast<int>(crossing.connectedAtStart),
|
||||
static_cast<int>(crossing.connectedAtEnd),
|
||||
crossing.centerXSubvoxels/qPerMetre,crossing.centerYSubvoxels/qPerMetre,
|
||||
crossing.topZSubvoxels/qPerMetre,crossing.bottomZSubvoxels/qPerMetre,
|
||||
crossing.maximumRouteOffsetMetres,crossing.maximumBoreRadiusMetres,
|
||||
crossing.minimumRadiusSubvoxels/qPerMetre,coreRadius,
|
||||
crossing.maximumRadiusSubvoxels/qPerMetre,minimumWall,
|
||||
crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED");
|
||||
}
|
||||
else
|
||||
std::printf("[PassageShortGroundedSpanPlan] id=%s segments=%zu..%zu span_m=%.3f threshold_m=%.3f connected=%d/%d width_m=6.000 thickness_m=3.000..5.000 status=%s\n",
|
||||
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
||||
gapLength,shortSpanMaximumMetres,static_cast<int>(crossing.connectedAtStart),
|
||||
static_cast<int>(crossing.connectedAtEnd),
|
||||
crossing.connectedAtStart&&crossing.connectedAtEnd?"PLANNED":"UNATTACHED");
|
||||
}
|
||||
if(std::none_of(PassageBridgeRanges.begin(),PassageBridgeRanges.end(),
|
||||
if(std::none_of(PassageVoidCrossings.begin(),PassageVoidCrossings.end(),
|
||||
[passageIndex](const auto& range){return range.passageIndex==passageIndex;}))
|
||||
std::printf("[PassageRockBridgePlan] id=%s spans=0 status=NOT_REQUIRED\n",
|
||||
std::printf("[PassageVoidColumnPlan] id=%s crossings=0 status=NOT_REQUIRED\n",
|
||||
passage.stableId.c_str());
|
||||
}
|
||||
cache.byLayout.emplace(ResolvedLayout->stableIdentity,PassageBridgeRanges);
|
||||
cache.byLayout.emplace(cacheKey,PassageVoidCrossings);
|
||||
}
|
||||
|
||||
static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z)
|
||||
@@ -6151,7 +6357,7 @@ private:
|
||||
std::vector<FFarConnectorSegment> FarConnectorSegments;
|
||||
std::unordered_map<FBinKey,std::vector<uint32>,FBinHash> FarConnectorSpatialBins;
|
||||
std::vector<FEditBrush> EditBrushes;
|
||||
std::vector<FStandalonePassageBridgeRange> PassageBridgeRanges;
|
||||
std::vector<FStandalonePassageVoidCrossing> PassageVoidCrossings;
|
||||
std::array<FAxisBins,3> AxisBins;
|
||||
std::unordered_map<FBinKey,FBinItems,FBinHash> SpatialBins;
|
||||
std::unordered_map<FBinKey,FBinItems,FBinHash> CriticalAirBins;
|
||||
|
||||
@@ -1542,27 +1542,43 @@ namespace VoxelForgeCore
|
||||
|
||||
float World::densityAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
std::vector<PassageBridgeRangeAuditV1> World::passageBridgeRanges() const
|
||||
std::vector<PassageVoidCrossingAuditV1> World::passageVoidCrossings() const
|
||||
{
|
||||
std::vector<PassageBridgeRangeAuditV1> result;
|
||||
std::vector<PassageVoidCrossingAuditV1> result;
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)return result;
|
||||
const auto& ranges=impl_->shape->GetPassageBridgeRanges();
|
||||
result.reserve(ranges.size());
|
||||
for(const auto& range:ranges)
|
||||
const auto& crossings=impl_->shape->GetPassageVoidCrossings();
|
||||
result.reserve(crossings.size());
|
||||
for(const auto& crossing:crossings)
|
||||
{
|
||||
PassageBridgeRangeAuditV1 value;
|
||||
value.passageIndex=range.passageIndex;value.firstSegment=range.firstSegment;
|
||||
value.lastSegment=range.lastSegment;value.unsupportedSamples=range.unsupportedSamples;
|
||||
value.unsupportedLengthMetres=range.unsupportedLengthMetres;
|
||||
value.startPointSubvoxels=range.startPoint;value.endPointSubvoxels=range.endPoint;
|
||||
PassageVoidCrossingAuditV1 value;
|
||||
value.passageIndex=crossing.passageIndex;value.firstSegment=crossing.firstSegment;
|
||||
value.lastSegment=crossing.lastSegment;value.unsupportedSamples=crossing.unsupportedSamples;
|
||||
value.unsupportedLengthMetres=crossing.unsupportedLengthMetres;
|
||||
value.startPointSubvoxels=crossing.startPoint;value.endPointSubvoxels=crossing.endPoint;
|
||||
value.needsColumn=crossing.needsColumn;
|
||||
value.connectedAtStart=crossing.connectedAtStart;
|
||||
value.connectedAtEnd=crossing.connectedAtEnd;
|
||||
value.columnAxisSubvoxels={crossing.centerXSubvoxels,crossing.centerYSubvoxels};
|
||||
value.columnTopZSubvoxels=crossing.topZSubvoxels;
|
||||
value.columnBottomZSubvoxels=crossing.bottomZSubvoxels;
|
||||
value.columnMinimumRadiusSubvoxels=crossing.minimumRadiusSubvoxels;
|
||||
value.columnNominalRadiusSubvoxels=crossing.nominalRadiusSubvoxels;
|
||||
value.columnMaximumRadiusSubvoxels=crossing.maximumRadiusSubvoxels;
|
||||
value.maximumRouteOffsetMetres=crossing.maximumRouteOffsetMetres;
|
||||
value.maximumBoreRadiusMetres=crossing.maximumBoreRadiusMetres;
|
||||
result.push_back(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool World::isPassageBridgeSample(std::size_t passageIndex,std::size_t segmentIndex,
|
||||
bool World::isPassageColumnSample(std::size_t passageIndex,std::size_t segmentIndex,
|
||||
Vec3d position) const
|
||||
{return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
||||
&&impl_->shape->IsResolvedPassageBridgeSample(passageIndex,segmentIndex,
|
||||
&&impl_->shape->IsResolvedPassageColumnSample(passageIndex,segmentIndex,
|
||||
{position.x,position.y,position.z});}
|
||||
bool World::isPassageShortSpanSample(std::size_t passageIndex,std::size_t segmentIndex,
|
||||
Vec3d position) const
|
||||
{return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
||||
&&impl_->shape->IsResolvedPassageShortSpanSample(passageIndex,segmentIndex,
|
||||
{position.x,position.y,position.z});}
|
||||
std::string World::densityDispatchPathAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape
|
||||
|
||||
Reference in new issue
Block a user