wip(tunnels): continuous world-core sweep, slope in degrees, bounded winding -- NOT MERGED: +18-30% worker CPU, visuals unconfirmed

Tunnel round from the owner's playtest. Luna's ablations traced the ribs to the world-space tunnel-core stage: disabling it removes them, while terracing, detail ops, roughness, the support floor, lateral warp, radius jitter, winding and LOD do not. The per-span capsule union is replaced by a deterministic Catmull-Rom sweep. Winding and graph expansion are now gated on bTunnelFloorEnabled (they ran even with the floor off). TunnelFloorGentleSlopeThreshold is authored in degrees (0-90, min 1 deg, DetSinCos conversion, legacy tangent values migrated: 0.3 -> 16.7 deg). The route length is capped at 6 waves x 640 = 3840 voxels, so 0.1 deg loads instead of freezing (2048 collapsed canonical reach; 24576 used ~2 GB). Room-wall trimming is behind voxel.TunnelMouthTrim (default OFF): with it on, a 36 m gap disconnects the walk.

Canonical walk: 20,907 / 10,981, connected (was 20,830 / 10,909); exports identical with -voxel.CrtFma3=0; TilePostReachProof, DetSinCos and strate crossing pass. NOT merged because: one S/M pair vs the tip DLL shows worker CPU +17.8% static / +30.3% moving and worse p95; and the orchestrator's look at the before/after renders is inconclusive (camera not matched; regular grooves still visible in the after interior). Next: owner checks the look in game; if smooth, make the sweep cheap before merging; if not, drop the sweep and keep the degrees/bound/wind-gate parts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-16 18:29:27 +02:00
1 parent bf9384a1d5
commit 0231072cc4
8 files changed
+1102 -212

No files matched your search

