passages: XY-smooth spline with planned height profile + open-air half-pipe (L112, v219)

Dense centripetal spline in XY, height piecewise-linear on the planner's certified profile,
radius/floor depth blended; open air = floor slab + 1 m side walls, no roof; audits sample the
generated centreline. All walk audits pass (grades <= 15 deg, headroom, shell, mouths, ripple);
Clang/GCC W1/W8/W12 deterministic; fast tier PASS; Linux UE builds. Chevron rings gone.
Open issues: jagged spikes along the half-pipe wall tops, visible step lines on the trough
floor; near-ready +7..+10% vs L109 (candidate list p95 4 -> 22). Not merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-29 20:39:20 +01:00
1 parent 5ec09d1ff1
commit 16554cd426
130 files changed
+8626 -624

No files matched your search

@@ -0,0 +1,205 @@
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <span>
#include <vector>
namespace VoxelForgeCore::PassageSpline
{
using FPoint = std::array<double,4>;
inline constexpr std::size_t ArcTableSteps = 32;
inline constexpr double UpperMouthFloorBlendRadiusMetres = 3.5;
struct FSegment
{
FPoint a{},b{};
double alongMetres=0.0,lengthMetres=0.0;
std::size_t controlSpan=0;
double tA=0.0,tB=1.0;
double plannedFloorA=0.0,plannedFloorB=0.0;
};
inline double KnotSpan(const FPoint& a,const FPoint& b,double unitsPerMetre)
{
const double dx=(b[0]-a[0])/unitsPerMetre;
const double dy=(b[1]-a[1])/unitsPerMetre;
return std::max(1.0e-6,std::sqrt(std::sqrt(dx*dx+dy*dy)));
}
inline FPoint EvaluateRaw(std::span<const FPoint> controls,std::size_t span,
double t,double unitsPerMetre)
{
const auto& p1=controls[span];
const auto& p2=controls[span+1];
const auto& p0=controls[span==0?0:span-1];
const auto& p3=controls[std::min(span+2,controls.size()-1)];
const double dt0=KnotSpan(p0,p1,unitsPerMetre);
const double dt1=KnotSpan(p1,p2,unitsPerMetre);
const double dt2=KnotSpan(p2,p3,unitsPerMetre);
const double t2=t*t,t3=t2*t;
FPoint result{};
for(const std::size_t axis:{0u,1u,3u})
{
const double m1=dt1*((p1[axis]-p0[axis])/dt0
-(p2[axis]-p0[axis])/(dt0+dt1)
+(p2[axis]-p1[axis])/dt1);
const double m2=dt1*((p2[axis]-p1[axis])/dt1
-(p3[axis]-p1[axis])/(dt1+dt2)
+(p3[axis]-p2[axis])/dt2);
result[axis]=(2.0*t3-3.0*t2+1.0)*p1[axis]
+(-2.0*t3+3.0*t2)*p2[axis]
+(t3-2.0*t2+t)*m1+(t3-t2)*m2;
}
result[2]=std::lerp(p1[2],p2[2],t);
result[3]=std::clamp(result[3],std::min(p1[3],p2[3]),
std::max(p1[3],p2[3]));
return result;
}
inline bool ClosestRouteHeightAtXY(std::span<const std::array<double,3>> route,
double x,double y,double& height,double& distanceSquared)
{
if(route.empty())return false;
distanceSquared=std::numeric_limits<double>::infinity();
const auto consider=[&](const std::array<double,3>& point)
{
const double dx=x-point[0],dy=y-point[1];
const double candidate=dx*dx+dy*dy;
if(candidate<distanceSquared)
{distanceSquared=candidate;height=point[2];}
};
if(route.size()==1)
{
consider(route.front());
return true;
}
for(std::size_t i=1;i<route.size();++i)
{
const auto& a=route[i-1];
const auto& b=route[i];
const double dx=b[0]-a[0],dy=b[1]-a[1];
const double lengthSquared=dx*dx+dy*dy;
const double t=lengthSquared>1.0e-12
?std::clamp(((x-a[0])*dx+(y-a[1])*dy)/lengthSquared,0.0,1.0):0.0;
const std::array<double,3> point={a[0]+t*dx,a[1]+t*dy,
std::lerp(a[2],b[2],t)};
consider(point);
}
return std::isfinite(distanceSquared);
}
inline double BlendUpperMouthFloorAtXY(
std::span<const std::array<double,3>> receivingRoute,double x,double y,
double plannedFloor,double mouthX,double mouthY,double mouthFloor,
double tangentX,double tangentY,
double blendRadiusMetres,double playerCenterAboveFloorMetres,
double unitsPerMetre=1.0)
{
if(receivingRoute.empty()||!(blendRadiusMetres>0.0)
||!(unitsPerMetre>0.0))return plannedFloor;
const double dx=(x-mouthX)/unitsPerMetre;
const double dy=(y-mouthY)/unitsPerMetre;
if(dx*tangentX+dy*tangentY>=0.0)return plannedFloor;
const double dz=(plannedFloor-mouthFloor)/unitsPerMetre;
const double distance=std::sqrt(dx*dx+dy*dy+dz*dz);
double routeHeight=0.0,routeDistanceSquared=0.0;
if(distance>blendRadiusMetres
||!ClosestRouteHeightAtXY(receivingRoute,x,y,routeHeight,
routeDistanceSquared)
||routeDistanceSquared>std::pow(2.0*blendRadiusMetres*unitsPerMetre,2.0))
return plannedFloor;
const double t=std::clamp(distance/blendRadiusMetres,0.0,1.0);
const double blend=t*t*(3.0-2.0*t);
const double receivingFloor=routeHeight
-playerCenterAboveFloorMetres*unitsPerMetre;
return std::lerp(plannedFloor,receivingFloor,blend);
}
inline std::vector<FSegment> BuildSegments(std::span<const FPoint> controls,
std::size_t spanCount,double spacingMetres,double unitsPerMetre=1.0,
std::span<const double> plannedFloorProfile={})
{
std::vector<FSegment> result;
if(controls.size()<2||!(spacingMetres>0.0)||!(unitsPerMetre>0.0))return result;
spanCount=std::min(spanCount,controls.size()-1);
double alongMetres=0.0;
for(std::size_t span=0;span<spanCount;++span)
{
std::array<double,ArcTableSteps+1> horizontalArc{};
auto previous=EvaluateRaw(controls,span,0.0,unitsPerMetre);
for(std::size_t step=1;step<=ArcTableSteps;++step)
{
const auto current=EvaluateRaw(controls,span,
static_cast<double>(step)/ArcTableSteps,unitsPerMetre);
const double dx=(current[0]-previous[0])/unitsPerMetre;
const double dy=(current[1]-previous[1])/unitsPerMetre;
horizontalArc[step]=horizontalArc[step-1]+std::sqrt(dx*dx+dy*dy);
previous=current;
}
const double spanLength=horizontalArc.back();
const std::size_t segmentCount=std::max<std::size_t>(1,
static_cast<std::size_t>(std::ceil(spanLength/spacingMetres)));
std::vector<FPoint> points;
std::vector<double> parameters;
points.reserve(segmentCount+1);
parameters.reserve(segmentCount+1);
points.push_back(EvaluateRaw(controls,span,0.0,unitsPerMetre));
parameters.push_back(0.0);
for(std::size_t segment=1;segment<=segmentCount;++segment)
{
const double targetArc=spanLength*static_cast<double>(segment)
/segmentCount;
const auto upper=std::lower_bound(horizontalArc.begin()+1,
horizontalArc.end(),targetArc);
const std::size_t upperIndex=static_cast<std::size_t>(
upper-horizontalArc.begin());
const std::size_t lowerIndex=upperIndex-1;
const double arcSpan=horizontalArc[upperIndex]-horizontalArc[lowerIndex];
const double arcT=arcSpan>1.0e-12
?std::clamp((targetArc-horizontalArc[lowerIndex])/arcSpan,0.0,1.0):0.0;
const double t=(static_cast<double>(lowerIndex)+arcT)/ArcTableSteps;
points.push_back(EvaluateRaw(controls,span,t,unitsPerMetre));
parameters.push_back(t);
}
std::vector<double> sampledHorizontalArc(points.size(),0.0);
for(std::size_t i=1;i<points.size();++i)
{
const double dx=(points[i][0]-points[i-1][0])/unitsPerMetre;
const double dy=(points[i][1]-points[i-1][1])/unitsPerMetre;
sampledHorizontalArc[i]=sampledHorizontalArc[i-1]
+std::sqrt(dx*dx+dy*dy);
}
const double sampledSpanLength=sampledHorizontalArc.back();
for(std::size_t i=0;i<points.size();++i)
{
const double progress=sampledSpanLength>1.0e-12
?sampledHorizontalArc[i]/sampledSpanLength:parameters[i];
points[i][2]=std::lerp(controls[span][2],controls[span+1][2],progress);
}
for(std::size_t i=1;i<points.size();++i)
{
const double dx=(points[i][0]-points[i-1][0])/unitsPerMetre;
const double dy=(points[i][1]-points[i-1][1])/unitsPerMetre;
const double dz=(points[i][2]-points[i-1][2])/unitsPerMetre;
const double length=std::sqrt(dx*dx+dy*dy+dz*dz);
const auto floorAt=[&](std::size_t index)
{
if(plannedFloorProfile.size()!=controls.size())
return points[index][2];
const double progress=sampledSpanLength>1.0e-12
?sampledHorizontalArc[index]/sampledSpanLength:parameters[index];
return std::lerp(plannedFloorProfile[span],
plannedFloorProfile[span+1],progress);
};
result.push_back({points[i-1],points[i],alongMetres,length,span,
parameters[i-1],parameters[i],floorAt(i-1),floorAt(i)});
alongMetres+=length;
}
}
return result;
}
}
@@ -12,6 +12,7 @@
#include <vector>
#include "VoxelForgeCore/Linux77Profiler.h"
#include "VoxelForgeCore/PassageSpline.h"
// UBT supplies this export/import macro for the runtime module. The standalone target has no
// module boundary, so leave it empty there. UBT fournit la macro; le viewer autonome reste neutre.
@@ -468,9 +469,9 @@ namespace VoxelForgeCore
// Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges.
// Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB.
// Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads.
// Version 218 resamples passages on a smooth spline and uses open-air half-pipe shells.
// Version 218 sweeps passages around a dense smooth spline and limits exposed shells to a trough.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 218;
// Version 219 smooths passage XY while mapping each planned height chord over its sampled horizontal arc.
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 219;
enum class FieldChannel : std::uint32_t
{
+113 -249
View File
@@ -7,6 +7,7 @@
#include "VoxelStrateManager.h"
#include "VoxelDensityPrimitives.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include "VoxelForgeCore/PassageSpline.h"
#include "VoxelForgeFieldShared.h"
#include "VoxelForgeCore/FloatingIslandInstrumentation.h"
#include "VoxelForgeCore/Linux77Profiler.h"
@@ -1223,11 +1224,16 @@ public:
double unionDistance=std::numeric_limits<double>::infinity();
double unionShellDistance=std::numeric_limits<double>::infinity();
const std::uint32_t reliefSeed=static_cast<std::uint32_t>(Owner?Owner->Seed:0);
const auto* queryStratum=FindBoundStratumForManagerZ(Z);
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
++passageIndex)
{
const auto& passage=ResolvedLayout->passages[passageIndex];
if(passage.controlPoints.size()<2)continue;
if(queryStratum&&queryStratum->Resolved
&&queryStratum->Resolved->passageCapabilities.hasNativeCaveField
&&passage.lowerStrateIndex==queryStratum->StrateIndex)
continue;
if(passageProfile)++passageProfile->passageIterations;
if(passageTileScopeMatches&&!passageTileQueries->hasRouteFeature(passageIndex))
continue;
@@ -1291,7 +1297,7 @@ public:
const double floorDepth=PassageFlatFloorDepthSubvoxels(
constants,segmentIndex,floorT,floorRadius,qPerMetre);
const double floorCenterZ=PassageFlatFloorCenterZSubvoxels(
constants,segmentIndex,floorT,segment,floorDepth,qPerMetre);
constants,segmentIndex,floorT,segment,qx,qy,floorDepth,qPerMetre);
const double flatDistance=PassageFlatBottomDistance(
static_cast<float>(roundDistance),floorCenterZ,qz,floorDepth,qPerMetre);
if(flatDistance<bestDistance
@@ -1340,7 +1346,7 @@ public:
const double floorDepth=PassageFlatFloorDepthSubvoxels(
constants,segmentIndex,floorT,floorRadius,qPerMetre);
const double floorCenterZ=PassageFlatFloorCenterZSubvoxels(
constants,segmentIndex,floorT,segment,floorDepth,qPerMetre);
constants,segmentIndex,floorT,segment,qx,qy,floorDepth,qPerMetre);
const double flatDistance=PassageFlatBottomDistance(
static_cast<float>(roundDistance),floorCenterZ,qz,floorDepth,qPerMetre);
const double alongMetres=segment.authoredAlongMetres
@@ -1453,17 +1459,22 @@ public:
// solid only in air, so it leaves host rock unchanged. Taking the union
// before writing either polarity makes every tunnel interior beat every
// overlapping tunnel shell.
// Preserve native graph air inside the passage interior before any shell
// can change its sign. Outside that interior, the shell may cover host air.
if(nativeTunnelFieldDensity>=0.0f&&unionDistance<=0.0)
// Preserve both native graph air and passage interior before any shell
// can change their sign. If they overlap, their positive air fields combine.
if(nativeTunnelFieldDensity>=0.0f)
{
const float passageAir=static_cast<float>(-unionDistance/Q);
const float passageAir=unionDistance<=0.0
?static_cast<float>(-unionDistance/Q)
:std::numeric_limits<float>::lowest();
return std::max(nativeTunnelFieldDensity,passageAir);
}
if(nativeFloorSupport!=std::numeric_limits<float>::lowest()
&&nativeFloorSupport<0.0f)
return std::min(nativeFloorSupport,unionDistance<=0.0
?static_cast<float>(-unionDistance/Q):density);
if(nativeTunnelFieldDensity!=std::numeric_limits<float>::lowest())
return unionDistance<=0.0
?static_cast<float>(-unionDistance/Q):density;
if(unionDistance<=0.0)
return static_cast<float>(-unionDistance/Q);
if(unionShellDistance<0.0&&density>=0.0f)
@@ -3368,50 +3379,23 @@ private:
{
double floorDepthMetres=0.0,totalPathLengthMetres=0.0;
double authoredTotalPathLengthMetres=0.0;
double upperMouthFloorDepthMetres=0.0,lowerMouthFloorDepthMetres=0.0;
double upperMouthX=0.0,upperMouthY=0.0;
double upperMouthPlannedFloor=0.0;
double upperMouthTangentX=0.0,upperMouthTangentY=0.0;
std::vector<std::array<double,3>> upperMouthReceivingRoute;
std::size_t routeEndControlIndex=0;
};
struct FPassageSegmentData
{
std::array<double,3> a{},b{},delta{};
std::array<std::int64_t,3> sweepMinimumSubvoxels{},sweepMaximumSubvoxels{};
double authoredCenterZA=0.0,authoredCenterZDelta=0.0;
double authoredFloorZA=0.0,authoredFloorZDelta=0.0;
double authoredAlongMetres=0.0,authoredLengthMetres=0.0;
double radiusA=0.0,radiusB=0.0,radiusDelta=0.0;
double lengthSq=0.0,horizontalLengthSq=0.0;
double lengthSubvoxels=0.0,horizontalLength=0.0;
bool routeSegment=false;
};
static double PassageCentripetalKnotSpan(
const std::array<double,4>& a,const std::array<double,4>& b)
{
const double dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2];
return std::max(1.0e-6,DeterministicSqrt(DeterministicSqrt(
dx*dx+dy*dy+dz*dz)));
}
static std::array<double,4> PassageCentripetalCatmullRom(
const std::array<double,4>& p0,const std::array<double,4>& p1,
const std::array<double,4>& p2,const std::array<double,4>& p3,
double t,double dt0,double dt1,double dt2)
{
std::array<double,4> result{};
const double t2=t*t,t3=t2*t;
for(std::size_t axis=0;axis<4;++axis)
{
const double m1=dt1*((p1[axis]-p0[axis])/dt0
-(p2[axis]-p0[axis])/(dt0+dt1)
+(p2[axis]-p1[axis])/dt1);
const double m2=dt1*((p2[axis]-p1[axis])/dt1
-(p3[axis]-p1[axis])/(dt1+dt2)
+(p3[axis]-p2[axis])/dt2);
result[axis]=(2.0*t3-3.0*t2+1.0)*p1[axis]
+(-2.0*t3+3.0*t2)*p2[axis]
+(t3-2.0*t2+t)*m1+(t3-t2)*m2;
}
result[3]=std::clamp(result[3],std::min(p1[3],p2[3]),
std::max(p1[3],p2[3]));
return result;
}
static double PassageFlatFloorDepthSubvoxels(
const FPassageComposerConstants& constants,std::size_t segmentIndex,double t,
double radiusSubvoxels,double qPerMetre)
@@ -3423,37 +3407,23 @@ private:
std::max(0.0,radiusSubvoxels-0.23*qPerMetre));
}
static double PassageRouteFloorDepthMetres(
const FPassageComposerConstants& constants,double alongMetres,double radiusMetres)
{
const double localRadiusDepth=std::max(0.0,radiusMetres-0.23);
const double mouthDistance=std::max(0.0,std::min(alongMetres,
constants.authoredTotalPathLengthMetres-alongMetres));
const double linear=std::clamp(mouthDistance/5.0,0.0,1.0);
const double ramp=linear*linear*(3.0-2.0*linear);
const double target=alongMetres<=constants.authoredTotalPathLengthMetres*0.5
?constants.upperMouthFloorDepthMetres
:constants.lowerMouthFloorDepthMetres;
// The mouth coordinate is the player center 1.13 m above the floor.
// Let its ramp reach the full radius there, then ease to radius-0.23 m
// through the body over five metres.
const double mouthDepth=std::clamp(target,0.0,std::max(0.0,radiusMetres));
return std::lerp(mouthDepth,localRadiusDepth,ramp);
}
static double PassageFlatFloorCenterZSubvoxels(
const FPassageComposerConstants& constants,std::size_t segmentIndex,double floorT,
const FPassageSegmentData& segment,double floorDepthSubvoxels,double qPerMetre)
const FPassageSegmentData& segment,double queryXSubvoxels,double queryYSubvoxels,
double floorDepthSubvoxels,double qPerMetre)
{
if(!segment.routeSegment||qPerMetre<=0.0)
return segment.a[2]+floorT*segment.delta[2];
const double radiusMetres=(segment.radiusA+floorT*segment.radiusDelta)/qPerMetre;
const double alongMetres=segment.authoredAlongMetres
+floorT*segment.authoredLengthMetres;
const double routeDepth=PassageRouteFloorDepthMetres(
constants,alongMetres,radiusMetres);
return segment.authoredCenterZA+floorT*segment.authoredCenterZDelta
-routeDepth*qPerMetre+floorDepthSubvoxels;
const double plannedFloor=segment.authoredFloorZA
+floorT*segment.authoredFloorZDelta;
const double mouthBlendFloor=VoxelForgeCore::PassageSpline::BlendUpperMouthFloorAtXY(
constants.upperMouthReceivingRoute,queryXSubvoxels,queryYSubvoxels,
plannedFloor,constants.upperMouthX,constants.upperMouthY,
constants.upperMouthPlannedFloor,
constants.upperMouthTangentX,constants.upperMouthTangentY,
VoxelForgeCore::PassageSpline::UpperMouthFloorBlendRadiusMetres,1.13,
qPerMetre);
return mouthBlendFloor+floorDepthSubvoxels;
}
static float PassageFlatBottomDistance(float roundDistanceSubvoxels,
@@ -4529,7 +4499,7 @@ private:
if(!ResolvedLayout)
{
if(tracePreparation)
std::printf("[L111PassagePrepare] passages=0 control_spans=0 segments=0 spacing_m=1.00 bytes=0 prepare_ms=%.3f\n",
std::printf("[L112PassagePrepare] passages=0 control_spans=0 segments=0 spacing_m=1.00 bytes=0 prepare_ms=%.3f\n",
std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-preparationStart).count());
return;
@@ -4541,7 +4511,6 @@ private:
const double qPerMetre=Q*1000000.0/
std::max<std::uint32_t>(1,ResolvedLayout->coreVoxelSizeMicrometres);
constexpr double PassageSegmentSpacingMetres=1.0;
constexpr std::size_t ArcLengthTableSteps=32;
std::size_t preparedSegments=0,controlSpans=0;
for(std::size_t passageIndex=0;passageIndex<ResolvedLayout->passages.size();
++passageIndex)
@@ -4558,87 +4527,76 @@ private:
auto& constants=PassageComposerConstants[passageIndex];
constants.routeEndControlIndex=routeEnd;
struct FRawPassageSegment
{
std::array<double,4> a{},b{};
double alongMetres=0.0,lengthMetres=0.0;
std::size_t span=0;
double tA=0.0,tB=1.0;
bool routeSegment=false;
};
std::vector<std::array<double,4>> controls;
using FRawPassageSegment=VoxelForgeCore::PassageSpline::FSegment;
std::vector<VoxelForgeCore::PassageSpline::FPoint> controls;
controls.reserve(passage.controlPoints.size());
std::vector<double> plannedFloorProfile;
plannedFloorProfile.reserve(passage.controlPoints.size());
for(const auto& point:passage.controlPoints)
{
controls.push_back({static_cast<double>(point.positionSubvoxels[0]),
static_cast<double>(point.positionSubvoxels[1]),
static_cast<double>(point.positionSubvoxels[2]),
static_cast<double>(point.radiusSubvoxels)});
std::vector<FRawPassageSegment> rawSegments;
rawSegments.reserve((passage.controlPoints.size()-1)*10);
plannedFloorProfile.push_back(static_cast<double>(
point.positionSubvoxels[2]-point.radiusSubvoxels));
}
const double authoredUpperMouthFloorDepth=std::max(0.0,
static_cast<double>(passage.controlPoints.front().positionSubvoxels[2]
-passage.upperMouthSubvoxels[2])/qPerMetre+1.13);
double upperMouthFloorDepth=authoredUpperMouthFloorDepth;
const auto upperStratum=std::find_if(ResolvedLayout->strates.begin(),
ResolvedLayout->strates.end(),[&](const auto& stratum)
{return stratum.strateIndex==passage.upperStrateIndex;});
if(upperStratum!=ResolvedLayout->strates.end()
&&upperStratum->generatorKind=="FloatingIslands")
{
std::vector<std::array<double,3>> route;
for(const auto& anchor:upperStratum->anchors)
if(anchor.kind=="route_point"&&anchor.values.size()>=3)
route.push_back({static_cast<double>(anchor.values[0]),
static_cast<double>(anchor.values[1]),
static_cast<double>(anchor.values[2])});
constants.upperMouthReceivingRoute=route;
double routeHeight=0.0,distanceSquared=0.0;
if(VoxelForgeCore::PassageSpline::ClosestRouteHeightAtXY(route,
controls.front()[0],controls.front()[1],routeHeight,distanceSquared)
&&distanceSquared<=std::pow(controls.front()[3]+2.0*qPerMetre,2.0))
upperMouthFloorDepth=std::max(upperMouthFloorDepth,
std::max(0.0,(controls.front()[2]-routeHeight)/qPerMetre+1.13));
}
controls.front()[3]=std::max(controls.front()[3],
upperMouthFloorDepth*qPerMetre);
const auto sampledSegments=VoxelForgeCore::PassageSpline::BuildSegments(
controls,controls.size()-1,PassageSegmentSpacingMetres,qPerMetre,
plannedFloorProfile);
constants.upperMouthX=controls.front()[0];
constants.upperMouthY=controls.front()[1];
constants.upperMouthPlannedFloor=plannedFloorProfile.front();
if(!sampledSegments.empty())
{
constants.upperMouthTangentX=sampledSegments.front().b[0]
-sampledSegments.front().a[0];
constants.upperMouthTangentY=sampledSegments.front().b[1]
-sampledSegments.front().a[1];
}
std::vector<FRawPassageSegment> rawSegments=sampledSegments;
std::vector<std::array<double,2>> routeRadiusSamples;
routeRadiusSamples.push_back({0.0,controls.front()[3]/qPerMetre});
double alongMetres=0.0,authoredTotalPathLengthMetres=0.0;
const auto curvePoint=[&](std::size_t span,double t)
std::size_t preparedSpan=std::numeric_limits<std::size_t>::max();
double authoredTotalPathLengthMetres=0.0;
for(const auto& raw:rawSegments)
{
const auto& p1=controls[span];
const auto& p2=controls[span+1];
const auto& p0=controls[span==0?0:span-1];
const auto& p3=controls[std::min(span+2,controls.size()-1)];
return PassageCentripetalCatmullRom(p0,p1,p2,p3,t,
PassageCentripetalKnotSpan(p0,p1),
PassageCentripetalKnotSpan(p1,p2),
PassageCentripetalKnotSpan(p2,p3));
};
for(std::size_t span=0;span+1<controls.size();++span)
{
++controlSpans;
std::array<double,ArcLengthTableSteps+1> arcLength{};
auto previous=curvePoint(span,0.0);
for(std::size_t step=1;step<=ArcLengthTableSteps;++step)
if(raw.controlSpan!=preparedSpan)
{
const auto current=curvePoint(span,
static_cast<double>(step)/ArcLengthTableSteps);
const double dx=(current[0]-previous[0])/qPerMetre;
const double dy=(current[1]-previous[1])/qPerMetre;
const double dz=(current[2]-previous[2])/qPerMetre;
arcLength[step]=arcLength[step-1]
+DeterministicSqrt(dx*dx+dy*dy+dz*dz);
previous=current;
preparedSpan=raw.controlSpan;
++controlSpans;
}
const double spanLength=arcLength.back();
const std::size_t segmentCount=std::max<std::size_t>(1,
static_cast<std::size_t>(std::ceil(
spanLength/PassageSegmentSpacingMetres)));
auto a=curvePoint(span,0.0);
double aT=0.0;
for(std::size_t segment=1;segment<=segmentCount;++segment)
{
const double targetArc=spanLength*static_cast<double>(segment)
/segmentCount;
const auto upper=std::lower_bound(arcLength.begin()+1,
arcLength.end(),targetArc);
const std::size_t upperIndex=static_cast<std::size_t>(
upper-arcLength.begin());
const std::size_t lowerIndex=upperIndex-1;
const double arcSpan=arcLength[upperIndex]-arcLength[lowerIndex];
const double arcT=arcSpan>1.0e-12
?std::clamp((targetArc-arcLength[lowerIndex])/arcSpan,0.0,1.0):0.0;
const double t=(static_cast<double>(lowerIndex)+arcT)
/ArcLengthTableSteps;
const auto b=curvePoint(span,t);
const double dx=(b[0]-a[0])/qPerMetre;
const double dy=(b[1]-a[1])/qPerMetre;
const double dz=(b[2]-a[2])/qPerMetre;
const double length=DeterministicSqrt(dx*dx+dy*dy+dz*dz);
rawSegments.push_back({a,b,alongMetres,length,span,aT,t,span<routeEnd});
alongMetres+=length;
if(span<routeEnd)
routeRadiusSamples.push_back({alongMetres,b[3]/qPerMetre});
a=b;
aT=t;
}
if(span+1==routeEnd)
authoredTotalPathLengthMetres=alongMetres;
if(raw.controlSpan<routeEnd)
routeRadiusSamples.push_back({raw.alongMetres+raw.lengthMetres,
raw.b[3]/qPerMetre});
if(raw.controlSpan+1==routeEnd)
authoredTotalPathLengthMetres=raw.alongMetres+raw.lengthMetres;
}
constants.authoredTotalPathLengthMetres=authoredTotalPathLengthMetres;
double minimumBodyRadius=std::numeric_limits<double>::infinity();
@@ -4650,131 +4608,35 @@ private:
minimumBodyRadius=std::min(minimumBodyRadius,sample[1]);
}
if(!std::isfinite(minimumBodyRadius))minimumBodyRadius=minimumRadius;
// Keep one flat-floor depth through the body and ease it into both mouths.
// Keep a stable body depth and limit it smoothly by the local bore radius.
constants.floorDepthMetres=std::max(0.5,minimumBodyRadius-0.23);
constants.upperMouthFloorDepthMetres=std::max(0.0,
static_cast<double>(passage.controlPoints.front().positionSubvoxels[2]
-passage.upperMouthSubvoxels[2])/qPerMetre+1.13);
constants.lowerMouthFloorDepthMetres=std::max(0.0,
static_cast<double>(passage.controlPoints[routeEnd].positionSubvoxels[2]
-passage.lowerMouthSubvoxels[2])/qPerMetre+1.13);
const auto nativeCaveMouth=std::find_if(ResolvedLayout->cavernEdges.begin(),
ResolvedLayout->cavernEdges.end(),[&](const auto& edge)
{
return edge.strateIndex==passage.lowerStrateIndex
&&edge.startAnchorStableId==passage.stableId+"/lower-mouth"
&&!edge.polylineMicrometres.empty();
});
if(nativeCaveMouth!=ResolvedLayout->cavernEdges.end())
{
const double routeFloorMetres=static_cast<double>(
passage.controlPoints[routeEnd].positionSubvoxels[2])/qPerMetre;
const double nativeFloorMetres=static_cast<double>(
nativeCaveMouth->polylineMicrometres.front()[2])/1000000.0;
constants.lowerMouthFloorDepthMetres=std::max(0.0,
routeFloorMetres-nativeFloorMetres);
}
struct FRouteFloorSample
{
std::array<double,4> point{};
double alongMetres=0.0,desiredFloorZMetres=0.0;
};
std::vector<FRouteFloorSample> routeFloorSamples;
routeFloorSamples.reserve(rawSegments.size()+1);
const auto appendRouteFloorSample=[&](const std::array<double,4>& point,
double along)
{
const double radiusMetres=point[3]/qPerMetre;
const double routeDepth=PassageRouteFloorDepthMetres(constants,along,
radiusMetres);
routeFloorSamples.push_back({point,along,point[2]/qPerMetre-routeDepth});
};
for(const auto& raw:rawSegments)
{
if(!raw.routeSegment)break;
if(routeFloorSamples.empty())appendRouteFloorSample(raw.a,raw.alongMetres);
appendRouteFloorSample(raw.b,raw.alongMetres+raw.lengthMetres);
}
if(routeFloorSamples.size()>1)
{
constexpr double MaximumWalkGradeTangent=0.2586407821;
std::vector<double> horizontalGradeLimits(routeFloorSamples.size()-1);
std::vector<double> remainingGradeAllowance(routeFloorSamples.size(),0.0);
for(std::size_t i=0;i+1<routeFloorSamples.size();++i)
{
const double dx=(routeFloorSamples[i+1].point[0]
-routeFloorSamples[i].point[0])/qPerMetre;
const double dy=(routeFloorSamples[i+1].point[1]
-routeFloorSamples[i].point[1])/qPerMetre;
horizontalGradeLimits[i]=MaximumWalkGradeTangent
*DeterministicSqrt(dx*dx+dy*dy);
}
for(std::size_t i=horizontalGradeLimits.size();i>0;--i)
remainingGradeAllowance[i-1]=remainingGradeAllowance[i]
+horizontalGradeLimits[i-1];
std::vector<double> walkableFloor(routeFloorSamples.size());
walkableFloor.front()=routeFloorSamples.front().desiredFloorZMetres;
walkableFloor.back()=routeFloorSamples.back().desiredFloorZMetres;
for(std::size_t i=1;i+1<routeFloorSamples.size();++i)
{
const double limit=horizontalGradeLimits[i-1];
const double remaining=remainingGradeAllowance[i];
const double low=std::max(walkableFloor[i-1]-limit,
walkableFloor.back()-remaining);
const double high=std::min(walkableFloor[i-1]+limit,
walkableFloor.back()+remaining);
walkableFloor[i]=std::clamp(
routeFloorSamples[i].desiredFloorZMetres,low,high);
}
for(std::size_t i=0;i+1<rawSegments.size();++i)
{
auto& raw=rawSegments[i];
if(!raw.routeSegment)break;
const auto& floorA=routeFloorSamples[i];
const auto& floorB=routeFloorSamples[i+1];
const double radiusA=floorA.point[3]/qPerMetre;
const double radiusB=floorB.point[3]/qPerMetre;
raw.a[2]=(walkableFloor[i]
+PassageRouteFloorDepthMetres(constants,floorA.alongMetres,radiusA))
*qPerMetre;
raw.b[2]=(walkableFloor[i+1]
+PassageRouteFloorDepthMetres(constants,floorB.alongMetres,radiusB))
*qPerMetre;
}
const std::size_t routeSegmentCount=routeFloorSamples.size()-1;
if(routeSegmentCount<rawSegments.size())
{
auto& firstCap=rawSegments[routeSegmentCount];
const double localDepth=std::max(0.0,
firstCap.a[3]/qPerMetre-0.23);
firstCap.a[2]=(walkableFloor.back()+localDepth)*qPerMetre;
}
}
constants.totalPathLengthMetres=authoredTotalPathLengthMetres;
auto& segmentData=PassageSegmentData[passageIndex];
segmentData.clear();
segmentData.reserve(rawSegments.size());
for(const auto& raw:rawSegments)
{
FPassageSegmentData data;
data.authoredCenterZA=raw.a[2];
data.authoredCenterZDelta=raw.b[2]-raw.a[2];
data.authoredFloorZA=raw.plannedFloorA;
data.authoredFloorZDelta=raw.plannedFloorB-raw.plannedFloorA;
data.authoredAlongMetres=raw.alongMetres;
data.authoredLengthMetres=raw.lengthMetres;
data.routeSegment=raw.routeSegment;
data.routeSegment=raw.controlSpan<routeEnd;
data.radiusA=raw.a[3];
data.radiusB=raw.b[3];
data.radiusDelta=raw.b[3]-raw.a[3];
const double alongB=raw.alongMetres+raw.lengthMetres;
const auto effectiveCenterZ=[&](double authoredZ,double radiusQ,double along)
const auto effectiveCenterZ=[&](double authoredZ,double radiusQ,
double plannedFloor,double along)
{
const double radiusMetres=radiusQ/qPerMetre;
const double localTubeDepth=std::min(constants.floorDepthMetres,
std::max(0.0,radiusMetres-0.23));
const double routeDepth=raw.routeSegment
?PassageRouteFloorDepthMetres(constants,along,radiusMetres)
:std::max(0.0,radiusMetres-0.23);
if(data.routeSegment)
return plannedFloor+localTubeDepth*qPerMetre;
if(std::abs(along-constants.totalPathLengthMetres)<=1.0e-9)
return plannedFloor+localTubeDepth*qPerMetre;
const double routeDepth=std::max(0.0,radiusMetres-0.23);
return authoredZ+(-routeDepth+localTubeDepth)*qPerMetre;
};
for(std::size_t axis=0;axis<3;++axis)
@@ -4783,8 +4645,10 @@ private:
data.b[axis]=raw.b[axis];
if(axis==2)
{
data.a[axis]=effectiveCenterZ(raw.a[2],raw.a[3],raw.alongMetres);
data.b[axis]=effectiveCenterZ(raw.b[2],raw.b[3],alongB);
data.a[axis]=effectiveCenterZ(raw.a[2],raw.a[3],
raw.plannedFloorA,raw.alongMetres);
data.b[axis]=effectiveCenterZ(raw.b[2],raw.b[3],
raw.plannedFloorB,alongB);
}
data.delta[axis]=data.b[axis]-data.a[axis];
}
@@ -4796,11 +4660,10 @@ private:
segmentData.push_back(data);
++preparedSegments;
}
constants.totalPathLengthMetres=authoredTotalPathLengthMetres;
PassageSegmentOffsets[passageIndex+1]=preparedSegments;
}
if(tracePreparation)
std::printf("[L111PassagePrepare] passages=%zu control_spans=%zu segments=%zu spacing_m=%.2f bytes=%zu prepare_ms=%.3f\n",
std::printf("[L112PassagePrepare] passages=%zu control_spans=%zu segments=%zu spacing_m=%.2f bytes=%zu prepare_ms=%.3f\n",
PassageSegmentData.size(),controlSpans,preparedSegments,
PassageSegmentSpacingMetres,preparedSegments*sizeof(FPassageSegmentData),
std::chrono::duration<double,std::milli>(
@@ -11523,7 +11386,8 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa
constants,candidate.segmentIndex,floorT,floorRadius,qPerMetre);
const double floorCenterZ=
FStandaloneShapeWorld::PassageFlatFloorCenterZSubvoxels(
constants,candidate.segmentIndex,floorT,segment,floorDepth,qPerMetre);
constants,candidate.segmentIndex,floorT,segment,qx,qy,floorDepth,
qPerMetre);
const double flatDistance=FStandaloneShapeWorld::PassageFlatBottomDistance(
static_cast<float>(signedDistance),floorCenterZ,qz,floorDepth,qPerMetre);
if(profile)++profileStats_->passageSweepSegmentSamplePairs;