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:
1 parent
bf9384a1d5
commit
0231072cc4
8 files changed
+1102
-212
No files matched your search
@@ -102,6 +102,30 @@ namespace
|
||||
// inside the chamber and recreates the broad raised shelf this hand-off is meant to remove.
|
||||
constexpr float VF_TunnelMouthBlendRadiusVoxels =
|
||||
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
|
||||
{
|
||||
@@ -372,8 +396,8 @@ namespace
|
||||
- VoxelPassageGeometry::TunnelFloorZ(A, FMath::Abs(RadiusA)));
|
||||
const float BaseGradient = FloorDelta / HorizontalRun;
|
||||
const float GentleThreshold = VoxelMath::IsFinite(
|
||||
Tunnel.TunnelFloorGentleSlopeThreshold)
|
||||
? FMath::Max(Tunnel.TunnelFloorGentleSlopeThreshold, 0.0f)
|
||||
Tunnel.TunnelFloorGentleSlopeGradient)
|
||||
? FMath::Max(Tunnel.TunnelFloorGentleSlopeGradient, 0.0f)
|
||||
: VoxelPassageGeometry::PlayerWalkableFloorMaxGradient;
|
||||
const float AvailableGradient = GentleThreshold - BaseGradient;
|
||||
if (!(AvailableGradient > 0.0f))
|
||||
@@ -434,8 +458,8 @@ namespace
|
||||
}
|
||||
|
||||
const float GentleThreshold = VoxelMath::IsFinite(
|
||||
Tunnel.TunnelFloorGentleSlopeThreshold)
|
||||
? FMath::Max(Tunnel.TunnelFloorGentleSlopeThreshold, 0.0f)
|
||||
Tunnel.TunnelFloorGentleSlopeGradient)
|
||||
? FMath::Max(Tunnel.TunnelFloorGentleSlopeGradient, 0.0f)
|
||||
: VoxelPassageGeometry::PlayerWalkableFloorMaxGradient;
|
||||
const float AvailableGradient = GentleThreshold - BaseGradient;
|
||||
if (!(AvailableGradient > 0.0f))
|
||||
@@ -873,6 +897,146 @@ namespace
|
||||
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(
|
||||
const FVector& Position,
|
||||
const TArray<FVector>& ControlPoints,
|
||||
@@ -880,6 +1044,7 @@ namespace
|
||||
const FCachedTunnel& Tunnel,
|
||||
float SDFBlendRadius,
|
||||
bool bApplyFloorCut,
|
||||
bool bUseContinuousCurve,
|
||||
const void* CacheIdentity, int32 TunnelIndex,
|
||||
const FVFFloorReliefColumnKey& ReliefColumn,
|
||||
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles,
|
||||
@@ -910,10 +1075,15 @@ namespace
|
||||
const FVector& B = ControlPointData[SegmentIndex + 1];
|
||||
const float RadiusA = ControlRadiusData[SegmentIndex];
|
||||
const float RadiusB = ControlRadiusData[SegmentIndex + 1];
|
||||
|
||||
float T = 0.0f;
|
||||
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);
|
||||
if (!bHasHorizontalProjection)
|
||||
{
|
||||
@@ -925,6 +1095,10 @@ namespace
|
||||
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
|
||||
? FloorProfileData + SegmentIndex : nullptr;
|
||||
const FVFTunnelFloorTerms FloorTerms = VF_TunnelFloorTerms(
|
||||
@@ -957,7 +1131,7 @@ namespace
|
||||
// excludes relief only long enough to decide whether the bounded relief can matter;
|
||||
// the final shift below includes the actual cached column value.
|
||||
// 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
|
||||
// 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.
|
||||
@@ -967,8 +1141,9 @@ namespace
|
||||
&& (Tunnel.bFloorRouteWasWound || Tunnel.bDramaticLedge);
|
||||
const bool bAnchorCapsule = bUseAuthoredProfile
|
||||
&& VoxelMath::IsFinite(NaturalFloorZ);
|
||||
const auto EvaluateAnchoredCapsule = [
|
||||
&Position, &A, &B, RadiusA, RadiusB, NaturalFloorZ, bAnchorCapsule]
|
||||
const auto EvaluateAnchoredShape = [
|
||||
&Position, &A, &B, RadiusA, RadiusB, CurvePoint, CurveRadius,
|
||||
NaturalFloorZ, bAnchorCapsule, bUseContinuousCurve]
|
||||
(float FloorZ) -> float
|
||||
{
|
||||
const float Shift = FloorZ - NaturalFloorZ;
|
||||
@@ -976,14 +1151,18 @@ namespace
|
||||
|| !VoxelMath::IsFinite(Shift)
|
||||
|| FMath::Abs(Shift) <= KINDA_SMALL_NUMBER)
|
||||
{
|
||||
return VoxelSDF::TaperedCapsule(
|
||||
return bUseContinuousCurve
|
||||
? FVector::Dist(Position, CurvePoint) - CurveRadius
|
||||
: VoxelSDF::TaperedCapsule(
|
||||
Position, A, B, RadiusA, RadiusB);
|
||||
}
|
||||
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);
|
||||
};
|
||||
float SegmentSDF = EvaluateAnchoredCapsule(FloorTerms.BaseFloorZ);
|
||||
float SegmentSDF = EvaluateAnchoredShape(FloorTerms.BaseFloorZ);
|
||||
|
||||
bool bEvaluateRelief = bApplyFloorCut;
|
||||
if (bEvaluateRelief && bHasReliefBound)
|
||||
@@ -1026,7 +1205,7 @@ namespace
|
||||
&& VoxelMath::IsFinite(FloorZ)
|
||||
&& FMath::Abs(FloorZ - FloorTerms.BaseFloorZ) > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
SegmentSDF = EvaluateAnchoredCapsule(FloorZ);
|
||||
SegmentSDF = EvaluateAnchoredShape(FloorZ);
|
||||
}
|
||||
const float SegmentResultSDF = bApplyFloorCut
|
||||
? VoxelSDF::SmoothMax(
|
||||
@@ -1070,6 +1249,7 @@ namespace
|
||||
return VF_EvaluateSweptTunnelChain(
|
||||
Position, ShapeControlPoints, ShapeControlRadii,
|
||||
Tunnel, SDFBlendRadius, bApplyFloorCut,
|
||||
bWorldChain,
|
||||
CacheIdentity, TunnelIndex, ReliefColumn,
|
||||
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
|
||||
RoomFloorOwnership);
|
||||
@@ -1090,6 +1270,7 @@ namespace
|
||||
return VF_EvaluateSweptTunnelChain(
|
||||
Position, Points, Radii,
|
||||
Tunnel, SDFBlendRadius, bApplyFloorCut,
|
||||
bWorldChain,
|
||||
CacheIdentity, TunnelIndex, ReliefColumn,
|
||||
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
|
||||
RoomFloorOwnership);
|
||||
@@ -1104,6 +1285,7 @@ namespace
|
||||
return VF_EvaluateSweptTunnelChain(
|
||||
Position, Points, Radii,
|
||||
Tunnel, SDFBlendRadius, bApplyFloorCut,
|
||||
bWorldChain,
|
||||
CacheIdentity, TunnelIndex, ReliefColumn,
|
||||
bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles,
|
||||
RoomFloorOwnership);
|
||||
@@ -2109,6 +2291,8 @@ static bool VF_ComputeRoomGraphReach(
|
||||
|| Params.TunnelFloorTerraceStepHeight < 0.0f
|
||||
|| Params.TunnelFloorMaxLedgeHeight < 0.0f
|
||||
|| Params.TunnelFloorGentleSlopeThreshold < 0.0f
|
||||
|| Params.TunnelFloorGentleSlopeThreshold
|
||||
> VoxelPassageGeometry::TunnelFloorGentleSlopeMaximumDegrees
|
||||
|| FMath::Abs(Params.FloorReliefStrength) > 1000000.0f
|
||||
|| FMath::Abs(Params.CaveWarpStrength) > 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
|
||||
// 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.
|
||||
const float GentleSlopeThreshold = FMath::Max(
|
||||
Params.TunnelFloorGentleSlopeThreshold, KINDA_SMALL_NUMBER);
|
||||
const float GentleSlopeThreshold = VoxelPassageGeometry::TunnelFloorGradientFromDegrees(
|
||||
Params.TunnelFloorGentleSlopeThreshold);
|
||||
const float MaxFloorDeltaEnvelope = 2.0f * EndpointVertical;
|
||||
const float RequiredRouteRunEnvelope = MaxFloorDeltaEnvelope
|
||||
/ GentleSlopeThreshold;
|
||||
const float RequiredRouteRunEnvelope = Params.bTunnelFloorEnabled
|
||||
? FMath::Min(
|
||||
VoxelPassageGeometry::TunnelFloorMaximumRouteLengthVoxels,
|
||||
MaxFloorDeltaEnvelope / FMath::Max(
|
||||
GentleSlopeThreshold, KINDA_SMALL_NUMBER))
|
||||
: 0.0f;
|
||||
const float OrganicLateralEnvelope = FMath::Min(
|
||||
FMath::Abs(Params.TunnelWarpStrength), TunnelLengthBound * 0.25f);
|
||||
const float RouteLateralEnvelope = FMath::Max(
|
||||
@@ -3335,6 +3523,104 @@ namespace
|
||||
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(
|
||||
const FStrateGenerationParams& Params,
|
||||
const FVFRoomLandingSite& Site,
|
||||
@@ -3821,7 +4107,6 @@ namespace
|
||||
1,
|
||||
TEXT("Memoize deterministic room player-fit points; 0 disables the memo."),
|
||||
ECVF_Default);
|
||||
|
||||
bool VF_FindPlayerFitPointForRoomMemoized(
|
||||
const FStrateGenerationParams& Params,
|
||||
const FVFRoomLandingSite& Site,
|
||||
@@ -6599,12 +6884,92 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
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)
|
||||
{
|
||||
const int32 I = GraphEdge.RoomA;
|
||||
const int32 J = GraphEdge.RoomB;
|
||||
const FBuildRoom& RoomA = BuildRooms[I];
|
||||
const FBuildRoom& RoomB = BuildRooms[J];
|
||||
FBuildRoom& RoomA = BuildRooms[I];
|
||||
FBuildRoom& RoomB = BuildRooms[J];
|
||||
|
||||
if (!PossibleTunnelTouchesSearchXY(RoomA, RoomB))
|
||||
{
|
||||
@@ -6627,62 +6992,24 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
const float RadA = FMath::Lerp(Params.TunnelMinRadius, Params.TunnelMaxRadius, FactorA);
|
||||
const float RadB = FMath::Lerp(Params.TunnelMinRadius, Params.TunnelMaxRadius, FactorB);
|
||||
|
||||
// --- ENDPOINT FLOOR ALIGNMENT ---
|
||||
// The old endpoint Z roll entered each room at an arbitrary height. When the two
|
||||
// rooms had different floor cuts, the tube then met one floor several metres above or
|
||||
// below the other and the player-fit graph saw a vertical severance. Anchor each tube
|
||||
// tangent to the two rooms' deterministic floor planes instead. Interior control
|
||||
// points still receive bounded vertical wander, so this removes the lip without
|
||||
// turning the whole network into a level grid.
|
||||
const float RoomFloorA = RoomFloorZFor(RoomA);
|
||||
const float RoomFloorB = RoomFloorZFor(RoomB);
|
||||
const float SafeFloorA = VoxelMath::IsFinite(RoomFloorA)
|
||||
? RoomFloorA : RoomA.Center.Z - RoomA.RadiusZ;
|
||||
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)
|
||||
// --- ENDPOINT ROOM-WALL TRIM + FLOOR ALIGNMENT ---
|
||||
// Resolve each endpoint against the actual hashed room boundary along the edge. The
|
||||
// old endpoint was the room centre/player-fit point, so its capsule necessarily ran
|
||||
// through the chamber. The trim leaves half a tunnel radius plus the SDF blend inside
|
||||
// the wall, which preserves a walkable overlap without projecting a floor slab into
|
||||
// the room.
|
||||
FVector TunnelDirection(
|
||||
RoomB.Center.X - RoomA.Center.X,
|
||||
RoomB.Center.Y - RoomA.Center.Y,
|
||||
0.0f);
|
||||
if (!TunnelDirection.Normalize())
|
||||
{
|
||||
const bool bHasPlayerFit = ResolvePlayerFitPoint(
|
||||
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);
|
||||
}
|
||||
TunnelDirection = FVector::ForwardVector;
|
||||
}
|
||||
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 ---
|
||||
FCachedTunnel CT;
|
||||
@@ -6700,7 +7027,9 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
CT.bTunnelFloorTerracingEnabled = Params.bTunnelFloorTerracingEnabled;
|
||||
CT.TunnelFloorTerraceStepHeight = Params.TunnelFloorTerraceStepHeight;
|
||||
CT.TunnelFloorMaxLedgeHeight = Params.TunnelFloorMaxLedgeHeight;
|
||||
CT.TunnelFloorGentleSlopeThreshold = Params.TunnelFloorGentleSlopeThreshold;
|
||||
CT.TunnelFloorGentleSlopeGradient =
|
||||
VoxelPassageGeometry::TunnelFloorGradientFromDegrees(
|
||||
Params.TunnelFloorGentleSlopeThreshold);
|
||||
CT.TunnelFloorLedgeCountPreference = Params.TunnelFloorLedgeCountPreference;
|
||||
CT.TunnelFloorMaxLedges = Params.TunnelFloorMaxLedges;
|
||||
|
||||
@@ -6724,10 +7053,8 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
const float DirectHorizontalRun = FVector2D(
|
||||
static_cast<float>(WorldEndB.X - WorldEndA.X),
|
||||
static_cast<float>(WorldEndB.Y - WorldEndA.Y)).Size();
|
||||
const float GentleThreshold = VoxelMath::IsFinite(
|
||||
Params.TunnelFloorGentleSlopeThreshold)
|
||||
? FMath::Max(Params.TunnelFloorGentleSlopeThreshold, 0.0f)
|
||||
: VoxelPassageGeometry::PlayerWalkableFloorMaxGradient;
|
||||
const float GentleThreshold = VoxelPassageGeometry::TunnelFloorGradientFromDegrees(
|
||||
Params.TunnelFloorGentleSlopeThreshold);
|
||||
const float SafeGentleThreshold = FMath::Max(
|
||||
GentleThreshold, KINDA_SMALL_NUMBER);
|
||||
const bool bDirectSteep = VoxelMath::IsFinite(WorldFloorA)
|
||||
@@ -6784,20 +7111,27 @@ void VoxelCaveMorphology::BuildChunkCache(
|
||||
FMath::Max(BaseSegments, MinSegmentsForLedges),
|
||||
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
|
||||
&& GentleThreshold > KINDA_SMALL_NUMBER;
|
||||
CT.bFloorRouteWasWound = bNeedsWind;
|
||||
const float RequiredHorizontalRun = bNeedsWind
|
||||
? FMath::Abs(FloorDelta) / SafeGentleThreshold
|
||||
? FMath::Min(
|
||||
VoxelPassageGeometry::TunnelFloorMaximumRouteLengthVoxels,
|
||||
FMath::Abs(FloorDelta) / SafeGentleThreshold)
|
||||
: DirectHorizontalRun;
|
||||
const int32 WindWaves = bNeedsWind
|
||||
? FMath::Clamp(
|
||||
FMath::CeilToInt(RequiredHorizontalRun
|
||||
/ FMath::Max(DirectHorizontalRun, 16.0f)),
|
||||
1, 6)
|
||||
1, VoxelPassageGeometry::TunnelFloorMaximumWindWaves)
|
||||
: 0;
|
||||
|
||||
FVector HorizontalAxis(
|
||||
WorldEndB.X - WorldEndA.X, WorldEndB.Y - WorldEndA.Y, 0.0f);
|
||||
FVector PerpA;
|
||||
@@ -7947,25 +8281,42 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
|
||||
const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2
|
||||
&& Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num();
|
||||
const FVector& BoundCenter = bHasWorldChain
|
||||
? Tunnel.WorldBoundCenter : Tunnel.BoundCenter;
|
||||
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
|
||||
bool bSDFNear = true;
|
||||
if (Tunnel.SDFInfluenceRadius > 0.0f
|
||||
&& VF_DistanceSquaredToAabb(
|
||||
WorldX, WorldY, WorldZ, CenterlineMin, CenterlineMax)
|
||||
> FMath::Square(InfluenceRadius))
|
||||
WorldX, WorldY, WorldZ,
|
||||
Tunnel.SDFCenterlineMin, Tunnel.SDFCenterlineMax)
|
||||
> FMath::Square(Tunnel.SDFInfluenceRadius))
|
||||
{
|
||||
return;
|
||||
bSDFNear = false;
|
||||
}
|
||||
if (BoundRadiusSq > 0.0f
|
||||
&& VF_FloatDistSquared(WorldX, WorldY, WorldZ, BoundCenter) > BoundRadiusSq)
|
||||
if (Tunnel.BoundRadiusSq > 0.0f
|
||||
&& 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;
|
||||
}
|
||||
@@ -7979,7 +8330,11 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
|
||||
VF_FindWorldTunnelMouthOwnership(Pos, Tunnel, Cache.SDFBlendRadius);
|
||||
const bool bRoomOwnsThisTunnel = RoomMouthOwnership.bValid;
|
||||
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())
|
||||
{
|
||||
|
||||
@@ -1139,7 +1139,7 @@ namespace
|
||||
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("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("WaterLevelRelative")) return Set(0.0f, 1.0f);
|
||||
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;
|
||||
float TunnelFloorTerraceStepHeight = 1.71f;
|
||||
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 TunnelFloorMaxLedges = 4096;
|
||||
bool bDramaticLedge = false;
|
||||
|
||||
@@ -54,6 +54,47 @@ namespace VoxelPassageGeometry
|
||||
// fallback threshold at which a room-to-room floor must become a terrace.
|
||||
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.
|
||||
// A tapered tube changes that floor by the radius delta, so the construction and the audit
|
||||
// use this same conservative floor delta.
|
||||
|
||||
@@ -48,6 +48,8 @@ class VOXELFORGE_API UVoxelStrateDefinition : public UPrimaryDataAsset
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void PostLoad() override;
|
||||
|
||||
//=========================================================================
|
||||
// IDENTIFICATION
|
||||
//=========================================================================
|
||||
|
||||
@@ -709,12 +709,13 @@ struct VOXELFORGE_API FStrateGenerationParams
|
||||
meta = (ClampMin = "0.0"))
|
||||
float TunnelFloorMaxLedgeHeight = 12.0f;
|
||||
|
||||
// Floor gradient threshold for a direct route. At or below it, the floor is a smooth ramp. A
|
||||
// steeper walk-critical edge receives a longer winding route; an eligible edge may receive
|
||||
// deterministic dramatic ledges instead. The default is tan(44 degrees).
|
||||
// Floor gradient threshold in degrees for a direct route. At or below it, the floor is a
|
||||
// smooth ramp. A steeper walk-critical edge receives a longer winding route; an eligible edge
|
||||
// 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",
|
||||
meta = (ClampMin = "0.0"))
|
||||
float TunnelFloorGentleSlopeThreshold = 0.9656888f;
|
||||
meta = (ClampMin = "0.0", ClampMax = "90.0", DisplayName = "Gentle Slope Threshold (degrees)"))
|
||||
float TunnelFloorGentleSlopeThreshold = 44.0f;
|
||||
|
||||
// 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
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "VoxelGenerator.h"
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "VoxelNoise.h"
|
||||
#include "VoxelPassageGeometry.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStackSampler.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
@@ -213,6 +214,49 @@ bool ParseRenderCamera(const FString& Text, FVector& OutCamera)
|
||||
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)
|
||||
{
|
||||
// This is the same topology policy as VoxelForgePlayerFitWindow.h. That header is test-only,
|
||||
@@ -261,8 +305,11 @@ float BoundarySealForArchetype(
|
||||
struct FExploreArguments
|
||||
{
|
||||
int32 Seed = 0;
|
||||
bool bSeedOverride = false;
|
||||
ECaveGeneratorType Archetype = ECaveGeneratorType::Maze;
|
||||
int32 Slot = 4;
|
||||
FString StrateReference;
|
||||
FString SettingsReference;
|
||||
bool bRender = true;
|
||||
bool bWalk = true;
|
||||
bool bExport = true;
|
||||
@@ -296,6 +343,8 @@ struct FExploreArguments
|
||||
float BudgetMinutes = DefaultBudgetMinutes;
|
||||
bool bSurfaceRoughnessOverride = false;
|
||||
float SurfaceRoughness = 0.0f;
|
||||
bool bTunnelWarpStrengthOverride = false;
|
||||
float TunnelWarpStrength = 0.0f;
|
||||
bool bRoughnessNoiseTypeOverride = false;
|
||||
EVoxelNoiseType RoughnessNoiseType = EVoxelNoiseType::FBM;
|
||||
bool bRoughnessFrequencyOverride = false;
|
||||
@@ -322,6 +371,13 @@ struct FExploreArguments
|
||||
int32 PassageFloorWrites = 1;
|
||||
bool bRenderCameraOverride = false;
|
||||
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
|
||||
{
|
||||
@@ -349,9 +405,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
FString ModesText = TEXT("render,walk,export");
|
||||
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("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 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
|
||||
@@ -427,6 +485,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
const bool bSurfaceRoughnessParsed = FParse::Value(
|
||||
*Params, TEXT("surfaceroughness="), OutArguments.SurfaceRoughness);
|
||||
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;
|
||||
const bool bRoughnessNoiseTypeSpecified = Params.Contains(
|
||||
TEXT("roughnesstype="), ESearchCase::IgnoreCase);
|
||||
@@ -510,6 +573,53 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
}
|
||||
}
|
||||
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;
|
||||
FParse::Value(*Params, TEXT("failurefocus="), FailureFocusRender);
|
||||
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.");
|
||||
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)
|
||||
{
|
||||
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
|
||||
&& (!bTunnelFloorGentleParsed
|
||||
|| !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;
|
||||
}
|
||||
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.");
|
||||
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())
|
||||
{
|
||||
@@ -946,15 +1077,58 @@ struct FExploreWorld
|
||||
|
||||
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)
|
||||
{
|
||||
OutError = TEXT("The synthetic layout would exceed the commandlet strate cap.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Settings = TStrongObjectPtr<UVoxelSettings>(
|
||||
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
if (!bUseAuthoredSettings)
|
||||
{
|
||||
Settings->Seed = Arguments.Seed;
|
||||
Settings->TotalStrates = NumStrates;
|
||||
Settings->InterStrateGapChunks = 0;
|
||||
@@ -962,12 +1136,39 @@ struct FExploreWorld
|
||||
Settings->EdgeSealThickness = 64.0f;
|
||||
Settings->OriginSpineRadius = 14.0f;
|
||||
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);
|
||||
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);
|
||||
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(
|
||||
TEXT("VoxelForgeExplore_%s_slot%d"),
|
||||
ArchetypeName(Index == Arguments.Slot
|
||||
@@ -997,13 +1198,121 @@ struct FExploreWorld
|
||||
{
|
||||
Definition->GenerationParams.WormStrength = Arguments.WormStrength;
|
||||
}
|
||||
}
|
||||
|
||||
const TSoftObjectPtr<UVoxelStrateDefinition> SoftDefinition(Definition);
|
||||
Settings->FixedStrates.Add(Index, SoftDefinition);
|
||||
if (!bUseAuthoredStrate)
|
||||
{
|
||||
Settings->StratePool.Add(SoftDefinition);
|
||||
}
|
||||
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>(
|
||||
NewObject<UVoxelStrateManager>(GetTransientPackage(), NAME_None, RF_Transient));
|
||||
if (!Manager->Initialize(Settings.Get(), Settings->Seed))
|
||||
@@ -1028,104 +1337,40 @@ struct FExploreWorld
|
||||
OutError = TEXT("The requested target slot has no resolved definition.");
|
||||
return false;
|
||||
}
|
||||
if (Arguments.bSurfaceRoughnessOverride)
|
||||
{
|
||||
// This is deliberately applied after layout construction and only to the target slot's
|
||||
// resolved definition. Passage placement and every neighbouring strate retain their
|
||||
// authored/default values; only the sampled target field gets the experiment knob.
|
||||
switch (Arguments.Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
Target.Definition->SlabParams.FloorRoughness = Arguments.SurfaceRoughness;
|
||||
Target.Definition->SlabParams.CeilingRoughness = Arguments.SurfaceRoughness;
|
||||
break;
|
||||
case ECaveGeneratorType::Maze:
|
||||
Target.Definition->MazeParams.SurfaceRoughness = Arguments.SurfaceRoughness;
|
||||
break;
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
Target.Definition->SurfaceParams.SurfaceRoughness = Arguments.SurfaceRoughness;
|
||||
break;
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
Target.Definition->VerticalShaftParams.SurfaceRoughness = Arguments.SurfaceRoughness;
|
||||
break;
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
Target.Definition->FloatingIslandParams.SurfaceRoughness = Arguments.SurfaceRoughness;
|
||||
break;
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
default:
|
||||
Target.Definition->GenerationParams.SurfaceRoughness = Arguments.SurfaceRoughness;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
const ECaveGeneratorType TargetArchetype = Target.Definition->GeneratorType;
|
||||
const FStrateGenerationParams& ReportParams = Target.Definition->GenerationParams;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeExplore][OwnerAssets] strate=%s settings=%s slot=%d "
|
||||
"seed=%d height_chunks=%d archetype=%s opstack=%d "
|
||||
"floor_enabled=%d floor_terracing=%d floor_step=%.9g max_ledge=%.9g "
|
||||
"gentle_slope_degrees=%.9g gentle_gradient=%.9g route_cap=%.9g "
|
||||
"wind_wave_cap=%d "
|
||||
"ledge_preference=%d max_ledges=%d tunnel_warp=%.9g terrain_ops=%d terrace_step=%.9g "
|
||||
"layer_spacing=%.9g rib_spacing=%.9g"),
|
||||
bUseAuthoredStrate ? *Arguments.StrateReference : TEXT("synthetic"),
|
||||
bUseAuthoredSettings ? *Arguments.SettingsReference : TEXT("synthetic"),
|
||||
Arguments.Slot, Settings->Seed, Target.Definition->StrateHeightInChunks,
|
||||
ArchetypeName(TargetArchetype), Target.Definition->bUseOperatorStack ? 1 : 0,
|
||||
ReportParams.bTunnelFloorEnabled ? 1 : 0,
|
||||
ReportParams.bTunnelFloorTerracingEnabled ? 1 : 0,
|
||||
ReportParams.TunnelFloorTerraceStepHeight,
|
||||
ReportParams.TunnelFloorMaxLedgeHeight,
|
||||
ReportParams.TunnelFloorGentleSlopeThreshold,
|
||||
VoxelPassageGeometry::TunnelFloorGradientFromDegrees(
|
||||
ReportParams.TunnelFloorGentleSlopeThreshold),
|
||||
VoxelPassageGeometry::TunnelFloorMaximumRouteLengthVoxels,
|
||||
VoxelPassageGeometry::TunnelFloorMaximumWindWaves,
|
||||
ReportParams.TunnelFloorLedgeCountPreference,
|
||||
ReportParams.TunnelFloorMaxLedges,
|
||||
ReportParams.TunnelWarpStrength,
|
||||
Target.Definition->TerrainOperations.Num(),
|
||||
ReportParams.TerraceStepHeight,
|
||||
ReportParams.LayerLineSpacing,
|
||||
ReportParams.RibbingSpacing);
|
||||
TargetBottomWorldZ = Target.BottomChunkZ * CHUNK_SIZE;
|
||||
TargetTopWorldZ = (Target.TopChunkZ + 1) * CHUNK_SIZE;
|
||||
TargetBoundarySealThickness = BoundarySealForArchetype(
|
||||
*Target.Definition, Arguments.Archetype);
|
||||
*Target.Definition, TargetArchetype);
|
||||
if (TargetTopWorldZ <= TargetBottomWorldZ)
|
||||
{
|
||||
OutError = TEXT("The target strate has no positive voxel-space height.");
|
||||
@@ -1315,6 +1560,22 @@ struct FExploreExportOutput
|
||||
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
|
||||
{
|
||||
bool bAvailable = false;
|
||||
@@ -1335,6 +1596,7 @@ struct FExploreRunOutput
|
||||
FExploreRenderOutput Render;
|
||||
FExploreWalkOutput Walk;
|
||||
FExploreExportOutput Export;
|
||||
FExploreDensitySliceOutput DensitySlice;
|
||||
double BudgetSeconds = 0.0;
|
||||
double ElapsedSeconds = 0.0;
|
||||
double WalkSeconds = 0.0;
|
||||
@@ -2776,7 +3038,12 @@ bool RunRender(
|
||||
OutOutput.bAllCamerasPlayerFit = true;
|
||||
FVector RouteFocus = FVector::ZeroVector;
|
||||
int32 RouteFocusCount = 0;
|
||||
if (CameraSeedWalk.bHasArrival)
|
||||
if (Arguments.bRenderTargetOverride)
|
||||
{
|
||||
RouteFocus = Arguments.RenderTarget;
|
||||
RouteFocusCount = 1;
|
||||
}
|
||||
else if (CameraSeedWalk.bHasArrival)
|
||||
{
|
||||
RouteFocus += CameraSeedWalk.ArrivalVoxels;
|
||||
++RouteFocusCount;
|
||||
@@ -4136,6 +4403,7 @@ void WriteTunnelFloorOverrides(
|
||||
{
|
||||
Writer.WriteValue(TEXT("gentle_slope"),
|
||||
static_cast<double>(Arguments.TunnelFloorGentle));
|
||||
Writer.WriteValue(TEXT("gentle_slope_units"), TEXT("degrees"));
|
||||
}
|
||||
Writer.WriteValue(TEXT("ledge_count_override"), Arguments.bTunnelFloorLedgesOverride);
|
||||
if (Arguments.bTunnelFloorLedgesOverride)
|
||||
@@ -4434,6 +4702,100 @@ bool RunExport(
|
||||
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)
|
||||
{
|
||||
Writer.WriteObjectStart(Key);
|
||||
@@ -4459,7 +4821,7 @@ FString BuildExploreJson(
|
||||
TSharedRef<FExploreJsonWriter> Writer =
|
||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Json);
|
||||
Writer->WriteObjectStart();
|
||||
Writer->WriteValue(TEXT("schema_version"), 6);
|
||||
Writer->WriteValue(TEXT("schema_version"), 7);
|
||||
Writer->WriteValue(TEXT("tool"), TEXT("VoxelForgeExplore"));
|
||||
Writer->WriteValue(TEXT("read_only_generation"), true);
|
||||
|
||||
@@ -4534,6 +4896,12 @@ FString BuildExploreJson(
|
||||
Writer->WriteValue(TEXT("sample_stacks_interval_us"),
|
||||
static_cast<double>(Arguments.SampleStacksIntervalUs));
|
||||
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("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes));
|
||||
Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory);
|
||||
@@ -4549,6 +4917,27 @@ FString BuildExploreJson(
|
||||
Arguments.bBlockEarlyOut ? 1 : 0));
|
||||
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"));
|
||||
const double SummaryMeshSeconds = Arguments.bExport
|
||||
? Output.Export.MeshSeconds
|
||||
@@ -4615,6 +5004,13 @@ FString BuildExploreJson(
|
||||
Writer->WriteObjectStart(TEXT("tile_ledger"));
|
||||
Writer->WriteValue(TEXT("order"), TEXT("z_then_y_then_x"));
|
||||
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("completed_tiles"), World.ExploreMeshTilesCompleted);
|
||||
Writer->WriteArrayStart(TEXT("tiles"));
|
||||
@@ -4629,6 +5025,8 @@ FString BuildExploreJson(
|
||||
Writer->WriteValue(TEXT("x"), TileX);
|
||||
Writer->WriteValue(TEXT("y"), TileY);
|
||||
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("geometry_hash"), World.ExploreTileGeometryHashes[TileIndex]);
|
||||
Writer->WriteObjectEnd();
|
||||
@@ -5750,7 +6148,26 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
||||
return 1;
|
||||
}
|
||||
|
||||
FExploreRunOutput Output;
|
||||
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)
|
||||
{
|
||||
const double Start = FPlatformTime::Seconds();
|
||||
@@ -5837,7 +6254,6 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
||||
}
|
||||
}
|
||||
|
||||
FExploreRunOutput Output;
|
||||
if (Budget.ShouldStop(TEXT("setup")))
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
|
||||
Reference in new issue
Block a user