feat(core): cavern graph v1, one continuous floor-capped tunnel into the landmark chamber (L46); R13
The flagged SW demo's ramp (57 carves + 107 support pillars, visibly ribbed) becomes one cubic-Bezier sweep: closest point by 17-point seed plus 5 Newton steps, a cubic floor profile, a rounded 2.6 m bore, and a hard floor cap applied after unions and relief that only adds support where the field is air. Optional resolved cavern-graph records (one room, one edge) enter canonical identity only when present; their bounds extend World::isTileShapeFree. Reachability passes (L43 control fails), headroom 2.8 m, floor ripple 1.8 mm (L45 ramp control fails at 17.9 mm), shape limits met; demo off: owner hashes identical to L45. fast/medium/full PASS. Research R13 (read-only): continuous SDF sweeps along curves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
f13b145f23
commit
e016f6a20f
14 files changed
+1753
-48
No files matched your search
@@ -707,6 +707,7 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::uint32_t ResolvedLayoutSchemaVersionV3 = 3;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||
|
||||
// Stable, bounded geometry edits composed into every Core field after the shared
|
||||
@@ -750,6 +751,42 @@ namespace VoxelForgeCore
|
||||
std::uint8_t yawQuarterTurns = 0;
|
||||
};
|
||||
|
||||
// Optional room and swept-edge records for a resolved cavern graph. Geometry is stored
|
||||
// in integer micrometres; bounds include smooth blending, relief, and floor support.
|
||||
struct ResolvedCavernRoomV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
std::array<std::int64_t, 3> centerMicrometres{};
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
std::int64_t floorMicrometres = 0;
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::uint64_t irregularitySeed = 0;
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
};
|
||||
|
||||
struct ResolvedCavernEdgeV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::string startAnchorStableId;
|
||||
std::string endRoomStableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
// A single cubic Bezier in XY. Its four Z values are the cubic floor profile.
|
||||
std::array<std::array<std::int64_t, 3>, 4> controlPointsMicrometres{};
|
||||
std::int64_t boreRadiusMicrometres = 0;
|
||||
std::int64_t boreHeightMicrometres = 0;
|
||||
std::int64_t floorSupportDepthMicrometres = 0;
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::uint64_t irregularitySeed = 0;
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
};
|
||||
|
||||
struct ResolvedLayoutAnchorV2
|
||||
{
|
||||
std::string kind;
|
||||
@@ -918,6 +955,10 @@ namespace VoxelForgeCore
|
||||
// Kept absent from canonical layout identity when no feature requests stamps.
|
||||
std::uint32_t stampVocabularyVersion = 0;
|
||||
std::vector<ResolvedLayoutStampV1> stamps;
|
||||
// Kept absent from canonical identity when there are no graph records.
|
||||
std::uint32_t cavernGraphVocabularyVersion = 0;
|
||||
std::vector<ResolvedCavernRoomV1> cavernRooms;
|
||||
std::vector<ResolvedCavernEdgeV1> cavernEdges;
|
||||
std::string stableIdentity;
|
||||
|
||||
bool finalize(std::string& error);
|
||||
@@ -970,6 +1011,12 @@ namespace VoxelForgeCore
|
||||
VFCORE_API ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout);
|
||||
|
||||
VFCORE_API bool ResolveResolvedCavernGraphV1(
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::span<const ResolvedCavernRoomV1> rooms,
|
||||
std::span<const ResolvedCavernEdgeV1> edges,
|
||||
std::string& error);
|
||||
|
||||
struct FloatingIslandBoundsAuditResult
|
||||
{
|
||||
std::uint64_t boxesTested = 0;
|
||||
|
||||
@@ -404,6 +404,56 @@ namespace
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
if(!s.cavernRooms.empty()||!s.cavernEdges.empty())
|
||||
{
|
||||
out<<",\"cavern_graph_vocabulary_version\":"
|
||||
<<s.cavernGraphVocabularyVersion<<",\"cavern_rooms\":[";
|
||||
for(std::size_t i=0;i<s.cavernRooms.size();++i)
|
||||
{
|
||||
if(i)out<<',';const auto& room=s.cavernRooms[i];
|
||||
out<<"{\"stable_id\":"<<LayoutEscape(room.stableId)
|
||||
<<",\"strate_index\":"<<room.strateIndex
|
||||
<<",\"center_um\":["<<room.centerMicrometres[0]<<','
|
||||
<<room.centerMicrometres[1]<<','<<room.centerMicrometres[2]
|
||||
<<"],\"half_extents_um\":["<<room.halfExtentsMicrometres[0]<<','
|
||||
<<room.halfExtentsMicrometres[1]<<','<<room.halfExtentsMicrometres[2]
|
||||
<<"],\"floor_um\":"<<room.floorMicrometres
|
||||
<<",\"smooth_width_um\":"<<room.smoothWidthMicrometres
|
||||
<<",\"irregularity_amplitude_um\":"<<room.irregularityAmplitudeMicrometres
|
||||
<<",\"irregularity_wavelength_um\":"<<room.irregularityWavelengthMicrometres
|
||||
<<",\"irregularity_seed\":"<<room.irregularitySeed
|
||||
<<",\"bounds_min_um\":["<<room.boundsMinimumMicrometres[0]<<','
|
||||
<<room.boundsMinimumMicrometres[1]<<','<<room.boundsMinimumMicrometres[2]
|
||||
<<"],\"bounds_max_um\":["<<room.boundsMaximumMicrometres[0]<<','
|
||||
<<room.boundsMaximumMicrometres[1]<<','<<room.boundsMaximumMicrometres[2]<<"]}";
|
||||
}
|
||||
out<<"],\"cavern_edges\":[";
|
||||
for(std::size_t i=0;i<s.cavernEdges.size();++i)
|
||||
{
|
||||
if(i)out<<',';const auto& edge=s.cavernEdges[i];
|
||||
out<<"{\"stable_id\":"<<LayoutEscape(edge.stableId)
|
||||
<<",\"start_anchor_stable_id\":"<<LayoutEscape(edge.startAnchorStableId)
|
||||
<<",\"end_room_stable_id\":"<<LayoutEscape(edge.endRoomStableId)
|
||||
<<",\"strate_index\":"<<edge.strateIndex<<",\"control_points_um\":[";
|
||||
for(std::size_t point=0;point<edge.controlPointsMicrometres.size();++point)
|
||||
{
|
||||
if(point)out<<',';const auto& p=edge.controlPointsMicrometres[point];
|
||||
out<<'['<<p[0]<<','<<p[1]<<','<<p[2]<<']';
|
||||
}
|
||||
out<<"],\"bore_radius_um\":"<<edge.boreRadiusMicrometres
|
||||
<<",\"bore_height_um\":"<<edge.boreHeightMicrometres
|
||||
<<",\"floor_support_depth_um\":"<<edge.floorSupportDepthMicrometres
|
||||
<<",\"smooth_width_um\":"<<edge.smoothWidthMicrometres
|
||||
<<",\"irregularity_amplitude_um\":"<<edge.irregularityAmplitudeMicrometres
|
||||
<<",\"irregularity_wavelength_um\":"<<edge.irregularityWavelengthMicrometres
|
||||
<<",\"irregularity_seed\":"<<edge.irregularitySeed
|
||||
<<",\"bounds_min_um\":["<<edge.boundsMinimumMicrometres[0]<<','
|
||||
<<edge.boundsMinimumMicrometres[1]<<','<<edge.boundsMinimumMicrometres[2]
|
||||
<<"],\"bounds_max_um\":["<<edge.boundsMaximumMicrometres[0]<<','
|
||||
<<edge.boundsMaximumMicrometres[1]<<','<<edge.boundsMaximumMicrometres[2]<<"]}";
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
out<<'}';return out.str();
|
||||
}
|
||||
std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key)
|
||||
@@ -566,6 +616,10 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
||||
std::stable_sort(strates.begin(),strates.end(),[](const auto& a,const auto& b)
|
||||
{return a.interiorTopMicrometres!=b.interiorTopMicrometres
|
||||
?a.interiorTopMicrometres>b.interiorTopMicrometres:a.order<b.order;});
|
||||
std::sort(cavernRooms.begin(),cavernRooms.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
std::sort(cavernEdges.begin(),cavernEdges.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
for(std::size_t i=0;i<strates.size();++i)
|
||||
{
|
||||
auto& x=strates[i];x.order=static_cast<std::int32_t>(i);
|
||||
@@ -870,6 +924,110 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
if(!LayoutStampSafe(*this,stamp,&safetyReason))
|
||||
add("stamp_placement",stamp.stableId+" is unsafe: "+safetyReason);
|
||||
}
|
||||
if(cavernRooms.empty()&&cavernEdges.empty())
|
||||
{
|
||||
if(cavernGraphVocabularyVersion!=0)
|
||||
add("cavern_graph_vocabulary_version",
|
||||
"cavern graph vocabulary version is set without graph records");
|
||||
}
|
||||
else if(cavernGraphVocabularyVersion!=ResolvedLayoutCavernGraphVocabularyVersionV1)
|
||||
add("cavern_graph_vocabulary_version",
|
||||
"cavern graph records require vocabulary version 1");
|
||||
if(cavernGraphVocabularyVersion!=0)
|
||||
{
|
||||
if(cavernRooms.empty()||cavernEdges.empty())
|
||||
add("cavern_graph_shape","a resolved cavern graph needs at least one room and one edge");
|
||||
std::vector<std::string> graphIds;
|
||||
const auto graphIdValid=[&](const std::string& id)
|
||||
{
|
||||
if(id.empty()||std::find(graphIds.begin(),graphIds.end(),id)!=graphIds.end())
|
||||
return false;
|
||||
graphIds.push_back(id);return true;
|
||||
};
|
||||
for(const auto& room:cavernRooms)
|
||||
{
|
||||
if(!graphIdValid(room.stableId))
|
||||
add("cavern_graph_identity","room has an empty or duplicate stable identity");
|
||||
if(!findStratum(room.strateIndex))
|
||||
add("cavern_graph_strate",room.stableId+" names an unknown strate");
|
||||
bool validExtents=true;
|
||||
for(const auto extent:room.halfExtentsMicrometres)
|
||||
validExtents=validExtents&&extent>=500000&&extent<=100000000;
|
||||
if(!validExtents||room.smoothWidthMicrometres<0
|
||||
||room.smoothWidthMicrometres>10000000
|
||||
||room.irregularityAmplitudeMicrometres<0
|
||||
||room.irregularityAmplitudeMicrometres>10000000
|
||||
||room.irregularityWavelengthMicrometres<0
|
||||
||room.irregularityWavelengthMicrometres>100000000
|
||||
||(room.irregularityAmplitudeMicrometres>0
|
||||
&&room.irregularityWavelengthMicrometres<=0))
|
||||
add("cavern_room_parameters",room.stableId+" has invalid or unbounded parameters");
|
||||
const std::int64_t pad=room.smoothWidthMicrometres
|
||||
+room.irregularityAmplitudeMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(room.boundsMinimumMicrometres[axis]
|
||||
!=room.centerMicrometres[axis]-room.halfExtentsMicrometres[axis]-pad
|
||||
||room.boundsMaximumMicrometres[axis]
|
||||
!=room.centerMicrometres[axis]+room.halfExtentsMicrometres[axis]+pad)
|
||||
add("cavern_room_bounds",room.stableId+" conservative bounds do not match its parameters");
|
||||
if(room.floorMicrometres<room.centerMicrometres[2]-room.halfExtentsMicrometres[2]
|
||||
||room.floorMicrometres>room.centerMicrometres[2]+room.halfExtentsMicrometres[2])
|
||||
add("cavern_room_floor",room.stableId+" floor lies outside its room envelope");
|
||||
}
|
||||
for(const auto& edge:cavernEdges)
|
||||
{
|
||||
if(!graphIdValid(edge.stableId))
|
||||
add("cavern_graph_identity","edge has an empty or duplicate stable identity");
|
||||
if(edge.startAnchorStableId.empty()||edge.endRoomStableId.empty())
|
||||
add("cavern_edge_binding",edge.stableId+" has a missing endpoint identity");
|
||||
const auto room=std::find_if(cavernRooms.begin(),cavernRooms.end(),
|
||||
[&](const auto& value){return value.stableId==edge.endRoomStableId;});
|
||||
if(room==cavernRooms.end()||room->strateIndex!=edge.strateIndex)
|
||||
add("cavern_edge_binding",edge.stableId+" does not terminate at a room in its strate");
|
||||
if(!findStratum(edge.strateIndex))
|
||||
add("cavern_graph_strate",edge.stableId+" names an unknown strate");
|
||||
if(edge.boreRadiusMicrometres<500000||edge.boreRadiusMicrometres>10000000
|
||||
||edge.boreHeightMicrometres<1000000||edge.boreHeightMicrometres>20000000
|
||||
||edge.floorSupportDepthMicrometres<0
|
||||
||edge.floorSupportDepthMicrometres>2000000
|
||||
||edge.smoothWidthMicrometres<0||edge.smoothWidthMicrometres>10000000
|
||||
||edge.irregularityAmplitudeMicrometres<0
|
||||
||edge.irregularityAmplitudeMicrometres>10000000
|
||||
||edge.irregularityWavelengthMicrometres<0
|
||||
||edge.irregularityWavelengthMicrometres>100000000
|
||||
||(edge.irregularityAmplitudeMicrometres>0
|
||||
&&edge.irregularityWavelengthMicrometres<=0))
|
||||
add("cavern_edge_parameters",edge.stableId+" has invalid or unbounded parameters");
|
||||
std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0];
|
||||
std::array<std::int64_t,3> high=low;
|
||||
for(const auto& point:edge.controlPointsMicrometres)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);}
|
||||
const std::int64_t xyPad=edge.boreRadiusMicrometres
|
||||
+edge.smoothWidthMicrometres+edge.irregularityAmplitudeMicrometres;
|
||||
const std::int64_t zPad=edge.smoothWidthMicrometres
|
||||
+edge.irregularityAmplitudeMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const std::int64_t pad=axis==2?zPad:xyPad;
|
||||
const std::int64_t lowSupport=axis==2?edge.floorSupportDepthMicrometres:0;
|
||||
const std::int64_t highBore=axis==2?edge.boreHeightMicrometres:0;
|
||||
if(edge.boundsMinimumMicrometres[axis]!=low[axis]-pad-lowSupport
|
||||
||edge.boundsMaximumMicrometres[axis]!=high[axis]+pad+highBore)
|
||||
add("cavern_edge_bounds",edge.stableId+" conservative bounds do not match its parameters");
|
||||
}
|
||||
if(room!=cavernRooms.end())
|
||||
{
|
||||
const auto& endpoint=edge.controlPointsMicrometres.back();
|
||||
if(endpoint[0]<room->centerMicrometres[0]-room->halfExtentsMicrometres[0]
|
||||
||endpoint[0]>room->centerMicrometres[0]+room->halfExtentsMicrometres[0]
|
||||
||endpoint[1]<room->centerMicrometres[1]-room->halfExtentsMicrometres[1]
|
||||
||endpoint[1]>room->centerMicrometres[1]+room->halfExtentsMicrometres[1]
|
||||
||std::abs(endpoint[2]-room->floorMicrometres)>500000)
|
||||
add("cavern_edge_portal",edge.stableId+" endpoint does not meet its room floor portal");
|
||||
}
|
||||
}
|
||||
}
|
||||
if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;}
|
||||
return true;
|
||||
}
|
||||
@@ -886,6 +1044,8 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
s.strateLayoutChunkSizeMicrometres=static_cast<std::uint32_t>(LayoutInt(root,"strate_layout_chunk_size_um"));
|
||||
s.strateLayoutChunkWidthSubvoxels=LayoutInt(root,"strate_layout_chunk_width_subvoxels");
|
||||
s.stampVocabularyVersion=static_cast<std::uint32_t>(LayoutInt(root,"stamp_vocabulary_version"));
|
||||
s.cavernGraphVocabularyVersion=static_cast<std::uint32_t>(
|
||||
LayoutInt(root,"cavern_graph_vocabulary_version"));
|
||||
for(const auto& v:root.Get("strates").AsArray())
|
||||
{
|
||||
ResolvedLayoutStratumV2 x;x.stableId=LayoutString(v,"stable_id");
|
||||
@@ -955,6 +1115,57 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
stamp.yawQuarterTurns=static_cast<std::uint8_t>(LayoutInt(v,"yaw_quarter_turns"));
|
||||
s.stamps.push_back(std::move(stamp));
|
||||
}
|
||||
for(const auto& v:root.Get("cavern_rooms").AsArray())
|
||||
{
|
||||
ResolvedCavernRoomV1 room;room.stableId=LayoutString(v,"stable_id");
|
||||
room.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||
const auto readTriplet=[&](std::string_view key,std::array<std::int64_t,3>& target)
|
||||
{
|
||||
const auto& values=v.Get(key).AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
target[axis]=values[axis].Integer();
|
||||
};
|
||||
readTriplet("center_um",room.centerMicrometres);
|
||||
readTriplet("half_extents_um",room.halfExtentsMicrometres);
|
||||
readTriplet("bounds_min_um",room.boundsMinimumMicrometres);
|
||||
readTriplet("bounds_max_um",room.boundsMaximumMicrometres);
|
||||
room.floorMicrometres=LayoutInt(v,"floor_um");
|
||||
room.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
||||
room.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
||||
room.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
||||
room.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
||||
s.cavernRooms.push_back(std::move(room));
|
||||
}
|
||||
for(const auto& v:root.Get("cavern_edges").AsArray())
|
||||
{
|
||||
ResolvedCavernEdgeV1 edge;edge.stableId=LayoutString(v,"stable_id");
|
||||
edge.startAnchorStableId=LayoutString(v,"start_anchor_stable_id");
|
||||
edge.endRoomStableId=LayoutString(v,"end_room_stable_id");
|
||||
edge.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||
const auto& points=v.Get("control_points_um").AsArray();
|
||||
for(std::size_t i=0;i<edge.controlPointsMicrometres.size()&&i<points.size();++i)
|
||||
{
|
||||
const auto& point=points[i].AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<point.size();++axis)
|
||||
edge.controlPointsMicrometres[i][axis]=point[axis].Integer();
|
||||
}
|
||||
const auto readTriplet=[&](std::string_view key,std::array<std::int64_t,3>& target)
|
||||
{
|
||||
const auto& values=v.Get(key).AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
target[axis]=values[axis].Integer();
|
||||
};
|
||||
readTriplet("bounds_min_um",edge.boundsMinimumMicrometres);
|
||||
readTriplet("bounds_max_um",edge.boundsMaximumMicrometres);
|
||||
edge.boreRadiusMicrometres=LayoutInt(v,"bore_radius_um");
|
||||
edge.boreHeightMicrometres=LayoutInt(v,"bore_height_um");
|
||||
edge.floorSupportDepthMicrometres=LayoutInt(v,"floor_support_depth_um");
|
||||
edge.smoothWidthMicrometres=LayoutInt(v,"smooth_width_um");
|
||||
edge.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
||||
edge.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
||||
edge.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
||||
s.cavernEdges.push_back(std::move(edge));
|
||||
}
|
||||
const std::string serializedIdentity=LayoutString(root,"stable_identity");
|
||||
if(!s.finalize(error))return std::nullopt;
|
||||
if(s.stableIdentity!=serializedIdentity){error="resolved layout stable identity does not match its canonical payload";return std::nullopt;}
|
||||
@@ -1080,6 +1291,67 @@ bool ResolveResolvedLayoutStampsV2(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
|
||||
bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
||||
std::span<const ResolvedCavernRoomV1> rooms,
|
||||
std::span<const ResolvedCavernEdgeV1> edges,std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
candidate.cavernRooms.clear();candidate.cavernEdges.clear();
|
||||
if(rooms.empty()&&edges.empty())
|
||||
{
|
||||
candidate.cavernGraphVocabularyVersion=0;
|
||||
if(!candidate.finalize(error))return false;
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
candidate.cavernGraphVocabularyVersion=ResolvedLayoutCavernGraphVocabularyVersionV1;
|
||||
candidate.cavernRooms.assign(rooms.begin(),rooms.end());
|
||||
candidate.cavernEdges.assign(edges.begin(),edges.end());
|
||||
for(auto& room:candidate.cavernRooms)
|
||||
{
|
||||
// The shared JSON value type stores numbers as IEEE-754 doubles. Keep the
|
||||
// serialized seed in the exact integer range, as the stamp vocabulary does.
|
||||
room.irregularitySeed=LayoutStampSeed(seed,room.strateIndex,room.stableId)
|
||||
&0x001fffffffffffffull;
|
||||
const std::int64_t pad=room.smoothWidthMicrometres
|
||||
+room.irregularityAmplitudeMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
room.boundsMinimumMicrometres[axis]=room.centerMicrometres[axis]
|
||||
-room.halfExtentsMicrometres[axis]-pad;
|
||||
room.boundsMaximumMicrometres[axis]=room.centerMicrometres[axis]
|
||||
+room.halfExtentsMicrometres[axis]+pad;
|
||||
}
|
||||
}
|
||||
for(auto& edge:candidate.cavernEdges)
|
||||
{
|
||||
edge.irregularitySeed=LayoutStampSeed(seed,edge.strateIndex,edge.stableId)
|
||||
&0x001fffffffffffffull;
|
||||
std::array<std::int64_t,3> low=edge.controlPointsMicrometres[0];
|
||||
std::array<std::int64_t,3> high=low;
|
||||
for(const auto& point:edge.controlPointsMicrometres)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{low[axis]=std::min(low[axis],point[axis]);high[axis]=std::max(high[axis],point[axis]);}
|
||||
const std::int64_t xyPad=edge.boreRadiusMicrometres
|
||||
+edge.smoothWidthMicrometres+edge.irregularityAmplitudeMicrometres;
|
||||
const std::int64_t zPad=edge.smoothWidthMicrometres
|
||||
+edge.irregularityAmplitudeMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const std::int64_t pad=axis==2?zPad:xyPad;
|
||||
edge.boundsMinimumMicrometres[axis]=low[axis]-pad
|
||||
-(axis==2?edge.floorSupportDepthMicrometres:0);
|
||||
edge.boundsMaximumMicrometres[axis]=high[axis]+pad
|
||||
+(axis==2?edge.boreHeightMicrometres:0);
|
||||
}
|
||||
}
|
||||
if(!candidate.finalize(error))return false;
|
||||
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
|
||||
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,strictError))
|
||||
{error="strict cavern graph validation failed: "+strictError;return false;}
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
|
||||
ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout)
|
||||
{
|
||||
|
||||
@@ -169,6 +169,8 @@ public:
|
||||
featureIndices_=shape.BuildPassageFeatureTileList(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
stampIndices_=shape.BuildStampTileList(minimumSubvoxels_,maximumSubvoxels_);
|
||||
cavernFeatureIndices_=shape.BuildCavernFeatureTileList(
|
||||
minimumSubvoxels_,maximumSubvoxels_);
|
||||
passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0);
|
||||
passageColumns_=passageRoutes_;
|
||||
passageMouthSupports_=passageRoutes_;
|
||||
@@ -219,12 +221,16 @@ public:
|
||||
std::size_t featureCount() const{return featureIndices_.size();}
|
||||
bool hasStampCandidates() const{return !stampIndices_.empty();}
|
||||
const std::vector<std::uint32_t>& stampCandidates() const{return stampIndices_;}
|
||||
bool hasCavernCandidates() const{return !cavernFeatureIndices_.empty();}
|
||||
const std::vector<std::uint32_t>& cavernFeatureCandidates() const
|
||||
{return cavernFeatureIndices_;}
|
||||
|
||||
private:
|
||||
const FStandaloneShapeWorld* shape_;
|
||||
std::array<std::int64_t,3> minimumSubvoxels_,maximumSubvoxels_;
|
||||
std::vector<std::uint32_t> featureIndices_;
|
||||
std::vector<std::uint32_t> stampIndices_;
|
||||
std::vector<std::uint32_t> cavernFeatureIndices_;
|
||||
std::vector<std::uint8_t> passageRoutes_,passageColumns_,passageMouthSupports_;
|
||||
bool routeFeatureOverlap_=false,columnZOverlap_=false;
|
||||
bool mayContainResolvedBoundaryBand_=true;
|
||||
@@ -1780,9 +1786,13 @@ public:
|
||||
|
||||
float ApplyResolvedStrateComposition(float density,float X,float Y,float Z) const
|
||||
{
|
||||
const float graphDensity=ApplyResolvedCavernGraph(density,X,Y,Z);
|
||||
const float composed=ApplyResolvedStratePassageCarves(
|
||||
ApplyResolvedStrateBoundaryBand(density,X,Y,Z),X,Y,Z);
|
||||
return StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z);
|
||||
ApplyResolvedStrateBoundaryBand(graphDensity,X,Y,Z),X,Y,Z);
|
||||
const float stamped=StampRuntime.empty()?composed:ApplyResolvedSdfStamps(composed,X,Y,Z);
|
||||
// Keep the explicit graph floor cap after smooth unions, relief, passages,
|
||||
// and decorative stamps so nothing can reopen the tunnel floor below profile.
|
||||
return ApplyResolvedCavernFloorCaps(stamped,X,Y,Z);
|
||||
}
|
||||
|
||||
bool ResolvedPassageIntersectsBox(const int32 minimum[3],const int32 maximum[3]) const
|
||||
@@ -3198,6 +3208,21 @@ private:
|
||||
std::uint64_t irregularitySeed=0;
|
||||
std::uint8_t quarterTurns=0;
|
||||
};
|
||||
struct FCavernRuntimeFeature
|
||||
{
|
||||
const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr;
|
||||
const VoxelForgeCore::ResolvedCavernEdgeV1* edge=nullptr;
|
||||
std::array<std::int64_t,3> boundsMinimumMicrometres{},boundsMaximumMicrometres{};
|
||||
std::array<float,3> centerVoxels{},halfExtentsVoxels{};
|
||||
std::array<std::array<float,3>,4> controlPointsVoxels{};
|
||||
float floorVoxels=0.0f;
|
||||
float boreRadiusVoxels=0.0f,boreHeightVoxels=0.0f;
|
||||
float floorSupportDepthVoxels=0.0f;
|
||||
float smoothWidthVoxels=0.0f;
|
||||
float irregularityAmplitudeVoxels=0.0f;
|
||||
float irregularityWavelengthVoxels=0.0f;
|
||||
std::uint64_t irregularitySeed=0;
|
||||
};
|
||||
|
||||
int BuildStampSpatialNode(std::size_t first,std::size_t end)
|
||||
{
|
||||
@@ -3346,6 +3371,104 @@ private:
|
||||
return result;
|
||||
}
|
||||
|
||||
void BuildCavernSpatialIndex()
|
||||
{
|
||||
CavernRuntime.clear();
|
||||
if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0
|
||||
||ResolvedLayout->cavernGraphVocabularyVersion
|
||||
!=VoxelForgeCore::ResolvedLayoutCavernGraphVocabularyVersionV1)return;
|
||||
const float micronsPerVoxel=static_cast<float>(Owner->CoreVoxelSizeMicrons);
|
||||
for(const auto& room:ResolvedLayout->cavernRooms)
|
||||
{
|
||||
FCavernRuntimeFeature runtime;runtime.room=&room;
|
||||
runtime.boundsMinimumMicrometres=room.boundsMinimumMicrometres;
|
||||
runtime.boundsMaximumMicrometres=room.boundsMaximumMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
runtime.centerVoxels[axis]=static_cast<float>(
|
||||
static_cast<double>(room.centerMicrometres[axis])/micronsPerVoxel);
|
||||
runtime.halfExtentsVoxels[axis]=static_cast<float>(
|
||||
static_cast<double>(room.halfExtentsMicrometres[axis])/micronsPerVoxel);
|
||||
}
|
||||
runtime.floorVoxels=static_cast<float>(
|
||||
static_cast<double>(room.floorMicrometres)/micronsPerVoxel);
|
||||
runtime.smoothWidthVoxels=static_cast<float>(
|
||||
static_cast<double>(room.smoothWidthMicrometres)/micronsPerVoxel);
|
||||
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
||||
static_cast<double>(room.irregularityAmplitudeMicrometres)/micronsPerVoxel);
|
||||
runtime.irregularityWavelengthVoxels=static_cast<float>(
|
||||
static_cast<double>(room.irregularityWavelengthMicrometres)/micronsPerVoxel);
|
||||
runtime.irregularitySeed=room.irregularitySeed;
|
||||
CavernRuntime.push_back(runtime);
|
||||
}
|
||||
for(const auto& edge:ResolvedLayout->cavernEdges)
|
||||
{
|
||||
FCavernRuntimeFeature runtime;runtime.edge=&edge;
|
||||
runtime.boundsMinimumMicrometres=edge.boundsMinimumMicrometres;
|
||||
runtime.boundsMaximumMicrometres=edge.boundsMaximumMicrometres;
|
||||
for(std::size_t point=0;point<4;++point)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
runtime.controlPointsVoxels[point][axis]=static_cast<float>(
|
||||
static_cast<double>(edge.controlPointsMicrometres[point][axis])
|
||||
/micronsPerVoxel);
|
||||
runtime.boreRadiusVoxels=static_cast<float>(
|
||||
static_cast<double>(edge.boreRadiusMicrometres)/micronsPerVoxel);
|
||||
runtime.boreHeightVoxels=static_cast<float>(
|
||||
static_cast<double>(edge.boreHeightMicrometres)/micronsPerVoxel);
|
||||
runtime.floorSupportDepthVoxels=static_cast<float>(
|
||||
static_cast<double>(edge.floorSupportDepthMicrometres)/micronsPerVoxel);
|
||||
runtime.smoothWidthVoxels=static_cast<float>(
|
||||
static_cast<double>(edge.smoothWidthMicrometres)/micronsPerVoxel);
|
||||
runtime.irregularityAmplitudeVoxels=static_cast<float>(
|
||||
static_cast<double>(edge.irregularityAmplitudeMicrometres)/micronsPerVoxel);
|
||||
runtime.irregularityWavelengthVoxels=static_cast<float>(
|
||||
static_cast<double>(edge.irregularityWavelengthMicrometres)/micronsPerVoxel);
|
||||
runtime.irregularitySeed=edge.irregularitySeed;
|
||||
CavernRuntime.push_back(runtime);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildCavernFeatureTileList(
|
||||
const std::array<std::int64_t,3>& minimumSubvoxels,
|
||||
const std::array<std::int64_t,3>& maximumSubvoxels) const
|
||||
{
|
||||
std::vector<std::uint32_t> result;
|
||||
if(CavernRuntime.empty())return result;
|
||||
if(!Owner||Owner->CoreVoxelSizeMicrons==0)
|
||||
{
|
||||
result.resize(CavernRuntime.size());
|
||||
for(std::size_t i=0;i<result.size();++i)result[i]=static_cast<std::uint32_t>(i);
|
||||
return result;
|
||||
}
|
||||
constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
std::array<long double,3> minimumMicrometres{},maximumMicrometres{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
if(minimumSubvoxels[axis]>maximumSubvoxels[axis])
|
||||
{
|
||||
result.resize(CavernRuntime.size());
|
||||
for(std::size_t i=0;i<result.size();++i)
|
||||
result[i]=static_cast<std::uint32_t>(i);
|
||||
return result;
|
||||
}
|
||||
minimumMicrometres[axis]=static_cast<long double>(minimumSubvoxels[axis])
|
||||
*Owner->CoreVoxelSizeMicrons/Q;
|
||||
maximumMicrometres[axis]=static_cast<long double>(maximumSubvoxels[axis])
|
||||
*Owner->CoreVoxelSizeMicrons/Q;
|
||||
}
|
||||
for(std::size_t index=0;index<CavernRuntime.size();++index)
|
||||
{
|
||||
const auto& feature=CavernRuntime[index];bool overlap=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
overlap=overlap&&feature.boundsMaximumMicrometres[axis]
|
||||
>=minimumMicrometres[axis]
|
||||
&&feature.boundsMinimumMicrometres[axis]
|
||||
<=maximumMicrometres[axis];
|
||||
if(overlap)result.push_back(static_cast<std::uint32_t>(index));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int BuildPassageNearestNode(std::size_t passageIndex,std::vector<std::size_t> segments)
|
||||
{
|
||||
const auto& points=ResolvedLayout->passages[passageIndex].controlPoints;
|
||||
@@ -3519,6 +3642,7 @@ private:
|
||||
void BuildPassageSpatialIndices()
|
||||
{
|
||||
BuildStampSpatialIndex();
|
||||
BuildCavernSpatialIndex();
|
||||
PassageTileFeatures.clear();PassageFeatureBounds.clear();
|
||||
PassageFeatureOrder.clear();PassageFeatureNodes.clear();
|
||||
PassageFeatureBoundsFailOpen=false;
|
||||
@@ -4459,6 +4583,19 @@ public:
|
||||
static_cast<std::int64_t>(BMax)*Q};
|
||||
if(!BuildStampTileList(minimum,maximum).empty())return false;
|
||||
}
|
||||
if(!CavernRuntime.empty())
|
||||
{
|
||||
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<std::int64_t,3> minimum={
|
||||
static_cast<std::int64_t>(Origin.X-Pad)*Q,
|
||||
static_cast<std::int64_t>(Origin.Y-Pad)*Q,
|
||||
static_cast<std::int64_t>(BMin)*Q};
|
||||
const std::array<std::int64_t,3> maximum={
|
||||
static_cast<std::int64_t>(Origin.X+(Cells+1)*Step)*Q,
|
||||
static_cast<std::int64_t>(Origin.Y+(Cells+1)*Step)*Q,
|
||||
static_cast<std::int64_t>(BMax)*Q};
|
||||
if(!BuildCavernFeatureTileList(minimum,maximum).empty())return false;
|
||||
}
|
||||
const float LayoutWidth=static_cast<float>(GetResolvedLayoutChunkWidthVoxels());
|
||||
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin)/LayoutWidth);
|
||||
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax)/LayoutWidth);
|
||||
@@ -4563,6 +4700,20 @@ public:
|
||||
if(!BuildStampTileList(minimum,maximum).empty())
|
||||
return Finish(EVoxelTileClass::Mixed,true);
|
||||
}
|
||||
if(!CavernRuntime.empty())
|
||||
{
|
||||
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<std::int64_t,3> minimum={
|
||||
static_cast<std::int64_t>(BMin[0])*Q,
|
||||
static_cast<std::int64_t>(BMin[1])*Q,
|
||||
static_cast<std::int64_t>(BMin[2])*Q};
|
||||
const std::array<std::int64_t,3> maximum={
|
||||
static_cast<std::int64_t>(BMax[0])*Q,
|
||||
static_cast<std::int64_t>(BMax[1])*Q,
|
||||
static_cast<std::int64_t>(BMax[2])*Q};
|
||||
if(!BuildCavernFeatureTileList(minimum,maximum).empty())
|
||||
return Finish(EVoxelTileClass::Mixed,true);
|
||||
}
|
||||
if(ResolvedPassageIntersectsBox(BMin,BMax))
|
||||
return Finish(EVoxelTileClass::Mixed,true);
|
||||
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
|
||||
@@ -5727,6 +5878,252 @@ private:
|
||||
apply(static_cast<std::uint32_t>(index));
|
||||
return composed;
|
||||
}
|
||||
float ApplyResolvedCavernGraph(float density,float X,float Y,float Z) const
|
||||
{
|
||||
if(CavernRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density;
|
||||
const auto* tileQueries=ActivePassageTileQueries;
|
||||
const bool scoped=tileQueries&&tileQueries->matches(*this);
|
||||
if(scoped&&!tileQueries->hasCavernCandidates())return density;
|
||||
const auto* bound=FindBoundStratumForManagerZ(Z);
|
||||
if(!bound)return density;
|
||||
const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons;
|
||||
const std::array<double,3> worldMicrometres={X*micronsPerVoxel,
|
||||
Y*micronsPerVoxel,Z*micronsPerVoxel};
|
||||
float composed=density;
|
||||
const auto apply=[&](std::uint32_t index)
|
||||
{
|
||||
if(index>=CavernRuntime.size())return;
|
||||
const auto& feature=CavernRuntime[index];
|
||||
const std::int32_t strateIndex=feature.room?feature.room->strateIndex
|
||||
:feature.edge?feature.edge->strateIndex:-1;
|
||||
if(strateIndex!=bound->StrateIndex)return;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(worldMicrometres[axis]<feature.boundsMinimumMicrometres[axis]
|
||||
||worldMicrometres[axis]>feature.boundsMaximumMicrometres[axis])return;
|
||||
if(feature.room)
|
||||
{
|
||||
const float localX=X-feature.centerVoxels[0];
|
||||
const float localY=Y-feature.centerVoxels[1];
|
||||
const float localZ=Z-feature.centerVoxels[2];
|
||||
if(Z<feature.floorVoxels)return;
|
||||
float distance=StampEllipsoidSdf(localX,localY,localZ,
|
||||
feature.halfExtentsVoxels);
|
||||
if(feature.irregularityAmplitudeVoxels>0.0f)
|
||||
{
|
||||
const float fade=SmoothUnit((Z-feature.floorVoxels)/0.8f);
|
||||
distance+=StampValueNoise(localX,localY,localZ,
|
||||
feature.irregularityWavelengthVoxels,feature.irregularitySeed)
|
||||
*feature.irregularityAmplitudeVoxels*fade;
|
||||
}
|
||||
composed=SmoothMaxDistance(composed,-distance,feature.smoothWidthVoxels);
|
||||
const float horizontal=std::sqrt(
|
||||
(localX*localX)/(feature.halfExtentsVoxels[0]
|
||||
*feature.halfExtentsVoxels[0])
|
||||
+(localY*localY)/(feature.halfExtentsVoxels[1]
|
||||
*feature.halfExtentsVoxels[1]));
|
||||
if(horizontal<=1.0f)
|
||||
composed=std::min(composed,Z-feature.floorVoxels);
|
||||
return;
|
||||
}
|
||||
if(!feature.edge)return;
|
||||
const auto bezier=[&](float t,std::size_t axis)
|
||||
{
|
||||
const float one=1.0f-t;
|
||||
return one*one*one*feature.controlPointsVoxels[0][axis]
|
||||
+3.0f*one*one*t*feature.controlPointsVoxels[1][axis]
|
||||
+3.0f*one*t*t*feature.controlPointsVoxels[2][axis]
|
||||
+t*t*t*feature.controlPointsVoxels[3][axis];
|
||||
};
|
||||
const auto derivative=[&](float t,std::size_t axis)
|
||||
{
|
||||
const float one=1.0f-t;
|
||||
return 3.0f*one*one*(feature.controlPointsVoxels[1][axis]
|
||||
-feature.controlPointsVoxels[0][axis])
|
||||
+6.0f*one*t*(feature.controlPointsVoxels[2][axis]
|
||||
-feature.controlPointsVoxels[1][axis])
|
||||
+3.0f*t*t*(feature.controlPointsVoxels[3][axis]
|
||||
-feature.controlPointsVoxels[2][axis]);
|
||||
};
|
||||
const auto secondDerivative=[&](float t,std::size_t axis)
|
||||
{
|
||||
return 6.0f*(1.0f-t)*(feature.controlPointsVoxels[2][axis]
|
||||
-2.0f*feature.controlPointsVoxels[1][axis]
|
||||
+feature.controlPointsVoxels[0][axis])
|
||||
+6.0f*t*(feature.controlPointsVoxels[3][axis]
|
||||
-2.0f*feature.controlPointsVoxels[2][axis]
|
||||
+feature.controlPointsVoxels[1][axis]);
|
||||
};
|
||||
float t=0.0f,bestDistanceSquared=std::numeric_limits<float>::infinity();
|
||||
for(int sample=0;sample<=16;++sample)
|
||||
{
|
||||
const float candidate=static_cast<float>(sample)/16.0f;
|
||||
const float dx=X-bezier(candidate,0),dy=Y-bezier(candidate,1);
|
||||
const float distanceSquared=dx*dx+dy*dy;
|
||||
if(distanceSquared<bestDistanceSquared)
|
||||
{bestDistanceSquared=distanceSquared;t=candidate;}
|
||||
}
|
||||
for(int iteration=0;iteration<5;++iteration)
|
||||
{
|
||||
const float dx=bezier(t,0)-X,dy=bezier(t,1)-Y;
|
||||
const float vx=derivative(t,0),vy=derivative(t,1);
|
||||
const float ax=secondDerivative(t,0),ay=secondDerivative(t,1);
|
||||
const float denominator=vx*vx+vy*vy+dx*ax+dy*ay;
|
||||
if(std::abs(denominator)<1.0e-5f)break;
|
||||
t=std::clamp(t-(dx*vx+dy*vy)/denominator,0.0f,1.0f);
|
||||
}
|
||||
const float centerX=bezier(t,0),centerY=bezier(t,1);
|
||||
const float floorZ=bezier(t,2);
|
||||
const float radial=std::hypot(X-centerX,Y-centerY);
|
||||
const float radius=feature.boreRadiusVoxels;
|
||||
const float vertical=(Z-floorZ)/feature.boreHeightVoxels;
|
||||
const float normalized=std::sqrt((radial/radius)*(radial/radius)
|
||||
+vertical*vertical);
|
||||
float distance=(normalized-1.0f)*std::min(radius,feature.boreHeightVoxels);
|
||||
if(feature.irregularityAmplitudeVoxels>0.0f)
|
||||
{
|
||||
const float fade=SmoothUnit((Z-floorZ)/0.8f);
|
||||
distance+=StampValueNoise(X-feature.controlPointsVoxels[0][0],
|
||||
Y-feature.controlPointsVoxels[0][1],Z-floorZ,
|
||||
feature.irregularityWavelengthVoxels,feature.irregularitySeed)
|
||||
*feature.irregularityAmplitudeVoxels*fade;
|
||||
}
|
||||
if(Z<floorZ)
|
||||
{
|
||||
if(composed>0.0f&&feature.floorSupportDepthVoxels>0.0f
|
||||
&&Z>=floorZ-feature.floorSupportDepthVoxels
|
||||
&&radial<radius-0.12f)
|
||||
{
|
||||
const float supportDistance=std::max(radial-(radius-0.12f),
|
||||
std::max(floorZ-feature.floorSupportDepthVoxels-Z,Z-floorZ));
|
||||
composed=std::min(composed,supportDistance);
|
||||
}
|
||||
return;
|
||||
}
|
||||
composed=SmoothMaxDistance(composed,-distance,feature.smoothWidthVoxels);
|
||||
// The hard profile cap is deliberately applied after the smooth union and
|
||||
// relief. Below it, native rock remains untouched; the narrow support above
|
||||
// fills only baseline air.
|
||||
if(radial<=radius)
|
||||
composed=std::min(composed,Z-floorZ);
|
||||
};
|
||||
if(scoped)
|
||||
for(const auto index:tileQueries->cavernFeatureCandidates())apply(index);
|
||||
else
|
||||
for(std::size_t index=0;index<CavernRuntime.size();++index)
|
||||
apply(static_cast<std::uint32_t>(index));
|
||||
return composed;
|
||||
}
|
||||
|
||||
float ApplyResolvedCavernFloorCaps(float density,float X,float Y,float Z) const
|
||||
{
|
||||
if(CavernRuntime.empty()||!Owner||Owner->CoreVoxelSizeMicrons==0)return density;
|
||||
const auto* tileQueries=ActivePassageTileQueries;
|
||||
const bool scoped=tileQueries&&tileQueries->matches(*this);
|
||||
if(scoped&&!tileQueries->hasCavernCandidates())return density;
|
||||
const auto* bound=FindBoundStratumForManagerZ(Z);
|
||||
if(!bound)return density;
|
||||
const double micronsPerVoxel=Owner->CoreVoxelSizeMicrons;
|
||||
const std::array<double,3> worldMicrometres={X*micronsPerVoxel,
|
||||
Y*micronsPerVoxel,Z*micronsPerVoxel};
|
||||
float capped=density;
|
||||
const auto apply=[&](std::uint32_t index)
|
||||
{
|
||||
if(index>=CavernRuntime.size())return;
|
||||
const auto& feature=CavernRuntime[index];
|
||||
const std::int32_t strateIndex=feature.room?feature.room->strateIndex
|
||||
:feature.edge?feature.edge->strateIndex:-1;
|
||||
if(strateIndex!=bound->StrateIndex)return;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(worldMicrometres[axis]<feature.boundsMinimumMicrometres[axis]
|
||||
||worldMicrometres[axis]>feature.boundsMaximumMicrometres[axis])return;
|
||||
if(feature.room)
|
||||
{
|
||||
if(Z<feature.floorVoxels)return;
|
||||
const float localX=X-feature.centerVoxels[0];
|
||||
const float localY=Y-feature.centerVoxels[1];
|
||||
const float horizontal=std::sqrt(
|
||||
(localX*localX)/(feature.halfExtentsVoxels[0]
|
||||
*feature.halfExtentsVoxels[0])
|
||||
+(localY*localY)/(feature.halfExtentsVoxels[1]
|
||||
*feature.halfExtentsVoxels[1]));
|
||||
if(horizontal<=1.0f)
|
||||
capped=std::min(capped,Z-feature.floorVoxels);
|
||||
return;
|
||||
}
|
||||
if(!feature.edge)return;
|
||||
const auto bezier=[&](float t,std::size_t axis)
|
||||
{
|
||||
const float one=1.0f-t;
|
||||
return one*one*one*feature.controlPointsVoxels[0][axis]
|
||||
+3.0f*one*one*t*feature.controlPointsVoxels[1][axis]
|
||||
+3.0f*one*t*t*feature.controlPointsVoxels[2][axis]
|
||||
+t*t*t*feature.controlPointsVoxels[3][axis];
|
||||
};
|
||||
const auto derivative=[&](float t,std::size_t axis)
|
||||
{
|
||||
const float one=1.0f-t;
|
||||
return 3.0f*one*one*(feature.controlPointsVoxels[1][axis]
|
||||
-feature.controlPointsVoxels[0][axis])
|
||||
+6.0f*one*t*(feature.controlPointsVoxels[2][axis]
|
||||
-feature.controlPointsVoxels[1][axis])
|
||||
+3.0f*t*t*(feature.controlPointsVoxels[3][axis]
|
||||
-feature.controlPointsVoxels[2][axis]);
|
||||
};
|
||||
const auto secondDerivative=[&](float t,std::size_t axis)
|
||||
{
|
||||
return 6.0f*(1.0f-t)*(feature.controlPointsVoxels[2][axis]
|
||||
-2.0f*feature.controlPointsVoxels[1][axis]
|
||||
+feature.controlPointsVoxels[0][axis])
|
||||
+6.0f*t*(feature.controlPointsVoxels[3][axis]
|
||||
-2.0f*feature.controlPointsVoxels[2][axis]
|
||||
+feature.controlPointsVoxels[1][axis]);
|
||||
};
|
||||
float t=0.0f,bestDistanceSquared=std::numeric_limits<float>::infinity();
|
||||
for(int sample=0;sample<=16;++sample)
|
||||
{
|
||||
const float candidate=static_cast<float>(sample)/16.0f;
|
||||
const float dx=X-bezier(candidate,0),dy=Y-bezier(candidate,1);
|
||||
const float distanceSquared=dx*dx+dy*dy;
|
||||
if(distanceSquared<bestDistanceSquared)
|
||||
{bestDistanceSquared=distanceSquared;t=candidate;}
|
||||
}
|
||||
for(int iteration=0;iteration<5;++iteration)
|
||||
{
|
||||
const float dx=bezier(t,0)-X,dy=bezier(t,1)-Y;
|
||||
const float vx=derivative(t,0),vy=derivative(t,1);
|
||||
const float ax=secondDerivative(t,0),ay=secondDerivative(t,1);
|
||||
const float denominator=vx*vx+vy*vy+dx*ax+dy*ay;
|
||||
if(std::abs(denominator)<1.0e-5f)break;
|
||||
t=std::clamp(t-(dx*vx+dy*vy)/denominator,0.0f,1.0f);
|
||||
}
|
||||
const float floorZ=bezier(t,2);
|
||||
const float radial=std::hypot(X-bezier(t,0),Y-bezier(t,1));
|
||||
if(Z<floorZ)
|
||||
{
|
||||
// Re-evaluate the narrow fallback after passages and stamps, since
|
||||
// those later operations may open air beneath an otherwise native
|
||||
// floor. Negative native density is preserved by the min operation.
|
||||
if(capped>0.0f&&feature.floorSupportDepthVoxels>0.0f
|
||||
&&Z>=floorZ-feature.floorSupportDepthVoxels
|
||||
&&radial<feature.boreRadiusVoxels-0.12f)
|
||||
{
|
||||
const float supportDistance=std::max(radial-
|
||||
(feature.boreRadiusVoxels-0.12f),
|
||||
std::max(floorZ-feature.floorSupportDepthVoxels-Z,Z-floorZ));
|
||||
capped=std::min(capped,supportDistance);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(radial<=feature.boreRadiusVoxels)
|
||||
capped=std::min(capped,Z-floorZ);
|
||||
};
|
||||
if(scoped)
|
||||
for(const auto index:tileQueries->cavernFeatureCandidates())apply(index);
|
||||
else
|
||||
for(std::size_t index=0;index<CavernRuntime.size();++index)
|
||||
apply(static_cast<std::uint32_t>(index));
|
||||
return capped;
|
||||
}
|
||||
static float SmoothUnit(float V)
|
||||
{
|
||||
V=std::clamp(V,0.0f,1.0f);
|
||||
@@ -8977,6 +9374,7 @@ private:
|
||||
std::vector<FStampSpatialNode> StampSpatialNodes;
|
||||
std::vector<std::uint32_t> StampSpatialOrder;
|
||||
std::vector<FStampRuntime> StampRuntime;
|
||||
std::vector<FCavernRuntimeFeature> CavernRuntime;
|
||||
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> PassageFeatureBounds;
|
||||
std::vector<FPassageTileFeature> PassageTileFeatures;
|
||||
std::vector<std::uint32_t> PassageFeatureOrder;
|
||||
|
||||
@@ -1334,6 +1334,34 @@ namespace VoxelForgeCore
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ResolvedCavernGraphTouchesBox(const ResolvedLayoutSnapshotV3& layout,
|
||||
const FIntVector& minimum,const FIntVector& maximum,
|
||||
std::uint32_t coreVoxelSizeMicrons)
|
||||
{
|
||||
if((layout.cavernRooms.empty()&&layout.cavernEdges.empty())
|
||||
||coreVoxelSizeMicrons==0)return false;
|
||||
const double tileMinimum[3]={static_cast<double>(minimum.X),
|
||||
static_cast<double>(minimum.Y),static_cast<double>(minimum.Z)};
|
||||
const double tileMaximum[3]={static_cast<double>(maximum.X),
|
||||
static_cast<double>(maximum.Y),static_cast<double>(maximum.Z)};
|
||||
const auto overlaps=[&](const auto& feature)
|
||||
{
|
||||
bool overlap=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double low=static_cast<double>(feature.boundsMinimumMicrometres[axis])
|
||||
/coreVoxelSizeMicrons;
|
||||
const double high=static_cast<double>(feature.boundsMaximumMicrometres[axis])
|
||||
/coreVoxelSizeMicrons;
|
||||
overlap=overlap&&high>=tileMinimum[axis]&&low<=tileMaximum[axis];
|
||||
}
|
||||
return overlap;
|
||||
};
|
||||
for(const auto& room:layout.cavernRooms)if(overlaps(room))return true;
|
||||
for(const auto& edge:layout.cavernEdges)if(overlaps(edge))return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool World::compile(const ShapeBuildRequest& request,const JobScheduler& scheduler,std::string& error)
|
||||
@@ -1418,6 +1446,8 @@ namespace VoxelForgeCore
|
||||
if(RouteTouchesBox(*selectedPlan,subMinimum,subMaximum))return false;
|
||||
if(ResolvedStampTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||
if(ResolvedCavernGraphTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||
if(ResolvedPassageTouchesBox(*impl_->resolvedLayout,boundsMin,boundsMax,
|
||||
impl_->owner->CoreVoxelSizeMicrons))return false;
|
||||
if(OwnerPassageTouchesBox(*impl_->owner,boundsMin,boundsMax))return false;
|
||||
|
||||
Reference in new issue
Block a user