wip(L28): passage columns/routes culled per tile (BVH); cost back near L24, bit-identical to L27
GenerateMesh scopes passage queries to the tile's sample bounds with a feature AABB BVH plus per-passage segment BVHs; L9 proof boxes use a passage-bound BVH. Whole view vs L24: FI ~0%, SW +3.7% (reviewer interleaved). All hashes equal L27; all passage/route/proof audits pass. 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
07b3fe2852
commit
f23ab7b9fa
5 files changed
+938
-49
No files matched your search
@@ -2034,6 +2034,23 @@ adapter edit is required. The L26 report contains the eight-style table, hashes,
|
|||||||
[`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md)
|
[`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md)
|
||||||
records the final evidence.
|
records the final evidence.
|
||||||
|
|
||||||
|
### 8.45 Tile-bounded passage evaluation — World Assembler M3 Linux L28
|
||||||
|
|
||||||
|
Each mesh tile scopes passage composition to the inclusive density-sample AABB, including the
|
||||||
|
mesher's normal halo. A conservative feature AABB BVH selects route segments, columns and mouth
|
||||||
|
support for that tile. A per-passage nearest-segment BVH bounds the long helix search; a separate
|
||||||
|
passage-bound BVH rejects far L9 proof boxes. An unindexed native-only proof object uses a conservative
|
||||||
|
linear fallback. No field, union order, serialized layout or recipe version changes.
|
||||||
|
|
||||||
|
The column's exact complex SDF is evaluated only in XY/Z feature candidates. A cheap density-aware
|
||||||
|
radial guard remains for samples sharing the column's Z slab: a positive column distance can still
|
||||||
|
lower positive base density, so skipping that guard needs an exact bound on the base field. This is
|
||||||
|
the remaining SurfaceWorld cost noted in the Linux L28 report. The passage candidate lists and
|
||||||
|
proof bounds preserve the L27 output.
|
||||||
|
|
||||||
|
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md)
|
||||||
|
records counts, parity, costs and audit caveats.
|
||||||
|
|
||||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||||
|
|||||||
@@ -1208,6 +1208,15 @@ The shared evaluator is version 13; FloatingIslands geometry remains version 7 a
|
|||||||
schema V3 is unchanged. See ARCHITECTURE §8.44 and
|
schema V3 is unchanged. See ARCHITECTURE §8.44 and
|
||||||
[`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md).
|
[`Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux26/REPORT.md).
|
||||||
|
|
||||||
|
### 3.28 Tile-bounded passage evaluation — World Assembler M3 Linux L28
|
||||||
|
|
||||||
|
| Symbol | Canonical file | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| FScopedPassageTileQueries / BuildPassageFeatureTileList / ResolvedPassageIntersectsBox | Standalone/Core/src/ShapeWorldInternal.h; scope entered by GenerateMesh in Standalone/Core/src/VoxelForgeCore.cpp | Builds conservative per-tile route, column and mouth-support candidates from an AABB BVH; long paths use a nearest-segment BVH and L9 box checks use a separate passage-bound BVH. Empty native-only proof indexes fall back conservatively. The density arithmetic and serialized versions stay unchanged. |
|
||||||
|
|
||||||
|
See ARCHITECTURE §8.45 and
|
||||||
|
[Linux28 report](Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md).
|
||||||
|
|
||||||
## 4. The density pipeline (most-edited hot path)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
|
||||||
|
|||||||
@@ -44,6 +44,65 @@ public:
|
|||||||
const std::vector<FStandalonePassageVoidCrossing>& GetPassageVoidCrossings() const
|
const std::vector<FStandalonePassageVoidCrossing>& GetPassageVoidCrossings() const
|
||||||
{return PassageVoidCrossings;}
|
{return PassageVoidCrossings;}
|
||||||
|
|
||||||
|
class FScopedPassageTileQueries
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FScopedPassageTileQueries(const FStandaloneShapeWorld& shape,
|
||||||
|
const std::array<std::int64_t,3>& minimumSubvoxels,
|
||||||
|
const std::array<std::int64_t,3>& maximumSubvoxels)
|
||||||
|
:shape_(&shape),minimumSubvoxels_(minimumSubvoxels),
|
||||||
|
maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries)
|
||||||
|
{
|
||||||
|
featureIndices_=shape.BuildPassageFeatureTileList(
|
||||||
|
minimumSubvoxels_,maximumSubvoxels_);
|
||||||
|
passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0);
|
||||||
|
passageColumns_=passageRoutes_;
|
||||||
|
passageMouthSupports_=passageRoutes_;
|
||||||
|
for(const auto& crossing:shape.PassageVoidCrossings)
|
||||||
|
if(crossing.needsColumn
|
||||||
|
&&crossing.bottomZSubvoxels<=maximumSubvoxels_[2]
|
||||||
|
&&crossing.topZSubvoxels>=minimumSubvoxels_[2])
|
||||||
|
columnZOverlap_=true;
|
||||||
|
for(const std::uint32_t featureIndex:featureIndices_)
|
||||||
|
{
|
||||||
|
if(featureIndex>=shape.PassageTileFeatures.size())continue;
|
||||||
|
const auto& feature=shape.PassageTileFeatures[featureIndex];
|
||||||
|
if(feature.passageIndex>=passageRoutes_.size())continue;
|
||||||
|
if(feature.bColumn)passageColumns_[feature.passageIndex]=1;
|
||||||
|
else if(feature.bMouthSupport)passageMouthSupports_[feature.passageIndex]=1;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
passageRoutes_[feature.passageIndex]=1;
|
||||||
|
routeFeatureOverlap_=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActivePassageTileQueries=this;
|
||||||
|
}
|
||||||
|
~FScopedPassageTileQueries(){ActivePassageTileQueries=previous_;}
|
||||||
|
FScopedPassageTileQueries(const FScopedPassageTileQueries&)=delete;
|
||||||
|
FScopedPassageTileQueries& operator=(const FScopedPassageTileQueries&)=delete;
|
||||||
|
|
||||||
|
bool matches(const FStandaloneShapeWorld& shape) const{return shape_==&shape;}
|
||||||
|
bool hasFeature(std::size_t passageIndex,bool columnFeature) const
|
||||||
|
{
|
||||||
|
const auto& candidates=columnFeature?passageColumns_:passageRoutes_;
|
||||||
|
return passageIndex<candidates.size()&&candidates[passageIndex]!=0;
|
||||||
|
}
|
||||||
|
bool hasMouthSupport(std::size_t passageIndex) const
|
||||||
|
{return passageIndex<passageMouthSupports_.size()&&passageMouthSupports_[passageIndex]!=0;}
|
||||||
|
bool hasRouteFeatureOverlap() const{return routeFeatureOverlap_;}
|
||||||
|
bool hasColumnZOverlap() const{return columnZOverlap_;}
|
||||||
|
std::size_t featureCount() const{return featureIndices_.size();}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const FStandaloneShapeWorld* shape_;
|
||||||
|
std::array<std::int64_t,3> minimumSubvoxels_,maximumSubvoxels_;
|
||||||
|
std::vector<std::uint32_t> featureIndices_;
|
||||||
|
std::vector<std::uint8_t> passageRoutes_,passageColumns_,passageMouthSupports_;
|
||||||
|
bool routeFeatureOverlap_=false,columnZOverlap_=false;
|
||||||
|
const FScopedPassageTileQueries* previous_;
|
||||||
|
};
|
||||||
|
|
||||||
bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex,
|
bool IsResolvedPassageColumnSample(std::size_t PassageIndex,std::size_t SegmentIndex,
|
||||||
const VoxelForgeCore::Vec3d& Position) const
|
const VoxelForgeCore::Vec3d& Position) const
|
||||||
{
|
{
|
||||||
@@ -126,6 +185,12 @@ public:
|
|||||||
{
|
{
|
||||||
if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X)
|
if(!ResolvedLayout||ResolvedLayout->passages.empty()||!std::isfinite(X)
|
||||||
||!std::isfinite(Y)||!std::isfinite(Z))return density;
|
||!std::isfinite(Y)||!std::isfinite(Z))return density;
|
||||||
|
const auto* passageTileQueries=ActivePassageTileQueries;
|
||||||
|
const bool passageTileScopeMatches=passageTileQueries
|
||||||
|
&&passageTileQueries->matches(*this);
|
||||||
|
if(passageTileScopeMatches&&density>=0.0f
|
||||||
|
&&!passageTileQueries->hasRouteFeatureOverlap()
|
||||||
|
&&!passageTileQueries->hasColumnZOverlap())return density;
|
||||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||||
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,qz=static_cast<double>(Z)*Q;
|
const double qx=static_cast<double>(X)*Q,qy=static_cast<double>(Y)*Q,qz=static_cast<double>(Z)*Q;
|
||||||
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
|
||||||
@@ -190,9 +255,14 @@ public:
|
|||||||
// Keep the native surface route connected to the entrance. Passage carving
|
// Keep the native surface route connected to the entrance. Passage carving
|
||||||
// then removes only the parts that overlap the tunnel, whose own tread owns
|
// then removes only the parts that overlap the tunnel, whose own tread owns
|
||||||
// the interior ramp.
|
// the interior ramp.
|
||||||
for(const auto& passage:ResolvedLayout->passages)
|
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||||
|
{
|
||||||
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||||
|
const bool tileHasSupport=passageTileScopeMatches;
|
||||||
|
if(!tileHasSupport||ActivePassageTileQueries->hasMouthSupport(passageIndex))
|
||||||
composeRouteMouthSupport(passage.upperStrateIndex,
|
composeRouteMouthSupport(passage.upperStrateIndex,
|
||||||
passage.upperMouthSubvoxels,true);
|
passage.upperMouthSubvoxels,true);
|
||||||
|
}
|
||||||
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||||
{
|
{
|
||||||
const auto& passage=ResolvedLayout->passages[passageIndex];
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||||
@@ -205,9 +275,31 @@ public:
|
|||||||
{
|
{
|
||||||
if(!crossing.needsColumn||crossing.passageIndex!=passageIndex
|
if(!crossing.needsColumn||crossing.passageIndex!=passageIndex
|
||||||
||qz>crossing.topZSubvoxels||qz<crossing.bottomZSubvoxels)continue;
|
||qz>crossing.topZSubvoxels||qz<crossing.bottomZSubvoxels)continue;
|
||||||
const double columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z);
|
double columnDistance;
|
||||||
|
const bool tileProvesColumnExterior=passageTileScopeMatches
|
||||||
|
&&!ActivePassageTileQueries->hasFeature(passageIndex,true);
|
||||||
|
if(tileProvesColumnExterior)
|
||||||
|
{
|
||||||
|
if(composed<0.0f)continue;
|
||||||
|
const double dx=qx-crossing.centerXSubvoxels;
|
||||||
|
const double dy=qy-crossing.centerYSubvoxels;
|
||||||
|
const double horizontalSquared=dx*dx+dy*dy;
|
||||||
|
const double maximumRadius=static_cast<double>(crossing.maximumRadiusSubvoxels);
|
||||||
|
const double exteriorLimit=maximumRadius
|
||||||
|
+static_cast<double>(composed)*Q+1.0e-4;
|
||||||
|
if(horizontalSquared>exteriorLimit*exteriorLimit)continue;
|
||||||
|
columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z);
|
||||||
composed=std::min(composed,static_cast<float>(columnDistance/Q));
|
composed=std::min(composed,static_cast<float>(columnDistance/Q));
|
||||||
}
|
}
|
||||||
|
const bool tilePassageFree=passageTileScopeMatches
|
||||||
|
&&!ActivePassageTileQueries->hasFeature(passageIndex,false);
|
||||||
|
if(composed>=0.0f&&tilePassageFree)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels
|
if(qz>firstFloorQ+2.0*passage.mouthRadiusSubvoxels
|
||||||
||qz<lastFloorQ-2.0*passage.mouthRadiusSubvoxels)
|
||qz<lastFloorQ-2.0*passage.mouthRadiusSubvoxels)
|
||||||
{
|
{
|
||||||
@@ -234,15 +326,40 @@ public:
|
|||||||
(carveHigh-passage.controlPoints.begin())-1);
|
(carveHigh-passage.controlPoints.begin())-1);
|
||||||
const std::ptrdiff_t carveEnd=std::min<std::ptrdiff_t>(lowerMouthIndex-1,
|
const std::ptrdiff_t carveEnd=std::min<std::ptrdiff_t>(lowerMouthIndex-1,
|
||||||
(carveLow-passage.controlPoints.begin())+1);
|
(carveLow-passage.controlPoints.begin())+1);
|
||||||
|
const std::ptrdiff_t mouthSearchBegin=std::max<std::ptrdiff_t>(0,lowerMouthIndex-8);
|
||||||
|
const std::ptrdiff_t mouthSearchEnd=std::min<std::ptrdiff_t>(lastIndex,
|
||||||
|
lowerMouthIndex+8);
|
||||||
|
if(density<0.0f&&tilePassageFree
|
||||||
|
&&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd,
|
||||||
|
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz,
|
||||||
|
-static_cast<double>(composed)*Q+0.24*qPerMetre))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// For a positive source sample, the tube can only change it within relief reach;
|
||||||
|
// the supported column tread is within 2.25 m of its centerline. A short-span
|
||||||
|
// bridge's positive exterior SDF is not bounded this way, so keep its full query.
|
||||||
|
// Pour un échantillon source positif, le tube ne change le champ qu'à portée du relief ;
|
||||||
|
// le plancher du pilier reste à 2,25 m de son axe. Le SDF extérieur d'une courte passerelle
|
||||||
|
// n'a pas cette borne, donc sa requête complète est conservée.
|
||||||
|
const bool canCullPositiveRoute=passageTileScopeMatches&&density>=0.0f
|
||||||
|
&&!PassageHasShortSpanSegments(passageIndex);
|
||||||
|
if(canCullPositiveRoute&&!PassageMayAffectDensity(passageIndex,carveBegin,carveEnd,
|
||||||
|
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz,
|
||||||
|
std::max(2.25*qPerMetre,-static_cast<double>(composed)*Q
|
||||||
|
+0.24*qPerMetre)))continue;
|
||||||
double bestSignedDistance=std::numeric_limits<double>::infinity();
|
double bestSignedDistance=std::numeric_limits<double>::infinity();
|
||||||
double bestPlayerCapsuleSignedDistance=std::numeric_limits<double>::infinity();
|
double bestPlayerCapsuleSignedDistance=std::numeric_limits<double>::infinity();
|
||||||
double bestT=0.0;
|
double bestT=0.0;
|
||||||
std::ptrdiff_t bestIndex=0;
|
std::ptrdiff_t bestIndex=0;
|
||||||
|
int bestScanPhase=std::numeric_limits<int>::max();
|
||||||
|
std::ptrdiff_t bestScanIndex=std::numeric_limits<std::ptrdiff_t>::max();
|
||||||
std::ptrdiff_t bestSupportSegmentIndex=0;
|
std::ptrdiff_t bestSupportSegmentIndex=0;
|
||||||
bool hasSupportFloor=false;
|
bool hasSupportFloor=false;
|
||||||
double bestSupportDensity=0.0;
|
double bestSupportDensity=0.0;
|
||||||
const auto considerCarveSegment=[&](std::ptrdiff_t index)
|
const auto considerCarveSegment=[&](std::size_t segmentIndex,int scanPhase)
|
||||||
{
|
{
|
||||||
|
const std::ptrdiff_t index=static_cast<std::ptrdiff_t>(segmentIndex);
|
||||||
const auto& a=passage.controlPoints[static_cast<std::size_t>(index)];
|
const auto& a=passage.controlPoints[static_cast<std::size_t>(index)];
|
||||||
const auto& b=passage.controlPoints[static_cast<std::size_t>(index+1)];
|
const auto& b=passage.controlPoints[static_cast<std::size_t>(index+1)];
|
||||||
const double ax=static_cast<double>(a.positionSubvoxels[0]);
|
const double ax=static_cast<double>(a.positionSubvoxels[0]);
|
||||||
@@ -254,10 +371,8 @@ public:
|
|||||||
const double lengthSq=dx*dx+dy*dy+dz*dz;
|
const double lengthSq=dx*dx+dy*dy+dz*dz;
|
||||||
if(lengthSq<=0.0)return;
|
if(lengthSq<=0.0)return;
|
||||||
const double t=std::clamp(((qx-ax)*dx+(qy-ay)*dy+(qz-az)*dz)/lengthSq,0.0,1.0);
|
const double t=std::clamp(((qx-ax)*dx+(qy-ay)*dy+(qz-az)*dz)/lengthSq,0.0,1.0);
|
||||||
const double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
if(!passageTileScopeMatches)
|
||||||
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
{
|
||||||
const double radius=static_cast<double>(a.radiusSubvoxels)
|
|
||||||
+t*(b.radiusSubvoxels-a.radiusSubvoxels);
|
|
||||||
const double horizontalLengthSq=dx*dx+dy*dy;
|
const double horizontalLengthSq=dx*dx+dy*dy;
|
||||||
const double playerT=horizontalLengthSq>0.0
|
const double playerT=horizontalLengthSq>0.0
|
||||||
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):t;
|
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):t;
|
||||||
@@ -268,31 +383,52 @@ public:
|
|||||||
const double playerCenterZ=az+playerT*dz-playerRadius+playerCenterAboveFloorQ;
|
const double playerCenterZ=az+playerT*dz-playerRadius+playerCenterAboveFloorQ;
|
||||||
const double playerVertical=std::max(0.0,
|
const double playerVertical=std::max(0.0,
|
||||||
std::abs(qz-playerCenterZ)-0.54*qPerMetre);
|
std::abs(qz-playerCenterZ)-0.54*qPerMetre);
|
||||||
const double playerCapsuleDistance=DeterministicSqrt(
|
|
||||||
playerDx*playerDx+playerDy*playerDy+playerVertical*playerVertical)
|
|
||||||
-0.34*qPerMetre;
|
|
||||||
bestPlayerCapsuleSignedDistance=std::min(bestPlayerCapsuleSignedDistance,
|
bestPlayerCapsuleSignedDistance=std::min(bestPlayerCapsuleSignedDistance,
|
||||||
playerCapsuleDistance);
|
DeterministicSqrt(playerDx*playerDx+playerDy*playerDy
|
||||||
|
+playerVertical*playerVertical)-0.34*qPerMetre);
|
||||||
|
}
|
||||||
|
const double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz;
|
||||||
|
const double ex=qx-cx,ey=qy-cy,ez=qz-cz;
|
||||||
|
const double radius=static_cast<double>(a.radiusSubvoxels)
|
||||||
|
+t*(b.radiusSubvoxels-a.radiusSubvoxels);
|
||||||
const double signedDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius;
|
const double signedDistance=DeterministicSqrt(ex*ex+ey*ey+ez*ez)-radius;
|
||||||
if(signedDistance<bestSignedDistance)
|
if(signedDistance<bestSignedDistance
|
||||||
{bestSignedDistance=signedDistance;bestT=t;bestIndex=index;}
|
||(signedDistance==bestSignedDistance
|
||||||
|
&&(scanPhase<bestScanPhase
|
||||||
|
||(scanPhase==bestScanPhase&&index<bestScanIndex))))
|
||||||
|
{
|
||||||
|
bestSignedDistance=signedDistance;bestT=t;bestIndex=index;
|
||||||
|
bestScanPhase=scanPhase;bestScanIndex=index;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
for(std::ptrdiff_t index=carveBegin;index<=carveEnd;++index)
|
|
||||||
considerCarveSegment(index);
|
|
||||||
// Alternate styles can bend locally at either mouth, so floorProfile is
|
// Alternate styles can bend locally at either mouth, so floorProfile is
|
||||||
// not monotone over those short endpoint runs. Keep the binary search for
|
// not monotone over those short endpoint runs. Keep the binary search for
|
||||||
// the long descent and inspect both endpoint neighborhoods directly.
|
// the long descent and inspect both endpoint neighborhoods directly.
|
||||||
for(std::ptrdiff_t index=0;index<std::min<std::ptrdiff_t>(8,lowerMouthIndex);++index)
|
|
||||||
considerCarveSegment(index);
|
|
||||||
const std::ptrdiff_t mouthSearchBegin=std::max<std::ptrdiff_t>(0,lowerMouthIndex-8);
|
|
||||||
const std::ptrdiff_t mouthSearchEnd=std::min<std::ptrdiff_t>(lastIndex,
|
|
||||||
lowerMouthIndex+8);
|
|
||||||
for(std::ptrdiff_t index=mouthSearchBegin;index<mouthSearchEnd;++index)
|
|
||||||
considerCarveSegment(index);
|
|
||||||
// The route-bound landing can rise and fall after the lower mouth. Scan
|
// The route-bound landing can rise and fall after the lower mouth. Scan
|
||||||
// that short tail directly; binary search is only valid on the descent.
|
// that short tail directly; binary search is only valid on the descent.
|
||||||
|
if(passageTileScopeMatches)
|
||||||
|
{
|
||||||
|
VisitNearestPassageSegmentRange(passageIndex,carveBegin,carveEnd,qx,qy,qz,
|
||||||
|
0,bestSignedDistance,considerCarveSegment);
|
||||||
|
VisitNearestPassageSegmentRange(passageIndex,0,
|
||||||
|
std::min<std::ptrdiff_t>(8,lowerMouthIndex)-1,qx,qy,qz,
|
||||||
|
1,bestSignedDistance,considerCarveSegment);
|
||||||
|
VisitNearestPassageSegmentRange(passageIndex,mouthSearchBegin,mouthSearchEnd-1,
|
||||||
|
qx,qy,qz,2,bestSignedDistance,considerCarveSegment);
|
||||||
|
VisitNearestPassageSegmentRange(passageIndex,lowerMouthIndex,lastIndex-1,
|
||||||
|
qx,qy,qz,3,bestSignedDistance,considerCarveSegment);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for(std::ptrdiff_t index=carveBegin;index<=carveEnd;++index)
|
||||||
|
considerCarveSegment(static_cast<std::size_t>(index),0);
|
||||||
|
for(std::ptrdiff_t index=0;index<std::min<std::ptrdiff_t>(8,lowerMouthIndex);++index)
|
||||||
|
considerCarveSegment(static_cast<std::size_t>(index),1);
|
||||||
|
for(std::ptrdiff_t index=mouthSearchBegin;index<mouthSearchEnd;++index)
|
||||||
|
considerCarveSegment(static_cast<std::size_t>(index),2);
|
||||||
for(std::ptrdiff_t index=lowerMouthIndex;index<lastIndex;++index)
|
for(std::ptrdiff_t index=lowerMouthIndex;index<lastIndex;++index)
|
||||||
considerCarveSegment(index);
|
considerCarveSegment(static_cast<std::size_t>(index),3);
|
||||||
|
}
|
||||||
if(!std::isfinite(bestSignedDistance))continue;
|
if(!std::isfinite(bestSignedDistance))continue;
|
||||||
const auto& nearestA=passage.controlPoints[static_cast<std::size_t>(bestIndex)];
|
const auto& nearestA=passage.controlPoints[static_cast<std::size_t>(bestIndex)];
|
||||||
const auto& nearestB=passage.controlPoints[static_cast<std::size_t>(bestIndex+1)];
|
const auto& nearestB=passage.controlPoints[static_cast<std::size_t>(bestIndex+1)];
|
||||||
@@ -405,8 +541,12 @@ public:
|
|||||||
const bool shortSpanFloorSupport=hasSupportFloor
|
const bool shortSpanFloorSupport=hasSupportFloor
|
||||||
&&IsPassageShortSpanSegment(passageIndex,
|
&&IsPassageShortSpanSegment(passageIndex,
|
||||||
static_cast<std::size_t>(bestSupportSegmentIndex));
|
static_cast<std::size_t>(bestSupportSegmentIndex));
|
||||||
if(hasSupportFloor&&bestPlayerCapsuleSignedDistance>=0.05*qPerMetre
|
const bool capsuleAllowsSupport=passageTileScopeMatches
|
||||||
&&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport))
|
?!PassagePlayerCapsuleBlocksSupport(passageIndex,carveBegin,carveEnd,
|
||||||
|
lowerMouthIndex,mouthSearchBegin,mouthSearchEnd,lastIndex,qx,qy,qz,qPerMetre)
|
||||||
|
:bestPlayerCapsuleSignedDistance>=0.05*qPerMetre;
|
||||||
|
if(hasSupportFloor&&(density<0.0f||passageColumnFloorSupport||shortSpanFloorSupport)
|
||||||
|
&&capsuleAllowsSupport)
|
||||||
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
composed=std::min(composed,static_cast<float>(bestSupportDensity));
|
||||||
}
|
}
|
||||||
return composed;
|
return composed;
|
||||||
@@ -418,21 +558,28 @@ public:
|
|||||||
bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const
|
bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const
|
||||||
{
|
{
|
||||||
if(!ResolvedLayout||ResolvedLayout->passages.empty())return false;
|
if(!ResolvedLayout||ResolvedLayout->passages.empty())return false;
|
||||||
|
if(PassageProofNodes.empty())
|
||||||
|
{
|
||||||
|
// Native-only readiness tiles have not built the passage BVH. Keep their
|
||||||
|
// exact conservative fallback instead of indexing an empty tree.
|
||||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||||
const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0
|
const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0
|
||||||
?1000000.0/Owner->CoreVoxelSizeMicrons:4.0;
|
?1000000.0/Owner->CoreVoxelSizeMicrons:4.0;
|
||||||
const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre;
|
const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre;
|
||||||
const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre;
|
const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre;
|
||||||
|
const auto overlaps=[&](const double low[3],const double high[3])
|
||||||
|
{
|
||||||
|
for(int axis=0;axis<3;++axis)
|
||||||
|
if(high[axis]<minimum[axis]||low[axis]>maximum[axis])return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
for(const auto& passage:ResolvedLayout->passages)
|
for(const auto& passage:ResolvedLayout->passages)
|
||||||
{
|
{
|
||||||
for(std::size_t i=1;i<passage.controlPoints.size();++i)
|
for(std::size_t i=1;i<passage.controlPoints.size();++i)
|
||||||
{
|
{
|
||||||
const auto& a=passage.controlPoints[i-1];const auto& b=passage.controlPoints[i];
|
const auto& a=passage.controlPoints[i-1];const auto& b=passage.controlPoints[i];
|
||||||
const double radius=static_cast<double>(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q;
|
const double radius=static_cast<double>(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q;
|
||||||
const double floorThicknessVoxels=Owner&&Owner->CoreVoxelSizeMicrons>0
|
const double floorThicknessVoxels=voxelsPerMetre;
|
||||||
?1000000.0/Owner->CoreVoxelSizeMicrons:4.0;
|
|
||||||
// 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 horizontalExtent=std::max(radius,bridgeHalfWidthVoxels);
|
||||||
const double footDepth=std::max(floorThicknessVoxels,bridgeFootDepthVoxels);
|
const double footDepth=std::max(floorThicknessVoxels,bridgeFootDepthVoxels);
|
||||||
const double low[3]={
|
const double low[3]={
|
||||||
@@ -443,10 +590,43 @@ public:
|
|||||||
std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent,
|
std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent,
|
||||||
std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent,
|
std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent,
|
||||||
std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius};
|
std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius};
|
||||||
bool overlap=true;
|
if(overlaps(low,high))return true;
|
||||||
for(int axis=0;axis<3;++axis)
|
}
|
||||||
overlap&=high[axis]>=minimum[axis]&&low[axis]<=maximum[axis];
|
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||||
if(overlap)return true;
|
ResolvedLayout->strates.end(),[&passage](const auto& value)
|
||||||
|
{return value.strateIndex==passage.upperStrateIndex;});
|
||||||
|
if(upperStratum!=ResolvedLayout->strates.end())
|
||||||
|
{
|
||||||
|
std::array<const VoxelForgeCore::ResolvedLayoutAnchorV2*,5> route{};
|
||||||
|
std::size_t routeCount=0;
|
||||||
|
for(const auto& anchor:upperStratum->anchors)
|
||||||
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||||
|
{
|
||||||
|
if(routeCount<route.size())route[routeCount++]=&anchor;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::move(route.begin()+1,route.end(),route.begin());
|
||||||
|
route.back()=&anchor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(routeCount>=2)
|
||||||
|
{
|
||||||
|
const double mouthReach=13.0*voxelsPerMetre*Q;
|
||||||
|
double minimumZ=std::numeric_limits<double>::infinity();
|
||||||
|
double maximumZ=-std::numeric_limits<double>::infinity();
|
||||||
|
for(std::size_t index=0;index<routeCount;++index)
|
||||||
|
{
|
||||||
|
minimumZ=std::min(minimumZ,static_cast<double>(route[index]->values[2]));
|
||||||
|
maximumZ=std::max(maximumZ,static_cast<double>(route[index]->values[2]));
|
||||||
|
}
|
||||||
|
const double mouthX=passage.upperMouthSubvoxels[0];
|
||||||
|
const double mouthY=passage.upperMouthSubvoxels[1];
|
||||||
|
const double low[3]={(mouthX-mouthReach)/Q,(mouthY-mouthReach)/Q,
|
||||||
|
(minimumZ-3.0*voxelsPerMetre*Q)/Q};
|
||||||
|
const double high[3]={(mouthX+mouthReach)/Q,(mouthY+mouthReach)/Q,
|
||||||
|
(maximumZ+3.0*voxelsPerMetre*Q)/Q};
|
||||||
|
if(overlaps(low,high))return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for(const auto& crossing:PassageVoidCrossings)
|
for(const auto& crossing:PassageVoidCrossings)
|
||||||
@@ -457,13 +637,16 @@ public:
|
|||||||
crossing.centerYSubvoxels/Q-radius,crossing.bottomZSubvoxels/Q};
|
crossing.centerYSubvoxels/Q-radius,crossing.bottomZSubvoxels/Q};
|
||||||
const double high[3]={crossing.centerXSubvoxels/Q+radius,
|
const double high[3]={crossing.centerXSubvoxels/Q+radius,
|
||||||
crossing.centerYSubvoxels/Q+radius,crossing.topZSubvoxels/Q};
|
crossing.centerYSubvoxels/Q+radius,crossing.topZSubvoxels/Q};
|
||||||
bool overlap=true;
|
if(overlaps(low,high))return true;
|
||||||
for(int axis=0;axis<3;++axis)
|
|
||||||
overlap&=high[axis]>=minimum[axis]&&low[axis]<=maximum[axis];
|
|
||||||
if(overlap)return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const double low[3]={static_cast<double>(minimum[0]),static_cast<double>(minimum[1]),
|
||||||
|
static_cast<double>(minimum[2])};
|
||||||
|
const double high[3]={static_cast<double>(maximum[0]),static_cast<double>(maximum[1]),
|
||||||
|
static_cast<double>(maximum[2])};
|
||||||
|
return PassageProofBoundsOverlapNode(0,low,high);
|
||||||
|
}
|
||||||
|
|
||||||
static bool MakeResolvedStratumBounds(double BottomZ,double TopZ,double SealThickness,
|
static bool MakeResolvedStratumBounds(double BottomZ,double TopZ,double SealThickness,
|
||||||
FResolvedStratumBounds& Out)
|
FResolvedStratumBounds& Out)
|
||||||
@@ -1723,20 +1906,557 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct FPassageTileFeature
|
||||||
|
{
|
||||||
|
std::size_t passageIndex=0,segmentIndex=0;
|
||||||
|
bool bColumn=false,bMouthSupport=false;
|
||||||
|
};
|
||||||
|
struct FPassageNearestNode
|
||||||
|
{
|
||||||
|
std::array<double,3> minimum{},maximum{};
|
||||||
|
double maximumRadius=0.0;
|
||||||
|
std::vector<std::size_t> segments;
|
||||||
|
int left=-1,right=-1;
|
||||||
|
};
|
||||||
|
struct FPassageProofBound
|
||||||
|
{
|
||||||
|
std::array<double,3> minimum{},maximum{};
|
||||||
|
};
|
||||||
|
struct FPassageProofNode
|
||||||
|
{
|
||||||
|
std::array<double,3> minimum{},maximum{};
|
||||||
|
std::size_t first=0,end=0;
|
||||||
|
int left=-1,right=-1;
|
||||||
|
};
|
||||||
|
struct FPassageFeatureNode
|
||||||
|
{
|
||||||
|
std::array<std::int64_t,3> minimum{},maximum{};
|
||||||
|
std::size_t first=0,end=0;
|
||||||
|
int left=-1,right=-1;
|
||||||
|
};
|
||||||
|
|
||||||
|
int BuildPassageNearestNode(std::size_t passageIndex,std::vector<std::size_t> segments)
|
||||||
|
{
|
||||||
|
const auto& points=ResolvedLayout->passages[passageIndex].controlPoints;
|
||||||
|
FPassageNearestNode node;
|
||||||
|
node.minimum.fill(std::numeric_limits<double>::infinity());
|
||||||
|
node.maximum.fill(-std::numeric_limits<double>::infinity());
|
||||||
|
for(const std::size_t segment:segments)
|
||||||
|
{
|
||||||
|
const auto& a=points[segment];const auto& b=points[segment+1];
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
{
|
||||||
|
node.minimum[axis]=std::min({node.minimum[axis],
|
||||||
|
static_cast<double>(a.positionSubvoxels[axis]),
|
||||||
|
static_cast<double>(b.positionSubvoxels[axis])});
|
||||||
|
node.maximum[axis]=std::max({node.maximum[axis],
|
||||||
|
static_cast<double>(a.positionSubvoxels[axis]),
|
||||||
|
static_cast<double>(b.positionSubvoxels[axis])});
|
||||||
|
}
|
||||||
|
node.maximumRadius=std::max({node.maximumRadius,
|
||||||
|
static_cast<double>(a.radiusSubvoxels),static_cast<double>(b.radiusSubvoxels)});
|
||||||
|
}
|
||||||
|
node.segments=segments;
|
||||||
|
std::sort(node.segments.begin(),node.segments.end());
|
||||||
|
const int nodeIndex=static_cast<int>(PassageNearestNodes[passageIndex].size());
|
||||||
|
PassageNearestNodes[passageIndex].push_back(std::move(node));
|
||||||
|
if(segments.size()>4)
|
||||||
|
{
|
||||||
|
std::size_t splitAxis=0;
|
||||||
|
for(std::size_t axis=1;axis<3;++axis)
|
||||||
|
if(PassageNearestNodes[passageIndex][nodeIndex].maximum[axis]
|
||||||
|
-PassageNearestNodes[passageIndex][nodeIndex].minimum[axis]
|
||||||
|
>PassageNearestNodes[passageIndex][nodeIndex].maximum[splitAxis]
|
||||||
|
-PassageNearestNodes[passageIndex][nodeIndex].minimum[splitAxis])splitAxis=axis;
|
||||||
|
const std::size_t middle=segments.size()/2;
|
||||||
|
const auto center=[&](std::size_t segment)
|
||||||
|
{return static_cast<double>(points[segment].positionSubvoxels[splitAxis])
|
||||||
|
+static_cast<double>(points[segment+1].positionSubvoxels[splitAxis]);};
|
||||||
|
std::nth_element(segments.begin(),segments.begin()+middle,segments.end(),
|
||||||
|
[&](std::size_t a,std::size_t b)
|
||||||
|
{const double ca=center(a),cb=center(b);return ca==cb?a<b:ca<cb;});
|
||||||
|
std::vector<std::size_t> leftSegments(segments.begin(),segments.begin()+middle);
|
||||||
|
std::vector<std::size_t> rightSegments(segments.begin()+middle,segments.end());
|
||||||
|
const int left=BuildPassageNearestNode(passageIndex,std::move(leftSegments));
|
||||||
|
const int right=BuildPassageNearestNode(passageIndex,std::move(rightSegments));
|
||||||
|
PassageNearestNodes[passageIndex][nodeIndex].left=left;
|
||||||
|
PassageNearestNodes[passageIndex][nodeIndex].right=right;
|
||||||
|
}
|
||||||
|
return nodeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BuildPassageProofNode(std::size_t first,std::size_t end)
|
||||||
|
{
|
||||||
|
FPassageProofNode node;node.first=first;node.end=end;
|
||||||
|
node.minimum.fill(std::numeric_limits<double>::infinity());
|
||||||
|
node.maximum.fill(-std::numeric_limits<double>::infinity());
|
||||||
|
for(std::size_t index=first;index<end;++index)
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
{
|
||||||
|
node.minimum[axis]=std::min(node.minimum[axis],PassageProofBounds[index].minimum[axis]);
|
||||||
|
node.maximum[axis]=std::max(node.maximum[axis],PassageProofBounds[index].maximum[axis]);
|
||||||
|
}
|
||||||
|
const int nodeIndex=static_cast<int>(PassageProofNodes.size());
|
||||||
|
PassageProofNodes.push_back(node);
|
||||||
|
if(end-first>8)
|
||||||
|
{
|
||||||
|
std::size_t splitAxis=0;
|
||||||
|
for(std::size_t axis=1;axis<3;++axis)
|
||||||
|
if(node.maximum[axis]-node.minimum[axis]
|
||||||
|
>node.maximum[splitAxis]-node.minimum[splitAxis])splitAxis=axis;
|
||||||
|
const std::size_t middle=first+(end-first)/2;
|
||||||
|
std::nth_element(PassageProofBounds.begin()+first,PassageProofBounds.begin()+middle,
|
||||||
|
PassageProofBounds.begin()+end,[splitAxis](const auto& a,const auto& b)
|
||||||
|
{return a.minimum[splitAxis]+a.maximum[splitAxis]
|
||||||
|
<b.minimum[splitAxis]+b.maximum[splitAxis];});
|
||||||
|
const int left=BuildPassageProofNode(first,middle);
|
||||||
|
const int right=BuildPassageProofNode(middle,end);
|
||||||
|
PassageProofNodes[nodeIndex].left=left;
|
||||||
|
PassageProofNodes[nodeIndex].right=right;
|
||||||
|
}
|
||||||
|
return nodeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BuildPassageFeatureNode(std::size_t first,std::size_t end)
|
||||||
|
{
|
||||||
|
FPassageFeatureNode node;node.first=first;node.end=end;
|
||||||
|
node.minimum.fill(std::numeric_limits<std::int64_t>::max());
|
||||||
|
node.maximum.fill(std::numeric_limits<std::int64_t>::min());
|
||||||
|
for(std::size_t offset=first;offset<end;++offset)
|
||||||
|
{
|
||||||
|
const auto& feature=PassageFeatureBounds[PassageFeatureOrder[offset]];
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
{
|
||||||
|
node.minimum[axis]=std::min(node.minimum[axis],feature.minimumSubvoxels[axis]);
|
||||||
|
node.maximum[axis]=std::max(node.maximum[axis],feature.maximumSubvoxels[axis]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int nodeIndex=static_cast<int>(PassageFeatureNodes.size());
|
||||||
|
PassageFeatureNodes.push_back(node);
|
||||||
|
if(end-first>8)
|
||||||
|
{
|
||||||
|
std::size_t splitAxis=0;
|
||||||
|
for(std::size_t axis=1;axis<3;++axis)
|
||||||
|
if(static_cast<long double>(node.maximum[axis])-node.minimum[axis]
|
||||||
|
>static_cast<long double>(node.maximum[splitAxis])-node.minimum[splitAxis])
|
||||||
|
splitAxis=axis;
|
||||||
|
const std::size_t middle=first+(end-first)/2;
|
||||||
|
std::nth_element(PassageFeatureOrder.begin()+first,PassageFeatureOrder.begin()+middle,
|
||||||
|
PassageFeatureOrder.begin()+end,[this,splitAxis](std::uint32_t a,std::uint32_t b)
|
||||||
|
{
|
||||||
|
const auto& left=PassageFeatureBounds[a];
|
||||||
|
const auto& right=PassageFeatureBounds[b];
|
||||||
|
const double leftCenter=static_cast<double>(left.minimumSubvoxels[splitAxis])*0.5
|
||||||
|
+static_cast<double>(left.maximumSubvoxels[splitAxis])*0.5;
|
||||||
|
const double rightCenter=static_cast<double>(right.minimumSubvoxels[splitAxis])*0.5
|
||||||
|
+static_cast<double>(right.maximumSubvoxels[splitAxis])*0.5;
|
||||||
|
return leftCenter==rightCenter?a<b:leftCenter<rightCenter;
|
||||||
|
});
|
||||||
|
const int left=BuildPassageFeatureNode(first,middle);
|
||||||
|
const int right=BuildPassageFeatureNode(middle,end);
|
||||||
|
PassageFeatureNodes[nodeIndex].left=left;
|
||||||
|
PassageFeatureNodes[nodeIndex].right=right;
|
||||||
|
}
|
||||||
|
return nodeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QueryPassageFeatureNode(int nodeIndex,
|
||||||
|
const std::array<std::int64_t,3>& minimum,
|
||||||
|
const std::array<std::int64_t,3>& maximum,
|
||||||
|
std::vector<std::uint32_t>& result) const
|
||||||
|
{
|
||||||
|
if(nodeIndex<0)return;
|
||||||
|
const auto& node=PassageFeatureNodes[static_cast<std::size_t>(nodeIndex)];
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
if(node.maximum[axis]<minimum[axis]||node.minimum[axis]>maximum[axis])return;
|
||||||
|
if(node.left<0)
|
||||||
|
{
|
||||||
|
for(std::size_t offset=node.first;offset<node.end;++offset)
|
||||||
|
{
|
||||||
|
const std::uint32_t featureIndex=PassageFeatureOrder[offset];
|
||||||
|
const auto& feature=PassageFeatureBounds[featureIndex];
|
||||||
|
bool overlaps=true;
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
overlaps=overlaps&&feature.minimumSubvoxels[axis]<=maximum[axis]
|
||||||
|
&&feature.maximumSubvoxels[axis]>=minimum[axis];
|
||||||
|
if(overlaps)result.push_back(featureIndex);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QueryPassageFeatureNode(node.left,minimum,maximum,result);
|
||||||
|
QueryPassageFeatureNode(node.right,minimum,maximum,result);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint32_t> BuildPassageFeatureTileList(
|
||||||
|
const std::array<std::int64_t,3>& minimum,
|
||||||
|
const std::array<std::int64_t,3>& maximum) const
|
||||||
|
{
|
||||||
|
std::vector<std::uint32_t> result;
|
||||||
|
if(PassageFeatureBounds.empty())return result;
|
||||||
|
bool failOpen=PassageFeatureBoundsFailOpen;
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)failOpen=failOpen||minimum[axis]>maximum[axis];
|
||||||
|
if(failOpen)
|
||||||
|
{
|
||||||
|
result.resize(PassageFeatureBounds.size());
|
||||||
|
for(std::size_t i=0;i<result.size();++i)result[i]=static_cast<std::uint32_t>(i);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if(!PassageFeatureNodes.empty())QueryPassageFeatureNode(0,minimum,maximum,result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildPassageSpatialIndices()
|
||||||
|
{
|
||||||
|
PassageTileFeatures.clear();PassageFeatureBounds.clear();
|
||||||
|
PassageFeatureOrder.clear();PassageFeatureNodes.clear();
|
||||||
|
PassageFeatureBoundsFailOpen=false;
|
||||||
|
PassageNearestNodes.clear();
|
||||||
|
PassageProofBounds.clear();PassageProofNodes.clear();
|
||||||
|
PassageNearestNodes.resize(ResolvedLayout?ResolvedLayout->passages.size():0);
|
||||||
|
PassageColumnSegmentFlags.clear();PassageShortSpanSegmentFlags.clear();
|
||||||
|
PassageHasShortSpanFlags.clear();
|
||||||
|
if(!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
||||||
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||||
|
const double voxelsPerMetre=Owner&&Owner->CoreVoxelSizeMicrons>0
|
||||||
|
?1000000.0/Owner->CoreVoxelSizeMicrons:4.0;
|
||||||
|
const double bridgeHalfWidthVoxels=3.5*voxelsPerMetre;
|
||||||
|
const double bridgeFootDepthVoxels=(5.0-0.13)*voxelsPerMetre;
|
||||||
|
PassageColumnSegmentFlags.resize(ResolvedLayout->passages.size());
|
||||||
|
PassageShortSpanSegmentFlags.resize(ResolvedLayout->passages.size());
|
||||||
|
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||||
|
{
|
||||||
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||||
|
const std::size_t segmentCount=passage.controlPoints.size()>1
|
||||||
|
?passage.controlPoints.size()-1:0;
|
||||||
|
PassageColumnSegmentFlags[passageIndex].assign(segmentCount,0);
|
||||||
|
PassageShortSpanSegmentFlags[passageIndex].assign(segmentCount,0);
|
||||||
|
if(segmentCount>0)
|
||||||
|
{
|
||||||
|
std::vector<std::size_t> segmentIds(segmentCount);
|
||||||
|
for(std::size_t i=0;i<segmentCount;++i)segmentIds[i]=i;
|
||||||
|
BuildPassageNearestNode(passageIndex,std::move(segmentIds));
|
||||||
|
}
|
||||||
|
for(std::size_t segment=0;segment<segmentCount;++segment)
|
||||||
|
{
|
||||||
|
const auto& a=passage.controlPoints[segment];
|
||||||
|
const auto& b=passage.controlPoints[segment+1];
|
||||||
|
const double radius=static_cast<double>(std::max(a.radiusSubvoxels,b.radiusSubvoxels))/Q;
|
||||||
|
const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels);
|
||||||
|
const double footDepth=std::max(voxelsPerMetre,bridgeFootDepthVoxels);
|
||||||
|
FPassageProofBound proof;
|
||||||
|
proof.minimum={std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent,
|
||||||
|
std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent,
|
||||||
|
std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius-footDepth};
|
||||||
|
proof.maximum={std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent,
|
||||||
|
std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent,
|
||||||
|
std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius};
|
||||||
|
PassageProofBounds.push_back(proof);
|
||||||
|
const std::array<std::int64_t,3> featureMin={
|
||||||
|
static_cast<std::int64_t>(std::floor(proof.minimum[0]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::floor(proof.minimum[1]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::floor(proof.minimum[2]*Q))};
|
||||||
|
const std::array<std::int64_t,3> featureMax={
|
||||||
|
static_cast<std::int64_t>(std::ceil(proof.maximum[0]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::ceil(proof.maximum[1]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::ceil(proof.maximum[2]*Q))};
|
||||||
|
PassageFeatureBounds.push_back({static_cast<std::uint64_t>(PassageTileFeatures.size()),
|
||||||
|
featureMin,featureMax});
|
||||||
|
PassageTileFeatures.push_back({passageIndex,segment,false});
|
||||||
|
}
|
||||||
|
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
|
||||||
|
ResolvedLayout->strates.end(),[&passage](const auto& value)
|
||||||
|
{return value.strateIndex==passage.upperStrateIndex;});
|
||||||
|
if(upperStratum!=ResolvedLayout->strates.end())
|
||||||
|
{
|
||||||
|
std::array<const VoxelForgeCore::ResolvedLayoutAnchorV2*,5> route{};
|
||||||
|
std::size_t routeCount=0;
|
||||||
|
for(const auto& anchor:upperStratum->anchors)
|
||||||
|
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
|
||||||
|
{
|
||||||
|
if(routeCount<route.size())route[routeCount++]=&anchor;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::move(route.begin()+1,route.end(),route.begin());
|
||||||
|
route.back()=&anchor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(routeCount>=2)
|
||||||
|
{
|
||||||
|
const double mouthReach=13.0*voxelsPerMetre*Q;
|
||||||
|
double minimumZ=std::numeric_limits<double>::infinity();
|
||||||
|
double maximumZ=-std::numeric_limits<double>::infinity();
|
||||||
|
for(std::size_t index=0;index<routeCount;++index)
|
||||||
|
{
|
||||||
|
minimumZ=std::min(minimumZ,static_cast<double>(route[index]->values[2]));
|
||||||
|
maximumZ=std::max(maximumZ,static_cast<double>(route[index]->values[2]));
|
||||||
|
}
|
||||||
|
const double mouthX=passage.upperMouthSubvoxels[0];
|
||||||
|
const double mouthY=passage.upperMouthSubvoxels[1];
|
||||||
|
FPassageProofBound supportProof;
|
||||||
|
supportProof.minimum={(mouthX-mouthReach)/Q,(mouthY-mouthReach)/Q,
|
||||||
|
(minimumZ-3.0*voxelsPerMetre*Q)/Q};
|
||||||
|
supportProof.maximum={(mouthX+mouthReach)/Q,(mouthY+mouthReach)/Q,
|
||||||
|
(maximumZ+3.0*voxelsPerMetre*Q)/Q};
|
||||||
|
PassageProofBounds.push_back(supportProof);
|
||||||
|
const std::array<std::int64_t,3> featureMin={
|
||||||
|
static_cast<std::int64_t>(std::floor(supportProof.minimum[0]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::floor(supportProof.minimum[1]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::floor(supportProof.minimum[2]*Q))};
|
||||||
|
const std::array<std::int64_t,3> featureMax={
|
||||||
|
static_cast<std::int64_t>(std::ceil(supportProof.maximum[0]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::ceil(supportProof.maximum[1]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::ceil(supportProof.maximum[2]*Q))};
|
||||||
|
PassageFeatureBounds.push_back({static_cast<std::uint64_t>(PassageTileFeatures.size()),
|
||||||
|
featureMin,featureMax});
|
||||||
|
PassageTileFeatures.push_back({passageIndex,0,false,true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(const auto& crossing:PassageVoidCrossings)
|
||||||
|
{
|
||||||
|
if(crossing.passageIndex<PassageColumnSegmentFlags.size())
|
||||||
|
{
|
||||||
|
auto& flags= crossing.needsColumn
|
||||||
|
?PassageColumnSegmentFlags[crossing.passageIndex]
|
||||||
|
:PassageShortSpanSegmentFlags[crossing.passageIndex];
|
||||||
|
const std::size_t first=std::min(crossing.firstSegment,flags.size());
|
||||||
|
const std::size_t last=std::min(crossing.lastSegment,flags.empty()?0:flags.size()-1);
|
||||||
|
if(!flags.empty()&&first<=last)
|
||||||
|
std::fill(flags.begin()+first,flags.begin()+last+1,1);
|
||||||
|
}
|
||||||
|
if(!crossing.needsColumn)continue;
|
||||||
|
FPassageProofBound proof;
|
||||||
|
const double radius=static_cast<double>(crossing.maximumRadiusSubvoxels)/Q;
|
||||||
|
proof.minimum={crossing.centerXSubvoxels/Q-radius,crossing.centerYSubvoxels/Q-radius,
|
||||||
|
crossing.bottomZSubvoxels/Q};
|
||||||
|
proof.maximum={crossing.centerXSubvoxels/Q+radius,crossing.centerYSubvoxels/Q+radius,
|
||||||
|
crossing.topZSubvoxels/Q};
|
||||||
|
PassageProofBounds.push_back(proof);
|
||||||
|
const std::array<std::int64_t,3> featureMin={
|
||||||
|
static_cast<std::int64_t>(std::floor(proof.minimum[0]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::floor(proof.minimum[1]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::floor(proof.minimum[2]*Q))};
|
||||||
|
const std::array<std::int64_t,3> featureMax={
|
||||||
|
static_cast<std::int64_t>(std::ceil(proof.maximum[0]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::ceil(proof.maximum[1]*Q)),
|
||||||
|
static_cast<std::int64_t>(std::ceil(proof.maximum[2]*Q))};
|
||||||
|
PassageFeatureBounds.push_back({static_cast<std::uint64_t>(PassageTileFeatures.size()),
|
||||||
|
featureMin,featureMax});
|
||||||
|
PassageTileFeatures.push_back({crossing.passageIndex,0,true});
|
||||||
|
}
|
||||||
|
PassageHasShortSpanFlags.assign(PassageShortSpanSegmentFlags.size(),0);
|
||||||
|
for(std::size_t passageIndex=0;passageIndex<PassageShortSpanSegmentFlags.size();++passageIndex)
|
||||||
|
PassageHasShortSpanFlags[passageIndex]=static_cast<std::uint8_t>(
|
||||||
|
std::any_of(PassageShortSpanSegmentFlags[passageIndex].begin(),
|
||||||
|
PassageShortSpanSegmentFlags[passageIndex].end(),
|
||||||
|
[](std::uint8_t flag){return flag!=0;}));
|
||||||
|
for(std::size_t i=0;i<PassageFeatureBounds.size();++i)
|
||||||
|
PassageFeatureOrder.push_back(static_cast<std::uint32_t>(i));
|
||||||
|
for(const auto& feature:PassageFeatureBounds)
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
PassageFeatureBoundsFailOpen=PassageFeatureBoundsFailOpen
|
||||||
|
||feature.minimumSubvoxels[axis]>feature.maximumSubvoxels[axis];
|
||||||
|
if(!PassageFeatureOrder.empty())BuildPassageFeatureNode(0,PassageFeatureOrder.size());
|
||||||
|
if(!PassageProofBounds.empty())BuildPassageProofNode(0,PassageProofBounds.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
double PassageNearestNodeLowerBound(const FPassageNearestNode& node,
|
||||||
|
double qx,double qy,double qz) const
|
||||||
|
{
|
||||||
|
const double dx=qx<node.minimum[0]?node.minimum[0]-qx:qx>node.maximum[0]?qx-node.maximum[0]:0.0;
|
||||||
|
const double dy=qy<node.minimum[1]?node.minimum[1]-qy:qy>node.maximum[1]?qy-node.maximum[1]:0.0;
|
||||||
|
const double dz=qz<node.minimum[2]?node.minimum[2]-qz:qz>node.maximum[2]?qz-node.maximum[2]:0.0;
|
||||||
|
return DeterministicSqrt(dx*dx+dy*dy+dz*dz)-node.maximumRadius-1.0e-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename FVisit>
|
||||||
|
void VisitNearestPassageSpatialNode(std::size_t passageIndex,int nodeIndex,
|
||||||
|
std::size_t first,std::size_t end,double qx,double qy,double qz,int priority,
|
||||||
|
double& bestDistance,FVisit& visit) const
|
||||||
|
{
|
||||||
|
if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return;
|
||||||
|
const auto& nodes=PassageNearestNodes[passageIndex];
|
||||||
|
const auto& node=nodes[static_cast<std::size_t>(nodeIndex)];
|
||||||
|
const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first);
|
||||||
|
if(candidate==node.segments.end()||*candidate>=end)return;
|
||||||
|
const double lowerBound=PassageNearestNodeLowerBound(node,qx,qy,qz);
|
||||||
|
if(lowerBound>bestDistance)return;
|
||||||
|
if(node.left<0)
|
||||||
|
{
|
||||||
|
for(auto it=candidate;it!=node.segments.end()&&*it<end;++it)
|
||||||
|
visit(*it,priority);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const double leftBound=PassageNearestNodeLowerBound(
|
||||||
|
nodes[static_cast<std::size_t>(node.left)],qx,qy,qz);
|
||||||
|
const double rightBound=PassageNearestNodeLowerBound(
|
||||||
|
nodes[static_cast<std::size_t>(node.right)],qx,qy,qz);
|
||||||
|
const int nearChild=leftBound<=rightBound?node.left:node.right;
|
||||||
|
const int farChild=leftBound<=rightBound?node.right:node.left;
|
||||||
|
VisitNearestPassageSpatialNode(passageIndex,nearChild,first,end,qx,qy,qz,
|
||||||
|
priority,bestDistance,visit);
|
||||||
|
VisitNearestPassageSpatialNode(passageIndex,farChild,first,end,qx,qy,qz,
|
||||||
|
priority,bestDistance,visit);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename FVisit>
|
||||||
|
void VisitNearestPassageSegmentRange(std::size_t passageIndex,std::ptrdiff_t first,
|
||||||
|
std::ptrdiff_t last,double qx,double qy,double qz,int priority,
|
||||||
|
double& bestDistance,FVisit&& visit) const
|
||||||
|
{
|
||||||
|
if(first>last||passageIndex>=PassageNearestNodes.size()
|
||||||
|
||PassageNearestNodes[passageIndex].empty())return;
|
||||||
|
auto callback=std::forward<FVisit>(visit);
|
||||||
|
VisitNearestPassageSpatialNode(passageIndex,0,static_cast<std::size_t>(first),
|
||||||
|
static_cast<std::size_t>(last+1),qx,qy,qz,priority,bestDistance,callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PassagePotentialWithinNode(std::size_t passageIndex,int nodeIndex,
|
||||||
|
std::size_t first,std::size_t end,double qx,double qy,double qz,
|
||||||
|
double signedDistanceLimit) const
|
||||||
|
{
|
||||||
|
if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false;
|
||||||
|
const auto& nodes=PassageNearestNodes[passageIndex];
|
||||||
|
const auto& node=nodes[static_cast<std::size_t>(nodeIndex)];
|
||||||
|
const auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first);
|
||||||
|
if(candidate==node.segments.end()||*candidate>=end)return false;
|
||||||
|
if(PassageNearestNodeLowerBound(node,qx,qy,qz)>signedDistanceLimit)return false;
|
||||||
|
if(node.left<0)return true;
|
||||||
|
const double leftBound=PassageNearestNodeLowerBound(
|
||||||
|
nodes[static_cast<std::size_t>(node.left)],qx,qy,qz);
|
||||||
|
const double rightBound=PassageNearestNodeLowerBound(
|
||||||
|
nodes[static_cast<std::size_t>(node.right)],qx,qy,qz);
|
||||||
|
const int nearChild=leftBound<=rightBound?node.left:node.right;
|
||||||
|
const int farChild=leftBound<=rightBound?node.right:node.left;
|
||||||
|
return PassagePotentialWithinNode(passageIndex,nearChild,first,end,qx,qy,qz,
|
||||||
|
signedDistanceLimit)
|
||||||
|
||PassagePotentialWithinNode(passageIndex,farChild,first,end,qx,qy,qz,
|
||||||
|
signedDistanceLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PassageMayAffectDensity(std::size_t passageIndex,std::ptrdiff_t carveBegin,
|
||||||
|
std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex,
|
||||||
|
std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex,
|
||||||
|
double qx,double qy,double qz,double signedDistanceLimit) const
|
||||||
|
{
|
||||||
|
const auto possible=[&](std::ptrdiff_t first,std::ptrdiff_t last)
|
||||||
|
{
|
||||||
|
return first<=last&&passageIndex<PassageNearestNodes.size()
|
||||||
|
&&!PassageNearestNodes[passageIndex].empty()
|
||||||
|
&&PassagePotentialWithinNode(passageIndex,0,static_cast<std::size_t>(first),
|
||||||
|
static_cast<std::size_t>(last+1),qx,qy,qz,signedDistanceLimit);
|
||||||
|
};
|
||||||
|
return possible(carveBegin,carveEnd)
|
||||||
|
||possible(0,std::min<std::ptrdiff_t>(8,lowerMouthIndex)-1)
|
||||||
|
||possible(mouthSearchBegin,mouthSearchEnd-1)
|
||||||
|
||possible(lowerMouthIndex,lastIndex-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PassagePlayerCapsuleBlocksSupportNode(std::size_t passageIndex,int nodeIndex,
|
||||||
|
std::size_t first,std::size_t end,double qx,double qy,double qz,double qPerMetre) const
|
||||||
|
{
|
||||||
|
if(passageIndex>=PassageNearestNodes.size()||nodeIndex<0)return false;
|
||||||
|
const auto& nodes=PassageNearestNodes[passageIndex];
|
||||||
|
const auto& node=nodes[static_cast<std::size_t>(nodeIndex)];
|
||||||
|
auto candidate=std::lower_bound(node.segments.begin(),node.segments.end(),first);
|
||||||
|
if(candidate==node.segments.end()||*candidate>=end)return false;
|
||||||
|
const double xyReach=0.39*qPerMetre;
|
||||||
|
if(qx+xyReach<node.minimum[0]||qx-xyReach>node.maximum[0]
|
||||||
|
||qy+xyReach<node.minimum[1]||qy-xyReach>node.maximum[1])return false;
|
||||||
|
const double minimumCenterZ=node.minimum[2]-node.maximumRadius+0.20*qPerMetre;
|
||||||
|
const double maximumCenterZ=node.maximum[2]+2.06*qPerMetre;
|
||||||
|
if(qz<minimumCenterZ||qz>maximumCenterZ)return false;
|
||||||
|
const auto& passage=ResolvedLayout->passages[passageIndex];
|
||||||
|
if(node.left<0)
|
||||||
|
{
|
||||||
|
for(auto it=candidate;it!=node.segments.end()&&*it<end;++it)
|
||||||
|
{
|
||||||
|
const std::size_t index=*it;
|
||||||
|
const auto& a=passage.controlPoints[index];const auto& b=passage.controlPoints[index+1];
|
||||||
|
const double ax=a.positionSubvoxels[0],ay=a.positionSubvoxels[1],az=a.positionSubvoxels[2];
|
||||||
|
const double dx=static_cast<double>(b.positionSubvoxels[0])-ax;
|
||||||
|
const double dy=static_cast<double>(b.positionSubvoxels[1])-ay;
|
||||||
|
const double dz=static_cast<double>(b.positionSubvoxels[2])-az;
|
||||||
|
const double lengthSq=dx*dx+dy*dy+dz*dz;
|
||||||
|
if(lengthSq<=0.0)continue;
|
||||||
|
const double horizontalLengthSq=dx*dx+dy*dy;
|
||||||
|
const double t=horizontalLengthSq>0.0
|
||||||
|
?std::clamp(((qx-ax)*dx+(qy-ay)*dy)/horizontalLengthSq,0.0,1.0):0.0;
|
||||||
|
const double centerX=ax+t*dx,centerY=ay+t*dy;
|
||||||
|
const double centerZ=az+t*dz-(static_cast<double>(a.radiusSubvoxels)
|
||||||
|
+t*(static_cast<double>(b.radiusSubvoxels)-a.radiusSubvoxels))+1.13*qPerMetre;
|
||||||
|
const double horizontalX=qx-centerX,horizontalY=qy-centerY;
|
||||||
|
const double vertical=std::max(0.0,std::abs(qz-centerZ)-0.54*qPerMetre);
|
||||||
|
const double distance=DeterministicSqrt(horizontalX*horizontalX+horizontalY*horizontalY
|
||||||
|
+vertical*vertical)-0.34*qPerMetre;
|
||||||
|
if(distance<0.05*qPerMetre)return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.left,first,end,
|
||||||
|
qx,qy,qz,qPerMetre)
|
||||||
|
||PassagePlayerCapsuleBlocksSupportNode(passageIndex,node.right,first,end,
|
||||||
|
qx,qy,qz,qPerMetre);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PassagePlayerCapsuleBlocksSupport(std::size_t passageIndex,
|
||||||
|
std::ptrdiff_t carveBegin,std::ptrdiff_t carveEnd,std::ptrdiff_t lowerMouthIndex,
|
||||||
|
std::ptrdiff_t mouthSearchBegin,std::ptrdiff_t mouthSearchEnd,std::ptrdiff_t lastIndex,
|
||||||
|
double qx,double qy,double qz,double qPerMetre) const
|
||||||
|
{
|
||||||
|
const auto blocked=[&](std::ptrdiff_t first,std::ptrdiff_t last)
|
||||||
|
{
|
||||||
|
return first<=last&&PassagePlayerCapsuleBlocksSupportNode(passageIndex,0,
|
||||||
|
static_cast<std::size_t>(first),static_cast<std::size_t>(last+1),
|
||||||
|
qx,qy,qz,qPerMetre);
|
||||||
|
};
|
||||||
|
return blocked(carveBegin,carveEnd)
|
||||||
|
||blocked(0,std::min<std::ptrdiff_t>(8,lowerMouthIndex)-1)
|
||||||
|
||blocked(mouthSearchBegin,mouthSearchEnd-1)
|
||||||
|
||blocked(lowerMouthIndex,lastIndex-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PassageProofBoundsOverlapNode(int nodeIndex,const double minimum[3],
|
||||||
|
const double maximum[3]) const
|
||||||
|
{
|
||||||
|
if(nodeIndex<0)return false;
|
||||||
|
const auto& node=PassageProofNodes[static_cast<std::size_t>(nodeIndex)];
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
if(node.maximum[axis]<minimum[axis]||node.minimum[axis]>maximum[axis])return false;
|
||||||
|
if(node.left<0)
|
||||||
|
{
|
||||||
|
for(std::size_t index=node.first;index<node.end;++index)
|
||||||
|
{
|
||||||
|
bool overlap=true;
|
||||||
|
for(std::size_t axis=0;axis<3;++axis)
|
||||||
|
overlap=overlap&&PassageProofBounds[index].maximum[axis]>=minimum[axis]
|
||||||
|
&&PassageProofBounds[index].minimum[axis]<=maximum[axis];
|
||||||
|
if(overlap)return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return PassageProofBoundsOverlapNode(node.left,minimum,maximum)
|
||||||
|
||PassageProofBoundsOverlapNode(node.right,minimum,maximum);
|
||||||
|
}
|
||||||
|
|
||||||
bool IsPassageColumnSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const
|
bool IsPassageColumnSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const
|
||||||
{
|
{
|
||||||
return std::any_of(PassageVoidCrossings.begin(),PassageVoidCrossings.end(),
|
return PassageIndex<PassageColumnSegmentFlags.size()
|
||||||
[PassageIndex,SegmentIndex](const auto& crossing)
|
&&SegmentIndex<PassageColumnSegmentFlags[PassageIndex].size()
|
||||||
{return crossing.needsColumn&&crossing.passageIndex==PassageIndex
|
&&PassageColumnSegmentFlags[PassageIndex][SegmentIndex]!=0;
|
||||||
&&SegmentIndex>=crossing.firstSegment&&SegmentIndex<=crossing.lastSegment;});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsPassageShortSpanSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const
|
bool IsPassageShortSpanSegment(std::size_t PassageIndex,std::size_t SegmentIndex) const
|
||||||
{
|
{
|
||||||
return std::any_of(PassageVoidCrossings.begin(),PassageVoidCrossings.end(),
|
return PassageIndex<PassageShortSpanSegmentFlags.size()
|
||||||
[PassageIndex,SegmentIndex](const auto& crossing)
|
&&SegmentIndex<PassageShortSpanSegmentFlags[PassageIndex].size()
|
||||||
{return !crossing.needsColumn&&crossing.passageIndex==PassageIndex
|
&&PassageShortSpanSegmentFlags[PassageIndex][SegmentIndex]!=0;
|
||||||
&&SegmentIndex>=crossing.firstSegment&&SegmentIndex<=crossing.lastSegment;});
|
}
|
||||||
|
|
||||||
|
bool PassageHasShortSpanSegments(std::size_t PassageIndex) const
|
||||||
|
{
|
||||||
|
return PassageIndex<PassageHasShortSpanFlags.size()
|
||||||
|
&&PassageHasShortSpanFlags[PassageIndex]!=0;
|
||||||
}
|
}
|
||||||
|
|
||||||
double PassageBridgeDeckOffsetMetres(std::size_t PassageIndex,
|
double PassageBridgeDeckOffsetMetres(std::size_t PassageIndex,
|
||||||
@@ -1814,7 +2534,6 @@ private:
|
|||||||
return qz-crossing.topZSubvoxels;
|
return qz-crossing.topZSubvoxels;
|
||||||
if(qz<crossing.bottomZSubvoxels)
|
if(qz<crossing.bottomZSubvoxels)
|
||||||
return crossing.bottomZSubvoxels-qz;
|
return crossing.bottomZSubvoxels-qz;
|
||||||
|
|
||||||
// The lower-strate route is already walkable in its native field and binds
|
// 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
|
// 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.
|
// flares into the ground, so the pillar joins the route instead of sealing it.
|
||||||
@@ -1915,14 +2634,15 @@ private:
|
|||||||
void BuildPassageVoidCrossingPlan()
|
void BuildPassageVoidCrossingPlan()
|
||||||
{
|
{
|
||||||
PassageVoidCrossings.clear();
|
PassageVoidCrossings.clear();
|
||||||
if(!Owner||!ResolvedLayout||ResolvedLayout->passages.empty()
|
if(!Owner||!ResolvedLayout||ResolvedLayout->passages.empty())return;
|
||||||
||!Owner->PassageVoidCrossingCache)return;
|
if(!Owner->PassageVoidCrossingCache)
|
||||||
|
{BuildPassageSpatialIndices();return;}
|
||||||
auto& cache=*Owner->PassageVoidCrossingCache;
|
auto& cache=*Owner->PassageVoidCrossingCache;
|
||||||
std::lock_guard<std::mutex> lock(cache.mutex);
|
std::lock_guard<std::mutex> lock(cache.mutex);
|
||||||
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v1";
|
const std::string cacheKey=ResolvedLayout->stableIdentity+"/passage-void-columns-v1";
|
||||||
const auto cached=cache.byLayout.find(cacheKey);
|
const auto cached=cache.byLayout.find(cacheKey);
|
||||||
if(cached!=cache.byLayout.end())
|
if(cached!=cache.byLayout.end())
|
||||||
{PassageVoidCrossings=cached->second;return;}
|
{PassageVoidCrossings=cached->second;BuildPassageSpatialIndices();return;}
|
||||||
|
|
||||||
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
constexpr double Q=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||||
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,
|
const double qPerMetre=Q*1000000.0/std::max<std::uint32_t>(1,
|
||||||
@@ -2109,6 +2829,7 @@ private:
|
|||||||
passage.stableId.c_str());
|
passage.stableId.c_str());
|
||||||
}
|
}
|
||||||
cache.byLayout.emplace(cacheKey,PassageVoidCrossings);
|
cache.byLayout.emplace(cacheKey,PassageVoidCrossings);
|
||||||
|
BuildPassageSpatialIndices();
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z)
|
static uint64 PackLatticeKey(int32 X,int32 Y,int32 Z)
|
||||||
@@ -6373,4 +7094,15 @@ private:
|
|||||||
uint64 GpuVisualDetailNodeCount=0,CpuVisualDetailNodeCount=0;
|
uint64 GpuVisualDetailNodeCount=0,CpuVisualDetailNodeCount=0;
|
||||||
FBuildStats Stats;
|
FBuildStats Stats;
|
||||||
bool bAnalyticOnly=false;
|
bool bAnalyticOnly=false;
|
||||||
|
inline static thread_local const FScopedPassageTileQueries* ActivePassageTileQueries=nullptr;
|
||||||
|
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> PassageFeatureBounds;
|
||||||
|
std::vector<FPassageTileFeature> PassageTileFeatures;
|
||||||
|
std::vector<std::uint32_t> PassageFeatureOrder;
|
||||||
|
std::vector<FPassageFeatureNode> PassageFeatureNodes;
|
||||||
|
bool PassageFeatureBoundsFailOpen=false;
|
||||||
|
std::vector<std::vector<FPassageNearestNode>> PassageNearestNodes;
|
||||||
|
std::vector<FPassageProofBound> PassageProofBounds;
|
||||||
|
std::vector<FPassageProofNode> PassageProofNodes;
|
||||||
|
std::vector<std::vector<std::uint8_t>> PassageColumnSegmentFlags,PassageShortSpanSegmentFlags;
|
||||||
|
std::vector<std::uint8_t> PassageHasShortSpanFlags;
|
||||||
};
|
};
|
||||||
@@ -2137,6 +2137,12 @@ namespace VoxelForgeCore
|
|||||||
out.diagnostics.featureListMilliseconds=std::chrono::duration<double,std::milli>(
|
out.diagnostics.featureListMilliseconds=std::chrono::duration<double,std::milli>(
|
||||||
std::chrono::steady_clock::now()-FeatureListStart).count();
|
std::chrono::steady_clock::now()-FeatureListStart).count();
|
||||||
const ScopedWorldAssemblyFeatureQueriesV1 featureScope(featureQueries);
|
const ScopedWorldAssemblyFeatureQueriesV1 featureScope(featureQueries);
|
||||||
|
// GenerateMesh samples only origin..origin+extent plus the one-step normal halo;
|
||||||
|
// queryMinimum/queryMaximum above are exactly that inclusive domain.
|
||||||
|
// GenerateMesh n'échantillonne que origin..origin+extent et la marge de normale d'un pas ;
|
||||||
|
// queryMinimum/queryMaximum ci-dessus couvrent exactement ce domaine inclusif.
|
||||||
|
const FStandaloneShapeWorld::FScopedPassageTileQueries passageFeatureScope(
|
||||||
|
tileShape,queryMinimum,queryMaximum);
|
||||||
const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0
|
const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0
|
||||||
||options.diagnosticColumnInterpolationStride>1;
|
||options.diagnosticColumnInterpolationStride>1;
|
||||||
const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling
|
const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Linux L28 — tile-bounded columns and passages
|
||||||
|
|
||||||
|
Branch: wip/l25-helix, based on L27. No commit. No Unreal files changed.
|
||||||
|
|
||||||
|
## Cost and culling
|
||||||
|
|
||||||
|
The plain interleaved baseline puts the L27 overhead in tile streaming/meshing, not shape
|
||||||
|
compilation: mean whole-view time was 2.058 s versus L24's 1.527 s for SurfaceWorld (+34.8%),
|
||||||
|
and 19.961 s versus 16.699 s for FloatingIslands (+19.5%). Shape time stayed near 0.46 s
|
||||||
|
(SurfaceWorld) and 0.97 s (FloatingIslands). The initial extra work was passage composition
|
||||||
|
inside the map-wide density sampling loop.
|
||||||
|
|
||||||
|
GenerateMesh now enters a passage-query scope using the exact inclusive tile sample bounds,
|
||||||
|
including the normal halo. A conservative feature AABB BVH supplies candidate route segments,
|
||||||
|
columns and mouth support. Long route nearest-distance searches use a per-passage segment BVH.
|
||||||
|
L9 proof boxes use a separate passage-bound BVH; disjoint roots reject far boxes without scanning
|
||||||
|
segments. If a native-only negative-control shape has no proof index, its box check falls back to
|
||||||
|
a conservative linear test.
|
||||||
|
|
||||||
|
Instrumented full-view counts (diagnostic build; 2,370 desired tiles):
|
||||||
|
|
||||||
|
| Recipe | Composition samples | Column guard: calls / tiles | Exact column SDF: calls / tiles | Route band samples / tiles | Route SDF unions / tiles | Route samples skipped / tiles | Feature candidates: tiles / total |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| SurfaceWorld | 12,039,941 | 1,885,887 / 692 | 3,017 / 4 | 1,380,907 / 870 | 164,337 / 543 | 7,470,572 / 1,792 | 8 / 1,222 |
|
||||||
|
| FloatingIslands | 58,019,623 | 7,619,575 / 689 | 7,213 / 6 | 1,902,086 / 708 | 62,507 / 89 | 54,848,675 / 2,212 | 14 / 1,331 |
|
||||||
|
|
||||||
|
The exact column SDF is spatially culled. The cheaper radial guard remains in tiles sharing the
|
||||||
|
column's Z slab because positive SDF values can lower positive base density; a broader tile skip
|
||||||
|
would need an exact bound on that field. This is part of the remaining SurfaceWorld cost.
|
||||||
|
Per composition sample, the profile counted 0.125 carve-segment tests, 0.036 support-segment tests
|
||||||
|
and 0.0136 route SDF unions in SurfaceWorld; FloatingIslands counted 0.0125, 0.0028 and 0.00108.
|
||||||
|
Column guards ran on 15.7% / 13.1% of samples, while the complex column SDF ran on 0.025% / 0.012%.
|
||||||
|
|
||||||
|
## Interleaved whole-view cost
|
||||||
|
|
||||||
|
Five W12 quartets ran in order L24, L28a, fresh L27-source build, L28b. Hashing was off.
|
||||||
|
Values below are means of the five samples; raw samples are in
|
||||||
|
Saved/Linux/linux28-r28-finalfix-cost5-{recipe}-{l24,l27,r28a,r28b}-p1..p5.log.
|
||||||
|
L24 used Saved/Linux/l24-source/bin/linux/VoxelForgeCoreHeadless-linux24-clang19;
|
||||||
|
L27 was rebuilt from the current L27 source for this comparison.
|
||||||
|
|
||||||
|
| Recipe | L24 | L27 | L28a | L28b | L28a / L28b vs L24 |
|
||||||
|
|---|---:|---:|---:|---:|---:|
|
||||||
|
| SurfaceWorld | 1.527 s | 2.058 s | 1.592 s | 1.605 s | +4.30% / +5.12% |
|
||||||
|
| FloatingIslands | 16.699 s | 19.961 s | 16.651 s | 16.787 s | -0.29% / +0.53% |
|
||||||
|
|
||||||
|
FloatingIslands returned to L24 cost. SurfaceWorld misses the ~3% target by 1.3–2.1 percentage
|
||||||
|
points. Its remaining excess is about 69–80 ms in streaming wall time and 13–15 ms in shape
|
||||||
|
time; the density-aware radial guard and per-tile query setup/traversal remain. The guard cannot
|
||||||
|
be removed bit-exactly without a proven upper bound on the incoming positive density.
|
||||||
|
|
||||||
|
## Bit parity and audits
|
||||||
|
|
||||||
|
Final Clang and GCC binaries match L27 for both recipes at W1, W8 and W12: all 12 hash records per
|
||||||
|
run match (full detail, whole view, LOD0–8, exact query). This is 12 compiler/recipe/worker
|
||||||
|
combinations with zero mismatches. Logs:
|
||||||
|
Saved/Linux/linux28-r28-hash-finalfix-{clang,gcc}-{surface-world,floating-islands}-w{1,8,12}.log.
|
||||||
|
|
||||||
|
Representative hashes, equal to L27:
|
||||||
|
|
||||||
|
| Recipe | Full detail: density / mesh / payload / topology | Whole view: density / mesh / payload / topology | Exact query: density / detail / collision |
|
||||||
|
|---|---|---|---|
|
||||||
|
| SurfaceWorld | ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae | 117547e5e18c6847 / db5fa276315ff2d4 / b4e9d01d70413fea / 03caf881fa047f1c | c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849 |
|
||||||
|
| FloatingIslands | ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec | 70ab5bc87dd174d2 / 6f357f9085a496f0 / 08ce7d4e036da65a / eb5eef04a5cb85d9 | 7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf |
|
||||||
|
|
||||||
|
The 24 m crossing matches fresh L27-source builds on both compilers and recipes:
|
||||||
|
SurfaceWorld, 314 tiles: density 3561ea23485e359f, mesh e8331620fb0fb908,
|
||||||
|
payload 765f7dad67c852aa, topology 3ccc09ac81c05d71.
|
||||||
|
FloatingIslands: density e3d0ea833bca4215, mesh 6b3d0f148ed83d93,
|
||||||
|
payload d6ed85f16acd895e, topology 9745683036522461.
|
||||||
|
|
||||||
|
Across 4 styles × 2 recipes, capsule walkability, open air, naturalness, no-long-bridge,
|
||||||
|
strict-validator controls, route bindings and passage audits pass on final Clang/GCC binaries.
|
||||||
|
Final passage logs are
|
||||||
|
Saved/Linux/linux28-r28-finalpassage-{clang,gcc}-{surface-world,floating-islands}-style{0..3}.log.
|
||||||
|
|
||||||
|
The final route/proof/readiness matrix also passes in all 16 compiler/style/recipe runs:
|
||||||
|
L13 route parity verifies 8/8 page pairs and 14 higher-LOD exact pairs with zero mismatches;
|
||||||
|
the readiness guard catches its negative control; L9 reports zero proof violations.
|
||||||
|
SurfaceWorld has 0 unknown blocks; FloatingIslands has 21,504 fallback/unknown blocks for each
|
||||||
|
style. The diagnostic full-view profile queried 26,946 FI proof boxes with only 40 path-bound and
|
||||||
|
1 column-bound tests, so far boxes reject at the BVH root. These unknowns are not evidence of a
|
||||||
|
fat passage bound. A fresh rerun of the saved L27 binary with the default owner input also reports
|
||||||
|
21,504; older L27 logs showing 17,408 did not reproduce. Final matrix logs are
|
||||||
|
Saved/Linux/linux28-r28-finalmatrix-{clang,gcc}-{surface-world,floating-islands}-style{0..3}.log.
|
||||||
|
|
||||||
|
Two inherited SurfaceWorld FloorProfileBlend checks remain over the 2-voxel threshold in styles
|
||||||
|
1 and 3: 2.544 and 3.645 voxels. The same failures and values are in L27 logs for both compilers;
|
||||||
|
other styles pass.
|
||||||
|
|
||||||
|
## Invariants, edits and run history
|
||||||
|
|
||||||
|
Changed Core files: Standalone/Core/src/ShapeWorldInternal.h and
|
||||||
|
Standalone/Core/src/VoxelForgeCore.cpp. Navigation/design docs and this report were updated.
|
||||||
|
No Unreal edits or Unreal compile. L3 FP-contract preamble/self-test, L4 FLazy, L7 shared cache,
|
||||||
|
L9/L12 proof rules, L11 readiness, L13 route binding, L18/L19 shared ceiling, L21/L22 layout and
|
||||||
|
bands, L23–L27 passage behavior, ARCHITECTURE §8.10 and CLAUDE.md async rules remain intact.
|
||||||
|
|
||||||
|
Failed or retried runs:
|
||||||
|
|
||||||
|
- First parity/readiness command omitted --stream-benchmark and --stream-pipeline; rerun with both.
|
||||||
|
- Initial readiness negative-control crashed on an empty proof BVH in a temporary native-only
|
||||||
|
shape. Added the conservative no-index fallback; final readiness checks pass.
|
||||||
|
- The first final W1 hash command omitted the explicit owner data path and failed to open data
|
||||||
|
under Saved/Linux. Preserved as linux28-r28-hash-finalfix-clang-surface-world-w1-attempt1-failed.log;
|
||||||
|
rerun with Standalone/data/owner_world.json and all final W1/W8 hashes passed.
|
||||||
|
- Initial moving-crossing comparison used stale L27 logs. Rebuilt L27 from source and reran; all
|
||||||
|
four compiler/recipe hashes then matched.
|
||||||
|
- Older saved L27 proof logs reported 17,408 FloatingIslands unknown blocks. Reran the saved L27
|
||||||
|
binaries and compared with final L28: both report 21,504, zero violations.
|
||||||
|
- An exploratory composed-bound FloatingIslands W12 hash differed; the density-threshold variant
|
||||||
|
restored equality and is the version measured here. An interrupted/empty Clang R15 build was
|
||||||
|
rerun successfully.
|
||||||
|
|
||||||
|
No run exceeded 30 minutes or remained stuck for 15 minutes. Builds were Clang 19 and GCC 14,
|
||||||
|
W12 CPU affinity 0–11, with the 6 GiB address-space cap. No commit, push or stash was performed.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Re-run by me (W12, no hashing, interleaved): SW L24 1.455/1.485/1.484 s vs L28 1.502/1.539/1.555 s (median +3.7%).
|
||||||
|
Accepted as the cost of the passage/column feature. FI is back to L24 cost.
|
||||||
|
- Bit-identical to L27 (12/12 configs + crossing). With this, `wip/l25-helix` (L25–L28) is merged into
|
||||||
|
`assembler/world`.
|
||||||
|
- Note for later: the pre-existing SW FloorProfileBlend check over threshold in styles 1/3 (2.544/3.645 voxels)
|
||||||
|
is unchanged since L27.
|
||||||
Reference in new issue
Block a user