+455 -100
View File
@@ -102,6 +102,30 @@ namespace
// inside the chamber and recreates the broad raised shelf this hand-off is meant to remove. // inside the chamber and recreates the broad raised shelf this hand-off is meant to remove.
constexpr float VF_TunnelMouthBlendRadiusVoxels = constexpr float VF_TunnelMouthBlendRadiusVoxels =
VoxelPassageGeometry::WalkableTunnelLandingApronVoxels; VoxelPassageGeometry::WalkableTunnelLandingApronVoxels;
// The boundary probe describes where the *centreline* may leave the room. Keep one complete
// landing-carve band plus the player radius behind that probe so the tapered end-cap and the
// fitted capsule can overlap the room on the sampled lattice. This is a finite mouth apron,
// not a centre-to-centre tunnel extension.
constexpr float VF_TunnelMouthPlayerFitOverlapVoxels =
VoxelPassageGeometry::LandingCarveBlendVoxels
+ VoxelPassageGeometry::PlayerRadiusVoxels;
static float VF_TunnelMouthInset(float TunnelRadius, float BlendRadius)
{
return FMath::Max(
0.5f,
FMath::Abs(TunnelRadius) * 0.5f
+ FMath::Max(FMath::Abs(BlendRadius) * 0.25f, 0.0f)
+ VF_TunnelMouthPlayerFitOverlapVoxels);
}
#if !UE_BUILD_SHIPPING
static TAutoConsoleVariable<int32> CVarVoxelForgeTunnelMouthTrim(
TEXT("voxel.TunnelMouthTrim"),
0,
TEXT("Development-only room-wall tunnel-mouth trim; 0 keeps the legacy room endpoint."),
ECVF_Default);
#endif
enum class EVFFloorReliefFeature : uint8 enum class EVFFloorReliefFeature : uint8
{ {
@@ -372,8 +396,8 @@ namespace
- VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA))); - VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)));
const float BaseGradient = FloorDelta / HorizontalRun; const float BaseGradient = FloorDelta / HorizontalRun;
const float GentleThreshold = VoxelMath::IsFinite( const float GentleThreshold = VoxelMath::IsFinite(
Tunnel.TunnelFloorGentleSlopeThreshold) Tunnel.TunnelFloorGentleSlopeGradient)
? FMath::Max(Tunnel.TunnelFloorGentleSlopeThreshold, 0.0f) ? FMath::Max(Tunnel.TunnelFloorGentleSlopeGradient, 0.0f)
: VoxelPassageGeometry::PlayerWalkableFloorMaxGradient; : VoxelPassageGeometry::PlayerWalkableFloorMaxGradient;
const float AvailableGradient = GentleThreshold - BaseGradient; const float AvailableGradient = GentleThreshold - BaseGradient;
if (!(AvailableGradient > 0.0f)) if (!(AvailableGradient > 0.0f))
@@ -434,8 +458,8 @@ namespace
} }
const float GentleThreshold = VoxelMath::IsFinite( const float GentleThreshold = VoxelMath::IsFinite(
Tunnel.TunnelFloorGentleSlopeThreshold) Tunnel.TunnelFloorGentleSlopeGradient)
? FMath::Max(Tunnel.TunnelFloorGentleSlopeThreshold, 0.0f) ? FMath::Max(Tunnel.TunnelFloorGentleSlopeGradient, 0.0f)
: VoxelPassageGeometry::PlayerWalkableFloorMaxGradient; : VoxelPassageGeometry::PlayerWalkableFloorMaxGradient;
const float AvailableGradient = GentleThreshold - BaseGradient; const float AvailableGradient = GentleThreshold - BaseGradient;
if (!(AvailableGradient > 0.0f)) if (!(AvailableGradient > 0.0f))
@@ -873,6 +897,146 @@ namespace
return true; return true;
} }
// A world-chain handoff is sampled after the cave-warped SDF chain has already carved the
// tunnel. It must follow the same continuous centreline, not union another tapered capsule
// at every control point. A fixed Catmull-Rom span gives adjacent spans a shared tangent;
// the fixed projection iterations keep the result deterministic and bounded.
struct FVFTunnelCubicSpan
{
FVector P0 = FVector::ZeroVector;
FVector P1 = FVector::ZeroVector;
FVector P2 = FVector::ZeroVector;
FVector P3 = FVector::ZeroVector;
float R0 = 0.0f;
float R1 = 0.0f;
float R2 = 0.0f;
float R3 = 0.0f;
FORCEINLINE FVector Position(float T) const
{
const float T2 = T * T;
const float T3 = T2 * T;
return (P1 * 2.0f
+ (P2 - P0) * T
+ (P0 * 2.0f - P1 * 5.0f + P2 * 4.0f - P3) * T2
+ (-P0 + P1 * 3.0f - P2 * 3.0f + P3) * T3) * 0.5f;
}
FORCEINLINE FVector FirstDerivative(float T) const
{
const float T2 = T * T;
return ((P2 - P0)
+ (P0 * 2.0f - P1 * 5.0f + P2 * 4.0f - P3) * (2.0f * T)
+ (-P0 + P1 * 3.0f - P2 * 3.0f + P3) * (3.0f * T2)) * 0.5f;
}
FORCEINLINE FVector SecondDerivative(float T) const
{
return ((P0 * 2.0f - P1 * 5.0f + P2 * 4.0f - P3) * 2.0f
+ (-P0 + P1 * 3.0f - P2 * 3.0f + P3) * (6.0f * T)) * 0.5f;
}
FORCEINLINE float Radius(float T) const
{
const float T2 = T * T;
const float T3 = T2 * T;
const float Value = (R1 * 2.0f
+ (R2 - R0) * T
+ (R0 * 2.0f - R1 * 5.0f + R2 * 4.0f - R3) * T2
+ (-R0 + R1 * 3.0f - R2 * 3.0f + R3) * T3) * 0.5f;
return FMath::Max(Value, 0.5f);
}
};
static FVFTunnelCubicSpan VF_MakeTunnelCubicSpan(
const TArray<FVector>& ControlPoints,
const TArray<float>& ControlRadii,
int32 SegmentIndex)
{
const int32 LastPoint = ControlPoints.Num() - 1;
const FVector& P1 = ControlPoints[SegmentIndex];
const FVector& P2 = ControlPoints[SegmentIndex + 1];
const FVector P0 = SegmentIndex > 0
? ControlPoints[SegmentIndex - 1] : P1 + (P1 - P2);
const FVector P3 = SegmentIndex + 2 <= LastPoint
? ControlPoints[SegmentIndex + 2] : P2 + (P2 - P1);
const float R1 = ControlRadii[SegmentIndex];
const float R2 = ControlRadii[SegmentIndex + 1];
const float R0 = SegmentIndex > 0
? ControlRadii[SegmentIndex - 1] : R1 + (R1 - R2);
const float R3 = SegmentIndex + 2 <= LastPoint
? ControlRadii[SegmentIndex + 2] : R2 + (R2 - R1);
FVFTunnelCubicSpan Span;
Span.P0 = P0;
Span.P1 = P1;
Span.P2 = P2;
Span.P3 = P3;
Span.R0 = R0;
Span.R1 = R1;
Span.R2 = R2;
Span.R3 = R3;
return Span;
}
static bool VF_ProjectTunnelCubicXY(
const FVector& Position,
const FVFTunnelCubicSpan& Span,
float& OutT,
float& OutDistanceSquared)
{
float BestT = 0.0f;
float BestDistanceSquared = FLT_MAX;
auto Consider = [&](float T)
{
const FVector Candidate = Span.Position(T);
const float DX = static_cast<float>(Candidate.X - Position.X);
const float DY = static_cast<float>(Candidate.Y - Position.Y);
const float DistanceSquared = DX * DX + DY * DY;
if (DistanceSquared < BestDistanceSquared)
{
BestDistanceSquared = DistanceSquared;
BestT = T;
}
};
constexpr int32 InitialSamples = 8;
for (int32 Sample = 0; Sample <= InitialSamples; ++Sample)
{
Consider(static_cast<float>(Sample) / InitialSamples);
}
float CandidateT = BestT;
for (int32 Iteration = 0; Iteration < 6; ++Iteration)
{
const FVector CurvePoint = Span.Position(CandidateT);
const FVector First = Span.FirstDerivative(CandidateT);
const FVector Second = Span.SecondDerivative(CandidateT);
const float DX = static_cast<float>(CurvePoint.X - Position.X);
const float DY = static_cast<float>(CurvePoint.Y - Position.Y);
const float FirstX = static_cast<float>(First.X);
const float FirstY = static_cast<float>(First.Y);
const float SecondX = static_cast<float>(Second.X);
const float SecondY = static_cast<float>(Second.Y);
const float Numerator = DX * FirstX + DY * FirstY;
const float Denominator = FirstX * FirstX + FirstY * FirstY
+ DX * SecondX + DY * SecondY;
if (!VoxelMath::IsFinite(Numerator)
|| !VoxelMath::IsFinite(Denominator)
|| FMath::Abs(Denominator) <= KINDA_SMALL_NUMBER)
{
break;
}
CandidateT = FMath::Clamp(
CandidateT - Numerator / Denominator, 0.0f, 1.0f);
Consider(CandidateT);
}
OutT = BestT;
OutDistanceSquared = BestDistanceSquared;
return VoxelMath::IsFinite(OutT)
&& VoxelMath::IsFinite(OutDistanceSquared);
}
static FVFTunnelShapeEvaluation VF_EvaluateSweptTunnelChain( static FVFTunnelShapeEvaluation VF_EvaluateSweptTunnelChain(
const FVector& Position, const FVector& Position,
const TArray<FVector>& ControlPoints, const TArray<FVector>& ControlPoints,
@@ -880,6 +1044,7 @@ namespace
const FCachedTunnel& Tunnel, const FCachedTunnel& Tunnel,
float SDFBlendRadius, float SDFBlendRadius,
bool bApplyFloorCut, bool bApplyFloorCut,
bool bUseContinuousCurve,
const void* CacheIdentity, int32 TunnelIndex, const void* CacheIdentity, int32 TunnelIndex,
const FVFFloorReliefColumnKey& ReliefColumn, const FVFFloorReliefColumnKey& ReliefColumn,
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles, const TArray<FTunnelFloorSegmentProfile>* FloorProfiles,
@@ -910,10 +1075,15 @@ namespace
const FVector& B = ControlPointData[SegmentIndex + 1]; const FVector& B = ControlPointData[SegmentIndex + 1];
const float RadiusA = ControlRadiusData[SegmentIndex]; const float RadiusA = ControlRadiusData[SegmentIndex];
const float RadiusB = ControlRadiusData[SegmentIndex + 1]; const float RadiusB = ControlRadiusData[SegmentIndex + 1];
float T = 0.0f; float T = 0.0f;
float HorizontalDistanceSquared = FLT_MAX; float HorizontalDistanceSquared = FLT_MAX;
const bool bHasHorizontalProjection = VF_ProjectTunnelSegmentXY( const FVFTunnelCubicSpan Span = bUseContinuousCurve
? VF_MakeTunnelCubicSpan(ControlPoints, ControlRadii, SegmentIndex)
: FVFTunnelCubicSpan();
const bool bHasHorizontalProjection = bUseContinuousCurve
? VF_ProjectTunnelCubicXY(
Position, Span, T, HorizontalDistanceSquared)
: VF_ProjectTunnelSegmentXY(
Position, A, B, T, HorizontalDistanceSquared); Position, A, B, T, HorizontalDistanceSquared);
if (!bHasHorizontalProjection) if (!bHasHorizontalProjection)
{ {
@@ -925,6 +1095,10 @@ namespace
continue; continue;
} }
const FVector CurvePoint = bUseContinuousCurve
? Span.Position(T) : FMath::Lerp(A, B, T);
const float CurveRadius = bUseContinuousCurve
? Span.Radius(T) : FMath::Lerp(RadiusA, RadiusB, T);
const FTunnelFloorSegmentProfile* Profile = FloorProfileData != nullptr const FTunnelFloorSegmentProfile* Profile = FloorProfileData != nullptr
? FloorProfileData + SegmentIndex : nullptr; ? FloorProfileData + SegmentIndex : nullptr;
const FVFTunnelFloorTerms FloorTerms = VF_TunnelFloorTerms( const FVFTunnelFloorTerms FloorTerms = VF_TunnelFloorTerms(
@@ -957,7 +1131,7 @@ namespace
// excludes relief only long enough to decide whether the bounded relief can matter; // excludes relief only long enough to decide whether the bounded relief can matter;
// the final shift below includes the actual cached column value. // the final shift below includes the actual cached column value.
// Every complete build-time profile owns the floor. A route that was deliberately // Every complete build-time profile owns the floor. A route that was deliberately
// wound, or that contains an explicit ledge transition, also moves the swept capsule // wound, or that contains an explicit ledge transition, also moves the swept curve
// with that authored floor. A direct smooth ramp keeps the established natural arch // with that authored floor. A direct smooth ramp keeps the established natural arch
// and lets the SmoothMax floor cut provide the walkable D-floor; this avoids changing // and lets the SmoothMax floor cut provide the walkable D-floor; this avoids changing
// headroom in every gentle tunnel merely because a profile is now always present. // headroom in every gentle tunnel merely because a profile is now always present.
@@ -967,8 +1141,9 @@ namespace
&& (Tunnel.bFloorRouteWasWound || Tunnel.bDramaticLedge); && (Tunnel.bFloorRouteWasWound || Tunnel.bDramaticLedge);
const bool bAnchorCapsule = bUseAuthoredProfile const bool bAnchorCapsule = bUseAuthoredProfile
&& VoxelMath::IsFinite(NaturalFloorZ); && VoxelMath::IsFinite(NaturalFloorZ);
const auto EvaluateAnchoredCapsule = [ const auto EvaluateAnchoredShape = [
&Position, &A, &B, RadiusA, RadiusB, NaturalFloorZ, bAnchorCapsule] &Position, &A, &B, RadiusA, RadiusB, CurvePoint, CurveRadius,
NaturalFloorZ, bAnchorCapsule, bUseContinuousCurve]
(float FloorZ) -> float (float FloorZ) -> float
{ {
const float Shift = FloorZ - NaturalFloorZ; const float Shift = FloorZ - NaturalFloorZ;
@@ -976,14 +1151,18 @@ namespace
|| !VoxelMath::IsFinite(Shift) || !VoxelMath::IsFinite(Shift)
|| FMath::Abs(Shift) <= KINDA_SMALL_NUMBER) || FMath::Abs(Shift) <= KINDA_SMALL_NUMBER)
{ {
return VoxelSDF::TaperedCapsule( return bUseContinuousCurve
? FVector::Dist(Position, CurvePoint) - CurveRadius
: VoxelSDF::TaperedCapsule(
Position, A, B, RadiusA, RadiusB); Position, A, B, RadiusA, RadiusB);
} }
const FVector Offset(0.0f, 0.0f, Shift); const FVector Offset(0.0f, 0.0f, Shift);
return VoxelSDF::TaperedCapsule( return bUseContinuousCurve
? FVector::Dist(Position, CurvePoint + Offset) - CurveRadius
: VoxelSDF::TaperedCapsule(
Position, A + Offset, B + Offset, RadiusA, RadiusB); Position, A + Offset, B + Offset, RadiusA, RadiusB);
}; };
float SegmentSDF = EvaluateAnchoredCapsule(FloorTerms.BaseFloorZ); float SegmentSDF = EvaluateAnchoredShape(FloorTerms.BaseFloorZ);
bool bEvaluateRelief = bApplyFloorCut; bool bEvaluateRelief = bApplyFloorCut;
if (bEvaluateRelief && bHasReliefBound) if (bEvaluateRelief && bHasReliefBound)
@@ -1026,7 +1205,7 @@ namespace
&& VoxelMath::IsFinite(FloorZ) && VoxelMath::IsFinite(FloorZ)
&& FMath::Abs(FloorZ - FloorTerms.BaseFloorZ) > KINDA_SMALL_NUMBER) && FMath::Abs(FloorZ - FloorTerms.BaseFloorZ) > KINDA_SMALL_NUMBER)
{ {
SegmentSDF = EvaluateAnchoredCapsule(FloorZ); SegmentSDF = EvaluateAnchoredShape(FloorZ);
} }
const float SegmentResultSDF = bApplyFloorCut const float SegmentResultSDF = bApplyFloorCut
? VoxelSDF::SmoothMax( ? VoxelSDF::SmoothMax(
@@ -1070,6 +1249,7 @@ namespace
return VF_EvaluateSweptTunnelChain( return VF_EvaluateSweptTunnelChain(
Position, ShapeControlPoints, ShapeControlRadii, Position, ShapeControlPoints, ShapeControlRadii,
Tunnel, SDFBlendRadius, bApplyFloorCut, Tunnel, SDFBlendRadius, bApplyFloorCut,
bWorldChain,
CacheIdentity, TunnelIndex, ReliefColumn, CacheIdentity, TunnelIndex, ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles, bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership); RoomFloorOwnership);
@@ -1090,6 +1270,7 @@ namespace
return VF_EvaluateSweptTunnelChain( return VF_EvaluateSweptTunnelChain(
Position, Points, Radii, Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut, Tunnel, SDFBlendRadius, bApplyFloorCut,
bWorldChain,
CacheIdentity, TunnelIndex, ReliefColumn, CacheIdentity, TunnelIndex, ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles, bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership); RoomFloorOwnership);
@@ -1104,6 +1285,7 @@ namespace
return VF_EvaluateSweptTunnelChain( return VF_EvaluateSweptTunnelChain(
Position, Points, Radii, Position, Points, Radii,
Tunnel, SDFBlendRadius, bApplyFloorCut, Tunnel, SDFBlendRadius, bApplyFloorCut,
bWorldChain,
CacheIdentity, TunnelIndex, ReliefColumn, CacheIdentity, TunnelIndex, ReliefColumn,
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles, bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
RoomFloorOwnership); RoomFloorOwnership);
@@ -2109,6 +2291,8 @@ static bool VF_ComputeRoomGraphReach(
|| Params.TunnelFloorTerraceStepHeight < 0.0f || Params.TunnelFloorTerraceStepHeight < 0.0f
|| Params.TunnelFloorMaxLedgeHeight < 0.0f || Params.TunnelFloorMaxLedgeHeight < 0.0f
|| Params.TunnelFloorGentleSlopeThreshold < 0.0f || Params.TunnelFloorGentleSlopeThreshold < 0.0f
|| Params.TunnelFloorGentleSlopeThreshold
> VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees
|| FMath::Abs(Params.FloorReliefStrength) > 1000000.0f || FMath::Abs(Params.FloorReliefStrength) > 1000000.0f
|| FMath::Abs(Params.CaveWarpStrength) > 1000000.0f || FMath::Abs(Params.CaveWarpStrength) > 1000000.0f
|| FMath::Abs(Params.RoomFloorCutMin) > 1000000.0f || FMath::Abs(Params.RoomFloorCutMin) > 1000000.0f
@@ -2200,11 +2384,15 @@ static bool VF_ComputeRoomGraphReach(
// quantising the floor into small risers. Bound the route's possible lateral amplitude with // quantising the floor into small risers. Bound the route's possible lateral amplitude with
// the same finite endpoint envelope used above. This is intentionally conservative: the // the same finite endpoint envelope used above. This is intentionally conservative: the
// actual chain uses a handful of switchback waves and never exceeds this reach. // actual chain uses a handful of switchback waves and never exceeds this reach.
const float GentleSlopeThreshold = FMath::Max( const float GentleSlopeThreshold = VoxelPassageGeometry::TunnelFloorGradientFromDegrees(
Params.TunnelFloorGentleSlopeThreshold, KINDA_SMALL_NUMBER); Params.TunnelFloorGentleSlopeThreshold);
const float MaxFloorDeltaEnvelope = 2.0f * EndpointVertical; const float MaxFloorDeltaEnvelope = 2.0f * EndpointVertical;
const float RequiredRouteRunEnvelope = MaxFloorDeltaEnvelope const float RequiredRouteRunEnvelope = Params.bTunnelFloorEnabled
/ GentleSlopeThreshold; ? FMath::Min(
VoxelPassageGeometry::TunnelFloorMaximumRouteLengthVoxels,
MaxFloorDeltaEnvelope / FMath::Max(
GentleSlopeThreshold, KINDA_SMALL_NUMBER))
: 0.0f;
const float OrganicLateralEnvelope = FMath::Min( const float OrganicLateralEnvelope = FMath::Min(
FMath::Abs(Params.TunnelWarpStrength), TunnelLengthBound * 0.25f); FMath::Abs(Params.TunnelWarpStrength), TunnelLengthBound * 0.25f);
const float RouteLateralEnvelope = FMath::Max( const float RouteLateralEnvelope = FMath::Max(
@@ -3335,6 +3523,104 @@ namespace
return -InternalDensity; return -InternalDensity;
} }
static bool VF_FindRoomMouthBoundary(
const FVFRoomLandingSite& Site,
const FStrateGenerationParams& Params,
uint32 Seed,
float StrateTopZ,
float StrateBottomZ,
const FVector& Anchor,
const FVector& Direction,
float TunnelRadius,
float BlendRadius,
FVector& OutMouthXY,
float& OutBoundaryDistance)
{
FVector DirectionXY(Direction.X, Direction.Y, 0.0f);
if (!DirectionXY.Normalize()
|| !VoxelMath::IsFinite(Anchor.X)
|| !VoxelMath::IsFinite(Anchor.Y)
|| !VoxelMath::IsFinite(Site.Center.Z))
{
return false;
}
// Probe both the room mid-height and the tunnel-centre height. The latter is important
// for vertically compressed/ellipsoidal rooms: their wall can be closer at the floor
// than at mid-height. Both probes use the exact hashed room shape and cave warp used by
// density evaluation. Positive landing density means air/inside.
const float RoomFloorZ = VF_RoomFloorZFromParts(
Site.Center, Site.RadiusXY, Site.RadiusZ, Site.Hash, Seed, Params);
const float LowerProbeZ = VoxelMath::IsFinite(RoomFloorZ)
? RoomFloorZ + FMath::Abs(TunnelRadius)
: Site.Center.Z;
const float UpperProbeZ = Site.Center.Z;
const auto IsInside = [&](float Distance) -> bool
{
const FVector Probe = Anchor + DirectionXY * Distance;
const float LowerDensity = VF_EvaluateRoomLandingDensity(
Site, Params, Seed, StrateTopZ, StrateBottomZ,
static_cast<float>(Probe.X), static_cast<float>(Probe.Y), LowerProbeZ);
const float UpperDensity = VF_EvaluateRoomLandingDensity(
Site, Params, Seed, StrateTopZ, StrateBottomZ,
static_cast<float>(Probe.X), static_cast<float>(Probe.Y), UpperProbeZ);
return VoxelMath::IsFinite(LowerDensity)
&& LowerDensity > 0.0f
&& VoxelMath::IsFinite(UpperDensity)
&& UpperDensity > 0.0f;
};
if (!IsInside(0.0f))
{
return false;
}
const float ShapeReach = VF_RoomShapeReachUpperBound(
Site.RadiusXY, Site.RadiusZ);
const float WarpReach = FMath::Abs(Params.CaveWarpStrength)
* VOXEL_NOISE_SCALE * 2.0f;
float High = ShapeReach + WarpReach
+ FMath::Max(8.0f, FMath::Abs(TunnelRadius) + FMath::Abs(BlendRadius));
if (!VoxelMath::IsFinite(High) || High <= 0.0f)
{
return false;
}
bool bOutside = !IsInside(High);
for (int32 Expansion = 0; !bOutside && Expansion < 4; ++Expansion)
{
High = FMath::Min(High * 2.0f, 4096.0f);
bOutside = !IsInside(High);
}
if (!bOutside)
{
return false;
}
float Low = 0.0f;
for (int32 Iteration = 0; Iteration < 24; ++Iteration)
{
const float Mid = (Low + High) * 0.5f;
if (IsInside(Mid))
{
Low = Mid;
}
else
{
High = Mid;
}
}
const float MouthInset = VF_TunnelMouthInset(TunnelRadius, BlendRadius);
OutBoundaryDistance = Low;
const float MouthDistance = FMath::Max(0.0f, Low - MouthInset);
OutMouthXY = Anchor + DirectionXY * MouthDistance;
OutMouthXY.Z = Site.Center.Z;
return VoxelMath::IsFinite(OutMouthXY.X)
&& VoxelMath::IsFinite(OutMouthXY.Y)
&& VoxelMath::IsFinite(OutBoundaryDistance);
}
bool VF_FindPlayerFitPointForRoom( bool VF_FindPlayerFitPointForRoom(
const FStrateGenerationParams& Params, const FStrateGenerationParams& Params,
const FVFRoomLandingSite& Site, const FVFRoomLandingSite& Site,
@@ -3821,7 +4107,6 @@ namespace
1, 1,
TEXT("Memoize deterministic room player-fit points; 0 disables the memo."), TEXT("Memoize deterministic room player-fit points; 0 disables the memo."),
ECVF_Default); ECVF_Default);
bool VF_FindPlayerFitPointForRoomMemoized( bool VF_FindPlayerFitPointForRoomMemoized(
const FStrateGenerationParams& Params, const FStrateGenerationParams& Params,
const FVFRoomLandingSite& Site, const FVFRoomLandingSite& Site,
@@ -6599,12 +6884,92 @@ void VoxelCaveMorphology::BuildChunkCache(
return Room.bHasPlayerFitPoint; return Room.bHasPlayerFitPoint;
}; };
#if !UE_BUILD_SHIPPING
const bool bTrimRoomMouth = CVarVoxelForgeTunnelMouthTrim.GetValueOnAnyThread() != 0;
#else
constexpr bool bTrimRoomMouth = false;
#endif
const auto BuildRoomMouth = [
&BuildRooms, &ResolvePlayerFitPoint, &RoomFloorZFor,
&Params, Seed, BlendK, bTrimRoomMouth](int32 RoomIndex, const FVector& Towards,
float TunnelRadius) -> FVector
{
FBuildRoom& Room = BuildRooms[RoomIndex];
FVector Anchor = Room.Center;
if (!Room.bIsOrigin
&& ResolvePlayerFitPoint(Room, Params, Seed))
{
Anchor = Room.PlayerFitPoint;
}
if (!bTrimRoomMouth)
{
// Keep the proven legacy endpoint as the safe default while the room-wall trim
// remains an explicit investigation switch. The endpoint still uses the room's
// deterministic floor and player-fit anchor, so the raw tunnel floor remains the
// authored tunnel surface rather than an added slab.
const float RoomFloorZ = RoomFloorZFor(Room);
const float SafeFloorZ = VoxelMath::IsFinite(RoomFloorZ)
? RoomFloorZ : Room.Center.Z - Room.RadiusZ;
FVector End = Room.Center + FVector(
0.0f, 0.0f, SafeFloorZ + FMath::Abs(TunnelRadius) - Room.Center.Z);
FVector WorldEnd = VF_UnwarpCavePoint(End, Params, Seed);
if (!Room.bIsOrigin && Room.bHasPlayerFitPoint)
{
WorldEnd = FVector(
Room.PlayerFitPoint.X,
Room.PlayerFitPoint.Y,
Room.PlayerFitPoint.Z - 1.0f + FMath::Abs(TunnelRadius));
}
return WorldEnd;
}
const FVFRoomLandingSite Site{
Room.Center,
Room.RadiusXY,
Room.RadiusZ,
Room.Hash,
Room.bIsOrigin};
FVector MouthXY = Anchor;
float BoundaryDistance = 0.0f;
if (!VF_FindRoomMouthBoundary(
Site, Params, Seed,
Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
Anchor, Towards, TunnelRadius, BlendK,
MouthXY, BoundaryDistance))
{
FVector FallbackDirection(Towards.X, Towards.Y, 0.0f);
if (!FallbackDirection.Normalize())
{
FallbackDirection = FVector::ForwardVector;
}
const float MouthInset = VF_TunnelMouthInset(TunnelRadius, BlendK);
MouthXY = Anchor + FallbackDirection * FMath::Max(
0.0f, FMath::Abs(Room.RadiusXY) - MouthInset);
}
(void)BoundaryDistance;
float MouthFloorZ = VF_RoomFloorZFromParts(
FVector(MouthXY.X, MouthXY.Y, Room.Center.Z),
Room.RadiusXY, Room.RadiusZ, Room.Hash, Seed, Params);
if (!VoxelMath::IsFinite(MouthFloorZ))
{
MouthFloorZ = RoomFloorZFor(Room);
}
if (!VoxelMath::IsFinite(MouthFloorZ))
{
MouthFloorZ = Room.Center.Z - Room.RadiusZ;
}
return FVector(
MouthXY.X, MouthXY.Y, MouthFloorZ + FMath::Abs(TunnelRadius));
};
for (const FResolvedGraphEdge& GraphEdge : GraphEdges) for (const FResolvedGraphEdge& GraphEdge : GraphEdges)
{ {
const int32 I = GraphEdge.RoomA; const int32 I = GraphEdge.RoomA;
const int32 J = GraphEdge.RoomB; const int32 J = GraphEdge.RoomB;
const FBuildRoom& RoomA = BuildRooms[I]; FBuildRoom& RoomA = BuildRooms[I];
const FBuildRoom& RoomB = BuildRooms[J]; FBuildRoom& RoomB = BuildRooms[J];
if (!PossibleTunnelTouchesSearchXY(RoomA, RoomB)) if (!PossibleTunnelTouchesSearchXY(RoomA, RoomB))
{ {
@@ -6627,62 +6992,24 @@ void VoxelCaveMorphology::BuildChunkCache(
const float RadA = FMath::Lerp(Params.TunnelMinRadius, Params.TunnelMaxRadius, FactorA); const float RadA = FMath::Lerp(Params.TunnelMinRadius, Params.TunnelMaxRadius, FactorA);
const float RadB = FMath::Lerp(Params.TunnelMinRadius, Params.TunnelMaxRadius, FactorB); const float RadB = FMath::Lerp(Params.TunnelMinRadius, Params.TunnelMaxRadius, FactorB);
// --- ENDPOINT FLOOR ALIGNMENT --- // --- ENDPOINT ROOM-WALL TRIM + FLOOR ALIGNMENT ---
// The old endpoint Z roll entered each room at an arbitrary height. When the two // Resolve each endpoint against the actual hashed room boundary along the edge. The
// rooms had different floor cuts, the tube then met one floor several metres above or // old endpoint was the room centre/player-fit point, so its capsule necessarily ran
// below the other and the player-fit graph saw a vertical severance. Anchor each tube // through the chamber. The trim leaves half a tunnel radius plus the SDF blend inside
// tangent to the two rooms' deterministic floor planes instead. Interior control // the wall, which preserves a walkable overlap without projecting a floor slab into
// points still receive bounded vertical wander, so this removes the lip without // the room.
// turning the whole network into a level grid. FVector TunnelDirection(
const float RoomFloorA = RoomFloorZFor(RoomA); RoomB.Center.X - RoomA.Center.X,
const float RoomFloorB = RoomFloorZFor(RoomB); RoomB.Center.Y - RoomA.Center.Y,
const float SafeFloorA = VoxelMath::IsFinite(RoomFloorA) 0.0f);
? RoomFloorA : RoomA.Center.Z - RoomA.RadiusZ; if (!TunnelDirection.Normalize())
const float SafeFloorB = VoxelMath::IsFinite(RoomFloorB)
? RoomFloorB : RoomB.Center.Z - RoomB.RadiusZ;
FVector EndA = RoomA.Center + FVector(0.0f, 0.0f, SafeFloorA
+ RadA - RoomA.Center.Z);
FVector EndB = RoomB.Center + FVector(0.0f, 0.0f, SafeFloorB
+ RadB - RoomB.Center.Z);
// The passage landing query is allowed to move inside a warped room until the
// capsule actually fits. Use that same deterministic fit anchor for graph mouths;
// otherwise a room whose centre needs a local correction gets a tunnel mouth at one
// point and the inter-strate/player-fit landing at another. Querying with zero
// lateral budget pins the source site to this exact room centre (the helper still
// performs its bounded local fit search). If a malformed asset cannot produce a
// source fit, retain the analytic room-floor fallback above. PlayerFitPoint is a
// feet point; the tunnel floor's authored support band ends half a voxel below it,
// and the route probe stands one voxel above the tunnel-floor plane. Subtracting a
// full voxel here makes that probe exactly the same feet point as the room landing.
FVector WorldEndA = VF_UnwarpCavePoint(EndA, Params, Seed);
FVector WorldEndB = VF_UnwarpCavePoint(EndB, Params, Seed);
if (!RoomA.bIsOrigin)
{ {
const bool bHasPlayerFit = ResolvePlayerFitPoint( TunnelDirection = FVector::ForwardVector;
BuildRooms[I], Params, Seed);
if (bHasPlayerFit)
{
WorldEndA = FVector(
RoomA.PlayerFitPoint.X,
RoomA.PlayerFitPoint.Y,
RoomA.PlayerFitPoint.Z - 1.0f + RadA);
EndA = VF_ApplyCaveWarp(WorldEndA, Params, Seed);
}
}
if (!RoomB.bIsOrigin)
{
const bool bHasPlayerFit = ResolvePlayerFitPoint(
BuildRooms[J], Params, Seed);
if (bHasPlayerFit)
{
WorldEndB = FVector(
RoomB.PlayerFitPoint.X,
RoomB.PlayerFitPoint.Y,
RoomB.PlayerFitPoint.Z - 1.0f + RadB);
EndB = VF_ApplyCaveWarp(WorldEndB, Params, Seed);
}
} }
const FVector WorldEndA = BuildRoomMouth(I, TunnelDirection, RadA);
const FVector WorldEndB = BuildRoomMouth(J, -TunnelDirection, RadB);
const FVector EndA = VF_ApplyCaveWarp(WorldEndA, Params, Seed);
const FVector EndB = VF_ApplyCaveWarp(WorldEndB, Params, Seed);
// --- BUILD CACHED TUNNEL --- // --- BUILD CACHED TUNNEL ---
FCachedTunnel CT; FCachedTunnel CT;
@@ -6700,7 +7027,9 @@ void VoxelCaveMorphology::BuildChunkCache(
CT.bTunnelFloorTerracingEnabled = Params.bTunnelFloorTerracingEnabled; CT.bTunnelFloorTerracingEnabled = Params.bTunnelFloorTerracingEnabled;
CT.TunnelFloorTerraceStepHeight = Params.TunnelFloorTerraceStepHeight; CT.TunnelFloorTerraceStepHeight = Params.TunnelFloorTerraceStepHeight;
CT.TunnelFloorMaxLedgeHeight = Params.TunnelFloorMaxLedgeHeight; CT.TunnelFloorMaxLedgeHeight = Params.TunnelFloorMaxLedgeHeight;
CT.TunnelFloorGentleSlopeThreshold = Params.TunnelFloorGentleSlopeThreshold; CT.TunnelFloorGentleSlopeGradient =
VoxelPassageGeometry::TunnelFloorGradientFromDegrees(
Params.TunnelFloorGentleSlopeThreshold);
CT.TunnelFloorLedgeCountPreference = Params.TunnelFloorLedgeCountPreference; CT.TunnelFloorLedgeCountPreference = Params.TunnelFloorLedgeCountPreference;
CT.TunnelFloorMaxLedges = Params.TunnelFloorMaxLedges; CT.TunnelFloorMaxLedges = Params.TunnelFloorMaxLedges;
@@ -6724,10 +7053,8 @@ void VoxelCaveMorphology::BuildChunkCache(
const float DirectHorizontalRun = FVector2D( const float DirectHorizontalRun = FVector2D(
static_cast<float>(WorldEndB.X - WorldEndA.X), static_cast<float>(WorldEndB.X - WorldEndA.X),
static_cast<float>(WorldEndB.Y - WorldEndA.Y)).Size(); static_cast<float>(WorldEndB.Y - WorldEndA.Y)).Size();
const float GentleThreshold = VoxelMath::IsFinite( const float GentleThreshold = VoxelPassageGeometry::TunnelFloorGradientFromDegrees(
Params.TunnelFloorGentleSlopeThreshold) Params.TunnelFloorGentleSlopeThreshold);
? FMath::Max(Params.TunnelFloorGentleSlopeThreshold, 0.0f)
: VoxelPassageGeometry::PlayerWalkableFloorMaxGradient;
const float SafeGentleThreshold = FMath::Max( const float SafeGentleThreshold = FMath::Max(
GentleThreshold, KINDA_SMALL_NUMBER); GentleThreshold, KINDA_SMALL_NUMBER);
const bool bDirectSteep = VoxelMath::IsFinite(WorldFloorA) const bool bDirectSteep = VoxelMath::IsFinite(WorldFloorA)
@@ -6784,20 +7111,27 @@ void VoxelCaveMorphology::BuildChunkCache(
FMath::Max(BaseSegments, MinSegmentsForLedges), FMath::Max(BaseSegments, MinSegmentsForLedges),
1, 32); 1, 32);
const bool bNeedsWind = bDirectSteep // A raw tunnel still needs a continuous, gentle route when its floor relief/ledge
// modifier is disabled. The wind is centreline geometry, not an added floor slab; it
// supplies the horizontal run for the authored floor plane and keeps the room-to-room
// graph connected. The rings observed in the owner asset came from the separate
// world-core handoff exposing span joins, not from this smooth route decision.
const bool bNeedsWind = Params.bTunnelFloorEnabled
&& bDirectSteep
&& !bDramaticLedge && !bDramaticLedge
&& GentleThreshold > KINDA_SMALL_NUMBER; && GentleThreshold > KINDA_SMALL_NUMBER;
CT.bFloorRouteWasWound = bNeedsWind; CT.bFloorRouteWasWound = bNeedsWind;
const float RequiredHorizontalRun = bNeedsWind const float RequiredHorizontalRun = bNeedsWind
? FMath::Abs(FloorDelta) / SafeGentleThreshold ? FMath::Min(
VoxelPassageGeometry::TunnelFloorMaximumRouteLengthVoxels,
FMath::Abs(FloorDelta) / SafeGentleThreshold)
: DirectHorizontalRun; : DirectHorizontalRun;
const int32 WindWaves = bNeedsWind const int32 WindWaves = bNeedsWind
? FMath::Clamp( ? FMath::Clamp(
FMath::CeilToInt(RequiredHorizontalRun FMath::CeilToInt(RequiredHorizontalRun
/ FMath::Max(DirectHorizontalRun, 16.0f)), / FMath::Max(DirectHorizontalRun, 16.0f)),
1, 6) 1, VoxelPassageGeometry::TunnelFloorMaximumWindWaves)
: 0; : 0;
FVector HorizontalAxis( FVector HorizontalAxis(
WorldEndB.X - WorldEndA.X, WorldEndB.Y - WorldEndA.Y, 0.0f); WorldEndB.X - WorldEndA.X, WorldEndB.Y - WorldEndA.Y, 0.0f);
FVector PerpA; FVector PerpA;
@@ -7947,25 +8281,42 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx]; const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2 const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2
&& Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num(); && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num();
const FVector& BoundCenter = bHasWorldChain bool bSDFNear = true;
? Tunnel.WorldBoundCenter : Tunnel.BoundCenter; if (Tunnel.SDFInfluenceRadius > 0.0f
const float BoundRadiusSq = bHasWorldChain
? Tunnel.WorldBoundRadiusSq : Tunnel.BoundRadiusSq;
const float InfluenceRadius = bHasWorldChain
? Tunnel.WorldInfluenceRadius : Tunnel.SDFInfluenceRadius;
const FVector& CenterlineMin = bHasWorldChain
? Tunnel.WorldCenterlineMin : Tunnel.SDFCenterlineMin;
const FVector& CenterlineMax = bHasWorldChain
? Tunnel.WorldCenterlineMax : Tunnel.SDFCenterlineMax;
if (InfluenceRadius > 0.0f
&& VF_DistanceSquaredToAabb( && VF_DistanceSquaredToAabb(
WorldX, WorldY, WorldZ, CenterlineMin, CenterlineMax) WorldX, WorldY, WorldZ,
> FMath::Square(InfluenceRadius)) Tunnel.SDFCenterlineMin, Tunnel.SDFCenterlineMax)
> FMath::Square(Tunnel.SDFInfluenceRadius))
{ {
return; bSDFNear = false;
} }
if (BoundRadiusSq > 0.0f if (Tunnel.BoundRadiusSq > 0.0f
&& VF_FloatDistSquared(WorldX, WorldY, WorldZ, BoundCenter) > BoundRadiusSq) && VF_FloatDistSquared(
WorldX, WorldY, WorldZ, Tunnel.BoundCenter) > Tunnel.BoundRadiusSq)
{
bSDFNear = false;
}
bool bWorldNear = false;
if (bHasWorldChain)
{
bWorldNear = true;
if (Tunnel.WorldInfluenceRadius > 0.0f
&& VF_DistanceSquaredToAabb(
WorldX, WorldY, WorldZ,
Tunnel.WorldCenterlineMin, Tunnel.WorldCenterlineMax)
> FMath::Square(Tunnel.WorldInfluenceRadius))
{
bWorldNear = false;
}
if (Tunnel.WorldBoundRadiusSq > 0.0f
&& VF_FloatDistSquared(
WorldX, WorldY, WorldZ, Tunnel.WorldBoundCenter)
> Tunnel.WorldBoundRadiusSq)
{
bWorldNear = false;
}
}
if (!bSDFNear && !bWorldNear)
{ {
return; return;
} }
@@ -7979,7 +8330,11 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
VF_FindWorldTunnelMouthOwnership(Pos, Tunnel, Cache.SDFBlendRadius); VF_FindWorldTunnelMouthOwnership(Pos, Tunnel, Cache.SDFBlendRadius);
const bool bRoomOwnsThisTunnel = RoomMouthOwnership.bValid; const bool bRoomOwnsThisTunnel = RoomMouthOwnership.bValid;
bool bRaisedTunnelFloor = false; bool bRaisedTunnelFloor = false;
if (bHasWorldChain) // The world chain is used only for its finite support-floor ownership band. Its air
// surface follows a continuous sweep, while the ordinary cave-warped chain remains the
// authoritative carved tunnel. Evaluating a second per-span capsule union creates a
// segmented zero set at every control-span join.
if (bHasWorldChain && Tunnel.bTunnelFloorEnabled)
{ {
if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled()) if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled())
{ {
@@ -1139,7 +1139,7 @@ namespace
if (FieldName == TEXT("TunnelEndpointZOffset")) return Set(0.0f, 1.0f); if (FieldName == TEXT("TunnelEndpointZOffset")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("TunnelFloorTerraceStepHeight")) return Set(0.25f, FMath::Max(2.0f, HQuarter)); if (FieldName == TEXT("TunnelFloorTerraceStepHeight")) return Set(0.25f, FMath::Max(2.0f, HQuarter));
if (FieldName == TEXT("TunnelFloorMaxLedgeHeight")) return Set(1.0f, FMath::Max(4.0f, HQuarter)); if (FieldName == TEXT("TunnelFloorMaxLedgeHeight")) return Set(1.0f, FMath::Max(4.0f, HQuarter));
if (FieldName == TEXT("TunnelFloorGentleSlopeThreshold")) return Set(0.0f, 1.5f); if (FieldName == TEXT("TunnelFloorGentleSlopeThreshold")) return Set(0.0f, 90.0f);
if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(4.0f, H * 0.15f)); if (FieldName == TEXT("SDFBlendRadius")) return Set(2.0f, FMath::Max(4.0f, H * 0.15f));
if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f); if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f);
if (FieldName == TEXT("CaveWarpStrength")) return Set(0.0f, HHalf); if (FieldName == TEXT("CaveWarpStrength")) return Set(0.0f, HHalf);
@@ -0,0 +1,74 @@
// Serialized strate-definition migrations.
#include "VoxelStrateDefinition.h"
#include "Serialization/CustomVersion.h"
#include "VoxelPassageGeometry.h"
namespace
{
// The field name is retained for package compatibility, but version 1 changes its meaning
// from a raw tangent/gradient to authored degrees. This GUID is local to the plugin.
const FGuid GVoxelForgeStrateSerializationVersion(
0xA76D5E31, 0x4B2C4F9A, 0xB8C1D204, 0x6E9F7738);
constexpr int32 GVoxelForgeStrateGentleSlopeDegreesVersion = 1;
FCustomVersionRegistration GVoxelForgeStrateSerializationRegistration(
GVoxelForgeStrateSerializationVersion,
GVoxelForgeStrateGentleSlopeDegreesVersion,
TEXT("VoxelForgeStrateSerialization"));
float LegacyGradientToDegrees(float LegacyGradient)
{
if (!VoxelMath::IsFinite(LegacyGradient) || LegacyGradient <= 0.0f)
{
return 0.0f;
}
if (LegacyGradient >= 4096.0f)
{
return VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees;
}
// Invert the old gradient without atan/tan. The same deterministic sine/cosine helper
// used by generation makes this migration independent of the host CRT.
float LowDegrees = 0.0f;
float HighDegrees = VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees;
for (int32 Iteration = 0; Iteration < 32; ++Iteration)
{
const float MidDegrees = (LowDegrees + HighDegrees) * 0.5f;
float SinValue = 0.0f;
float CosValue = 0.0f;
VoxelMath::DetSinCos(
SinValue, CosValue, MidDegrees * (PI / 180.0f));
const float MidGradient = FMath::Min(
FMath::Abs(SinValue) / FMath::Max(FMath::Abs(CosValue), 1.0e-3f),
4096.0f);
if (MidGradient < LegacyGradient)
{
LowDegrees = MidDegrees;
}
else
{
HighDegrees = MidDegrees;
}
}
return FMath::Clamp(
(LowDegrees + HighDegrees) * 0.5f,
0.0f,
VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees);
}
}
void UVoxelStrateDefinition::PostLoad()
{
Super::PostLoad();
if (GetLinkerCustomVersion(GVoxelForgeStrateSerializationVersion)
>= GVoxelForgeStrateGentleSlopeDegreesVersion)
{
return;
}
const float LegacyGradient = GenerationParams.TunnelFloorGentleSlopeThreshold;
GenerationParams.TunnelFloorGentleSlopeThreshold =
LegacyGradientToDegrees(LegacyGradient);
}
@@ -716,7 +716,8 @@ struct FCachedTunnel
bool bTunnelFloorTerracingEnabled = true; bool bTunnelFloorTerracingEnabled = true;
float TunnelFloorTerraceStepHeight = 1.71f; float TunnelFloorTerraceStepHeight = 1.71f;
float TunnelFloorMaxLedgeHeight = 12.0f; float TunnelFloorMaxLedgeHeight = 12.0f;
float TunnelFloorGentleSlopeThreshold = 0.9656888f; // Cached internal gradient derived from the authored degree value. This is not serialized.
float TunnelFloorGentleSlopeGradient = 0.9656888f;
int32 TunnelFloorLedgeCountPreference = 0; int32 TunnelFloorLedgeCountPreference = 0;
int32 TunnelFloorMaxLedges = 4096; int32 TunnelFloorMaxLedges = 4096;
bool bDramaticLedge = false; bool bDramaticLedge = false;
@@ -54,6 +54,47 @@ namespace VoxelPassageGeometry
// fallback threshold at which a room-to-room floor must become a terrace. // fallback threshold at which a room-to-room floor must become a terrace.
constexpr float PlayerWalkableFloorMaxGradient = 0.9656887748070738f; // tan(44 degrees) constexpr float PlayerWalkableFloorMaxGradient = 0.9656887748070738f; // tan(44 degrees)
// TunnelFloorGentleSlopeThreshold is authored as degrees. Zero is still a legal UI value,
// but the construction path uses this small positive floor so a malformed/overly strict
// value cannot turn the graph collect window into an unbounded switchback search.
constexpr float TunnelFloorGentleSlopeMinimumDegrees = 1.0f;
constexpr float TunnelFloorGentleSlopeMaximumDegrees = 90.0f;
// The route solver uses at most six half-waves. The canonical tunnel envelope is the default
// 360-voxel tunnel reach plus its endpoint correction; round that direct-run envelope up to
// 640 voxels, then derive the total cap from the same wave limit. This leaves room above the
// proven canonical route while keeping the 0.1-degree collect window finite and bounded.
constexpr int32 TunnelFloorMaximumWindWaves = 6;
constexpr float TunnelFloorMaximumSingleWaveRunVoxels = 640.0f;
constexpr float TunnelFloorMaximumRouteLengthVoxels =
static_cast<float>(TunnelFloorMaximumWindWaves)
* TunnelFloorMaximumSingleWaveRunVoxels;
FORCEINLINE float EffectiveTunnelFloorGentleSlopeDegrees(float AuthoredDegrees)
{
if (!VoxelMath::IsFinite(AuthoredDegrees))
{
return 44.0f;
}
return FMath::Clamp(
FMath::Max(AuthoredDegrees, TunnelFloorGentleSlopeMinimumDegrees),
TunnelFloorGentleSlopeMinimumDegrees,
TunnelFloorGentleSlopeMaximumDegrees);
}
FORCEINLINE float TunnelFloorGradientFromDegrees(float AuthoredDegrees)
{
const float Degrees = EffectiveTunnelFloorGentleSlopeDegrees(AuthoredDegrees);
float SinValue = 0.0f;
float CosValue = 0.0f;
VoxelMath::DetSinCos(
SinValue, CosValue, Degrees * (PI / 180.0f));
// At 90 degrees the mathematical tangent is unbounded. The route solver only needs a
// finite comparison, so preserve the ordering while keeping every downstream bound finite.
return FMath::Min(
FMath::Abs(SinValue) / FMath::Max(FMath::Abs(CosValue), 1.0e-3f),
4096.0f);
}
// The walkability contract is about the floor carried by the tube, not only its centreline. // The walkability contract is about the floor carried by the tube, not only its centreline.
// A tapered tube changes that floor by the radius delta, so the construction and the audit // A tapered tube changes that floor by the radius delta, so the construction and the audit
// use this same conservative floor delta. // use this same conservative floor delta.
@@ -48,6 +48,8 @@ class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
GENERATED_BODY() GENERATED_BODY()
public: public:
virtual void PostLoad() override;
//========================================================================= //=========================================================================
// IDENTIFICATION // IDENTIFICATION
//========================================================================= //=========================================================================
+6 -5
View File
@@ -709,12 +709,13 @@ struct VOXELFORGE_API FStrateGenerationParams
meta = (ClampMin = "0.0")) meta = (ClampMin = "0.0"))
float TunnelFloorMaxLedgeHeight = 12.0f; float TunnelFloorMaxLedgeHeight = 12.0f;
// Floor gradient threshold for a direct route. At or below it, the floor is a smooth ramp. A // Floor gradient threshold in degrees for a direct route. At or below it, the floor is a
// steeper walk-critical edge receives a longer winding route; an eligible edge may receive // smooth ramp. A steeper walk-critical edge receives a longer winding route; an eligible edge
// deterministic dramatic ledges instead. The default is tan(44 degrees). // may receive deterministic dramatic ledges instead. Internally this is converted with
// VoxelMath::DetSinCos; the old serialized tangent value is migrated on asset load.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor", UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Tunnels|Floor",
meta = (ClampMin = "0.0")) meta = (ClampMin = "0.0", ClampMax = "90.0", DisplayName = "Gentle Slope Threshold (degrees)"))
float TunnelFloorGentleSlopeThreshold = 0.9656888f; float TunnelFloorGentleSlopeThreshold = 44.0f;
// Preferred number of large ledges for a steep eligible floor. Zero selects the deterministic // Preferred number of large ledges for a steep eligible floor. Zero selects the deterministic
// height-driven count; positive values request at least that many transitions, subject to the // height-driven count; positive values request at least that many transitions, subject to the
@@ -34,6 +34,7 @@
#include "VoxelGenerator.h" #include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.h" #include "VoxelMarchingCubesMesher.h"
#include "VoxelNoise.h" #include "VoxelNoise.h"
#include "VoxelPassageGeometry.h"
#include "VoxelSettings.h" #include "VoxelSettings.h"
#include "VoxelStackSampler.h" #include "VoxelStackSampler.h"
#include "VoxelStrateDefinition.h" #include "VoxelStrateDefinition.h"
@@ -213,6 +214,49 @@ bool ParseRenderCamera(const FString& Text, FVector& OutCamera)
return true; return true;
} }
bool ParseDensitySlice(
const FString& Text,
FVector& OutOrigin,
FVector& OutDirection,
int32& OutSamples,
float& OutStep)
{
TArray<FString> Components;
Text.ParseIntoArray(Components, TEXT(","), true);
if (Components.Num() != 8)
{
return false;
}
const float OriginX = FCString::Atof(*Components[0]);
const float OriginY = FCString::Atof(*Components[1]);
const float OriginZ = FCString::Atof(*Components[2]);
const float DirectionX = FCString::Atof(*Components[3]);
const float DirectionY = FCString::Atof(*Components[4]);
const float DirectionZ = FCString::Atof(*Components[5]);
const int32 Samples = FCString::Atoi(*Components[6]);
const float Step = FCString::Atof(*Components[7]);
if (!FMath::IsFinite(OriginX) || !FMath::IsFinite(OriginY)
|| !FMath::IsFinite(OriginZ) || !FMath::IsFinite(DirectionX)
|| !FMath::IsFinite(DirectionY) || !FMath::IsFinite(DirectionZ)
|| !FMath::IsFinite(Step) || Samples < 2 || Samples > 4096
|| Step <= 0.0f)
{
return false;
}
FVector Direction(DirectionX, DirectionY, DirectionZ);
if (!Direction.Normalize())
{
return false;
}
OutOrigin = FVector(OriginX, OriginY, OriginZ);
OutDirection = Direction;
OutSamples = Samples;
OutStep = Step;
return true;
}
bool NeedsOriginInWindow(ECaveGeneratorType Archetype) bool NeedsOriginInWindow(ECaveGeneratorType Archetype)
{ {
// This is the same topology policy as VoxelForgePlayerFitWindow.h. That header is test-only, // This is the same topology policy as VoxelForgePlayerFitWindow.h. That header is test-only,
@@ -261,8 +305,11 @@ float BoundarySealForArchetype(
struct FExploreArguments struct FExploreArguments
{ {
int32 Seed = 0; int32 Seed = 0;
bool bSeedOverride = false;
ECaveGeneratorType Archetype = ECaveGeneratorType::Maze; ECaveGeneratorType Archetype = ECaveGeneratorType::Maze;
int32 Slot = 4; int32 Slot = 4;
FString StrateReference;
FString SettingsReference;
bool bRender = true; bool bRender = true;
bool bWalk = true; bool bWalk = true;
bool bExport = true; bool bExport = true;
@@ -296,6 +343,8 @@ struct FExploreArguments
float BudgetMinutes = DefaultBudgetMinutes; float BudgetMinutes = DefaultBudgetMinutes;
bool bSurfaceRoughnessOverride = false; bool bSurfaceRoughnessOverride = false;
float SurfaceRoughness = 0.0f; float SurfaceRoughness = 0.0f;
bool bTunnelWarpStrengthOverride = false;
float TunnelWarpStrength = 0.0f;
bool bRoughnessNoiseTypeOverride = false; bool bRoughnessNoiseTypeOverride = false;
EVoxelNoiseType RoughnessNoiseType = EVoxelNoiseType::FBM; EVoxelNoiseType RoughnessNoiseType = EVoxelNoiseType::FBM;
bool bRoughnessFrequencyOverride = false; bool bRoughnessFrequencyOverride = false;
@@ -322,6 +371,13 @@ struct FExploreArguments
int32 PassageFloorWrites = 1; int32 PassageFloorWrites = 1;
bool bRenderCameraOverride = false; bool bRenderCameraOverride = false;
FVector RenderCamera = FVector::ZeroVector; FVector RenderCamera = FVector::ZeroVector;
bool bRenderTargetOverride = false;
FVector RenderTarget = FVector::ZeroVector;
bool bDensitySlice = false;
FVector DensitySliceOrigin = FVector::ZeroVector;
FVector DensitySliceDirection = FVector::ForwardVector;
int32 DensitySliceSamples = 257;
float DensitySliceStep = 0.5f;
bool IsDensityProfilingEnabled() const bool IsDensityProfilingEnabled() const
{ {
@@ -349,9 +405,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
FString ModesText = TEXT("render,walk,export"); FString ModesText = TEXT("render,walk,export");
FString OutText; FString OutText;
FParse::Value(*Params, TEXT("seed="), OutArguments.Seed); OutArguments.bSeedOverride = FParse::Value(*Params, TEXT("seed="), OutArguments.Seed);
FParse::Value(*Params, TEXT("archetype="), ArchetypeText); FParse::Value(*Params, TEXT("archetype="), ArchetypeText);
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot); FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference);
FParse::Value(*Params, TEXT("settingsref="), OutArguments.SettingsReference);
FParse::Value(*Params, TEXT("modes="), ModesText); FParse::Value(*Params, TEXT("modes="), ModesText);
// FParse::Value treats a comma as a command-line separator in some UE commandlet // FParse::Value treats a comma as a command-line separator in some UE commandlet
// launch paths. Recover the complete token so both -modes=walk,export and the // launch paths. Recover the complete token so both -modes=walk,export and the
@@ -427,6 +485,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
const bool bSurfaceRoughnessParsed = FParse::Value( const bool bSurfaceRoughnessParsed = FParse::Value(
*Params, TEXT("surfaceroughness="), OutArguments.SurfaceRoughness); *Params, TEXT("surfaceroughness="), OutArguments.SurfaceRoughness);
OutArguments.bSurfaceRoughnessOverride = bSurfaceRoughnessSpecified; OutArguments.bSurfaceRoughnessOverride = bSurfaceRoughnessSpecified;
const bool bTunnelWarpStrengthSpecified = Params.Contains(
TEXT("tunnelwarp="), ESearchCase::IgnoreCase);
const bool bTunnelWarpStrengthParsed = FParse::Value(
*Params, TEXT("tunnelwarp="), OutArguments.TunnelWarpStrength);
OutArguments.bTunnelWarpStrengthOverride = bTunnelWarpStrengthSpecified;
FString RoughnessNoiseTypeText; FString RoughnessNoiseTypeText;
const bool bRoughnessNoiseTypeSpecified = Params.Contains( const bool bRoughnessNoiseTypeSpecified = Params.Contains(
TEXT("roughnesstype="), ESearchCase::IgnoreCase); TEXT("roughnesstype="), ESearchCase::IgnoreCase);
@@ -510,6 +573,53 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
} }
} }
OutArguments.bRenderCameraOverride = bRenderCameraSpecified; OutArguments.bRenderCameraOverride = bRenderCameraSpecified;
FString RenderTargetText;
const bool bRenderTargetSpecified = Params.Contains(
TEXT("rendertarget="), ESearchCase::IgnoreCase);
bool bRenderTargetParsed = false;
if (bRenderTargetSpecified)
{
const int32 TargetOffset = Params.Find(TEXT("rendertarget="), ESearchCase::IgnoreCase);
if (TargetOffset != INDEX_NONE)
{
RenderTargetText = Params.Mid(TargetOffset + 13);
const int32 TargetEnd = RenderTargetText.Find(TEXT(" "));
if (TargetEnd != INDEX_NONE)
{
RenderTargetText.LeftInline(TargetEnd);
}
RenderTargetText.TrimQuotesInline();
bRenderTargetParsed = ParseRenderCamera(RenderTargetText, OutArguments.RenderTarget);
}
}
OutArguments.bRenderTargetOverride = bRenderTargetSpecified;
FString DensitySliceText;
const bool bDensitySliceSpecified = Params.Contains(
TEXT("densityslice="), ESearchCase::IgnoreCase);
bool bDensitySliceParsed = false;
if (bDensitySliceSpecified)
{
const int32 SliceOffset = Params.Find(TEXT("densityslice="), ESearchCase::IgnoreCase);
if (SliceOffset != INDEX_NONE)
{
DensitySliceText = Params.Mid(SliceOffset + 13);
const int32 SliceEnd = DensitySliceText.Find(TEXT(" "));
if (SliceEnd != INDEX_NONE)
{
DensitySliceText.LeftInline(SliceEnd);
}
DensitySliceText.TrimQuotesInline();
bDensitySliceParsed = ParseDensitySlice(
DensitySliceText,
OutArguments.DensitySliceOrigin,
OutArguments.DensitySliceDirection,
OutArguments.DensitySliceSamples,
OutArguments.DensitySliceStep);
}
}
OutArguments.bDensitySlice = bDensitySliceSpecified;
int32 FailureFocusRender = 0; int32 FailureFocusRender = 0;
FParse::Value(*Params, TEXT("failurefocus="), FailureFocusRender); FParse::Value(*Params, TEXT("failurefocus="), FailureFocusRender);
OutArguments.bFailureFocusRender = FailureFocusRender != 0; OutArguments.bFailureFocusRender = FailureFocusRender != 0;
@@ -656,6 +766,14 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutError = TEXT("surfaceroughness must be finite and greater than or equal to zero."); OutError = TEXT("surfaceroughness must be finite and greater than or equal to zero.");
return false; return false;
} }
if (OutArguments.bTunnelWarpStrengthOverride
&& (!bTunnelWarpStrengthParsed
|| !FMath::IsFinite(OutArguments.TunnelWarpStrength)
|| OutArguments.TunnelWarpStrength < 0.0f))
{
OutError = TEXT("tunnelwarp must be finite and greater than or equal to zero.");
return false;
}
if (OutArguments.bRoughnessNoiseTypeOverride && !bRoughnessNoiseTypeParsed) if (OutArguments.bRoughnessNoiseTypeOverride && !bRoughnessNoiseTypeParsed)
{ {
OutError = TEXT("roughnesstype must be FBM, Ridged, Mixed, or Cellular."); OutError = TEXT("roughnesstype must be FBM, Ridged, Mixed, or Cellular.");
@@ -704,9 +822,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
if (OutArguments.bTunnelFloorGentleOverride if (OutArguments.bTunnelFloorGentleOverride
&& (!bTunnelFloorGentleParsed && (!bTunnelFloorGentleParsed
|| !FMath::IsFinite(OutArguments.TunnelFloorGentle) || !FMath::IsFinite(OutArguments.TunnelFloorGentle)
|| OutArguments.TunnelFloorGentle < 0.0f)) || OutArguments.TunnelFloorGentle < 0.0f
|| OutArguments.TunnelFloorGentle
> VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees))
{ {
OutError = TEXT("tunnelfloorgentle must be finite and non-negative."); OutError = TEXT("tunnelfloorgentle must be finite degrees in [0,90].");
return false; return false;
} }
if (OutArguments.bTunnelFloorLedgesOverride if (OutArguments.bTunnelFloorLedgesOverride
@@ -756,6 +876,17 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
OutError = TEXT("rendercamera must be three finite comma-separated voxel coordinates."); OutError = TEXT("rendercamera must be three finite comma-separated voxel coordinates.");
return false; return false;
} }
if (OutArguments.bRenderTargetOverride && !bRenderTargetParsed)
{
OutError = TEXT("rendertarget must be three finite comma-separated voxel coordinates.");
return false;
}
if (OutArguments.bDensitySlice && !bDensitySliceParsed)
{
OutError = TEXT(
"densityslice must be origin x,y,z, direction x,y,z, sample count, and positive step.");
return false;
}
if (OutText.IsEmpty()) if (OutText.IsEmpty())
{ {
@@ -946,15 +1077,58 @@ struct FExploreWorld
bool Build(const FExploreArguments& Arguments, FString& OutError) bool Build(const FExploreArguments& Arguments, FString& OutError)
{ {
const int32 NumStrates = Arguments.Slot + 2; const bool bUseAuthoredSettings = !Arguments.SettingsReference.IsEmpty();
const bool bUseAuthoredStrate = !Arguments.StrateReference.IsEmpty();
UVoxelStrateDefinition* AuthoredDefinition = nullptr;
if (bUseAuthoredStrate)
{
AuthoredDefinition = LoadObject<UVoxelStrateDefinition>(
nullptr, *Arguments.StrateReference);
if (AuthoredDefinition == nullptr)
{
OutError = FString::Printf(
TEXT("Could not load staged strate asset '%s'."),
*Arguments.StrateReference);
return false;
}
}
if (bUseAuthoredSettings)
{
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
nullptr, *Arguments.SettingsReference);
if (AuthoredSettings == nullptr)
{
OutError = FString::Printf(
TEXT("Could not load staged settings asset '%s'."),
*Arguments.SettingsReference);
return false;
}
Settings = TStrongObjectPtr<UVoxelSettings>(
DuplicateObject<UVoxelSettings>(AuthoredSettings, GetTransientPackage()));
if (!Settings.IsValid())
{
OutError = TEXT("Could not duplicate the staged settings asset into the transient package.");
return false;
}
}
else
{
Settings = TStrongObjectPtr<UVoxelSettings>(
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
}
const int32 NumStrates = bUseAuthoredSettings
? FMath::Max(Settings->TotalStrates, Arguments.Slot + 2)
: Arguments.Slot + 2;
if (NumStrates > MaxSyntheticStrates) if (NumStrates > MaxSyntheticStrates)
{ {
OutError = TEXT("The synthetic layout would exceed the commandlet strate cap."); OutError = TEXT("The synthetic layout would exceed the commandlet strate cap.");
return false; return false;
} }
Settings = TStrongObjectPtr<UVoxelSettings>( if (!bUseAuthoredSettings)
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient)); {
Settings->Seed = Arguments.Seed; Settings->Seed = Arguments.Seed;
Settings->TotalStrates = NumStrates; Settings->TotalStrates = NumStrates;
Settings->InterStrateGapChunks = 0; Settings->InterStrateGapChunks = 0;
@@ -962,12 +1136,39 @@ struct FExploreWorld
Settings->EdgeSealThickness = 64.0f; Settings->EdgeSealThickness = 64.0f;
Settings->OriginSpineRadius = 14.0f; Settings->OriginSpineRadius = 14.0f;
Settings->bOpenSurfaceEntry = true; Settings->bOpenSurfaceEntry = true;
}
else if (Arguments.bSeedOverride)
{
Settings->Seed = Arguments.Seed;
}
// The transient fixed-definition layout may need to extend an authored settings asset
// for a requested interior slot. Keep that duplicate's advertised count consistent; the
// loaded package remains untouched.
Settings->TotalStrates = NumStrates;
if (bUseAuthoredStrate)
{
// The owner’s snapshot states that every slot uses DA_Strate3. Clone it per slot so
// commandlet overrides never mutate a loaded package or leak between slots.
Settings->StratePool.Reset();
Settings->FixedStrates.Reset();
}
Definitions.Reserve(NumStrates); Definitions.Reserve(NumStrates);
for (int32 Index = 0; Index < NumStrates; ++Index) for (int32 Index = 0; Index < NumStrates; ++Index)
{ {
UVoxelStrateDefinition* Definition = NewObject<UVoxelStrateDefinition>( UVoxelStrateDefinition* Definition = bUseAuthoredStrate
? DuplicateObject<UVoxelStrateDefinition>(
AuthoredDefinition, GetTransientPackage())
: NewObject<UVoxelStrateDefinition>(
GetTransientPackage(), NAME_None, RF_Transient); GetTransientPackage(), NAME_None, RF_Transient);
if (Definition == nullptr)
{
OutError = FString::Printf(
TEXT("Could not create transient strate definition for slot %d."), Index);
return false;
}
if (!bUseAuthoredStrate)
{
Definition->StrateName = FText::FromString(FString::Printf( Definition->StrateName = FText::FromString(FString::Printf(
TEXT("VoxelForgeExplore_%s_slot%d"), TEXT("VoxelForgeExplore_%s_slot%d"),
ArchetypeName(Index == Arguments.Slot ArchetypeName(Index == Arguments.Slot
@@ -997,13 +1198,121 @@ struct FExploreWorld
{ {
Definition->GenerationParams.WormStrength = Arguments.WormStrength; Definition->GenerationParams.WormStrength = Arguments.WormStrength;
} }
}
const TSoftObjectPtr<UVoxelStrateDefinition> SoftDefinition(Definition); const TSoftObjectPtr<UVoxelStrateDefinition> SoftDefinition(Definition);
Settings->FixedStrates.Add(Index, SoftDefinition); Settings->FixedStrates.Add(Index, SoftDefinition);
if (!bUseAuthoredStrate)
{
Settings->StratePool.Add(SoftDefinition); Settings->StratePool.Add(SoftDefinition);
}
Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition)); Definitions.Add(TStrongObjectPtr<UVoxelStrateDefinition>(Definition));
} }
auto ApplyTargetOverrides = [&Arguments](UVoxelStrateDefinition* Definition) -> bool
{
if (Definition == nullptr) { return false; }
const ECaveGeneratorType TargetArchetype = Definition->GeneratorType;
if (Arguments.bSurfaceRoughnessOverride)
{
switch (TargetArchetype)
{
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
Definition->SlabParams.FloorRoughness = Arguments.SurfaceRoughness;
Definition->SlabParams.CeilingRoughness = Arguments.SurfaceRoughness;
break;
case ECaveGeneratorType::Maze:
Definition->MazeParams.SurfaceRoughness = Arguments.SurfaceRoughness;
break;
case ECaveGeneratorType::SurfaceWorld:
Definition->SurfaceParams.SurfaceRoughness = Arguments.SurfaceRoughness;
break;
case ECaveGeneratorType::VerticalShafts:
Definition->VerticalShaftParams.SurfaceRoughness = Arguments.SurfaceRoughness;
break;
case ECaveGeneratorType::FloatingIslands:
Definition->FloatingIslandParams.SurfaceRoughness = Arguments.SurfaceRoughness;
break;
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
default:
Definition->GenerationParams.SurfaceRoughness = Arguments.SurfaceRoughness;
break;
}
}
if (Arguments.bRoughnessNoiseTypeOverride
|| Arguments.bRoughnessFrequencyOverride)
{
if (TargetArchetype != ECaveGeneratorType::TunnelNetwork
&& TargetArchetype != ECaveGeneratorType::Underwater)
{
return false;
}
if (Arguments.bRoughnessNoiseTypeOverride)
{
Definition->GenerationParams.RoughnessNoiseType = Arguments.RoughnessNoiseType;
}
if (Arguments.bRoughnessFrequencyOverride)
{
Definition->GenerationParams.RoughnessFrequency = Arguments.RoughnessFrequency;
}
}
FStrateGenerationParams& TunnelParams = Definition->GenerationParams;
if (Arguments.bTunnelWarpStrengthOverride)
{
if (TargetArchetype != ECaveGeneratorType::TunnelNetwork
&& TargetArchetype != ECaveGeneratorType::Underwater)
{
return false;
}
TunnelParams.TunnelWarpStrength = Arguments.TunnelWarpStrength;
}
if (Arguments.bTunnelFloorEnabledOverride)
{
TunnelParams.bTunnelFloorEnabled = Arguments.TunnelFloorEnabled != 0;
}
if (Arguments.bTunnelFloorTerracingOverride)
{
TunnelParams.bTunnelFloorTerracingEnabled = Arguments.TunnelFloorTerracing != 0;
}
if (Arguments.bTunnelFloorStepOverride)
{
TunnelParams.TunnelFloorTerraceStepHeight = Arguments.TunnelFloorStep;
}
if (Arguments.bTunnelFloorMaxLedgeOverride)
{
TunnelParams.TunnelFloorMaxLedgeHeight = Arguments.TunnelFloorMaxLedge;
}
if (Arguments.bTunnelFloorGentleOverride)
{
TunnelParams.TunnelFloorGentleSlopeThreshold = Arguments.TunnelFloorGentle;
}
if (Arguments.bTunnelFloorLedgesOverride)
{
TunnelParams.TunnelFloorLedgeCountPreference = Arguments.TunnelFloorLedges;
}
if (Arguments.bTunnelFloorMaxLedgesOverride)
{
TunnelParams.TunnelFloorMaxLedges = Arguments.TunnelFloorMaxLedges;
}
if (Arguments.bRoomMouthRiseOverride)
{
TunnelParams.RoomMouthRiseStrength = Arguments.RoomMouthRise;
}
if (Arguments.bRoomMouthRiseBlendOverride)
{
TunnelParams.RoomMouthRiseBlendVoxels = Arguments.RoomMouthRiseBlend;
}
return true;
};
if (!Definitions.IsValidIndex(Arguments.Slot)
|| !ApplyTargetOverrides(Definitions[Arguments.Slot].Get()))
{
OutError = TEXT("The target definition rejected a commandlet override.");
return false;
}
Manager = TStrongObjectPtr<UVoxelStrateManager>( Manager = TStrongObjectPtr<UVoxelStrateManager>(
NewObject<UVoxelStrateManager>(GetTransientPackage(), NAME_None, RF_Transient)); NewObject<UVoxelStrateManager>(GetTransientPackage(), NAME_None, RF_Transient));
if (!Manager->Initialize(Settings.Get(), Settings->Seed)) if (!Manager->Initialize(Settings.Get(), Settings->Seed))
@@ -1028,104 +1337,40 @@ struct FExploreWorld
OutError = TEXT("The requested target slot has no resolved definition."); OutError = TEXT("The requested target slot has no resolved definition.");
return false; return false;
} }
if (Arguments.bSurfaceRoughnessOverride) const ECaveGeneratorType TargetArchetype = Target.Definition->GeneratorType;
{ const FStrateGenerationParams& ReportParams = Target.Definition->GenerationParams;
// This is deliberately applied after layout construction and only to the target slot's UE_LOG(LogTemp, Display,
// resolved definition. Passage placement and every neighbouring strate retain their TEXT("[VoxelForgeExplore][OwnerAssets] strate=%s settings=%s slot=%d "
// authored/default values; only the sampled target field gets the experiment knob. "seed=%d height_chunks=%d archetype=%s opstack=%d "
switch (Arguments.Archetype) "floor_enabled=%d floor_terracing=%d floor_step=%.9g max_ledge=%.9g "
{ "gentle_slope_degrees=%.9g gentle_gradient=%.9g route_cap=%.9g "
case ECaveGeneratorType::FlatPlain: "wind_wave_cap=%d "
case ECaveGeneratorType::CrystalChamber: "ledge_preference=%d max_ledges=%d tunnel_warp=%.9g terrain_ops=%d terrace_step=%.9g "
Target.Definition->SlabParams.FloorRoughness = Arguments.SurfaceRoughness; "layer_spacing=%.9g rib_spacing=%.9g"),
Target.Definition->SlabParams.CeilingRoughness = Arguments.SurfaceRoughness; bUseAuthoredStrate ? *Arguments.StrateReference : TEXT("synthetic"),
break; bUseAuthoredSettings ? *Arguments.SettingsReference : TEXT("synthetic"),
case ECaveGeneratorType::Maze: Arguments.Slot, Settings->Seed, Target.Definition->StrateHeightInChunks,
Target.Definition->MazeParams.SurfaceRoughness = Arguments.SurfaceRoughness; ArchetypeName(TargetArchetype), Target.Definition->bUseOperatorStack ? 1 : 0,
break; ReportParams.bTunnelFloorEnabled ? 1 : 0,
case ECaveGeneratorType::SurfaceWorld: ReportParams.bTunnelFloorTerracingEnabled ? 1 : 0,
Target.Definition->SurfaceParams.SurfaceRoughness = Arguments.SurfaceRoughness; ReportParams.TunnelFloorTerraceStepHeight,
break; ReportParams.TunnelFloorMaxLedgeHeight,
case ECaveGeneratorType::VerticalShafts: ReportParams.TunnelFloorGentleSlopeThreshold,
Target.Definition->VerticalShaftParams.SurfaceRoughness = Arguments.SurfaceRoughness; VoxelPassageGeometry::TunnelFloorGradientFromDegrees(
break; ReportParams.TunnelFloorGentleSlopeThreshold),
case ECaveGeneratorType::FloatingIslands: VoxelPassageGeometry::TunnelFloorMaximumRouteLengthVoxels,
Target.Definition->FloatingIslandParams.SurfaceRoughness = Arguments.SurfaceRoughness; VoxelPassageGeometry::TunnelFloorMaximumWindWaves,
break; ReportParams.TunnelFloorLedgeCountPreference,
case ECaveGeneratorType::TunnelNetwork: ReportParams.TunnelFloorMaxLedges,
case ECaveGeneratorType::Underwater: ReportParams.TunnelWarpStrength,
default: Target.Definition->TerrainOperations.Num(),
Target.Definition->GenerationParams.SurfaceRoughness = Arguments.SurfaceRoughness; ReportParams.TerraceStepHeight,
break; ReportParams.LayerLineSpacing,
} ReportParams.RibbingSpacing);
}
if (Arguments.bRoughnessNoiseTypeOverride
|| Arguments.bRoughnessFrequencyOverride)
{
if (Arguments.Archetype != ECaveGeneratorType::TunnelNetwork
&& Arguments.Archetype != ECaveGeneratorType::Underwater)
{
OutError = TEXT(
"roughnesstype/roughnessfrequency are only supported by TunnelNetwork or Underwater.");
return false;
}
if (Arguments.bRoughnessNoiseTypeOverride)
{
Target.Definition->GenerationParams.RoughnessNoiseType =
Arguments.RoughnessNoiseType;
}
if (Arguments.bRoughnessFrequencyOverride)
{
Target.Definition->GenerationParams.RoughnessFrequency =
Arguments.RoughnessFrequency;
}
}
// Floor controls are applied only to the requested target definition. They are
// commandlet overrides for measured archetype comparisons; authored assets still carry
// the defaults/rolled values in FStrateGenerationParams.
FStrateGenerationParams& TargetTunnelParams = Target.Definition->GenerationParams;
if (Arguments.bTunnelFloorEnabledOverride)
{
TargetTunnelParams.bTunnelFloorEnabled =
Arguments.TunnelFloorEnabled != 0;
}
if (Arguments.bTunnelFloorTerracingOverride)
{
TargetTunnelParams.bTunnelFloorTerracingEnabled =
Arguments.TunnelFloorTerracing != 0;
}
if (Arguments.bTunnelFloorStepOverride)
{
TargetTunnelParams.TunnelFloorTerraceStepHeight = Arguments.TunnelFloorStep;
}
if (Arguments.bTunnelFloorMaxLedgeOverride)
{
TargetTunnelParams.TunnelFloorMaxLedgeHeight = Arguments.TunnelFloorMaxLedge;
}
if (Arguments.bTunnelFloorGentleOverride)
{
TargetTunnelParams.TunnelFloorGentleSlopeThreshold = Arguments.TunnelFloorGentle;
}
if (Arguments.bTunnelFloorLedgesOverride)
{
TargetTunnelParams.TunnelFloorLedgeCountPreference = Arguments.TunnelFloorLedges;
}
if (Arguments.bTunnelFloorMaxLedgesOverride)
{
TargetTunnelParams.TunnelFloorMaxLedges = Arguments.TunnelFloorMaxLedges;
}
if (Arguments.bRoomMouthRiseOverride)
{
TargetTunnelParams.RoomMouthRiseStrength = Arguments.RoomMouthRise;
}
if (Arguments.bRoomMouthRiseBlendOverride)
{
TargetTunnelParams.RoomMouthRiseBlendVoxels = Arguments.RoomMouthRiseBlend;
}
TargetBottomWorldZ = Target.BottomChunkZ * CHUNK_SIZE; TargetBottomWorldZ = Target.BottomChunkZ * CHUNK_SIZE;
TargetTopWorldZ = (Target.TopChunkZ + 1) * CHUNK_SIZE; TargetTopWorldZ = (Target.TopChunkZ + 1) * CHUNK_SIZE;
TargetBoundarySealThickness = BoundarySealForArchetype( TargetBoundarySealThickness = BoundarySealForArchetype(
*Target.Definition, Arguments.Archetype); *Target.Definition, TargetArchetype);
if (TargetTopWorldZ <= TargetBottomWorldZ) if (TargetTopWorldZ <= TargetBottomWorldZ)
{ {
OutError = TEXT("The target strate has no positive voxel-space height."); OutError = TEXT("The target strate has no positive voxel-space height.");
@@ -1315,6 +1560,22 @@ struct FExploreExportOutput
bool bTruncated = false; bool bTruncated = false;
}; };
struct FExploreDensitySliceOutput
{
FString Status;
FString RefusalReason;
FString FileName;
int32 SampleCount = 0;
int32 FiniteSampleCount = 0;
int32 NonFiniteSampleCount = 0;
int32 ZeroCrossingCount = 0;
int32 SaturatedSampleCount = 0;
float MinDensity = 0.0f;
float MaxDensity = 0.0f;
float MaxAbsDensity = 0.0f;
double Seconds = 0.0;
};
struct FExploreProfilerComparison struct FExploreProfilerComparison
{ {
bool bAvailable = false; bool bAvailable = false;
@@ -1335,6 +1596,7 @@ struct FExploreRunOutput
FExploreRenderOutput Render; FExploreRenderOutput Render;
FExploreWalkOutput Walk; FExploreWalkOutput Walk;
FExploreExportOutput Export; FExploreExportOutput Export;
FExploreDensitySliceOutput DensitySlice;
double BudgetSeconds = 0.0; double BudgetSeconds = 0.0;
double ElapsedSeconds = 0.0; double ElapsedSeconds = 0.0;
double WalkSeconds = 0.0; double WalkSeconds = 0.0;
@@ -2776,7 +3038,12 @@ bool RunRender(
OutOutput.bAllCamerasPlayerFit = true; OutOutput.bAllCamerasPlayerFit = true;
FVector RouteFocus = FVector::ZeroVector; FVector RouteFocus = FVector::ZeroVector;
int32 RouteFocusCount = 0; int32 RouteFocusCount = 0;
if (CameraSeedWalk.bHasArrival) if (Arguments.bRenderTargetOverride)
{
RouteFocus = Arguments.RenderTarget;
RouteFocusCount = 1;
}
else if (CameraSeedWalk.bHasArrival)
{ {
RouteFocus += CameraSeedWalk.ArrivalVoxels; RouteFocus += CameraSeedWalk.ArrivalVoxels;
++RouteFocusCount; ++RouteFocusCount;
@@ -4136,6 +4403,7 @@ void WriteTunnelFloorOverrides(
{ {
Writer.WriteValue(TEXT("gentle_slope"), Writer.WriteValue(TEXT("gentle_slope"),
static_cast<double>(Arguments.TunnelFloorGentle)); static_cast<double>(Arguments.TunnelFloorGentle));
Writer.WriteValue(TEXT("gentle_slope_units"), TEXT("degrees"));
} }
Writer.WriteValue(TEXT("ledge_count_override"), Arguments.bTunnelFloorLedgesOverride); Writer.WriteValue(TEXT("ledge_count_override"), Arguments.bTunnelFloorLedgesOverride);
if (Arguments.bTunnelFloorLedgesOverride) if (Arguments.bTunnelFloorLedgesOverride)
@@ -4434,6 +4702,100 @@ bool RunExport(
return !OutOutput.bTruncated; return !OutOutput.bTruncated;
} }
bool RunDensitySlice(
const FExploreArguments& Arguments,
FExploreWorld& World,
FExploreDensitySliceOutput& OutOutput,
FExploreBudget& Budget)
{
OutOutput = FExploreDensitySliceOutput();
OutOutput.FileName = TEXT("density_slice.csv");
OutOutput.SampleCount = Arguments.DensitySliceSamples;
const double StartSeconds = FPlatformTime::Seconds();
FString Csv;
Csv.Reserve(Arguments.DensitySliceSamples * 96);
Csv += TEXT("index,x_voxels,y_voxels,z_voxels,density\r\n");
float PreviousDensity = 0.0f;
bool bHavePreviousDensity = false;
float MinDensity = TNumericLimits<float>::Max();
float MaxDensity = -TNumericLimits<float>::Max();
float MaxAbsDensity = 0.0f;
for (int32 Index = 0; Index < Arguments.DensitySliceSamples; ++Index)
{
if ((Index & 127) == 0 && Budget.ShouldStop(TEXT("density-slice")))
{
OutOutput.Status = TEXT("truncated");
OutOutput.RefusalReason = TEXT("The wall-clock budget stopped the density slice.");
OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds;
return false;
}
const FVector Position = Arguments.DensitySliceOrigin
+ Arguments.DensitySliceDirection
* (Arguments.DensitySliceStep * static_cast<float>(Index));
const float Density = World.Generator->GetDensityAt(
Position.X, Position.Y, Position.Z);
Csv += FString::Printf(
TEXT("%d,%.9g,%.9g,%.9g,%.9g\r\n"),
Index, Position.X, Position.Y, Position.Z, Density);
if (!FMath::IsFinite(Density))
{
++OutOutput.NonFiniteSampleCount;
bHavePreviousDensity = false;
continue;
}
++OutOutput.FiniteSampleCount;
MinDensity = FMath::Min(MinDensity, Density);
MaxDensity = FMath::Max(MaxDensity, Density);
MaxAbsDensity = FMath::Max(MaxAbsDensity, FMath::Abs(Density));
if (FMath::Abs(Density) >= 0.999f)
{
++OutOutput.SaturatedSampleCount;
}
if (bHavePreviousDensity
&& ((PreviousDensity < 0.0f && Density > 0.0f)
|| (PreviousDensity > 0.0f && Density < 0.0f)))
{
++OutOutput.ZeroCrossingCount;
}
PreviousDensity = Density;
bHavePreviousDensity = true;
}
if (OutOutput.FiniteSampleCount > 0)
{
OutOutput.MinDensity = MinDensity;
OutOutput.MaxDensity = MaxDensity;
OutOutput.MaxAbsDensity = MaxAbsDensity;
}
const FString Path = FPaths::Combine(Arguments.OutDirectory, OutOutput.FileName);
if (!FFileHelper::SaveStringToFile(
Csv, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
OutOutput.Status = TEXT("error");
OutOutput.RefusalReason = TEXT("Could not write density_slice.csv.");
OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds;
return false;
}
OutOutput.Status = TEXT("ok");
OutOutput.Seconds = FPlatformTime::Seconds() - StartSeconds;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeDensitySlice] file=%s samples=%d finite=%d non_finite=%d "
"zero_crossings=%d saturated=%d min=%.9g max=%.9g max_abs=%.9g "
"origin=(%.3f,%.3f,%.3f) direction=(%.6f,%.6f,%.6f) step=%.6g"),
*Path, OutOutput.SampleCount, OutOutput.FiniteSampleCount,
OutOutput.NonFiniteSampleCount, OutOutput.ZeroCrossingCount,
OutOutput.SaturatedSampleCount, OutOutput.MinDensity,
OutOutput.MaxDensity, OutOutput.MaxAbsDensity,
Arguments.DensitySliceOrigin.X, Arguments.DensitySliceOrigin.Y,
Arguments.DensitySliceOrigin.Z, Arguments.DensitySliceDirection.X,
Arguments.DensitySliceDirection.Y, Arguments.DensitySliceDirection.Z,
Arguments.DensitySliceStep);
return true;
}
void WritePlayerDimensions(FExploreJsonWriter& Writer, const TCHAR* Key) void WritePlayerDimensions(FExploreJsonWriter& Writer, const TCHAR* Key)
{ {
Writer.WriteObjectStart(Key); Writer.WriteObjectStart(Key);
@@ -4459,7 +4821,7 @@ FString BuildExploreJson(
TSharedRef<FExploreJsonWriter> Writer = TSharedRef<FExploreJsonWriter> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json); TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
Writer->WriteObjectStart(); Writer->WriteObjectStart();
Writer->WriteValue(TEXT("schema_version"), 6); Writer->WriteValue(TEXT("schema_version"), 7);
Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore")); Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore"));
Writer->WriteValue(TEXT("read_only_generation"), true); Writer->WriteValue(TEXT("read_only_generation"), true);
@@ -4534,6 +4896,12 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("sample_stacks_interval_us"), Writer->WriteValue(TEXT("sample_stacks_interval_us"),
static_cast<double>(Arguments.SampleStacksIntervalUs)); static_cast<double>(Arguments.SampleStacksIntervalUs));
Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod); Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod);
Writer->WriteValue(TEXT("density_slice_requested"), Arguments.bDensitySlice);
Writer->WriteValue(TEXT("render_target_override"), Arguments.bRenderTargetOverride);
if (Arguments.bRenderTargetOverride)
{
WriteJsonVector(*Writer, TEXT("render_target_voxels"), Arguments.RenderTarget, 1.0f);
}
Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds); Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds);
Writer->WriteValue(TEXT("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes)); Writer->WriteValue(TEXT("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes));
Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory); Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory);
@@ -4549,6 +4917,27 @@ FString BuildExploreJson(
Arguments.bBlockEarlyOut ? 1 : 0)); Arguments.bBlockEarlyOut ? 1 : 0));
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("density_slice"));
Writer->WriteValue(TEXT("status"), Output.DensitySlice.Status);
Writer->WriteValue(TEXT("file"), Output.DensitySlice.FileName);
Writer->WriteValue(TEXT("refusal_or_error"), Output.DensitySlice.RefusalReason);
Writer->WriteValue(TEXT("sample_count"), Output.DensitySlice.SampleCount);
Writer->WriteValue(TEXT("finite_sample_count"), Output.DensitySlice.FiniteSampleCount);
Writer->WriteValue(TEXT("non_finite_sample_count"), Output.DensitySlice.NonFiniteSampleCount);
Writer->WriteValue(TEXT("zero_crossing_count"), Output.DensitySlice.ZeroCrossingCount);
Writer->WriteValue(TEXT("saturated_sample_count"), Output.DensitySlice.SaturatedSampleCount);
Writer->WriteValue(TEXT("min_density"), static_cast<double>(Output.DensitySlice.MinDensity));
Writer->WriteValue(TEXT("max_density"), static_cast<double>(Output.DensitySlice.MaxDensity));
Writer->WriteValue(TEXT("max_abs_density"), static_cast<double>(Output.DensitySlice.MaxAbsDensity));
Writer->WriteValue(TEXT("seconds"), Output.DensitySlice.Seconds);
if (Arguments.bDensitySlice)
{
WriteJsonVector(*Writer, TEXT("origin_voxels"), Arguments.DensitySliceOrigin, 1.0f);
WriteJsonVector(*Writer, TEXT("direction"), Arguments.DensitySliceDirection, 1.0f);
Writer->WriteValue(TEXT("step_voxels"), static_cast<double>(Arguments.DensitySliceStep));
}
Writer->WriteObjectEnd();
Writer->WriteObjectStart(TEXT("summary")); Writer->WriteObjectStart(TEXT("summary"));
const double SummaryMeshSeconds = Arguments.bExport const double SummaryMeshSeconds = Arguments.bExport
? Output.Export.MeshSeconds ? Output.Export.MeshSeconds
@@ -4615,6 +5004,13 @@ FString BuildExploreJson(
Writer->WriteObjectStart(TEXT("tile_ledger")); Writer->WriteObjectStart(TEXT("tile_ledger"));
Writer->WriteValue(TEXT("order"), TEXT("z_then_y_then_x")); Writer->WriteValue(TEXT("order"), TEXT("z_then_y_then_x"));
Writer->WriteValue(TEXT("tile_cells"), CHUNK_SIZE); Writer->WriteValue(TEXT("tile_cells"), CHUNK_SIZE);
int32 TileLod = 0;
for (int32 SampleStep = Arguments.ExportStep; SampleStep > 1; SampleStep >>= 1)
{
++TileLod;
}
Writer->WriteValue(TEXT("sample_step"), Arguments.ExportStep);
Writer->WriteValue(TEXT("lod"), TileLod);
Writer->WriteValue(TEXT("tile_count"), World.ExploreMeshTileCount); Writer->WriteValue(TEXT("tile_count"), World.ExploreMeshTileCount);
Writer->WriteValue(TEXT("completed_tiles"), World.ExploreMeshTilesCompleted); Writer->WriteValue(TEXT("completed_tiles"), World.ExploreMeshTilesCompleted);
Writer->WriteArrayStart(TEXT("tiles")); Writer->WriteArrayStart(TEXT("tiles"));
@@ -4629,6 +5025,8 @@ FString BuildExploreJson(
Writer->WriteValue(TEXT("x"), TileX); Writer->WriteValue(TEXT("x"), TileX);
Writer->WriteValue(TEXT("y"), TileY); Writer->WriteValue(TEXT("y"), TileY);
Writer->WriteValue(TEXT("z"), TileZ); Writer->WriteValue(TEXT("z"), TileZ);
Writer->WriteValue(TEXT("sample_step"), Arguments.ExportStep);
Writer->WriteValue(TEXT("lod"), TileLod);
Writer->WriteValue(TEXT("triangle_count"), World.ExploreTileTriangleCounts[TileIndex]); Writer->WriteValue(TEXT("triangle_count"), World.ExploreTileTriangleCounts[TileIndex]);
Writer->WriteValue(TEXT("geometry_hash"), World.ExploreTileGeometryHashes[TileIndex]); Writer->WriteValue(TEXT("geometry_hash"), World.ExploreTileGeometryHashes[TileIndex]);
Writer->WriteObjectEnd(); Writer->WriteObjectEnd();
@@ -5750,7 +6148,26 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
return 1; return 1;
} }
FExploreRunOutput Output;
bool bRequestedModeFailed = false; bool bRequestedModeFailed = false;
if (Arguments.bDensitySlice)
{
const double Start = FPlatformTime::Seconds();
const bool bDensitySliceOk = !Budget.bTruncated
&& RunDensitySlice(Arguments, World, Output.DensitySlice, Budget);
if (!bDensitySliceOk && !Budget.bTruncated)
{
bRequestedModeFailed = true;
UE_LOG(LogTemp, Error,
TEXT("[VoxelForgeExplore] density slice failed: %s"),
*Output.DensitySlice.RefusalReason);
}
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeExplore] density slice %.3fs (%s)"),
FPlatformTime::Seconds() - Start,
bDensitySliceOk ? TEXT("ok")
: (Budget.bTruncated ? TEXT("truncated") : TEXT("error")));
}
if (Arguments.bOpBounds) if (Arguments.bOpBounds)
{ {
const double Start = FPlatformTime::Seconds(); const double Start = FPlatformTime::Seconds();
@@ -5837,7 +6254,6 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
} }
} }
FExploreRunOutput Output;
if (Budget.ShouldStop(TEXT("setup"))) if (Budget.ShouldStop(TEXT("setup")))
{ {
UE_LOG(LogTemp, Warning, UE_LOG(LogTemp, Warning,