wip(assembler): round S3 -- far ground from the native heightfield; LOD7/8 tiles 36-66% cheaper
Far-shell repair (81-96% of S2's LOD7/8 tile time) is skipped for certified SurfaceWorld tiles; ground and cap come from the shared planar heightfield. Near LOD0-3 hashes equal S2; far recipe version 1 salts LOD5+ identities. Cold first view 6.6 -> 3.4 s (aim 1.57 s). Not a visual sign-off: distant cap shows floating pieces, sprint streaming arrives late. 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
6ddb834b1c
commit
d0f97bf79a
26 files changed
+976
-30
No files matched your search
@@ -128,6 +128,17 @@ namespace VoxelForgeCore
|
||||
double meshingMilliseconds = 0.0;
|
||||
double featureListMilliseconds = 0.0;
|
||||
double farShellMilliseconds = 0.0;
|
||||
double farStratumShellMilliseconds = 0.0;
|
||||
double farConnectorShellMilliseconds = 0.0;
|
||||
double farHorizonMilliseconds = 0.0;
|
||||
std::uint64_t farStratumFieldCalls = 0;
|
||||
std::uint64_t farConnectorFieldCalls = 0;
|
||||
std::uint64_t farShellTrianglesAdded = 0;
|
||||
std::uint64_t horizonGroundTriangles = 0;
|
||||
std::uint64_t horizonGroundTrianglesRemoved = 0;
|
||||
std::uint64_t horizonCapTriangles = 0;
|
||||
std::uint32_t farHorizonVersion = 0;
|
||||
bool farShellRepairSkipped = false;
|
||||
bool tileWasClassified = false;
|
||||
bool meshingSkipped = false;
|
||||
bool featureRefinedResolution = false;
|
||||
@@ -984,6 +995,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;
|
||||
};
|
||||
|
||||
class VFCORE_API World
|
||||
|
||||
@@ -311,7 +311,10 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
||||
if(TMin>TMax){bIntersects=false;break;}
|
||||
}
|
||||
if(!bIntersects||TMax-TMin<1.0e-6)continue;
|
||||
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/128.0)));
|
||||
// LOD7 and LOD8 share a 256-voxel connector station grid; near LOD keeps 128-voxel checks.
|
||||
// LOD7 et LOD8 partagent des stations aux 256 voxels; pres du joueur, garder les pas de 128.
|
||||
const double ConnectorStationStep=Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0;
|
||||
const int32 AxialSampleCount=std::max(1,static_cast<int32>(std::ceil(Length/ConnectorStationStep)));
|
||||
std::vector<uint8> MissingMasks(static_cast<size_t>(AxialSampleCount),0);
|
||||
std::vector<std::array<double,4>> SourceRadii(static_cast<size_t>(AxialSampleCount));
|
||||
for(auto& Radii:SourceRadii)Radii.fill(0.0);
|
||||
@@ -429,7 +432,7 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV
|
||||
}
|
||||
}
|
||||
std::vector<FRing> Rings;
|
||||
const double MaxRingStep=128.0;
|
||||
const double MaxRingStep=Key.Level>=7?std::max(256.0,static_cast<double>(Step)):128.0;
|
||||
double CurrentT=RunMin;
|
||||
Rings.push_back(MakeRing(CurrentT,RunStart,RunEnd,RunRadiusMask));
|
||||
while(CurrentT<RunMax-1.0e-7)
|
||||
@@ -901,6 +904,67 @@ inline std::vector<double> FindFarStratumCrossings(const FStandaloneShapeWorld&
|
||||
return Crossings;
|
||||
}
|
||||
|
||||
inline bool FindNearestFarStratumCrossing(const FStandaloneShapeWorld& Shape,
|
||||
double X,double Y,double ZMin,double ZMax,double Prediction,double& OutCrossing)
|
||||
{
|
||||
if(ZMax<=ZMin||!std::isfinite(Prediction))return false;
|
||||
const int32 Center=std::clamp(static_cast<int32>(std::floor(Prediction)),
|
||||
static_cast<int32>(std::ceil(ZMin)),static_cast<int32>(std::floor(ZMax)));
|
||||
double PreviousLeft=Center,PreviousRight=Center;
|
||||
bool PreviousLeftSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(PreviousLeft))<0.0f;
|
||||
bool PreviousRightSolid=PreviousLeftSolid;
|
||||
const int32 MaximumRadius=std::max(Center-static_cast<int32>(std::floor(ZMin)),
|
||||
static_cast<int32>(std::ceil(ZMax))-Center)+1;
|
||||
for(int32 Radius=1;Radius<=MaximumRadius;++Radius)
|
||||
{
|
||||
bool bFound=false;
|
||||
double Best=0.0,BestDistance=std::numeric_limits<double>::infinity();
|
||||
const auto CheckBracket=[&](double A,double B,bool ASolid)
|
||||
{
|
||||
double Lo=std::min(A,B),Hi=std::max(A,B);
|
||||
for(int32 I=0;I<6;++I)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;
|
||||
if((Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Mid))<0.0f)==ASolid)
|
||||
{
|
||||
if(A<B)Lo=Mid;else Hi=Mid;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(A<B)Hi=Mid;else Lo=Mid;
|
||||
}
|
||||
}
|
||||
const double Crossing=(Lo+Hi)*0.5;
|
||||
const double Difference=std::abs(Crossing-Prediction);
|
||||
if(Difference<BestDistance)
|
||||
{Best=Crossing;BestDistance=Difference;bFound=true;}
|
||||
};
|
||||
|
||||
const double Left=std::max(ZMin,static_cast<double>(Center-Radius));
|
||||
if(Left<PreviousLeft-1.0e-9)
|
||||
{
|
||||
const bool LeftSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Left))<0.0f;
|
||||
if(LeftSolid!=PreviousLeftSolid)CheckBracket(Left,PreviousLeft,LeftSolid);
|
||||
PreviousLeft=Left;PreviousLeftSolid=LeftSolid;
|
||||
}
|
||||
const double Right=std::min(ZMax,static_cast<double>(Center+Radius));
|
||||
if(Right>PreviousRight+1.0e-9)
|
||||
{
|
||||
const bool RightSolid=Shape.DensityAt(static_cast<float>(X),static_cast<float>(Y),
|
||||
static_cast<float>(Right))<0.0f;
|
||||
if(RightSolid!=PreviousRightSolid)CheckBracket(PreviousRight,Right,PreviousRightSolid);
|
||||
PreviousRight=Right;PreviousRightSolid=RightSolid;
|
||||
}
|
||||
if(bFound)
|
||||
{OutCrossing=Best;return true;}
|
||||
if(Left<=ZMin+1.0e-9&&Right>=ZMax-1.0e-9)break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FStandaloneShapeWorld& Shape,
|
||||
const FVoxelTileKey& Key,int32 Step,FVoxelMeshData& Mesh)
|
||||
{
|
||||
@@ -992,12 +1056,9 @@ inline void AppendFarStratumShells(const FStandaloneOwnerWorld& World,const FSta
|
||||
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);});
|
||||
if(!FindNearestFarStratumCrossing(Shape,Vertex.X,Vertex.Y,
|
||||
ZMin,ZMax,PredictedZ,Vertex.Z))
|
||||
{bValidPatch=false;break;}
|
||||
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)
|
||||
|
||||
@@ -210,6 +210,13 @@ namespace VoxelForgeCore
|
||||
&&std::isfinite(brush.strength)&&std::isfinite(brush.falloff)&&brush.falloff>=0.0f;
|
||||
}
|
||||
|
||||
std::uint64_t SaltFarHorizonHash(std::uint64_t Hash,std::uint32_t Version)
|
||||
{
|
||||
AddWord(Hash,0x464152484f52495aull,8);
|
||||
AddWord(Hash,Version,4);
|
||||
return Hash;
|
||||
}
|
||||
|
||||
FStandaloneShapeWorld::EDetailVariant ToNative(DetailVariant value)
|
||||
{
|
||||
switch(value)
|
||||
@@ -226,6 +233,7 @@ namespace VoxelForgeCore
|
||||
double YSub = 0.0;
|
||||
double ZSub = 0.0;
|
||||
double GroundSub = 0.0;
|
||||
double CapSub = 0.0;
|
||||
FVector Normal = FVector(0.0,0.0,-1.0);
|
||||
FColor Color = FColor::White;
|
||||
};
|
||||
@@ -234,6 +242,7 @@ namespace VoxelForgeCore
|
||||
{
|
||||
std::uint64_t Cells = 0;
|
||||
std::uint64_t Triangles = 0;
|
||||
std::uint64_t RemovedTriangles = 0;
|
||||
bool Refined = false;
|
||||
};
|
||||
|
||||
@@ -252,6 +261,7 @@ namespace VoxelForgeCore
|
||||
Out.YSub=A.YSub+(B.YSub-A.YSub)*T;
|
||||
Out.ZSub=A.ZSub+(B.ZSub-A.ZSub)*T;
|
||||
Out.GroundSub=A.GroundSub+(B.GroundSub-A.GroundSub)*T;
|
||||
Out.CapSub=A.CapSub+(B.CapSub-A.CapSub)*T;
|
||||
Out.Normal=NormalizeCapNormal(A.Normal+(B.Normal-A.Normal)*T);
|
||||
const auto LerpByte=[T](uint8 AValue,uint8 BValue)
|
||||
{return static_cast<uint8>(std::clamp(std::lround(AValue+(BValue-AValue)*T),0l,255l));};
|
||||
@@ -287,11 +297,286 @@ namespace VoxelForgeCore
|
||||
return OutputCount;
|
||||
}
|
||||
|
||||
FNativeCapRefinementStats RefineLod8NativeCap(const FStandaloneOwnerWorld& Owner,
|
||||
FNativeCapRefinementStats RefineFarNativeGround(const FStandaloneOwnerWorld& Owner,
|
||||
const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,FVoxelMeshData& Mesh)
|
||||
{
|
||||
FNativeCapRefinementStats Stats;
|
||||
if(Key.Level<8||!Owner.Generator)return Stats;
|
||||
if(Key.Level<5||!Owner.Generator)return Stats;
|
||||
const int32 GridStepVoxels=std::min(128,1<<Key.Level);
|
||||
constexpr int64 Scale=WorldAssemblySubvoxelsPerVoxel;
|
||||
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;
|
||||
const int32 Cells=TileExtent/GridStepVoxels;
|
||||
const double TileMinZ=static_cast<double>(TileOrigin.Z)*Scale;
|
||||
const double TileMaxZ=static_cast<double>(TileOrigin.Z+TileExtent)*Scale;
|
||||
const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,Mesh.Vertices.Num());
|
||||
const bool bHasUVs=Mesh.UVs.Num()==Mesh.Vertices.Num();
|
||||
const bool bHasColors=Mesh.Colors.Num()==Mesh.Vertices.Num();
|
||||
std::vector<FVector> AddedVertices,AddedNormals;
|
||||
std::vector<FVector2f> AddedUVs;
|
||||
std::vector<FColor> AddedColors;
|
||||
std::vector<int32> AddedTriangles;
|
||||
const int32 OldCeilingTriangleCount=std::max(0,Mesh.NumCeilingTriangles);
|
||||
const int32 OldCeilingStart=std::max(0,Mesh.Triangles.Num()-OldCeilingTriangleCount*3);
|
||||
|
||||
struct FGroundProfile
|
||||
{
|
||||
int32 Cells=0,GridStep=0;
|
||||
double MinimumZ=0.0,MaximumZ=0.0;
|
||||
std::vector<double> Ground,Cap;
|
||||
};
|
||||
std::vector<FGroundProfile> GroundProfiles;
|
||||
|
||||
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;
|
||||
struct FGridSample
|
||||
{
|
||||
double Ground=0.0,Cap=0.0;
|
||||
FVector Normal=FVector(0.0,0.0,1.0);
|
||||
FColor Color=FColor::White;
|
||||
int32 VertexIndex=INDEX_NONE;
|
||||
};
|
||||
std::vector<FGridSample> Grid(static_cast<std::size_t>(Cells+1)*(Cells+1));
|
||||
const auto GridIndex=[Cells](int32 X,int32 Y)
|
||||
{return static_cast<std::size_t>(Y)*(Cells+1)+X;};
|
||||
for(int32 Y=0;Y<=Cells;++Y)
|
||||
for(int32 X=0;X<=Cells;++X)
|
||||
{
|
||||
const int64 XSub=(static_cast<int64>(TileOrigin.X)+static_cast<int64>(X)*GridStepVoxels)*Scale;
|
||||
const int64 YSub=(static_cast<int64>(TileOrigin.Y)+static_cast<int64>(Y)*GridStepVoxels)*Scale;
|
||||
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);
|
||||
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.Ground/Scale),
|
||||
Dominant,Neighbor,Blend);
|
||||
Sample.Color=FColor(static_cast<uint8>(std::clamp(Dominant,0,255)),0,
|
||||
static_cast<uint8>(std::clamp(std::lround(Blend*255.0f),0l,255l)),
|
||||
static_cast<uint8>(std::clamp(Neighbor,0,255)));
|
||||
}
|
||||
for(int32 Y=0;Y<=Cells;++Y)
|
||||
for(int32 X=0;X<=Cells;++X)
|
||||
{
|
||||
const int32 Left=std::max(0,X-1),Right=std::min(Cells,X+1);
|
||||
const int32 Down=std::max(0,Y-1),Up=std::min(Cells,Y+1);
|
||||
const double DX=static_cast<double>(Right-Left)*GridStepSubvoxels;
|
||||
const double DY=static_cast<double>(Up-Down)*GridStepSubvoxels;
|
||||
const double SlopeX=DX>0.0?(Grid[GridIndex(Right,Y)].Ground
|
||||
-Grid[GridIndex(Left,Y)].Ground)/DX:0.0;
|
||||
const double SlopeY=DY>0.0?(Grid[GridIndex(X,Up)].Ground
|
||||
-Grid[GridIndex(X,Down)].Ground)/DY:0.0;
|
||||
auto& Sample=Grid[GridIndex(X,Y)];
|
||||
Sample.Normal=NormalizeCapNormal(FVector(-SlopeX,-SlopeY,1.0));
|
||||
Sample.Color.G=static_cast<uint8>(std::clamp(
|
||||
std::lround((1.0-std::abs(Sample.Normal.Z))*255.0),0l,255l));
|
||||
}
|
||||
|
||||
FGroundProfile Profile;
|
||||
Profile.Cells=Cells;Profile.GridStep=GridStepVoxels;
|
||||
Profile.MinimumZ=static_cast<double>(std::min(Plan.bottomChunkZ,Plan.topChunkZ))*CHUNK_SIZE;
|
||||
Profile.MaximumZ=static_cast<double>(std::max(Plan.bottomChunkZ,Plan.topChunkZ)+1)*CHUNK_SIZE;
|
||||
Profile.Ground.reserve(Grid.size());Profile.Cap.reserve(Grid.size());
|
||||
for(const auto& Sample:Grid)
|
||||
{Profile.Ground.push_back(Sample.Ground/Scale);Profile.Cap.push_back(Sample.Cap/Scale);}
|
||||
GroundProfiles.push_back(std::move(Profile));
|
||||
|
||||
// Partager les sommets du profil entre cellules reduit le payload d'horizon.
|
||||
// Share regular profile vertices across adjacent cells to bound horizon payload.
|
||||
for(int32 Y=0;Y<=Cells;++Y)
|
||||
for(int32 X=0;X<=Cells;++X)
|
||||
{
|
||||
auto& Sample=Grid[GridIndex(X,Y)];
|
||||
if(Sample.Ground>=Sample.Cap||Sample.Ground<TileMinZ||Sample.Ground>TileMaxZ)continue;
|
||||
const int64 XSub=(static_cast<int64>(TileOrigin.X)+static_cast<int64>(X)*GridStepVoxels)*Scale;
|
||||
const int64 YSub=(static_cast<int64>(TileOrigin.Y)+static_cast<int64>(Y)*GridStepVoxels)*Scale;
|
||||
const double VoxelSizeCm=static_cast<double>(Owner.CoreVoxelSizeMicrons)/10000.0;
|
||||
Sample.VertexIndex=static_cast<int32>(AddedVertices.size());
|
||||
AddedVertices.emplace_back(static_cast<double>(TileOrigin.X+X*GridStepVoxels)*VoxelSizeCm,
|
||||
static_cast<double>(TileOrigin.Y+Y*GridStepVoxels)*VoxelSizeCm,
|
||||
Sample.Ground/Scale*VoxelSizeCm);
|
||||
AddedNormals.push_back(Sample.Normal);
|
||||
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(XSub/Scale),static_cast<float>(YSub/Scale));
|
||||
if(bHasColors)AddedColors.push_back(Sample.Color);
|
||||
}
|
||||
|
||||
for(int32 Y=0;Y<Cells;++Y)
|
||||
for(int32 X=0;X<Cells;++X)
|
||||
{
|
||||
const int32 CellX[4]={X,X+1,X+1,X};
|
||||
const int32 CellY[4]={Y,Y,Y+1,Y+1};
|
||||
std::array<int32,4> GridVertices{};
|
||||
bool bCompleteCell=true;
|
||||
for(int32 Corner=0;Corner<4;++Corner)
|
||||
{
|
||||
GridVertices[Corner]=Grid[GridIndex(CellX[Corner],CellY[Corner])].VertexIndex;
|
||||
bCompleteCell=bCompleteCell&&GridVertices[Corner]!=INDEX_NONE;
|
||||
}
|
||||
if(bCompleteCell)
|
||||
{
|
||||
AddedTriangles.insert(AddedTriangles.end(),{GridVertices[0],GridVertices[1],GridVertices[2],
|
||||
GridVertices[0],GridVertices[2],GridVertices[3]});
|
||||
++Stats.Cells;Stats.Triangles+=2;
|
||||
continue;
|
||||
}
|
||||
std::array<FNativeCapVertex,16> PolygonA{},PolygonB{};
|
||||
for(int32 Corner=0;Corner<4;++Corner)
|
||||
{
|
||||
const int32 GX=CellX[Corner],GY=CellY[Corner];
|
||||
const auto& Sample=Grid[GridIndex(GX,GY)];
|
||||
PolygonA[Corner].XSub=static_cast<double>(TileOrigin.X+GX*GridStepVoxels)*Scale;
|
||||
PolygonA[Corner].YSub=static_cast<double>(TileOrigin.Y+GY*GridStepVoxels)*Scale;
|
||||
PolygonA[Corner].GroundSub=Sample.Ground;
|
||||
PolygonA[Corner].CapSub=Sample.Cap;
|
||||
PolygonA[Corner].ZSub=Sample.Ground;
|
||||
PolygonA[Corner].Normal=Sample.Normal;
|
||||
PolygonA[Corner].Color=Sample.Color;
|
||||
}
|
||||
int32 Count=ClipNativeCapPolygon(PolygonA.data(),4,PolygonB.data(),
|
||||
[](const auto& V){return V.CapSub-V.ZSub;});
|
||||
Count=ClipNativeCapPolygon(PolygonB.data(),Count,PolygonA.data(),
|
||||
[TileMinZ](const auto& V){return V.ZSub-TileMinZ;});
|
||||
Count=ClipNativeCapPolygon(PolygonA.data(),Count,PolygonB.data(),
|
||||
[TileMaxZ](const auto& V){return TileMaxZ-V.ZSub;});
|
||||
if(Count<3)continue;
|
||||
double TwiceArea=0.0;
|
||||
for(int32 I=0;I<Count;++I)
|
||||
{
|
||||
const auto& A=PolygonB[I];const auto& B=PolygonB[(I+1)%Count];
|
||||
TwiceArea+=A.XSub*B.YSub-B.XSub*A.YSub;
|
||||
}
|
||||
if(std::abs(TwiceArea)<1.0e-5)continue;
|
||||
++Stats.Cells;
|
||||
const int32 Base=static_cast<int32>(AddedVertices.size());
|
||||
for(int32 I=0;I<Count;++I)
|
||||
{
|
||||
const auto& V=PolygonB[I];
|
||||
const double VoxelSizeCm=static_cast<double>(Owner.CoreVoxelSizeMicrons)/10000.0;
|
||||
AddedVertices.emplace_back(V.XSub/Scale*VoxelSizeCm,
|
||||
V.YSub/Scale*VoxelSizeCm,V.ZSub/Scale*VoxelSizeCm);
|
||||
AddedNormals.push_back(V.Normal);
|
||||
if(bHasUVs)AddedUVs.emplace_back(static_cast<float>(V.XSub/Scale),
|
||||
static_cast<float>(V.YSub/Scale));
|
||||
if(bHasColors)AddedColors.push_back(V.Color);
|
||||
}
|
||||
for(int32 Fan=1;Fan+1<Count;++Fan)
|
||||
{
|
||||
AddedTriangles.insert(AddedTriangles.end(),{Base,Base+Fan,Base+Fan+1});
|
||||
++Stats.Triangles;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(AddedTriangles.empty())return Stats;
|
||||
|
||||
// Retirer les faces de sol MC proches du profil exact remplace le terrain coarse
|
||||
// au lieu de le superposer au nouveau maillage d'horizon.
|
||||
// Remove coarse upward MC faces around the exact profile instead of overlaying them.
|
||||
const double VoxelSizeCm=static_cast<double>(Owner.CoreVoxelSizeMicrons)/10000.0;
|
||||
const int32 OldSurfaceTriangleCount=OldCeilingStart/3;
|
||||
std::vector<uint8> RemoveOldGround(static_cast<std::size_t>(OldSurfaceTriangleCount),0);
|
||||
const TArray<uint8>& ExistingMasks=Mesh.TriangleSkirtFaceMasks;
|
||||
const auto InterpolateProfile=[](const std::vector<double>& Values,const FGroundProfile& Profile,
|
||||
double LocalX,double LocalY)
|
||||
{
|
||||
const double GridX=std::clamp(LocalX/Profile.GridStep,0.0,static_cast<double>(Profile.Cells));
|
||||
const double GridY=std::clamp(LocalY/Profile.GridStep,0.0,static_cast<double>(Profile.Cells));
|
||||
const int32 X=std::min(Profile.Cells-1,static_cast<int32>(std::floor(GridX)));
|
||||
const int32 Y=std::min(Profile.Cells-1,static_cast<int32>(std::floor(GridY)));
|
||||
const double TX=GridX-X,TY=GridY-Y;
|
||||
const std::size_t Width=static_cast<std::size_t>(Profile.Cells+1);
|
||||
const auto At=[&](int32 PX,int32 PY){return Values[static_cast<std::size_t>(PY)*Width+PX];};
|
||||
if(TX>=TY)
|
||||
return At(X,Y)*(1.0-TX)+At(X+1,Y)*(TX-TY)+At(X+1,Y+1)*TY;
|
||||
return At(X,Y)*(1.0-TY)+At(X+1,Y+1)*TX+At(X,Y+1)*(TY-TX);
|
||||
};
|
||||
for(int32 Triangle=0;Triangle<OldSurfaceTriangleCount;++Triangle)
|
||||
{
|
||||
if(Triangle<ExistingMasks.Num()&&ExistingMasks[Triangle]!=0)continue;
|
||||
const int32 IA=Mesh.Triangles[Triangle*3],IB=Mesh.Triangles[Triangle*3+1],IC=Mesh.Triangles[Triangle*3+2];
|
||||
if(IA<0||IB<0||IC<0||IA>=OldSurfaceVertices||IB>=OldSurfaceVertices||IC>=OldSurfaceVertices)continue;
|
||||
const FVector& A=Mesh.Vertices[IA];const FVector& B=Mesh.Vertices[IB];const FVector& C=Mesh.Vertices[IC];
|
||||
double NormalZ=0.0;
|
||||
if(Mesh.Normals.Num()==Mesh.Vertices.Num())
|
||||
NormalZ=(Mesh.Normals[IA].Z+Mesh.Normals[IB].Z+Mesh.Normals[IC].Z)/3.0;
|
||||
else
|
||||
NormalZ=FVector::CrossProduct(B-A,C-A).GetSafeNormal().Z;
|
||||
if(NormalZ<0.35)continue;
|
||||
const double X=((A.X+B.X+C.X)/3.0)/VoxelSizeCm;
|
||||
const double Y=((A.Y+B.Y+C.Y)/3.0)/VoxelSizeCm;
|
||||
const double Z=((A.Z+B.Z+C.Z)/3.0)/VoxelSizeCm;
|
||||
const double LocalX=X-TileOrigin.X,LocalY=Y-TileOrigin.Y;
|
||||
if(LocalX<0.0||LocalY<0.0||LocalX>TileExtent||LocalY>TileExtent)continue;
|
||||
for(const auto& Profile:GroundProfiles)
|
||||
{
|
||||
if(LocalX>Profile.Cells*Profile.GridStep||LocalY>Profile.Cells*Profile.GridStep)continue;
|
||||
const double Ground=InterpolateProfile(Profile.Ground,Profile,LocalX,LocalY);
|
||||
const double Cap=InterpolateProfile(Profile.Cap,Profile,LocalX,LocalY);
|
||||
if(Cap-Ground<=1.0||Ground<TileMinZ/Scale||Ground>TileMaxZ/Scale
|
||||
||Ground<Profile.MinimumZ||Ground>Profile.MaximumZ)continue;
|
||||
const double ReplacementBand=std::max(32.0,2.0*Profile.GridStep);
|
||||
if(Z>=Ground-ReplacementBand)
|
||||
{
|
||||
RemoveOldGround[static_cast<std::size_t>(Triangle)]=1;
|
||||
++Stats.RemovedTriangles;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const int32 AddedVertexCount=static_cast<int32>(AddedVertices.size());
|
||||
auto InsertSurfaceVertices=[&](auto& Existing,const auto& Added)
|
||||
{
|
||||
using TArrayType=std::remove_reference_t<decltype(Existing)>;
|
||||
TArrayType Combined;Combined.Reserve(Existing.Num()+static_cast<int32>(Added.size()));
|
||||
for(int32 I=0;I<OldSurfaceVertices;++I)Combined.Add(Existing[I]);
|
||||
for(const auto& Value:Added)Combined.Add(Value);
|
||||
for(int32 I=OldSurfaceVertices;I<Existing.Num();++I)Combined.Add(Existing[I]);
|
||||
Existing=std::move(Combined);
|
||||
};
|
||||
InsertSurfaceVertices(Mesh.Vertices,AddedVertices);
|
||||
InsertSurfaceVertices(Mesh.Normals,AddedNormals);
|
||||
if(bHasUVs)InsertSurfaceVertices(Mesh.UVs,AddedUVs);
|
||||
if(bHasColors)InsertSurfaceVertices(Mesh.Colors,AddedColors);
|
||||
for(int32& Index:Mesh.Triangles)if(Index>=OldSurfaceVertices)Index+=AddedVertexCount;
|
||||
const int32 OldTriangleCount=Mesh.Triangles.Num()/3;
|
||||
TArray<uint8> OldMasks=Mesh.TriangleSkirtFaceMasks;OldMasks.SetNum(OldTriangleCount);
|
||||
TArray<int32> CombinedTriangles;CombinedTriangles.Reserve(
|
||||
Mesh.Triangles.Num()+static_cast<int32>(AddedTriangles.size()));
|
||||
TArray<uint8> CombinedMasks;CombinedMasks.Reserve(
|
||||
OldTriangleCount+static_cast<int32>(AddedTriangles.size()/3));
|
||||
for(int32 Triangle=0;Triangle<OldCeilingStart/3;++Triangle)
|
||||
{
|
||||
if(RemoveOldGround[static_cast<std::size_t>(Triangle)])continue;
|
||||
for(int32 Corner=0;Corner<3;++Corner)CombinedTriangles.Add(Mesh.Triangles[Triangle*3+Corner]);
|
||||
CombinedMasks.Add(OldMasks[Triangle]);
|
||||
}
|
||||
for(const int32 Index:AddedTriangles)CombinedTriangles.Add(OldSurfaceVertices+Index);
|
||||
for(std::size_t I=0;I<AddedTriangles.size()/3;++I)CombinedMasks.Add(0);
|
||||
for(int32 Triangle=OldCeilingStart/3;Triangle<OldTriangleCount;++Triangle)
|
||||
{
|
||||
for(int32 Corner=0;Corner<3;++Corner)CombinedTriangles.Add(Mesh.Triangles[Triangle*3+Corner]);
|
||||
CombinedMasks.Add(OldMasks[Triangle]);
|
||||
}
|
||||
Mesh.Triangles=std::move(CombinedTriangles);
|
||||
Mesh.TriangleSkirtFaceMasks=std::move(CombinedMasks);
|
||||
Mesh.NumSurfaceVertices=OldSurfaceVertices+AddedVertexCount;
|
||||
Stats.Refined=true;
|
||||
return Stats;
|
||||
}
|
||||
|
||||
FNativeCapRefinementStats RefineFarNativeCap(const FStandaloneOwnerWorld& Owner,
|
||||
const WorldAssemblyManifest& Manifest,const FVoxelTileKey& Key,FVoxelMeshData& Mesh)
|
||||
{
|
||||
FNativeCapRefinementStats Stats;
|
||||
if(Key.Level<5||!Owner.Generator)return Stats;
|
||||
constexpr int32 GridStepVoxels=128;
|
||||
constexpr int64 Scale=WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr int64 GridStepSubvoxels=GridStepVoxels*Scale;
|
||||
@@ -916,6 +1201,8 @@ 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)
|
||||
{error="far-horizon recipe version must be nonzero";return std::nullopt;}
|
||||
TArray<FVoxelHybridSharpFeatureBox> sharpFeatures;
|
||||
if(options.mesherRecipe!=MesherRecipe::marchingCubesV1&&level==0&&impl_->worldAssembly)
|
||||
{
|
||||
@@ -1001,6 +1288,42 @@ namespace VoxelForgeCore
|
||||
const bool bHasWorldAssembly=impl_->worldAssembly!=nullptr;
|
||||
const bool bNeedsFarShell=bHasWorldAssembly&&key.lod>=7
|
||||
&&(tileShape.HasFarWorld()||impl_->shape->HasFarWorld());
|
||||
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;
|
||||
}
|
||||
bool bHasNativeSurfaceHorizonPlan=false;
|
||||
bool bHasIslandPlanInTile=false;
|
||||
if(impl_->worldAssembly)
|
||||
{
|
||||
const int64 TileMinimumZ=static_cast<int64>(origin.Z);
|
||||
const int64 TileMaximumZ=static_cast<int64>(origin.Z)+extent;
|
||||
for(const auto& Plan:impl_->worldAssembly->plan.strates)
|
||||
{
|
||||
const int64 PlanMinimumZ=static_cast<int64>(std::min(Plan.bottomChunkZ,Plan.topChunkZ))*CHUNK_SIZE;
|
||||
const int64 PlanMaximumZ=(static_cast<int64>(std::max(Plan.bottomChunkZ,Plan.topChunkZ))+1)*CHUNK_SIZE;
|
||||
bHasNativeSurfaceHorizonPlan=bHasNativeSurfaceHorizonPlan
|
||||
||(TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ);
|
||||
}
|
||||
for(const auto& Entry:impl_->worldAssembly->floatingIslands)
|
||||
{
|
||||
const int64 PlanMinimumZ=static_cast<int64>(std::min(Entry.bottomChunkZ,Entry.topChunkZ))*CHUNK_SIZE;
|
||||
const int64 PlanMaximumZ=(static_cast<int64>(std::max(Entry.bottomChunkZ,Entry.topChunkZ))+1)*CHUNK_SIZE;
|
||||
bHasIslandPlanInTile=bHasIslandPlanInTile
|
||||
||(TileMinimumZ<=PlanMaximumZ&&TileMaximumZ>=PlanMinimumZ);
|
||||
}
|
||||
}
|
||||
const bool bUseNativeSurfaceHorizon=key.lod>=5&&!bOverlapsPlayerEdit
|
||||
&&bHasNativeSurfaceHorizonPlan&&!bHasIslandPlanInTile;
|
||||
const bool bSkipFarShellRepair=key.lod>=7&&bUseNativeSurfaceHorizon;
|
||||
FStandaloneShapeWorld::ResetThreadClassificationStats();
|
||||
VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats();
|
||||
bool bNativeSurfaceTile=false;
|
||||
@@ -1082,18 +1405,6 @@ namespace VoxelForgeCore
|
||||
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(
|
||||
@@ -1190,21 +1501,52 @@ 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;
|
||||
const auto FarShellStart=std::chrono::steady_clock::now();
|
||||
const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape;
|
||||
if(farShape.HasFarWorld())
|
||||
out.diagnostics.farShellRepairSkipped=bSkipFarShellRepair;
|
||||
if(farShape.HasFarWorld()&&!bSkipFarShellRepair)
|
||||
{
|
||||
const auto BeforeStratum=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
const int32 BeforeStratumTriangles=mesh.Triangles.Num()/3;
|
||||
const auto StratumStart=std::chrono::steady_clock::now();
|
||||
AppendFarStratumShells(world,farShape,nativeKey,step,mesh);
|
||||
const auto AfterStratum=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
out.diagnostics.farStratumShellMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-StratumStart).count();
|
||||
out.diagnostics.farStratumFieldCalls=
|
||||
AfterStratum.NativeFloatFieldCalls-BeforeStratum.NativeFloatFieldCalls;
|
||||
out.diagnostics.farShellTrianglesAdded=static_cast<std::uint64_t>(
|
||||
std::max(0,mesh.Triangles.Num()/3-BeforeStratumTriangles));
|
||||
|
||||
const auto BeforeConnector=AfterStratum;
|
||||
const int32 BeforeConnectorTriangles=mesh.Triangles.Num()/3;
|
||||
const auto ConnectorStart=std::chrono::steady_clock::now();
|
||||
AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh);
|
||||
const auto AfterConnector=FStandaloneShapeWorld::GetThreadClassificationStats();
|
||||
out.diagnostics.farConnectorShellMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-ConnectorStart).count();
|
||||
out.diagnostics.farConnectorFieldCalls=
|
||||
AfterConnector.NativeFloatFieldCalls-BeforeConnector.NativeFloatFieldCalls;
|
||||
out.diagnostics.farShellTrianglesAdded+=static_cast<std::uint64_t>(
|
||||
std::max(0,mesh.Triangles.Num()/3-BeforeConnectorTriangles));
|
||||
}
|
||||
out.diagnostics.farShellMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-FarShellStart).count();
|
||||
if(impl_->worldAssembly&&key.lod>=8)
|
||||
if(impl_->worldAssembly&&key.lod>=5&&!bOverlapsPlayerEdit)
|
||||
{
|
||||
const auto CapStats=RefineLod8NativeCap(world,*impl_->worldAssembly,nativeKey,mesh);
|
||||
const auto HorizonStart=std::chrono::steady_clock::now();
|
||||
const auto 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();
|
||||
out.diagnostics.capRefined=CapStats.Refined;
|
||||
out.diagnostics.capRefinedCells=CapStats.Cells;
|
||||
out.diagnostics.capRefinedTriangles=CapStats.Triangles;
|
||||
out.diagnostics.horizonGroundTriangles=GroundStats.Triangles;
|
||||
out.diagnostics.horizonGroundTrianglesRemoved=GroundStats.RemovedTriangles;
|
||||
out.diagnostics.horizonCapTriangles=CapStats.Triangles;
|
||||
}
|
||||
|
||||
out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
|
||||
@@ -1270,6 +1612,12 @@ 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)
|
||||
{
|
||||
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
|
||||
out.payloadHash=SaltFarHorizonHash(out.payloadHash,options.farHorizonVersion);
|
||||
out.topologyHash=SaltFarHorizonHash(out.topologyHash,options.farHorizonVersion);
|
||||
}
|
||||
out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true;
|
||||
}
|
||||
return out;
|
||||
|
||||
Reference in new issue
Block a user