wip(assembler): M1 round 6 -- owner walk fixes: 25 cm geometry scale, one height datum, carve/fill sign, faster relief meshing
Owner saw content floating above native terrain, spiky triangles, reversed carve/fill and late relief. Causes: shared mesher placed Core tiles on a 12.5 cm grid; content/atmosphere asked strate ranges in 12.5 cm voxels; Core edits add density where the legacy diff subtracts. Horizon LOD8 bridge edge 5.657 -> 2.265 cells. Relief mesh p95 60.8 -> 42.0 ms (LOD0). New BuildHost checks for scale, height, blueprint carve/fill and per-kind streaming lag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
f67b519e75
commit
61633cb492
18 files changed
+1708
-142
No files matched your search
@@ -976,47 +976,60 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
||||
const double Offset=-(GX*(PX-X)+GY*(PY-Y))/GZ;
|
||||
return std::clamp(Crossing+Offset,ZMin,ZMax);
|
||||
};
|
||||
const int32 Base=static_cast<int32>(NewVertices.size());
|
||||
const double Xs[4]={X0,X1,X1,X0},Ys[4]={Y0,Y0,Y1,Y1};
|
||||
std::array<double,4> CornerZ{};
|
||||
std::array<FVector,4> CornerNormals{};
|
||||
// Les correctifs lointains suivent chaque cellule de la grille, sans grande nappe.
|
||||
// Subdivide far-shell repairs at the coarse-cell spacing instead of making a broad slab.
|
||||
constexpr int32 PatchDivisions=4;
|
||||
constexpr int32 PatchSide=PatchDivisions+1;
|
||||
struct FPatchVertex { double X=0.0,Y=0.0,Z=0.0;FVector Normal=FVector(0,0,1); };
|
||||
std::array<FPatchVertex,PatchSide*PatchSide> Patch{};
|
||||
bool bValidPatch=true;
|
||||
for(int32 Corner=0;Corner<4;++Corner)
|
||||
for(int32 PY=0;PY<PatchSide&&bValidPatch;++PY)
|
||||
for(int32 PX=0;PX<PatchSide;++PX)
|
||||
{
|
||||
const double PredictedZ=SurfaceZ(Xs[Corner],Ys[Corner]);
|
||||
const std::vector<double> CornerCrossings=FindFarStratumCrossings(
|
||||
Shape,Xs[Corner],Ys[Corner],ZMin,ZMax);
|
||||
if(CornerCrossings.empty()){bValidPatch=false;break;}
|
||||
CornerZ[Corner]=*std::min_element(CornerCrossings.begin(),CornerCrossings.end(),
|
||||
FPatchVertex& Vertex=Patch[static_cast<std::size_t>(PY*PatchSide+PX)];
|
||||
Vertex.X=X0+(X1-X0)*static_cast<double>(PX)/PatchDivisions;
|
||||
Vertex.Y=Y0+(Y1-Y0)*static_cast<double>(PY)/PatchDivisions;
|
||||
const double PredictedZ=SurfaceZ(Vertex.X,Vertex.Y);
|
||||
const std::vector<double> Crossings=FindFarStratumCrossings(
|
||||
Shape,Vertex.X,Vertex.Y,ZMin,ZMax);
|
||||
if(Crossings.empty()){bValidPatch=false;break;}
|
||||
Vertex.Z=*std::min_element(Crossings.begin(),Crossings.end(),
|
||||
[PredictedZ](double A,double B)
|
||||
{return std::abs(A-PredictedZ)<std::abs(B-PredictedZ);});
|
||||
const float CornerGX=Density(Xs[Corner]+2,Ys[Corner],CornerZ[Corner])
|
||||
-Density(Xs[Corner]-2,Ys[Corner],CornerZ[Corner]);
|
||||
const float CornerGY=Density(Xs[Corner],Ys[Corner]+2,CornerZ[Corner])
|
||||
-Density(Xs[Corner],Ys[Corner]-2,CornerZ[Corner]);
|
||||
const float CornerGZ=Density(Xs[Corner],Ys[Corner],CornerZ[Corner]+2)
|
||||
-Density(Xs[Corner],Ys[Corner],CornerZ[Corner]-2);
|
||||
CornerNormals[Corner]=FVector(CornerGX,CornerGY,CornerGZ);
|
||||
if(CornerNormals[Corner].SizeSquared()<1.0e-8)
|
||||
CornerNormals[Corner]=Normal;
|
||||
else CornerNormals[Corner].Normalize();
|
||||
const float VertexGX=Density(Vertex.X+2,Vertex.Y,Vertex.Z)
|
||||
-Density(Vertex.X-2,Vertex.Y,Vertex.Z);
|
||||
const float VertexGY=Density(Vertex.X,Vertex.Y+2,Vertex.Z)
|
||||
-Density(Vertex.X,Vertex.Y-2,Vertex.Z);
|
||||
const float VertexGZ=Density(Vertex.X,Vertex.Y,Vertex.Z+2)
|
||||
-Density(Vertex.X,Vertex.Y,Vertex.Z-2);
|
||||
Vertex.Normal=FVector(VertexGX,VertexGY,VertexGZ);
|
||||
if(Vertex.Normal.SizeSquared()<1.0e-8)Vertex.Normal=Normal;
|
||||
else Vertex.Normal.Normalize();
|
||||
}
|
||||
// A center crossing alone cannot justify a large tangent quad: it can
|
||||
// bridge air over an isolated far-LOD feature and draw as a horizon slab.
|
||||
// Keep the fallback only when the source field has a matching crossing
|
||||
// at every corner, and place each corner on that source crossing.
|
||||
// Une seule traversee centrale ne justifie pas une grande nappe de terrain.
|
||||
// A center crossing alone cannot justify a broad patch that bridges horizon relief.
|
||||
// Require a source-field crossing at every coarse-cell sample before emitting it.
|
||||
if(!bValidPatch)continue;
|
||||
for(int32 Corner=0;Corner<4;++Corner)
|
||||
const int32 Base=static_cast<int32>(NewVertices.size());
|
||||
for(const FPatchVertex& Vertex:Patch)
|
||||
{
|
||||
NewVertices.emplace_back(Xs[Corner]*25.0,Ys[Corner]*25.0,CornerZ[Corner]*25.0);
|
||||
NewNormals.push_back(CornerNormals[Corner]);
|
||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>((Xs[Corner]-Origin.X)/256.0),
|
||||
static_cast<float>((Ys[Corner]-Origin.Y)/256.0));
|
||||
NewVertices.emplace_back(Vertex.X*25.0,Vertex.Y*25.0,Vertex.Z*25.0);
|
||||
NewNormals.push_back(Vertex.Normal);
|
||||
if(bHasUVs)NewUVs.emplace_back(static_cast<float>((Vertex.X-Origin.X)/256.0),
|
||||
static_cast<float>((Vertex.Y-Origin.Y)/256.0));
|
||||
if(bHasColors)NewColors.push_back(Color);
|
||||
}
|
||||
const bool bBelowSolid=Density(X,Y,Crossing-1.0)<0.0f;
|
||||
if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{Base,Base+1,Base+3,Base+1,Base+2,Base+3});
|
||||
else NewTriangles.insert(NewTriangles.end(),{Base,Base+3,Base+1,Base+1,Base+3,Base+2});
|
||||
for(int32 PY=0;PY<PatchDivisions;++PY)
|
||||
for(int32 PX=0;PX<PatchDivisions;++PX)
|
||||
{
|
||||
const int32 I0=Base+PY*PatchSide+PX;
|
||||
const int32 I1=I0+1;
|
||||
const int32 I2=I1+PatchSide;
|
||||
const int32 I3=I0+PatchSide;
|
||||
if(bBelowSolid)NewTriangles.insert(NewTriangles.end(),{I0,I1,I3,I1,I2,I3});
|
||||
else NewTriangles.insert(NewTriangles.end(),{I0,I3,I1,I1,I3,I2});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,8 +369,8 @@ namespace VoxelForgeCore
|
||||
const int32 GY=static_cast<int32>(Index/static_cast<std::size_t>(Cells+1));
|
||||
const double XSub=static_cast<double>(TileOrigin.X+GX*GridStepVoxels)*Scale;
|
||||
const double YSub=static_cast<double>(TileOrigin.Y+GY*GridStepVoxels)*Scale;
|
||||
AddedVertices.emplace_back(XSub/Scale*VOXEL_SIZE,YSub/Scale*VOXEL_SIZE,
|
||||
Sample.Cap/Scale*VOXEL_SIZE);
|
||||
AddedVertices.emplace_back(XSub/Scale*25.0,YSub/Scale*25.0,
|
||||
Sample.Cap/Scale*25.0);
|
||||
AddedNormals.push_back(Sample.Normal);
|
||||
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(XSub/Scale),
|
||||
static_cast<float>(YSub/Scale));
|
||||
@@ -425,8 +425,8 @@ namespace VoxelForgeCore
|
||||
for(int32 I=0;I<Count;++I)
|
||||
{
|
||||
const auto& V=PolygonB[I];
|
||||
AddedVertices.emplace_back(V.XSub/Scale*VOXEL_SIZE,
|
||||
V.YSub/Scale*VOXEL_SIZE,V.ZSub/Scale*VOXEL_SIZE);
|
||||
AddedVertices.emplace_back(V.XSub/Scale*25.0,
|
||||
V.YSub/Scale*25.0,V.ZSub/Scale*25.0);
|
||||
AddedNormals.push_back(V.Normal);
|
||||
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(V.XSub/Scale),
|
||||
static_cast<float>(V.YSub/Scale));
|
||||
@@ -893,11 +893,13 @@ namespace VoxelForgeCore
|
||||
const FStandaloneShapeWorld& tileShape=page?*page:*impl_->shape;
|
||||
UVoxelMarchingCubesMesher mesher;
|
||||
mesher.SetGenerator(world.Generator.get());
|
||||
mesher.SetGeometryVoxelSizeCm(25.0f);
|
||||
mesher.SetStandaloneShapeWorld(&tileShape);
|
||||
mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors;
|
||||
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
|
||||
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
|
||||
mesher.bGenerateSkirts=world.bGenerateSkirts;mesher.SkirtCells=static_cast<float>(world.SkirtCells);
|
||||
mesher.bGenerateSkirts=world.bGenerateSkirts;
|
||||
mesher.SkirtCells=static_cast<float>(world.SkirtCells)*12.5f/25.0f;
|
||||
mesher.LODOctaveDrop=world.LODOctaveDrop;
|
||||
const int32 level=key.lod,extent=CHUNK_SIZE<<level;
|
||||
const FVoxelTileKey nativeKey=ToNative(key);
|
||||
@@ -942,10 +944,16 @@ namespace VoxelForgeCore
|
||||
FStandaloneShapeWorld::ResetThreadClassificationStats();
|
||||
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats();
|
||||
bool bNativeSurfaceTile=false;
|
||||
bool bNativeSurfaceBandSafe=false;
|
||||
if(bHasWorldAssembly)
|
||||
{
|
||||
const auto Classification=tileShape.ClassifyBoxDetailed(origin,step,cells);
|
||||
bNativeSurfaceTile=Classification.bNativeSurface;
|
||||
// NativeSurfaceDensityAt consumes only the bounded manifest heightfield and its
|
||||
// bounded type-2/type-4 features. Unknown tile occupancy does not make that Z support
|
||||
// unknown; edits and cross-strate tiles are still excluded below.
|
||||
// NativeSurfaceDensityAt ne consomme que le champ et les volumes bornes du manifeste.
|
||||
bNativeSurfaceBandSafe=bNativeSurfaceTile;
|
||||
if(bNativeSurfaceTile)
|
||||
{
|
||||
out.diagnostics.tileWasClassified=true;
|
||||
@@ -983,8 +991,107 @@ namespace VoxelForgeCore
|
||||
}
|
||||
}
|
||||
const auto MeshingStart=std::chrono::steady_clock::now();
|
||||
int32 MeshBandMinZVoxel=options.bandMinZVoxel;
|
||||
int32 MeshBandMaxZVoxel=options.bandMaxZVoxel;
|
||||
bool bNativeHeightfieldBandLimited=false;
|
||||
if(bHasWorldAssembly&&bNativeSurfaceBandSafe
|
||||
&&!options.hashDensity&&!options.captureDensitySamples&&world.Manager!=nullptr)
|
||||
{
|
||||
// La surface 2D et les bornes de volume du manifeste definissent tout le support Z.
|
||||
// The 2D surface and manifest volume bounds cover every possible density crossing.
|
||||
const double TileCenterZ=static_cast<double>(origin.Z)+static_cast<double>(extent)*0.5;
|
||||
const int32 CenterChunkZ=FMath::FloorToInt(TileCenterZ/static_cast<double>(CHUNK_SIZE));
|
||||
const int32 StrateIndex=world.Manager->GetStrateIndex(
|
||||
static_cast<float>(TileCenterZ*VOXEL_SIZE));
|
||||
const auto PlanIt=std::find_if(impl_->worldAssembly->plan.strates.begin(),
|
||||
impl_->worldAssembly->plan.strates.end(),[StrateIndex,CenterChunkZ](const auto& Plan)
|
||||
{return Plan.strateIndex==StrateIndex&&CenterChunkZ<=Plan.topChunkZ
|
||||
&&CenterChunkZ>=Plan.bottomChunkZ;});
|
||||
if(PlanIt!=impl_->worldAssembly->plan.strates.end())
|
||||
{
|
||||
constexpr std::int64_t SubvoxelScale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::int64_t TileMinimum[3]={
|
||||
static_cast<std::int64_t>(origin.X-step)*SubvoxelScale,
|
||||
static_cast<std::int64_t>(origin.Y-step)*SubvoxelScale,
|
||||
static_cast<std::int64_t>(origin.Z-step)*SubvoxelScale};
|
||||
const std::int64_t TileMaximum[3]={
|
||||
static_cast<std::int64_t>(origin.X+extent+step)*SubvoxelScale,
|
||||
static_cast<std::int64_t>(origin.Y+extent+step)*SubvoxelScale,
|
||||
static_cast<std::int64_t>(origin.Z+extent+step)*SubvoxelScale};
|
||||
bool bOverlapsPlayerEdit=false;
|
||||
if(tileShape.HasEditTransaction())
|
||||
{
|
||||
const FIntVector EditMinimum=tileShape.GetEditBoundsMin();
|
||||
const FIntVector EditMaximum=tileShape.GetEditBoundsMax();
|
||||
bOverlapsPlayerEdit=EditMinimum.X<=origin.X+extent+step
|
||||
&&EditMaximum.X>=origin.X-step
|
||||
&&EditMinimum.Y<=origin.Y+extent+step
|
||||
&&EditMaximum.Y>=origin.Y-step
|
||||
&&EditMinimum.Z<=origin.Z+extent+step
|
||||
&&EditMaximum.Z>=origin.Z-step;
|
||||
}
|
||||
if(!bOverlapsPlayerEdit)
|
||||
{
|
||||
const VoxelForgeCore::Heightfield2D Heightfield(
|
||||
impl_->worldAssembly->recipe,*PlanIt);
|
||||
double RelevantMinimum=std::numeric_limits<double>::infinity();
|
||||
double RelevantMaximum=-std::numeric_limits<double>::infinity();
|
||||
const double TileMinimumZ=static_cast<double>(origin.Z-step);
|
||||
const double TileMaximumZ=static_cast<double>(origin.Z+extent+step);
|
||||
for(int32 GridY=0;GridY<=cells+2;++GridY)
|
||||
for(int32 GridX=0;GridX<=cells+2;++GridX)
|
||||
{
|
||||
const std::int64_t XSub=static_cast<std::int64_t>(origin.X-step+GridX*step)
|
||||
*SubvoxelScale;
|
||||
const std::int64_t YSub=static_cast<std::int64_t>(origin.Y-step+GridY*step)
|
||||
*SubvoxelScale;
|
||||
const auto Column=Heightfield.point(XSub,YSub,0);
|
||||
const double Ground=static_cast<double>(Column.groundHeightSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
const double Cap=static_cast<double>(Column.capHeightSubvoxels)
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
if(Ground>=TileMinimumZ&&Ground<=TileMaximumZ)
|
||||
{
|
||||
RelevantMinimum=std::min(RelevantMinimum,Ground);
|
||||
RelevantMaximum=std::max(RelevantMaximum,Ground);
|
||||
}
|
||||
if(Cap>=TileMinimumZ&&Cap<=TileMaximumZ)
|
||||
{
|
||||
RelevantMinimum=std::min(RelevantMinimum,Cap);
|
||||
RelevantMaximum=std::max(RelevantMaximum,Cap);
|
||||
}
|
||||
}
|
||||
for(const auto& Feature:PlanIt->features)
|
||||
{
|
||||
if(Feature.type!=2&&Feature.type!=4)continue;
|
||||
const bool bOverlapsXY=Feature.minimumSubvoxels[0]<=TileMaximum[0]
|
||||
&&Feature.maximumSubvoxels[0]>=TileMinimum[0]
|
||||
&&Feature.minimumSubvoxels[1]<=TileMaximum[1]
|
||||
&&Feature.maximumSubvoxels[1]>=TileMinimum[1];
|
||||
const bool bOverlapsTileZ=Feature.minimumSubvoxels[2]<=TileMaximum[2]
|
||||
&&Feature.maximumSubvoxels[2]>=TileMinimum[2];
|
||||
if(!bOverlapsXY||!bOverlapsTileZ)continue;
|
||||
const double FeatureMinimum=static_cast<double>(Feature.minimumSubvoxels[2])
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
const double FeatureMaximum=static_cast<double>(Feature.maximumSubvoxels[2])
|
||||
/static_cast<double>(SubvoxelScale);
|
||||
RelevantMinimum=std::min(RelevantMinimum,FeatureMinimum);
|
||||
RelevantMaximum=std::max(RelevantMaximum,FeatureMaximum);
|
||||
}
|
||||
if(std::isfinite(RelevantMinimum)&&std::isfinite(RelevantMaximum))
|
||||
{
|
||||
const int32 NativeBandMinimum=FMath::FloorToInt(RelevantMinimum)-step;
|
||||
const int32 NativeBandMaximum=FMath::CeilToInt(RelevantMaximum)+step;
|
||||
MeshBandMinZVoxel=FMath::Max(MeshBandMinZVoxel,NativeBandMinimum);
|
||||
MeshBandMaxZVoxel=FMath::Min(MeshBandMaxZVoxel,NativeBandMaximum);
|
||||
bNativeHeightfieldBandLimited=static_cast<std::int64_t>(MeshBandMaxZVoxel)
|
||||
-MeshBandMinZVoxel<static_cast<std::int64_t>(extent)+2*step;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr,
|
||||
options.bandMinZVoxel,options.bandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr);
|
||||
const double MeshingMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-MeshingStart).count();
|
||||
@@ -1027,8 +1134,9 @@ namespace VoxelForgeCore
|
||||
out.diagnostics.unknownBoxes=Counts.UnknownBoxes;
|
||||
out.diagnostics.tileClass=TileClass::unknown;
|
||||
}
|
||||
out.diagnostics.meshingMilliseconds=MeshingMilliseconds;
|
||||
}
|
||||
out.diagnostics.heightfieldBandLimited=bNativeHeightfieldBandLimited;
|
||||
out.diagnostics.meshingMilliseconds=MeshingMilliseconds;
|
||||
out.meshPayloadAllocatedBytes=static_cast<std::uint64_t>(mesh.Vertices.GetAllocatedSize())
|
||||
+static_cast<std::uint64_t>(mesh.Normals.GetAllocatedSize())
|
||||
+static_cast<std::uint64_t>(mesh.UVs.GetAllocatedSize())
|
||||
|
||||
Reference in new issue
Block a user