feat(core): ore vein records and a materialAt query, off by default (L50); research R18, R19
Versioned capsule vein records (stable ID, resource ID, endpoints, radius, seeded irregularity) resolved into the snapshot only when requested, placed beside the route and away from passages, landings and landmarks (forced-passage control rejected). The bound field table gains materialAt (host + two resource IDs + Q16 weights) behind a per-tile BVH; mesh export v2 carries per-vertex materials; a measurement-only yield computes removed resource volume (0 on already-air space). With veins on, every density, mesh, payload and topology hash is unchanged; demo off: owner hashes identical to L47. fast/medium/full PASS. Research R18 (cavern-graph planner) and R19 (why FloatingIslands costs ~10x SurfaceWorld). 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
b9ccff4ad6
commit
ca85e876ad
15 files changed
+1573
-19
No files matched your search
@@ -81,6 +81,9 @@ namespace VoxelForgeCore
|
||||
Vec3d normal;
|
||||
Vec2f uv;
|
||||
ColorRgba8 color;
|
||||
std::uint16_t hostMaterialId = 1;
|
||||
std::array<std::uint16_t, 2> resourceMaterialIds{};
|
||||
std::array<std::uint16_t, 2> resourceWeightsQ16{};
|
||||
};
|
||||
|
||||
struct MeshGroups
|
||||
@@ -163,6 +166,10 @@ namespace VoxelForgeCore
|
||||
double capRefinementExclusiveMilliseconds = 0.0;
|
||||
double meshAssemblyMilliseconds = 0.0;
|
||||
double meshingMilliseconds = 0.0;
|
||||
double materialQueryMilliseconds = 0.0;
|
||||
std::uint64_t materialQueries = 0;
|
||||
std::uint64_t materialCandidateChecks = 0;
|
||||
bool materialTileOutsideEveryVein = true;
|
||||
double featureListMilliseconds = 0.0;
|
||||
double farShellMilliseconds = 0.0;
|
||||
double farStratumShellMilliseconds = 0.0;
|
||||
@@ -216,7 +223,9 @@ namespace VoxelForgeCore
|
||||
std::uint64_t densitySampleCount = 0;
|
||||
std::uint64_t meshPayloadAllocatedBytes = 0;
|
||||
std::uint64_t densityHash = 0, meshHash = 0, payloadHash = 0, topologyHash = 0;
|
||||
bool hasDensityHash = false, hasMeshHash = false, hasPayloadHash = false, hasTopologyHash = false;
|
||||
std::uint64_t materialHash = 0;
|
||||
bool hasDensityHash = false, hasMeshHash = false, hasPayloadHash = false,
|
||||
hasTopologyHash = false, hasMaterialHash = false;
|
||||
};
|
||||
|
||||
struct WorldConfiguration
|
||||
@@ -708,6 +717,7 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||
|
||||
// Stable, bounded geometry edits composed into every Core field after the shared
|
||||
@@ -787,6 +797,36 @@ namespace VoxelForgeCore
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
};
|
||||
|
||||
// Immutable material-only deposits. The capsule and its conservative bounds are in
|
||||
// micrometres; bounds include irregularity and the blend envelope.
|
||||
struct ResolvedLayoutOreVeinRequestV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
std::uint16_t resourceMaterialId = 0;
|
||||
std::array<std::array<std::int64_t, 3>, 2> anchorEndpointsMicrometres{};
|
||||
std::int64_t placementRadiusMicrometres = 0;
|
||||
std::int64_t radiusMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t blendWidthMicrometres = 0;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutOreVeinV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
std::uint16_t resourceMaterialId = 0;
|
||||
std::array<std::array<std::int64_t, 3>, 2> endpointsMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
std::int64_t radiusMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t blendWidthMicrometres = 0;
|
||||
std::uint64_t irregularitySeed = 0;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutAnchorV2
|
||||
{
|
||||
std::string kind;
|
||||
@@ -959,6 +999,9 @@ namespace VoxelForgeCore
|
||||
std::uint32_t cavernGraphVocabularyVersion = 0;
|
||||
std::vector<ResolvedCavernRoomV1> cavernRooms;
|
||||
std::vector<ResolvedCavernEdgeV1> cavernEdges;
|
||||
// Omitted from canonical identity when no records are present.
|
||||
std::uint32_t oreVeinVocabularyVersion = 0;
|
||||
std::vector<ResolvedLayoutOreVeinV1> oreVeins;
|
||||
std::string stableIdentity;
|
||||
|
||||
bool finalize(std::string& error);
|
||||
@@ -1016,6 +1059,40 @@ namespace VoxelForgeCore
|
||||
std::span<const ResolvedCavernRoomV1> rooms,
|
||||
std::span<const ResolvedCavernEdgeV1> edges,
|
||||
std::string& error);
|
||||
VFCORE_API bool ResolveResolvedLayoutOreVeinsV1(
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::span<const ResolvedLayoutOreVeinRequestV1> requests,
|
||||
std::string& error);
|
||||
|
||||
struct ResolvedLayoutOreVeinPlacementAuditV1
|
||||
{
|
||||
std::uint32_t negativeControlsRun = 0;
|
||||
std::uint32_t negativeControlsRejected = 0;
|
||||
bool passed = false;
|
||||
};
|
||||
VFCORE_API ResolvedLayoutOreVeinPlacementAuditV1 AuditResolvedLayoutOreVeinPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout);
|
||||
|
||||
struct MaterialSampleV1
|
||||
{
|
||||
std::uint16_t hostMaterialId = 1;
|
||||
std::array<std::uint16_t, 2> resourceMaterialIds{};
|
||||
std::array<std::uint16_t, 2> resourceWeightsQ16{};
|
||||
};
|
||||
|
||||
struct ResourceVolumeV1
|
||||
{
|
||||
std::uint16_t resourceMaterialId = 0;
|
||||
std::uint64_t weightedSamplesQ16 = 0;
|
||||
double volumeCubicMetres = 0.0;
|
||||
};
|
||||
|
||||
struct OreMiningYieldV1
|
||||
{
|
||||
std::uint64_t solidSamplesRemoved = 0;
|
||||
double sampleSpacingMetres = 0.0;
|
||||
std::vector<ResourceVolumeV1> resources;
|
||||
};
|
||||
|
||||
struct FloatingIslandBoundsAuditResult
|
||||
{
|
||||
@@ -1611,6 +1688,12 @@ namespace VoxelForgeCore
|
||||
ShapeBuildStats buildStats() const;
|
||||
|
||||
float densityAt(Vec3d voxelPosition) const;
|
||||
// Companion material query for the bound strate evaluator; coordinates use Core voxels.
|
||||
MaterialSampleV1 materialAt(Vec3d voxelPosition) const;
|
||||
// Measure-only carve estimate. This does not apply an edit or grant inventory.
|
||||
OreMiningYieldV1 measureOreMiningYieldForSphere(
|
||||
Vec3d centerVoxel, double radiusMetres,
|
||||
double sampleSpacingMetres = 0.25) const;
|
||||
// Audit-only view of density-field columns and short spans supporting passage voids.
|
||||
std::vector<PassageVoidCrossingAuditV1> passageVoidCrossings() const;
|
||||
bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex,
|
||||
|
||||
@@ -283,6 +283,187 @@ namespace
|
||||
}
|
||||
return true;
|
||||
}
|
||||
double LayoutSegmentDistance(const std::array<double,3>& p0,
|
||||
const std::array<double,3>& p1,const std::array<double,3>& q0,
|
||||
const std::array<double,3>& q1)
|
||||
{
|
||||
std::array<double,3> u{},v{},w{};
|
||||
double a=0.0,b=0.0,c=0.0,d=0.0,e=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
u[axis]=p1[axis]-p0[axis];v[axis]=q1[axis]-q0[axis];
|
||||
w[axis]=p0[axis]-q0[axis];
|
||||
a+=u[axis]*u[axis];b+=u[axis]*v[axis];
|
||||
c+=v[axis]*v[axis];d+=u[axis]*w[axis];e+=v[axis]*w[axis];
|
||||
}
|
||||
const double denominator=a*c-b*b;
|
||||
double sN=0.0,sD=denominator,tN=0.0,tD=denominator;
|
||||
if(denominator<1.0e-18){sN=0.0;sD=1.0;tN=e;tD=c;}
|
||||
else
|
||||
{
|
||||
sN=b*e-c*d;tN=a*e-b*d;
|
||||
if(sN<0.0){sN=0.0;tN=e;tD=c;}
|
||||
else if(sN>sD){sN=sD;tN=e+b;tD=c;}
|
||||
}
|
||||
if(tN<0.0)
|
||||
{
|
||||
tN=0.0;
|
||||
if(-d<0.0)sN=0.0;
|
||||
else if(-d>a)sN=sD;
|
||||
else{sN=-d;sD=a;}
|
||||
}
|
||||
else if(tN>tD)
|
||||
{
|
||||
tN=tD;
|
||||
if(-d+b<0.0)sN=0.0;
|
||||
else if(-d+b>a)sN=sD;
|
||||
else{sN=-d+b;sD=a;}
|
||||
}
|
||||
const double s=std::abs(sN)<1.0e-18?0.0:sN/sD;
|
||||
const double t=std::abs(tN)<1.0e-18?0.0:tN/tD;
|
||||
double distanceSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double delta=w[axis]+s*u[axis]-t*v[axis];
|
||||
distanceSquared+=delta*delta;
|
||||
}
|
||||
return std::sqrt(std::max(0.0,distanceSquared));
|
||||
}
|
||||
std::array<double,3> LayoutOrePointUm(const std::array<std::int64_t,3>& point)
|
||||
{return {static_cast<double>(point[0]),static_cast<double>(point[1]),
|
||||
static_cast<double>(point[2])};}
|
||||
bool LayoutOreVeinSafe(const ResolvedLayoutSnapshotV3& layout,
|
||||
const ResolvedLayoutOreVeinV1& vein,std::string* reason=nullptr)
|
||||
{
|
||||
const auto fail=[&](const char* text)
|
||||
{if(reason)*reason=text;return false;};
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==vein.strateIndex;});
|
||||
if(strate==layout.strates.end()||!strate->content.presentInCurrentField)
|
||||
return fail("vein strate has no current content band");
|
||||
if(vein.boundsMinimumMicrometres[2]<=strate->content.bottomMicrometres
|
||||
||vein.boundsMaximumMicrometres[2]>=strate->content.topMicrometres)
|
||||
return fail("vein bounds touch or cross the ceiling/bedrock content faces");
|
||||
for(const auto* band:{&strate->ceiling,&strate->bedrock})
|
||||
if(band->presentInCurrentField
|
||||
&&vein.boundsMinimumMicrometres[2]<band->topMicrometres
|
||||
&&vein.boundsMaximumMicrometres[2]>band->bottomMicrometres)
|
||||
return fail("vein bounds overlap a ceiling or bedrock band");
|
||||
const auto veinA=LayoutOrePointUm(vein.endpointsMicrometres[0]);
|
||||
const auto veinB=LayoutOrePointUm(vein.endpointsMicrometres[1]);
|
||||
const double coreRadius=static_cast<double>(vein.radiusMicrometres
|
||||
+vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres);
|
||||
const double coreMicrometres=std::max<std::uint32_t>(1,layout.coreVoxelSizeMicrometres);
|
||||
constexpr double RouteClearanceMicrometres=100000.0;
|
||||
const auto qToUm=[&](const std::array<std::int64_t,3>& q)
|
||||
{return std::array<double,3>{q[0]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
q[1]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
q[2]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel};};
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=vein.strateIndex
|
||||
&&passage.lowerStrateIndex!=vein.strateIndex)continue;
|
||||
const auto upper=qToUm(passage.upperMouthSubvoxels);
|
||||
const auto lower=qToUm(passage.lowerMouthSubvoxels);
|
||||
const double mouthRadius=passage.mouthRadiusSubvoxels*coreMicrometres
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(LayoutSegmentDistance(veinA,veinB,upper,upper)
|
||||
<=coreRadius+mouthRadius+RouteClearanceMicrometres
|
||||
||LayoutSegmentDistance(veinA,veinB,lower,lower)
|
||||
<=coreRadius+mouthRadius+RouteClearanceMicrometres)
|
||||
return fail("vein overlaps a clear passage landing");
|
||||
for(std::size_t i=0;i+1<passage.controlPoints.size();++i)
|
||||
{
|
||||
const auto a=qToUm(passage.controlPoints[i].positionSubvoxels);
|
||||
const auto b=qToUm(passage.controlPoints[i+1].positionSubvoxels);
|
||||
const double radius=std::max(passage.controlPoints[i].radiusSubvoxels,
|
||||
passage.controlPoints[i+1].radiusSubvoxels)*coreMicrometres
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(LayoutSegmentDistance(veinA,veinB,a,b)
|
||||
<=coreRadius+radius+RouteClearanceMicrometres)
|
||||
return fail("vein enters a clear passage corridor");
|
||||
}
|
||||
}
|
||||
std::vector<const ResolvedLayoutAnchorV2*> route;
|
||||
for(const auto& anchor:strate->anchors)
|
||||
{
|
||||
if(anchor.values.size()<3)continue;
|
||||
if(anchor.kind=="route_point")route.push_back(&anchor);
|
||||
if(anchor.kind.find("landing")!=std::string::npos)
|
||||
{
|
||||
const std::array<double,3> landing={
|
||||
anchor.values[0]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
anchor.values[1]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
anchor.values[2]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel};
|
||||
const double radius=anchor.values.size()>=4
|
||||
?anchor.values[3]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel:3000000.0;
|
||||
if(LayoutSegmentDistance(veinA,veinB,landing,landing)
|
||||
<=coreRadius+radius+RouteClearanceMicrometres)
|
||||
return fail("vein overlaps a resolved landing footprint");
|
||||
}
|
||||
}
|
||||
for(std::size_t i=0;i+1<route.size();++i)
|
||||
{
|
||||
const auto qPoint=[&](const ResolvedLayoutAnchorV2& anchor)
|
||||
{return std::array<double,3>{anchor.values[0]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
anchor.values[1]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
anchor.values[2]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel};};
|
||||
const auto a=qPoint(*route[i]),b=qPoint(*route[i+1]);
|
||||
const double routeRadius=std::max(route[i]->values.size()>=4?route[i]->values[3]:0,
|
||||
route[i+1]->values.size()>=4?route[i+1]->values[3]:0)
|
||||
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(LayoutSegmentDistance(veinA,veinB,a,b)
|
||||
<=coreRadius+routeRadius+RouteClearanceMicrometres)
|
||||
return fail("vein enters a clear strate route corridor");
|
||||
}
|
||||
for(const auto& room:layout.cavernRooms)
|
||||
if(room.strateIndex==vein.strateIndex)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(vein.boundsMinimumMicrometres[axis]
|
||||
<=room.boundsMaximumMicrometres[axis]
|
||||
&&vein.boundsMaximumMicrometres[axis]
|
||||
>=room.boundsMinimumMicrometres[axis])
|
||||
return fail("vein overlaps a cavern landmark footprint");
|
||||
for(const auto& edge:layout.cavernEdges)
|
||||
{
|
||||
if(edge.strateIndex!=vein.strateIndex)continue;
|
||||
std::array<double,3> previous{};
|
||||
const auto bezier=[&](double t)
|
||||
{
|
||||
const double one=1.0-t;
|
||||
const std::array<double,4> weights={one*one*one,3.0*one*one*t,
|
||||
3.0*one*t*t,t*t*t};
|
||||
std::array<double,3> point{};
|
||||
for(std::size_t i=0;i<4;++i)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
point[axis]+=weights[i]*edge.controlPointsMicrometres[i][axis];
|
||||
return point;
|
||||
};
|
||||
previous=bezier(0.0);
|
||||
for(int step=1;step<=256;++step)
|
||||
{
|
||||
const auto next=bezier(static_cast<double>(step)/256.0);
|
||||
if(LayoutSegmentDistance(veinA,veinB,previous,next)
|
||||
<=coreRadius+edge.boreRadiusMicrometres+RouteClearanceMicrometres)
|
||||
return fail("vein enters a landmark passage corridor");
|
||||
previous=next;
|
||||
}
|
||||
}
|
||||
for(const auto& stamp:layout.stamps)
|
||||
{
|
||||
if(stamp.strateIndex!=vein.strateIndex
|
||||
||(stamp.kind!=ResolvedSdfStampKindV1::fillDais
|
||||
&&stamp.kind!=ResolvedSdfStampKindV1::fillPillar))continue;
|
||||
bool overlaps=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
overlaps=overlaps&&vein.boundsMinimumMicrometres[axis]
|
||||
<=stamp.boundsMaximumMicrometres[axis]
|
||||
&&vein.boundsMaximumMicrometres[axis]
|
||||
>=stamp.boundsMinimumMicrometres[axis];
|
||||
if(overlaps)return fail("vein overlaps a resolved landmark solid footprint");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::string LayoutHex(std::uint64_t value)
|
||||
{std::ostringstream out;out<<std::hex<<std::setfill('0')<<std::setw(16)<<value;return out.str();}
|
||||
std::string LayoutBandJson(const ResolvedLayoutBandV2& band)
|
||||
@@ -454,6 +635,32 @@ namespace
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
if(!s.oreVeins.empty())
|
||||
{
|
||||
out<<",\"ore_vein_vocabulary_version\":"<<s.oreVeinVocabularyVersion
|
||||
<<",\"ore_veins\":[";
|
||||
for(std::size_t i=0;i<s.oreVeins.size();++i)
|
||||
{
|
||||
if(i)out<<',';const auto& vein=s.oreVeins[i];
|
||||
out<<"{\"stable_id\":"<<LayoutEscape(vein.stableId)
|
||||
<<",\"strate_index\":"<<vein.strateIndex
|
||||
<<",\"resource_material_id\":"<<vein.resourceMaterialId
|
||||
<<",\"endpoints_um\":[["<<vein.endpointsMicrometres[0][0]<<','
|
||||
<<vein.endpointsMicrometres[0][1]<<','<<vein.endpointsMicrometres[0][2]
|
||||
<<"],["<<vein.endpointsMicrometres[1][0]<<','
|
||||
<<vein.endpointsMicrometres[1][1]<<','<<vein.endpointsMicrometres[1][2]
|
||||
<<"]],\"bounds_min_um\":["<<vein.boundsMinimumMicrometres[0]<<','
|
||||
<<vein.boundsMinimumMicrometres[1]<<','<<vein.boundsMinimumMicrometres[2]
|
||||
<<"],\"bounds_max_um\":["<<vein.boundsMaximumMicrometres[0]<<','
|
||||
<<vein.boundsMaximumMicrometres[1]<<','<<vein.boundsMaximumMicrometres[2]
|
||||
<<"],\"radius_um\":"<<vein.radiusMicrometres
|
||||
<<",\"irregularity_amplitude_um\":"<<vein.irregularityAmplitudeMicrometres
|
||||
<<",\"irregularity_wavelength_um\":"<<vein.irregularityWavelengthMicrometres
|
||||
<<",\"blend_width_um\":"<<vein.blendWidthMicrometres
|
||||
<<",\"irregularity_seed\":"<<vein.irregularitySeed<<'}';
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
out<<'}';return out.str();
|
||||
}
|
||||
std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key)
|
||||
@@ -620,6 +827,8 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
||||
{return a.stableId<b.stableId;});
|
||||
std::sort(cavernEdges.begin(),cavernEdges.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
std::sort(oreVeins.begin(),oreVeins.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);
|
||||
@@ -1028,6 +1237,55 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
if(oreVeins.empty())
|
||||
{
|
||||
if(oreVeinVocabularyVersion!=0)
|
||||
add("ore_vein_vocabulary_version",
|
||||
"ore-vein vocabulary version is set without records");
|
||||
}
|
||||
else if(oreVeinVocabularyVersion!=ResolvedLayoutOreVeinVocabularyVersionV1)
|
||||
add("ore_vein_vocabulary_version",
|
||||
"ore-vein records require vocabulary version 1");
|
||||
std::vector<std::string> oreVeinIds;
|
||||
for(const auto& vein:oreVeins)
|
||||
{
|
||||
if(vein.stableId.empty()
|
||||
||std::find(oreVeinIds.begin(),oreVeinIds.end(),vein.stableId)!=oreVeinIds.end())
|
||||
add("ore_vein_identity","ore vein has an empty or duplicate stable identity");
|
||||
else oreVeinIds.push_back(vein.stableId);
|
||||
if(vein.resourceMaterialId==0||vein.radiusMicrometres<250000
|
||||
||vein.radiusMicrometres>20000000
|
||||
||vein.irregularityAmplitudeMicrometres<0
|
||||
||vein.irregularityAmplitudeMicrometres>10000000
|
||||
||vein.irregularityWavelengthMicrometres<0
|
||||
||vein.irregularityWavelengthMicrometres>100000000
|
||||
||(vein.irregularityAmplitudeMicrometres>0
|
||||
&&vein.irregularityWavelengthMicrometres<=0)
|
||||
||vein.blendWidthMicrometres<50000
|
||||
||vein.blendWidthMicrometres>2000000
|
||||
||vein.irregularitySeed>0x001fffffffffffffull)
|
||||
add("ore_vein_parameters",vein.stableId+" has invalid or unbounded parameters");
|
||||
const std::int64_t pad=vein.radiusMicrometres
|
||||
+vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
if(vein.endpointsMicrometres[0][axis]<-1000000000000ll
|
||||
||vein.endpointsMicrometres[0][axis]>1000000000000ll
|
||||
||vein.endpointsMicrometres[1][axis]<-1000000000000ll
|
||||
||vein.endpointsMicrometres[1][axis]>1000000000000ll)
|
||||
add("ore_vein_bounds",vein.stableId+" endpoint is outside the bounded world envelope");
|
||||
if(vein.boundsMinimumMicrometres[axis]
|
||||
!=std::min(vein.endpointsMicrometres[0][axis],
|
||||
vein.endpointsMicrometres[1][axis])-pad
|
||||
||vein.boundsMaximumMicrometres[axis]
|
||||
!=std::max(vein.endpointsMicrometres[0][axis],
|
||||
vein.endpointsMicrometres[1][axis])+pad)
|
||||
add("ore_vein_bounds",vein.stableId+" conservative bounds do not match its parameters");
|
||||
}
|
||||
std::string safetyReason;
|
||||
if(!LayoutOreVeinSafe(*this,vein,&safetyReason))
|
||||
add("ore_vein_placement",vein.stableId+" is unsafe: "+safetyReason);
|
||||
}
|
||||
if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;}
|
||||
return true;
|
||||
}
|
||||
@@ -1046,6 +1304,8 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
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"));
|
||||
s.oreVeinVocabularyVersion=static_cast<std::uint32_t>(
|
||||
LayoutInt(root,"ore_vein_vocabulary_version"));
|
||||
for(const auto& v:root.Get("strates").AsArray())
|
||||
{
|
||||
ResolvedLayoutStratumV2 x;x.stableId=LayoutString(v,"stable_id");
|
||||
@@ -1166,6 +1426,33 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
edge.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
||||
s.cavernEdges.push_back(std::move(edge));
|
||||
}
|
||||
for(const auto& v:root.Get("ore_veins").AsArray())
|
||||
{
|
||||
ResolvedLayoutOreVeinV1 vein;vein.stableId=LayoutString(v,"stable_id");
|
||||
vein.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||
vein.resourceMaterialId=static_cast<std::uint16_t>(LayoutInt(v,"resource_material_id"));
|
||||
const auto& endpoints=v.Get("endpoints_um").AsArray();
|
||||
for(std::size_t endpoint=0;endpoint<2&&endpoint<endpoints.size();++endpoint)
|
||||
{
|
||||
const auto& values=endpoints[endpoint].AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
vein.endpointsMicrometres[endpoint][axis]=values[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",vein.boundsMinimumMicrometres);
|
||||
readTriplet("bounds_max_um",vein.boundsMaximumMicrometres);
|
||||
vein.radiusMicrometres=LayoutInt(v,"radius_um");
|
||||
vein.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
||||
vein.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
||||
vein.blendWidthMicrometres=LayoutInt(v,"blend_width_um");
|
||||
vein.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
||||
s.oreVeins.push_back(std::move(vein));
|
||||
}
|
||||
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;}
|
||||
@@ -1352,6 +1639,148 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
|
||||
bool ResolveResolvedLayoutOreVeinsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
||||
std::span<const ResolvedLayoutOreVeinRequestV1> requests,std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
candidate.oreVeins.clear();candidate.oreVeinVocabularyVersion=0;
|
||||
if(requests.empty())
|
||||
{
|
||||
if(!candidate.finalize(error))return false;
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
candidate.oreVeinVocabularyVersion=ResolvedLayoutOreVeinVocabularyVersionV1;
|
||||
std::vector<ResolvedLayoutOreVeinRequestV1> ordered(requests.begin(),requests.end());
|
||||
std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
std::vector<std::string> requestIds;
|
||||
candidate.oreVeins.reserve(ordered.size());
|
||||
for(const auto& request:ordered)
|
||||
{
|
||||
if(request.stableId.empty()
|
||||
||std::find(requestIds.begin(),requestIds.end(),request.stableId)!=requestIds.end())
|
||||
{error="ore-vein requests require non-empty unique stable IDs";return false;}
|
||||
requestIds.push_back(request.stableId);
|
||||
if(request.resourceMaterialId==0||request.placementRadiusMicrometres<0
|
||||
||request.placementRadiusMicrometres>10000000
|
||||
||request.radiusMicrometres<250000||request.radiusMicrometres>20000000
|
||||
||request.irregularityAmplitudeMicrometres<0
|
||||
||request.irregularityAmplitudeMicrometres>10000000
|
||||
||request.irregularityWavelengthMicrometres<0
|
||||
||request.irregularityWavelengthMicrometres>100000000
|
||||
||(request.irregularityAmplitudeMicrometres>0
|
||||
&&request.irregularityWavelengthMicrometres<=0)
|
||||
||request.blendWidthMicrometres<50000
|
||||
||request.blendWidthMicrometres>2000000)
|
||||
{error="ore-vein request "+request.stableId+" has invalid or unbounded parameters";return false;}
|
||||
const auto strate=std::find_if(candidate.strates.begin(),candidate.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==request.strateIndex;});
|
||||
if(strate==candidate.strates.end())
|
||||
{error="ore-vein request "+request.stableId+" names an unknown strate";return false;}
|
||||
|
||||
const std::uint64_t baseSeed=LayoutStampSeed(seed,request.strateIndex,
|
||||
std::string("ore-vein/")+request.stableId)&0x001fffffffffffffull;
|
||||
std::uint64_t state=baseSeed;
|
||||
const auto randomOffset=[&](std::uint64_t bits)
|
||||
{
|
||||
const std::int64_t radius=request.placementRadiusMicrometres;
|
||||
if(radius==0)return std::int64_t{0};
|
||||
const std::uint64_t span=static_cast<std::uint64_t>(radius)*2+1;
|
||||
return static_cast<std::int64_t>(bits%span)-radius;
|
||||
};
|
||||
bool placed=false;
|
||||
std::string lastUnsafeReason="all candidates were outside the bounded content volume";
|
||||
const std::uint32_t attempts=request.placementRadiusMicrometres==0?1u:512u;
|
||||
for(std::uint32_t attempt=0;attempt<attempts;++attempt)
|
||||
{
|
||||
std::array<std::int64_t,3> offset{};
|
||||
if(request.placementRadiusMicrometres>0)
|
||||
{
|
||||
offset={randomOffset(LayoutStampNext(state)),
|
||||
randomOffset(LayoutStampNext(state)),
|
||||
randomOffset(LayoutStampNext(state))/3};
|
||||
const long double radius=request.placementRadiusMicrometres;
|
||||
const long double distanceSquared=static_cast<long double>(offset[0])*offset[0]
|
||||
+static_cast<long double>(offset[1])*offset[1]
|
||||
+static_cast<long double>(offset[2])*offset[2];
|
||||
if(distanceSquared>radius*radius)continue;
|
||||
}
|
||||
ResolvedLayoutOreVeinV1 vein;vein.stableId=request.stableId;
|
||||
vein.strateIndex=request.strateIndex;
|
||||
vein.resourceMaterialId=request.resourceMaterialId;
|
||||
vein.radiusMicrometres=request.radiusMicrometres;
|
||||
vein.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres;
|
||||
vein.irregularityWavelengthMicrometres=request.irregularityWavelengthMicrometres;
|
||||
vein.blendWidthMicrometres=request.blendWidthMicrometres;
|
||||
vein.irregularitySeed=baseSeed;
|
||||
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
vein.endpointsMicrometres[endpoint][axis]
|
||||
=request.anchorEndpointsMicrometres[endpoint][axis]+offset[axis];
|
||||
const std::int64_t pad=vein.radiusMicrometres
|
||||
+vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
vein.boundsMinimumMicrometres[axis]=std::min(
|
||||
vein.endpointsMicrometres[0][axis],vein.endpointsMicrometres[1][axis])-pad;
|
||||
vein.boundsMaximumMicrometres[axis]=std::max(
|
||||
vein.endpointsMicrometres[0][axis],vein.endpointsMicrometres[1][axis])+pad;
|
||||
}
|
||||
if(!LayoutOreVeinSafe(candidate,vein,&lastUnsafeReason))continue;
|
||||
candidate.oreVeins.push_back(std::move(vein));placed=true;break;
|
||||
}
|
||||
if(!placed)
|
||||
{error="ore-vein request "+request.stableId
|
||||
+" has no deterministic passage-safe content placement: "+lastUnsafeReason;return false;}
|
||||
}
|
||||
if(!candidate.finalize(error))return false;
|
||||
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
|
||||
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,strictError))
|
||||
{error="strict ore-vein placement validation failed: "+strictError;return false;}
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
|
||||
ResolvedLayoutOreVeinPlacementAuditV1 AuditResolvedLayoutOreVeinPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout)
|
||||
{
|
||||
ResolvedLayoutOreVeinPlacementAuditV1 result;
|
||||
if(layout.passages.empty()||layout.passages.front().controlPoints.empty())return result;
|
||||
const auto& passage=layout.passages.front();
|
||||
const auto& control=passage.controlPoints.front().positionSubvoxels;
|
||||
ResolvedLayoutOreVeinV1 forced;forced.stableId="negative-control/forced-clear-passage";
|
||||
forced.strateIndex=passage.upperStrateIndex;forced.resourceMaterialId=1;
|
||||
forced.radiusMicrometres=500000;forced.blendWidthMicrometres=500000;
|
||||
forced.irregularitySeed=1;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const std::int64_t point=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(control[axis])*layout.coreVoxelSizeMicrometres
|
||||
/WorldAssemblySubvoxelsPerVoxel));
|
||||
forced.endpointsMicrometres[0][axis]=point;
|
||||
forced.endpointsMicrometres[1][axis]=point;
|
||||
forced.boundsMinimumMicrometres[axis]=point-1000000;
|
||||
forced.boundsMaximumMicrometres[axis]=point+1000000;
|
||||
}
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
candidate.oreVeinVocabularyVersion=ResolvedLayoutOreVeinVocabularyVersionV1;
|
||||
candidate.oreVeins.push_back(forced);
|
||||
std::string finalizeError;
|
||||
++result.negativeControlsRun;
|
||||
if(candidate.finalize(finalizeError))
|
||||
{
|
||||
std::vector<ResolvedLayoutIssueV2> issues;std::string validationError;
|
||||
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,validationError)
|
||||
&&std::any_of(issues.begin(),issues.end(),[](const auto& issue)
|
||||
{return issue.code=="ore_vein_placement"
|
||||
&&issue.detail.find("clear passage")!=std::string::npos;}))
|
||||
++result.negativeControlsRejected;
|
||||
}
|
||||
result.passed=result.negativeControlsRun>0
|
||||
&&result.negativeControlsRejected==result.negativeControlsRun;
|
||||
return result;
|
||||
}
|
||||
|
||||
ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout)
|
||||
{
|
||||
|
||||
@@ -369,6 +369,7 @@ public:
|
||||
}
|
||||
std::sort(BoundStratumFields.begin(),BoundStratumFields.end(),
|
||||
[](const auto& A,const auto& B){return A.BottomChunkZ<B.BottomChunkZ;});
|
||||
BuildOreVeinSpatialIndex();
|
||||
}
|
||||
|
||||
const FBoundStratumField* FindBoundStratumForChunk(int32 ChunkZ) const
|
||||
@@ -2972,6 +2973,27 @@ public:
|
||||
return AnalyticDensityAt(X,Y,Z);
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> OreVeinTileCandidates(
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum) const
|
||||
{return BuildOreVeinTileList(minimum,maximum);}
|
||||
|
||||
VoxelForgeCore::MaterialSampleV1 EvaluateMaterialAt(float X,float Y,float Z,
|
||||
std::span<const std::uint32_t> candidates,
|
||||
std::uint64_t* candidateChecks=nullptr) const
|
||||
{
|
||||
if(SharedDensitySource)
|
||||
return SharedDensitySource->MaterialAt(X,Y,Z,candidates,candidateChecks);
|
||||
return MaterialAt(X,Y,Z,candidates,candidateChecks);
|
||||
}
|
||||
|
||||
VoxelForgeCore::MaterialSampleV1 MaterialAt(float X,float Y,float Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->MaterialAt(X,Y,Z);
|
||||
const auto candidates=BuildOreVeinPointList(X,Y,Z);
|
||||
return EvaluateMaterialAt(X,Y,Z,candidates);
|
||||
}
|
||||
|
||||
std::string DensityDispatchPathAt(float X,float Y,float Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->DensityDispatchPathAt(X,Y,Z);
|
||||
@@ -3208,6 +3230,25 @@ private:
|
||||
std::uint64_t irregularitySeed=0;
|
||||
std::uint8_t quarterTurns=0;
|
||||
};
|
||||
struct FOreVeinSpatialBound
|
||||
{
|
||||
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
||||
};
|
||||
struct FOreVeinSpatialNode
|
||||
{
|
||||
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
||||
std::size_t first=0,end=0;
|
||||
int left=-1,right=-1;
|
||||
};
|
||||
struct FOreVeinRuntime
|
||||
{
|
||||
const VoxelForgeCore::ResolvedLayoutOreVeinV1* record=nullptr;
|
||||
std::array<std::array<double,3>,2> endpointsVoxels{};
|
||||
double radiusVoxels=0.0,irregularityAmplitudeVoxels=0.0;
|
||||
double irregularityWavelengthVoxels=0.0,blendWidthVoxels=0.0;
|
||||
std::uint64_t irregularitySeed=0;
|
||||
std::uint16_t resourceMaterialId=0;
|
||||
};
|
||||
struct FCavernRuntimeFeature
|
||||
{
|
||||
const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr;
|
||||
@@ -3349,6 +3390,258 @@ private:
|
||||
if(!StampSpatialOrder.empty())BuildStampSpatialNode(0,StampSpatialOrder.size());
|
||||
}
|
||||
|
||||
int BuildOreVeinSpatialNode(std::size_t first,std::size_t end)
|
||||
{
|
||||
FOreVeinSpatialNode node;node.first=first;node.end=end;
|
||||
node.minimumSubvoxels.fill(std::numeric_limits<std::int64_t>::max());
|
||||
node.maximumSubvoxels.fill(std::numeric_limits<std::int64_t>::min());
|
||||
for(std::size_t offset=first;offset<end;++offset)
|
||||
{
|
||||
const auto& bound=OreVeinSpatialBounds[OreVeinSpatialOrder[offset]];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
node.minimumSubvoxels[axis]=std::min(node.minimumSubvoxels[axis],
|
||||
bound.minimumSubvoxels[axis]);
|
||||
node.maximumSubvoxels[axis]=std::max(node.maximumSubvoxels[axis],
|
||||
bound.maximumSubvoxels[axis]);
|
||||
}
|
||||
}
|
||||
const int nodeIndex=static_cast<int>(OreVeinSpatialNodes.size());
|
||||
OreVeinSpatialNodes.push_back(node);
|
||||
if(end-first>8)
|
||||
{
|
||||
std::size_t splitAxis=0;
|
||||
for(std::size_t axis=1;axis<3;++axis)
|
||||
if(static_cast<long double>(node.maximumSubvoxels[axis])
|
||||
-node.minimumSubvoxels[axis]
|
||||
>static_cast<long double>(node.maximumSubvoxels[splitAxis])
|
||||
-node.minimumSubvoxels[splitAxis])splitAxis=axis;
|
||||
const std::size_t middle=first+(end-first)/2;
|
||||
std::nth_element(OreVeinSpatialOrder.begin()+first,
|
||||
OreVeinSpatialOrder.begin()+middle,OreVeinSpatialOrder.begin()+end,
|
||||
[this,splitAxis](std::uint32_t a,std::uint32_t b)
|
||||
{
|
||||
const auto& left=OreVeinSpatialBounds[a];
|
||||
const auto& right=OreVeinSpatialBounds[b];
|
||||
const long double leftCenter=static_cast<long double>(
|
||||
left.minimumSubvoxels[splitAxis])+left.maximumSubvoxels[splitAxis];
|
||||
const long double rightCenter=static_cast<long double>(
|
||||
right.minimumSubvoxels[splitAxis])+right.maximumSubvoxels[splitAxis];
|
||||
return leftCenter==rightCenter?a<b:leftCenter<rightCenter;
|
||||
});
|
||||
OreVeinSpatialNodes[nodeIndex].left=BuildOreVeinSpatialNode(first,middle);
|
||||
OreVeinSpatialNodes[nodeIndex].right=BuildOreVeinSpatialNode(middle,end);
|
||||
}
|
||||
return nodeIndex;
|
||||
}
|
||||
|
||||
void QueryOreVeinSpatialNode(int nodeIndex,
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum,
|
||||
std::vector<std::uint32_t>& result) const
|
||||
{
|
||||
if(nodeIndex<0)return;
|
||||
const auto& node=OreVeinSpatialNodes[static_cast<std::size_t>(nodeIndex)];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(node.maximumSubvoxels[axis]<minimum[axis]
|
||||
||node.minimumSubvoxels[axis]>maximum[axis])return;
|
||||
if(node.left<0)
|
||||
{
|
||||
for(std::size_t offset=node.first;offset<node.end;++offset)
|
||||
{
|
||||
const std::uint32_t index=OreVeinSpatialOrder[offset];
|
||||
const auto& bound=OreVeinSpatialBounds[index];bool overlap=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
overlap=overlap&&bound.minimumSubvoxels[axis]<=maximum[axis]
|
||||
&&bound.maximumSubvoxels[axis]>=minimum[axis];
|
||||
if(overlap)result.push_back(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
QueryOreVeinSpatialNode(node.left,minimum,maximum,result);
|
||||
QueryOreVeinSpatialNode(node.right,minimum,maximum,result);
|
||||
}
|
||||
|
||||
void BuildOreVeinSpatialIndex()
|
||||
{
|
||||
OreVeinSpatialBounds.clear();OreVeinSpatialOrder.clear();OreVeinSpatialNodes.clear();
|
||||
OreVeinRuntime.clear();
|
||||
if(!ResolvedLayout||ResolvedLayout->oreVeins.empty()||!Owner
|
||||
||Owner->CoreVoxelSizeMicrons==0)return;
|
||||
constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const long double scale=Q/Owner->CoreVoxelSizeMicrons;
|
||||
OreVeinSpatialBounds.reserve(ResolvedLayout->oreVeins.size());
|
||||
OreVeinRuntime.reserve(ResolvedLayout->oreVeins.size());
|
||||
for(const auto& vein:ResolvedLayout->oreVeins)
|
||||
{
|
||||
FOreVeinSpatialBound bound;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
bound.minimumSubvoxels[axis]=static_cast<std::int64_t>(std::floor(
|
||||
static_cast<long double>(vein.boundsMinimumMicrometres[axis])*scale));
|
||||
bound.maximumSubvoxels[axis]=static_cast<std::int64_t>(std::ceil(
|
||||
static_cast<long double>(vein.boundsMaximumMicrometres[axis])*scale));
|
||||
}
|
||||
OreVeinSpatialBounds.push_back(bound);
|
||||
FOreVeinRuntime runtime;runtime.record=&vein;
|
||||
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
runtime.endpointsVoxels[endpoint][axis]=
|
||||
static_cast<double>(vein.endpointsMicrometres[endpoint][axis])
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.radiusVoxels=static_cast<double>(vein.radiusMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.irregularityAmplitudeVoxels=
|
||||
static_cast<double>(vein.irregularityAmplitudeMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.irregularityWavelengthVoxels=
|
||||
static_cast<double>(vein.irregularityWavelengthMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.blendWidthVoxels=static_cast<double>(vein.blendWidthMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.irregularitySeed=vein.irregularitySeed;
|
||||
runtime.resourceMaterialId=vein.resourceMaterialId;
|
||||
OreVeinRuntime.push_back(runtime);
|
||||
}
|
||||
OreVeinSpatialOrder.resize(OreVeinSpatialBounds.size());
|
||||
for(std::size_t i=0;i<OreVeinSpatialOrder.size();++i)
|
||||
OreVeinSpatialOrder[i]=static_cast<std::uint32_t>(i);
|
||||
if(!OreVeinSpatialOrder.empty())BuildOreVeinSpatialNode(0,OreVeinSpatialOrder.size());
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildOreVeinTileList(
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum) const
|
||||
{
|
||||
std::vector<std::uint32_t> result;
|
||||
if(OreVeinSpatialBounds.empty())return result;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(minimum[axis]>maximum[axis])
|
||||
{
|
||||
result.resize(OreVeinSpatialBounds.size());
|
||||
for(std::size_t i=0;i<result.size();++i)
|
||||
result[i]=static_cast<std::uint32_t>(i);
|
||||
return result;
|
||||
}
|
||||
QueryOreVeinSpatialNode(0,minimum,maximum,result);
|
||||
std::sort(result.begin(),result.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildOreVeinPointList(float X,float Y,float Z) const
|
||||
{
|
||||
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const auto toQ=[](float value)
|
||||
{
|
||||
const long double scaled=static_cast<long double>(value)*Q;
|
||||
if(!std::isfinite(value)||scaled<std::numeric_limits<std::int64_t>::min()
|
||||
||scaled>std::numeric_limits<std::int64_t>::max())return std::int64_t{0};
|
||||
return static_cast<std::int64_t>(std::llround(scaled));
|
||||
};
|
||||
const std::array<std::int64_t,3> point={toQ(X),toQ(Y),toQ(Z)};
|
||||
return BuildOreVeinTileList(point,point);
|
||||
}
|
||||
|
||||
VoxelForgeCore::MaterialSampleV1 MaterialAt(float X,float Y,float Z,
|
||||
std::span<const std::uint32_t> candidates,
|
||||
std::uint64_t* candidateChecks=nullptr) const
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
MaterialSampleV1 result;
|
||||
if(!ResolvedLayout||OreVeinRuntime.empty()||candidates.empty()
|
||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return result;
|
||||
const FBoundStratumField* bound=FindBoundStratumForZ(Z);
|
||||
if(!bound)return result;
|
||||
const std::array<double,3> point={X,Y,Z};
|
||||
const auto noiseAt=[](std::uint64_t seed,std::int64_t x,std::int64_t y,std::int64_t z)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
for(const std::uint64_t word:{seed,static_cast<std::uint64_t>(x),
|
||||
static_cast<std::uint64_t>(y),static_cast<std::uint64_t>(z)})
|
||||
for(int byte=0;byte<8;++byte)
|
||||
{hash^=static_cast<std::uint8_t>(word>>(byte*8));hash*=1099511628211ull;}
|
||||
hash^=hash>>30;hash*=0xbf58476d1ce4e5b9ull;
|
||||
hash^=hash>>27;hash*=0x94d049bb133111ebull;hash^=hash>>31;
|
||||
return static_cast<double>(static_cast<std::uint32_t>(hash>>32))/2147483647.5-1.0;
|
||||
};
|
||||
const auto fade=[](double value){return value*value*(3.0-2.0*value);};
|
||||
for(const std::uint32_t candidate:candidates)
|
||||
{
|
||||
if(candidateChecks)++*candidateChecks;
|
||||
if(candidate>=OreVeinRuntime.size())continue;
|
||||
const auto& vein=OreVeinRuntime[candidate];
|
||||
if(!vein.record||vein.record->strateIndex!=bound->StrateIndex)continue;
|
||||
const double wavelength=vein.irregularityWavelengthVoxels;
|
||||
double irregularity=0.0;
|
||||
if(vein.irregularityAmplitudeVoxels>0.0&&wavelength>0.0)
|
||||
{
|
||||
std::array<std::int64_t,3> cell{};std::array<double,3> t{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double coordinate=point[axis]/wavelength;
|
||||
cell[axis]=static_cast<std::int64_t>(std::floor(coordinate));
|
||||
t[axis]=fade(coordinate-cell[axis]);
|
||||
}
|
||||
double layer[2][2]{};
|
||||
for(int z=0;z<2;++z)for(int y=0;y<2;++y)
|
||||
{
|
||||
const double a=noiseAt(vein.irregularitySeed,cell[0],cell[1]+y,cell[2]+z);
|
||||
const double b=noiseAt(vein.irregularitySeed,cell[0]+1,cell[1]+y,cell[2]+z);
|
||||
layer[z][y]=std::lerp(a,b,t[0]);
|
||||
}
|
||||
irregularity=std::lerp(std::lerp(layer[0][0],layer[0][1],t[1]),
|
||||
std::lerp(layer[1][0],layer[1][1],t[1]),t[2]);
|
||||
}
|
||||
const auto& a=vein.endpointsVoxels[0];const auto& b=vein.endpointsVoxels[1];
|
||||
double lengthSquared=0.0,dot=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{const double delta=b[axis]-a[axis];lengthSquared+=delta*delta;dot+=(point[axis]-a[axis])*delta;}
|
||||
const double t=lengthSquared>0.0?std::clamp(dot/lengthSquared,0.0,1.0):0.0;
|
||||
double distanceSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{const double delta=point[axis]-(a[axis]+t*(b[axis]-a[axis]));distanceSquared+=delta*delta;}
|
||||
const double distance=std::sqrt(distanceSquared);
|
||||
const double radius=std::max(0.0,vein.radiusVoxels
|
||||
+irregularity*vein.irregularityAmplitudeVoxels);
|
||||
if(distance>=radius+vein.blendWidthVoxels)continue;
|
||||
double weight=1.0;
|
||||
if(distance>radius)
|
||||
{
|
||||
const double u=std::clamp((radius+vein.blendWidthVoxels-distance)
|
||||
/vein.blendWidthVoxels,0.0,1.0);
|
||||
weight=u*u*(3.0-2.0*u);
|
||||
}
|
||||
const auto weightQ16=static_cast<std::uint16_t>(std::clamp(
|
||||
std::llround(weight*65535.0),0ll,65535ll));
|
||||
if(weightQ16==0)continue;
|
||||
std::size_t slot=2;
|
||||
for(std::size_t index=0;index<2;++index)
|
||||
if(result.resourceMaterialIds[index]==vein.resourceMaterialId)
|
||||
{slot=index;break;}
|
||||
if(slot==2)
|
||||
for(std::size_t index=0;index<2;++index)
|
||||
if(result.resourceMaterialIds[index]==0){slot=index;break;}
|
||||
if(slot<2)
|
||||
{
|
||||
result.resourceMaterialIds[slot]=vein.resourceMaterialId;
|
||||
const std::uint32_t sum=static_cast<std::uint32_t>(
|
||||
result.resourceWeightsQ16[slot])+weightQ16;
|
||||
result.resourceWeightsQ16[slot]=static_cast<std::uint16_t>(
|
||||
std::min<std::uint32_t>(65535,sum));
|
||||
}
|
||||
}
|
||||
const std::uint32_t total=static_cast<std::uint32_t>(result.resourceWeightsQ16[0])
|
||||
+result.resourceWeightsQ16[1];
|
||||
if(total>65535)
|
||||
{
|
||||
result.resourceWeightsQ16[0]=static_cast<std::uint16_t>(
|
||||
static_cast<std::uint64_t>(result.resourceWeightsQ16[0])*65535/total);
|
||||
result.resourceWeightsQ16[1]=static_cast<std::uint16_t>(
|
||||
65535-result.resourceWeightsQ16[0]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildStampTileList(
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum) const
|
||||
@@ -9412,6 +9705,10 @@ private:
|
||||
std::vector<FStampSpatialNode> StampSpatialNodes;
|
||||
std::vector<std::uint32_t> StampSpatialOrder;
|
||||
std::vector<FStampRuntime> StampRuntime;
|
||||
std::vector<FOreVeinSpatialBound> OreVeinSpatialBounds;
|
||||
std::vector<FOreVeinSpatialNode> OreVeinSpatialNodes;
|
||||
std::vector<std::uint32_t> OreVeinSpatialOrder;
|
||||
std::vector<FOreVeinRuntime> OreVeinRuntime;
|
||||
std::vector<FCavernRuntimeFeature> CavernRuntime;
|
||||
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> PassageFeatureBounds;
|
||||
std::vector<FPassageTileFeature> PassageTileFeatures;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <new>
|
||||
@@ -216,6 +217,19 @@ namespace VoxelForgeCore
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::uint64_t HashMaterialSamples(std::span<const MeshVertex> vertices)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
AddWord(hash,vertices.size(),8);
|
||||
for(const MeshVertex& vertex:vertices)
|
||||
{
|
||||
AddWord(hash,vertex.hostMaterialId,2);
|
||||
for(const std::uint16_t id:vertex.resourceMaterialIds)AddWord(hash,id,2);
|
||||
for(const std::uint16_t weight:vertex.resourceWeightsQ16)AddWord(hash,weight,2);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::uint64_t HashBrushTransaction(const EditTransaction& transaction)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
@@ -1706,6 +1720,62 @@ namespace VoxelForgeCore
|
||||
|
||||
float World::densityAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
MaterialSampleV1 World::materialAt(Vec3d position) const
|
||||
{
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)
|
||||
return {};
|
||||
return impl_->shape->MaterialAt(static_cast<float>(position.x),
|
||||
static_cast<float>(position.y),static_cast<float>(position.z));
|
||||
}
|
||||
OreMiningYieldV1 World::measureOreMiningYieldForSphere(Vec3d centerVoxel,
|
||||
double radiusMetres,double sampleSpacingMetres) const
|
||||
{
|
||||
OreMiningYieldV1 result;result.sampleSpacingMetres=sampleSpacingMetres;
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape
|
||||
||!std::isfinite(radiusMetres)||!std::isfinite(sampleSpacingMetres)
|
||||
||radiusMetres<=0.0||sampleSpacingMetres<0.05||sampleSpacingMetres>2.0)
|
||||
return result;
|
||||
const std::uint32_t voxelMicrometres=impl_->configuration.voxelSizeMicrons;
|
||||
if(voxelMicrometres==0)return result;
|
||||
const double voxelMetres=static_cast<double>(voxelMicrometres)/1000000.0;
|
||||
const double radiusVoxels=radiusMetres/voxelMetres;
|
||||
const double spacingVoxels=sampleSpacingMetres/voxelMetres;
|
||||
const int steps=static_cast<int>(std::ceil(radiusMetres/sampleSpacingMetres));
|
||||
const double radiusSquared=radiusMetres*radiusMetres;
|
||||
std::map<std::uint16_t,std::uint64_t> weightedSamples;
|
||||
for(int z=-steps;z<=steps;++z)
|
||||
for(int y=-steps;y<=steps;++y)
|
||||
for(int x=-steps;x<=steps;++x)
|
||||
{
|
||||
const double dx=x*sampleSpacingMetres;
|
||||
const double dy=y*sampleSpacingMetres;
|
||||
const double dz=z*sampleSpacingMetres;
|
||||
const double distanceSquared=dx*dx+dy*dy+dz*dz;
|
||||
if(distanceSquared>radiusSquared)continue;
|
||||
const Vec3d point={centerVoxel.x+x*spacingVoxels,
|
||||
centerVoxel.y+y*spacingVoxels,centerVoxel.z+z*spacingVoxels};
|
||||
const float before=impl_->shape->DensityAt(static_cast<float>(point.x),
|
||||
static_cast<float>(point.y),static_cast<float>(point.z));
|
||||
const double carveDensity=radiusVoxels
|
||||
-std::sqrt(distanceSquared)/voxelMetres;
|
||||
const double after=std::max(static_cast<double>(before),carveDensity);
|
||||
if(!(before<0.0f&&after>=0.0))continue;
|
||||
++result.solidSamplesRemoved;
|
||||
const MaterialSampleV1 material=materialAt(point);
|
||||
for(std::size_t slot=0;slot<2;++slot)
|
||||
if(material.resourceMaterialIds[slot]
|
||||
&&material.resourceWeightsQ16[slot])
|
||||
weightedSamples[material.resourceMaterialIds[slot]]
|
||||
+=material.resourceWeightsQ16[slot];
|
||||
}
|
||||
const double sampleVolume=sampleSpacingMetres*sampleSpacingMetres
|
||||
*sampleSpacingMetres;
|
||||
result.resources.reserve(weightedSamples.size());
|
||||
for(const auto& [id,weighted]:weightedSamples)
|
||||
result.resources.push_back({id,weighted,
|
||||
static_cast<double>(weighted)/65535.0*sampleVolume});
|
||||
return result;
|
||||
}
|
||||
std::vector<PassageVoidCrossingAuditV1> World::passageVoidCrossings() const
|
||||
{
|
||||
std::vector<PassageVoidCrossingAuditV1> result;
|
||||
@@ -2636,6 +2706,21 @@ namespace VoxelForgeCore
|
||||
out.skirtFaceMasks.assign(mesh.TriangleSkirtFaceMasks.begin(),
|
||||
mesh.TriangleSkirtFaceMasks.end());
|
||||
out.skirtFaceMasks.resize(mesh.Triangles.Num()/3,0);
|
||||
constexpr std::int64_t MaterialSubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<std::int64_t,3> materialTileMinimum={
|
||||
(static_cast<std::int64_t>(origin.X)-step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Y)-step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Z)-step)*MaterialSubvoxelsPerVoxel};
|
||||
const std::array<std::int64_t,3> materialTileMaximum={
|
||||
(static_cast<std::int64_t>(origin.X)+extent+step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Y)+extent+step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Z)+extent+step)*MaterialSubvoxelsPerVoxel};
|
||||
const auto materialCandidates=tileShape.OreVeinTileCandidates(
|
||||
materialTileMinimum,materialTileMaximum);
|
||||
out.diagnostics.materialTileOutsideEveryVein=materialCandidates.empty();
|
||||
const double geometryVoxelSizeCm=static_cast<double>(world.CoreVoxelSizeMicrons)/10000.0;
|
||||
std::uint64_t materialQueries=0,materialCandidateChecks=0;
|
||||
const auto materialQueryStart=std::chrono::steady_clock::now();
|
||||
out.vertices.reserve(static_cast<std::size_t>(mesh.Vertices.Num()));
|
||||
for(int32 i=0;i<mesh.Vertices.Num();++i)
|
||||
{
|
||||
@@ -2643,9 +2728,25 @@ namespace VoxelForgeCore
|
||||
const FVector normal=i<mesh.Normals.Num()?mesh.Normals[i]:FVector::ZeroVector;
|
||||
const FVector2f uv=i<mesh.UVs.Num()?mesh.UVs[i]:FVector2f(0.0f,0.0f);
|
||||
const FColor color=i<mesh.Colors.Num()?mesh.Colors[i]:FColor(0,0,0,0);
|
||||
MaterialSampleV1 material;
|
||||
if(!materialCandidates.empty())
|
||||
{
|
||||
++materialQueries;
|
||||
material=tileShape.EvaluateMaterialAt(
|
||||
static_cast<float>(p.X/geometryVoxelSizeCm),
|
||||
static_cast<float>(p.Y/geometryVoxelSizeCm),
|
||||
static_cast<float>(p.Z/geometryVoxelSizeCm),materialCandidates,
|
||||
&materialCandidateChecks);
|
||||
}
|
||||
out.vertices.push_back({{p.X,p.Y,p.Z},{normal.X,normal.Y,normal.Z},{uv.X,uv.Y},
|
||||
{color.R,color.G,color.B,color.A}});
|
||||
{color.R,color.G,color.B,color.A},material.hostMaterialId,
|
||||
material.resourceMaterialIds,material.resourceWeightsQ16});
|
||||
}
|
||||
out.diagnostics.materialQueries=materialQueries;
|
||||
out.diagnostics.materialCandidateChecks=materialCandidateChecks;
|
||||
out.diagnostics.materialQueryMilliseconds=materialQueries
|
||||
?std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-materialQueryStart).count():0.0;
|
||||
out.indices.reserve(static_cast<std::size_t>(mesh.Triangles.Num()));
|
||||
for(const int32 index:mesh.Triangles)out.indices.push_back(static_cast<std::uint32_t>(index));
|
||||
if(options.captureDensitySamples)out.debugDensitySamples=std::move(densityValues);
|
||||
@@ -2657,6 +2758,7 @@ namespace VoxelForgeCore
|
||||
{ densityHash^=std::bit_cast<std::uint32_t>(value);densityHash*=1099511628211ull; }
|
||||
out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh);
|
||||
out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh);
|
||||
out.materialHash=HashMaterialSamples(out.vertices);
|
||||
if(key.lod>=4)
|
||||
{
|
||||
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
|
||||
@@ -2664,6 +2766,7 @@ namespace VoxelForgeCore
|
||||
out.topologyHash=SaltFarHorizonHash(out.topologyHash,options.farHorizonVersion);
|
||||
}
|
||||
out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true;
|
||||
out.hasMaterialHash=true;
|
||||
}
|
||||
out.diagnostics.meshAssemblyMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-MeshAssemblyStart).count();
|
||||
|
||||
Reference in new issue
Block a user