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:
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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user