wip(assembler): round S4 -- continuous far ceiling at the strate's upper boundary

S3's far cap sat up to 8.2 m above the near roof and read as floating pieces.
The far render cap now projects onto the owning strate's upper boundary
(density unchanged), on a global XY lattice, LOD4 16-voxel grid + LOD5-8
128-voxel. Cap agreement 0.05 cm (100 cm control detected), 2520/2520 upward
rays hit roof on the owner walk. Far recipe v2; LOD0-3 hashes equal S2/S3.
Tile p95 flat; cold clipmap +4.4%. Far-cliff fluting left open (coarse MC step).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 00:41:13 +02:00
1 parent d0f97bf79a
commit 9d3c50ca95
14 files changed
+1376 -34

No files matched your search

@@ -195,7 +195,7 @@ namespace VoxelForgeCore
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1;
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 4;
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 5;
enum class FieldChannel : std::uint32_t
{
@@ -662,6 +662,13 @@ namespace VoxelForgeCore
bool isValid() const noexcept;
HeightfieldPointSample point(std::int64_t xSubvoxels, std::int64_t ySubvoxels,
std::int64_t zSubvoxels) const noexcept;
// The far render surface is clipped to this strate's owned upper boundary; density stays unchanged.
// La surface lointaine est bornee au haut de la strate; la densite reste inchangee.
std::int64_t farSurfaceCapHeightSubvoxels(std::int64_t xSubvoxels,
std::int64_t ySubvoxels) const noexcept;
// Conservative global Z extent of this declared far roof, including biome and cap features.
// Bornes Z prudentes du plafond lointain declare, avec les biomes et les fonctions de cap.
std::array<std::int64_t, 2> capHeightBounds() const noexcept;
WorldAssemblyFeatureTileListV1 buildFeatureTileList(
const std::array<std::int64_t, 3>& tileMinimumSubvoxels,
const std::array<std::int64_t, 3>& tileMaximumSubvoxels) const;
@@ -995,11 +1002,11 @@ namespace VoxelForgeCore
// Validation control only; never participates in recipe/world identity.
// Controle de validation seulement, sans effet sur l'identite du monde.
bool useAssemblyFeatureQueries = true;
// Bump when the far-horizon mesh representation changes. Far tile mesh hashes are
// salted with this version; density and LOD0-3 hashes are intentionally unchanged.
// Incremente quand la representation d'horizon lointain change; les hashes de maillage
// lointain incluent cette version, sans toucher a la densite ni aux LOD0-3.
std::uint32_t farHorizonVersion = 1;
// Bump when the distant cap representation changes. LOD4-8 mesh hashes are salted;
// density and LOD0-3 hashes remain unchanged.
// Incremente quand le plafond lointain change. Les hashes LOD4-8 incluent cette version;
// densite et hashes LOD0-3 restent inchanges.
std::uint32_t farHorizonVersion = 2;
};
class VFCORE_API World
+81 -15
View File
@@ -10,6 +10,7 @@
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <set>
namespace vfcore::detail
{
@@ -331,11 +332,11 @@ namespace VoxelForgeCore
for(const auto& Plan:Manifest.plan.strates)
{
const VoxelForgeCore::Heightfield2D Block(Manifest.recipe,Plan);
if(!Block.isValid())continue;
const int64 StratumMinZ=static_cast<int64>(Plan.bottomChunkZ)*CHUNK_SIZE*Scale;
const int64 StratumMaxZ=(static_cast<int64>(Plan.topChunkZ)+1)*CHUNK_SIZE*Scale;
if(TileMaxZ<StratumMinZ||TileMinZ>StratumMaxZ)continue;
const VoxelForgeCore::Heightfield2D Block(Manifest.recipe,Plan);
if(!Block.isValid())continue;
struct FGridSample
{
double Ground=0.0,Cap=0.0;
@@ -576,10 +577,10 @@ namespace VoxelForgeCore
const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,FVoxelMeshData& Mesh)
{
FNativeCapRefinementStats Stats;
if(Key.Level<5||!Owner.Generator)return Stats;
constexpr int32 GridStepVoxels=128;
if(Key.Level<4||!Owner.Generator)return Stats;
const int32 GridStepVoxels=Key.Level<5?std::min(128,1<<Key.Level):128;
constexpr int64 Scale=WorldAssemblySubvoxelsPerVoxel;
constexpr int64 GridStepSubvoxels=GridStepVoxels*Scale;
const int64 GridStepSubvoxels=static_cast<int64>(GridStepVoxels)*Scale;
const FIntVector TileOrigin=Key.OriginVoxels();
const int32 TileExtent=Key.ExtentVoxels();
if(TileExtent<=0||TileExtent%GridStepVoxels!=0)return Stats;
@@ -599,11 +600,10 @@ namespace VoxelForgeCore
for(const auto& Plan:Manifest.plan.strates)
{
const int64 StratumMinZ=static_cast<int64>(Plan.bottomChunkZ)*CHUNK_SIZE*Scale;
const int64 StratumMaxZ=(static_cast<int64>(Plan.topChunkZ)+1)*CHUNK_SIZE*Scale;
if(TileMaxZ<StratumMinZ||TileMinZ>StratumMaxZ)continue;
const VoxelForgeCore::Heightfield2D Block(Manifest.recipe,Plan);
if(!Block.isValid())continue;
const auto CapHeightBounds=Block.capHeightBounds();
if(TileMaxZ<CapHeightBounds[0]||TileMinZ>CapHeightBounds[1])continue;
struct FGridSample
{
@@ -623,7 +623,7 @@ namespace VoxelForgeCore
const auto Column=Block.point(XSub,YSub,0);
auto& Sample=Grid[GridIndex(X,Y)];
Sample.Ground=static_cast<double>(Column.groundHeightSubvoxels);
Sample.Cap=static_cast<double>(Column.capHeightSubvoxels);
Sample.Cap=static_cast<double>(Block.farSurfaceCapHeightSubvoxels(XSub,YSub));
int32 Dominant=0,Neighbor=0;float Blend=0.0f;
Owner.Generator->GetBiomeMaterialAt(static_cast<float>(XSub)/Scale,
static_cast<float>(YSub)/Scale,static_cast<float>(Sample.Cap/Scale),
@@ -1160,6 +1160,71 @@ namespace VoxelForgeCore
VoxelClipmapDesiredTiles::Build(center,minZ,maxZ,parameters,nativeTiles);
std::vector<TileKey> tiles;tiles.reserve(static_cast<std::size_t>(nativeTiles.Num()));
for(const FVoxelTileKey& key:nativeTiles)tiles.push_back(FromNative(key));
if(impl_->worldAssembly&&!impl_->worldAssembly->plan.strates.empty())
{
constexpr std::int64_t SubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel;
const auto FloorDiv64=[](std::int64_t Value,std::int64_t Divisor)
{
const std::int64_t Quotient=Value/Divisor,Remainder=Value%Divisor;
return Quotient-((Remainder!=0&&Remainder<0)?1:0);
};
std::set<std::array<std::int32_t,3>> FarSurfaceColumns;
std::set<std::array<std::int32_t,4>> ExistingKeys;
for(const TileKey& Key:tiles)
{
ExistingKeys.insert({Key.coordinate.x,Key.coordinate.y,Key.coordinate.z,Key.lod});
if(Key.lod>=4&&Key.lod<=8)
FarSurfaceColumns.insert({Key.coordinate.x,Key.coordinate.y,Key.lod});
}
for(const auto& Plan:impl_->worldAssembly->plan.strates)
{
const Heightfield2D Field(impl_->worldAssembly->recipe,Plan);
if(!Field.isValid())continue;
const auto CapBounds=Field.capHeightBounds();
for(const auto& Column:FarSurfaceColumns)
{
const std::int32_t Lod=Column[2];
const std::int64_t Extent=static_cast<std::int64_t>(32)<<Lod;
const std::int64_t TileHeightSubvoxels=Extent*SubvoxelsPerVoxel;
const std::int64_t MinimumBand=FloorDiv64(CapBounds[0],TileHeightSubvoxels);
const std::int64_t MaximumBand=FloorDiv64(CapBounds[1],TileHeightSubvoxels);
for(std::int64_t Band=MinimumBand;Band<=MaximumBand;++Band)
{
if(Band<MIN_int32||Band>MAX_int32)continue;
const std::array<std::int32_t,4> Identity={Column[0],Column[1],
static_cast<std::int32_t>(Band),Lod};
if(!ExistingKeys.insert(Identity).second)continue;
const std::int64_t CandidateMinX=static_cast<std::int64_t>(Identity[0])*Extent;
const std::int64_t CandidateMinY=static_cast<std::int64_t>(Identity[1])*Extent;
const std::int64_t CandidateMinZ=
static_cast<std::int64_t>(Identity[2])*Extent;
bool bOverlapsClipmapTile=false;
for(const TileKey& Existing:tiles)
{
const std::int64_t ExistingExtent=static_cast<std::int64_t>(32)<<Existing.lod;
const std::int64_t ExistingMinX=static_cast<std::int64_t>(Existing.coordinate.x)*ExistingExtent;
const std::int64_t ExistingMinY=static_cast<std::int64_t>(Existing.coordinate.y)*ExistingExtent;
const std::int64_t ExistingMinZ=static_cast<std::int64_t>(Existing.coordinate.z)*ExistingExtent;
const bool bOverlapX=ExistingMinX<CandidateMinX+Extent
&&CandidateMinX<ExistingMinX+ExistingExtent;
const bool bOverlapY=ExistingMinY<CandidateMinY+Extent
&&CandidateMinY<ExistingMinY+ExistingExtent;
const bool bOverlapZ=ExistingMinZ<CandidateMinZ+Extent
&&CandidateMinZ<ExistingMinZ+ExistingExtent;
if(bOverlapX&&bOverlapY&&bOverlapZ)
{bOverlapsClipmapTile=true;break;}
}
// Preserve the clipmap's non-overlapping 3D partition. Where another
// requested tile already occupies this cap volume, that tile owns the
// shared roof surface; never add an overlapping LOD sibling.
// Garder la partition 3D sans recouvrement. Une tuile deja presente
// possede la surface de toit partagee; ne pas ajouter un voisin LOD superpose.
if(bOverlapsClipmapTile)continue;
tiles.push_back({{Identity[0],Identity[1],Identity[2]},Identity[3]});
}
}
}
}
std::sort(tiles.begin(),tiles.end(),[&](const TileKey& a,const TileKey& b)
{
const double da=TileDistanceSquared(a,{center.X,center.Y,center.Z});
@@ -1201,7 +1266,7 @@ namespace VoxelForgeCore
&&options.mesherRecipe!=MesherRecipe::worldAssemblerHybridV1
&&options.mesherRecipe!=MesherRecipe::worldAssemblerHybridV2)
{error="tile mesher recipe version is unsupported";return std::nullopt;}
if(level>=5&&options.farHorizonVersion==0)
if(level>=4&&options.farHorizonVersion==0)
{error="far-horizon recipe version must be nonzero";return std::nullopt;}
TArray<FVoxelHybridSharpFeatureBox> sharpFeatures;
if(options.mesherRecipe!=MesherRecipe::marchingCubesV1&&level==0&&impl_->worldAssembly)
@@ -1501,7 +1566,7 @@ namespace VoxelForgeCore
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-MeshingStart).count();
out.diagnostics.farHorizonVersion=key.lod>=5?options.farHorizonVersion:0;
out.diagnostics.farHorizonVersion=key.lod>=4?options.farHorizonVersion:0;
const auto FarShellStart=std::chrono::steady_clock::now();
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape;
out.diagnostics.farShellRepairSkipped=bSkipFarShellRepair;
@@ -1533,11 +1598,12 @@ namespace VoxelForgeCore
}
out.diagnostics.farShellMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-FarShellStart).count();
if(impl_->worldAssembly&&key.lod>=5&&!bOverlapsPlayerEdit)
if(impl_->worldAssembly&&key.lod>=4&&!bOverlapsPlayerEdit)
{
const auto HorizonStart=std::chrono::steady_clock::now();
const auto GroundStats=RefineFarNativeGround(
world,*impl_->worldAssembly,nativeKey,mesh);
FNativeCapRefinementStats GroundStats;
if(key.lod>=4)
GroundStats=RefineFarNativeGround(world,*impl_->worldAssembly,nativeKey,mesh);
const auto CapStats=RefineFarNativeCap(world,*impl_->worldAssembly,nativeKey,mesh);
out.diagnostics.farHorizonMilliseconds=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-HorizonStart).count();
@@ -1612,7 +1678,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);
if(key.lod>=5)
if(key.lod>=4)
{
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
out.payloadHash=SaltFarHorizonHash(out.payloadHash,options.farHorizonVersion);
+61 -1
View File
@@ -1463,8 +1463,9 @@ namespace world_assembly_detail
+secondaryHeight*(65536-primaryWeight))/65536;
out.biomeId=primary->stableId;out.neighborBiomeId=secondary->stableId;
out.biomeWeightQ16=primaryWeight;
out.capHeightSubvoxels=plan.capHeightSubvoxels+MulQ16(plan.capUndulationSubvoxels,
std::int64_t cap=plan.capHeightSubvoxels+MulQ16(plan.capUndulationSubvoxels,
Noise2D(x,y,plan.capWavelengthSubvoxels,recipe.seed,0x4341505f554e4431ull));
out.capHeightSubvoxels=cap;
out.waterSurfaceSubvoxels=plan.waterSurfaceSubvoxels;
return out;
}
@@ -1816,6 +1817,65 @@ bool Heightfield2D::isValid() const noexcept
});
}
std::array<std::int64_t,2> Heightfield2D::capHeightBounds() const noexcept
{
if(!isValid())return {0,0};
long double Minimum=std::numeric_limits<long double>::infinity();
long double Maximum=-std::numeric_limits<long double>::infinity();
const auto IncludeBiome=[&](const WorldAssemblyBiomeProfileV1& Biome)
{
const long double Base=Biome.hasCapProfile
?static_cast<long double>(Biome.capHeightSubvoxels)
:static_cast<long double>(plan_->capHeightSubvoxels);
const long double Undulation=Biome.hasCapProfile
?static_cast<long double>(Biome.capUndulationSubvoxels)
:static_cast<long double>(plan_->capUndulationSubvoxels);
const long double Detail=Biome.hasCapProfile
?static_cast<long double>(Biome.capDetailAmplitudeSubvoxels)
:static_cast<long double>(plan_->capDetailAmplitudeSubvoxels);
const long double Ridge=Biome.hasCapProfile
?static_cast<long double>(Biome.capRidgeAmplitudeSubvoxels)
:static_cast<long double>(plan_->capRidgeAmplitudeSubvoxels);
Minimum=std::min(Minimum,Base-Undulation-Detail-Ridge);
Maximum=std::max(Maximum,Base+Undulation);
};
if(recipe_->biomes.empty())
{
const WorldAssemblyBiomeProfileV1 Fallback{};
IncludeBiome(Fallback);
}
else for(const auto& Biome:recipe_->biomes)IncludeBiome(Biome);
long double FeatureAmplitude=0.0L;
for(const auto& Feature:plan_->features)
if(Feature.type==3)
FeatureAmplitude+=std::abs(static_cast<long double>(Feature.parametersSubvoxels[0]));
Minimum-=FeatureAmplitude;Maximum+=FeatureAmplitude;
if(recipe_->evaluatorVersion>=5)
{
const long double TopSubvoxels=(static_cast<long double>(plan_->topChunkZ)+1.0L)
*CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel;
Minimum=std::min(Minimum,TopSubvoxels);
Maximum=std::min(Maximum,TopSubvoxels);
}
const long double Int64Minimum=static_cast<long double>(std::numeric_limits<std::int64_t>::min());
const long double Int64Maximum=static_cast<long double>(std::numeric_limits<std::int64_t>::max());
return {static_cast<std::int64_t>(std::clamp(Minimum,Int64Minimum,Int64Maximum)),
static_cast<std::int64_t>(std::clamp(Maximum,Int64Minimum,Int64Maximum))};
}
std::int64_t Heightfield2D::farSurfaceCapHeightSubvoxels(std::int64_t x,
std::int64_t y) const noexcept
{
if(!isValid())return 0;
const HeightfieldPointSample column=point(x,y,0);
if(recipe_->evaluatorVersion<5)return column.capHeightSubvoxels;
// Clip only the far render surface. Keep the near density evaluator bit-identical.
// Borner uniquement le plafond lointain. L'evaluateur de densite proche reste identique.
const std::int64_t ownedUpperBoundary=(static_cast<std::int64_t>(plan_->topChunkZ)+1)
*CoreTileVoxelCount*WorldAssemblySubvoxelsPerVoxel;
return std::min(column.capHeightSubvoxels,ownedUpperBoundary);
}
WorldAssemblyFeatureTileListV1 Heightfield2D::buildFeatureTileList(
const std::array<std::int64_t,3>& minimum,
const std::array<std::int64_t,3>& maximum) const