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:
1 parent
d0f97bf79a
commit
9d3c50ca95
14 files changed
+1376
-34
No files matched your search
@@ -24,6 +24,7 @@
|
||||
#include "VoxelClipmapDesiredTiles.h"
|
||||
#include "VoxelCollisionDesiredTiles.h"
|
||||
#include "VoxelPreEditDensityCache.h"
|
||||
#include "RealtimeMeshLibrary.h"
|
||||
// IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc
|
||||
// APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController n'est
|
||||
// complet que par transitivité : on l'inclut explicitement.
|
||||
@@ -12471,21 +12472,30 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
// (default config already maps material slot = polygroup index); only config sections that
|
||||
// exist — the bHas* flags come from the worker. Collision at level 0 only (T1.c), both groups.
|
||||
const uint64 CollisionConfigStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
|
||||
const int32 NumIndices = Result.NumTriangles * 3;
|
||||
const int32 FirstCapIndex = FMath::Clamp(
|
||||
NumIndices - Result.NumCeilingTriangles * 3, 0, NumIndices);
|
||||
if (bHasGroundTris)
|
||||
{
|
||||
FRealtimeMeshSectionConfig GroundConfig(0);
|
||||
GroundConfig.bCastsShadow = bCastShadow;
|
||||
RTMesh->UpdateSectionConfig(
|
||||
const FRealtimeMeshStreamRange GroundRange =
|
||||
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
|
||||
0, Result.NumVertices, 0, FirstCapIndex);
|
||||
RTMesh->CreateSection(
|
||||
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 0),
|
||||
GroundConfig, /*bShouldCreateCollision*/ bWantsCollision);
|
||||
GroundConfig, GroundRange, /*bShouldCreateCollision*/ bWantsCollision);
|
||||
}
|
||||
if (bHasCeilingTris)
|
||||
{
|
||||
FRealtimeMeshSectionConfig CapConfig(1);
|
||||
CapConfig.bCastsShadow = false; // the cap never casts (see above)
|
||||
RTMesh->UpdateSectionConfig(
|
||||
const FRealtimeMeshStreamRange CapRange =
|
||||
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
|
||||
0, Result.NumVertices, FirstCapIndex, NumIndices);
|
||||
RTMesh->CreateSection(
|
||||
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1),
|
||||
CapConfig, /*bShouldCreateCollision*/ bWantsCollision);
|
||||
CapConfig, CapRange, /*bShouldCreateCollision*/ bWantsCollision);
|
||||
}
|
||||
const double CollisionConfigSeconds = bProfileApply
|
||||
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - CollisionConfigStartCycles)
|
||||
|
||||
@@ -15221,7 +15221,8 @@ bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World,
|
||||
if(Other==Ground8.ByXY.end())continue;
|
||||
const auto Column=Field.point(Pair.first.first,Pair.first.second,0);
|
||||
const std::int64_t Ground=Column.groundHeightSubvoxels;
|
||||
const std::int64_t Cap=Column.capHeightSubvoxels;
|
||||
const std::int64_t Cap=Field.farSurfaceCapHeightSubvoxels(
|
||||
Pair.first.first,Pair.first.second);
|
||||
if(Ground>=Cap)continue;
|
||||
const double GroundVoxel=static_cast<double>(Ground)/SubvoxelsPerVoxel;
|
||||
if(GroundVoxel<Bounds7[2]||GroundVoxel>Bounds7[5]
|
||||
@@ -15263,7 +15264,8 @@ bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World,
|
||||
const std::int64_t YSub=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(RouteYSub)/GridPeriod))*GridPeriod;
|
||||
const auto WalkColumn=Field.point(XSub,YSub,0);
|
||||
if(WalkColumn.groundHeightSubvoxels>=WalkColumn.capHeightSubvoxels)continue;
|
||||
const std::int64_t WalkCap=Field.farSurfaceCapHeightSubvoxels(XSub,YSub);
|
||||
if(WalkColumn.groundHeightSubvoxels>=WalkCap)continue;
|
||||
const std::int64_t XVoxel=XSub/SubvoxelsPerVoxel,YVoxel=YSub/SubvoxelsPerVoxel;
|
||||
const std::int64_t WalkGroundVoxel=WalkColumn.groundHeightSubvoxels/SubvoxelsPerVoxel;
|
||||
const auto MakeKey=[](std::int64_t X,std::int64_t Y,std::int64_t Z,std::int64_t Extent,int32 LOD)
|
||||
@@ -15293,7 +15295,8 @@ bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World,
|
||||
if(Other==Ground8.ByXY.end())continue;
|
||||
const auto Column=Field.point(Pair.first.first,Pair.first.second,0);
|
||||
const std::int64_t Ground=Column.groundHeightSubvoxels;
|
||||
if(Ground>=Column.capHeightSubvoxels)continue;
|
||||
if(Ground>=Field.farSurfaceCapHeightSubvoxels(
|
||||
Pair.first.first,Pair.first.second))continue;
|
||||
const auto Bounds7=TileBounds(Key7),Bounds8=TileBounds(Key8);
|
||||
const double GroundVoxel=static_cast<double>(Ground)/SubvoxelsPerVoxel;
|
||||
if(GroundVoxel<Bounds7[2]||GroundVoxel>Bounds7[5]
|
||||
@@ -15373,6 +15376,554 @@ bool RunCoreFarHorizonAgreementAudit(VoxelForgeCore::World& World,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunCoreFarCapTileCensus(VoxelForgeCore::World& World,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& Assembly,FString& OutError)
|
||||
{
|
||||
OutError.Reset();
|
||||
constexpr std::int64_t SubvoxelsPerVoxel=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
constexpr std::int64_t ProfileGridStep=128;
|
||||
using FTileId=std::array<std::int32_t,4>;
|
||||
using FXYLod=std::array<std::int32_t,3>;
|
||||
const auto IdFor=[](const VoxelForgeCore::TileKey& Key)
|
||||
{return FTileId{Key.coordinate.x,Key.coordinate.y,Key.coordinate.z,Key.lod};};
|
||||
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);
|
||||
};
|
||||
|
||||
const VoxelForgeCore::WorldAssemblyStratumPlanV1* Plan=nullptr;
|
||||
for(const auto& Candidate:Assembly.plan.strates)
|
||||
if(!Candidate.route.empty()) {Plan=&Candidate;break;}
|
||||
if(Plan==nullptr)
|
||||
{OutError=TEXT("Far-cap census has no SurfaceWorld walk route.");return false;}
|
||||
const VoxelForgeCore::Heightfield2D Field(Assembly.recipe,*Plan);
|
||||
if(!Field.isValid())
|
||||
{OutError=TEXT("Far-cap census received an invalid SurfaceWorld heightfield.");return false;}
|
||||
|
||||
const auto& WalkStart=Plan->route.front();
|
||||
const VoxelForgeCore::Vec3d Camera{
|
||||
static_cast<double>(WalkStart.positionSubvoxels[0])/SubvoxelsPerVoxel,
|
||||
static_cast<double>(WalkStart.positionSubvoxels[1])/SubvoxelsPerVoxel,
|
||||
static_cast<double>(WalkStart.positionSubvoxels[2])/SubvoxelsPerVoxel};
|
||||
const std::vector<VoxelForgeCore::TileKey> WalkKeys=World.desiredTiles(Camera);
|
||||
std::set<FTileId> Desired;
|
||||
std::set<FXYLod> FarXYTiles;
|
||||
double ClipMaxZ=-std::numeric_limits<double>::infinity();
|
||||
for(const auto& Key:WalkKeys)
|
||||
{
|
||||
Desired.insert(IdFor(Key));
|
||||
const double Extent=static_cast<double>(32<<Key.lod);
|
||||
ClipMaxZ=std::max(ClipMaxZ,(static_cast<double>(Key.coordinate.z)+1.0)*Extent);
|
||||
if(Key.lod>=5&&Key.lod<=8)
|
||||
FarXYTiles.insert({Key.coordinate.x,Key.coordinate.y,Key.lod});
|
||||
}
|
||||
std::set<FTileId> CapBands;
|
||||
std::uint64_t ProfileGridSamples=0, ProfileSamplesWithoutRoof=0;
|
||||
std::set<FXYLod> FarXYTilesWithoutRoof;
|
||||
for(const FXYLod& XY:FarXYTiles)
|
||||
{
|
||||
const std::int32_t Lod=XY[2];
|
||||
const std::int64_t Extent=static_cast<std::int64_t>(32)<<Lod;
|
||||
const std::int64_t OriginX=static_cast<std::int64_t>(XY[0])*Extent;
|
||||
const std::int64_t OriginY=static_cast<std::int64_t>(XY[1])*Extent;
|
||||
for(std::int64_t Y=0;Y<=Extent;Y+=ProfileGridStep)
|
||||
for(std::int64_t X=0;X<=Extent;X+=ProfileGridStep)
|
||||
{
|
||||
++ProfileGridSamples;
|
||||
const auto Column=Field.point((OriginX+X)*SubvoxelsPerVoxel,
|
||||
(OriginY+Y)*SubvoxelsPerVoxel,0);
|
||||
const std::int64_t FarCap=Field.farSurfaceCapHeightSubvoxels(
|
||||
(OriginX+X)*SubvoxelsPerVoxel,(OriginY+Y)*SubvoxelsPerVoxel);
|
||||
if(FarCap<=Column.groundHeightSubvoxels)
|
||||
{
|
||||
++ProfileSamplesWithoutRoof;
|
||||
FarXYTilesWithoutRoof.insert(XY);
|
||||
continue;
|
||||
}
|
||||
const std::int64_t Band=FloorDiv64(FarCap,
|
||||
Extent*SubvoxelsPerVoxel);
|
||||
if(Band>=MIN_int32&&Band<=MAX_int32)
|
||||
CapBands.insert({XY[0],XY[1],static_cast<std::int32_t>(Band),Lod});
|
||||
if(FarCap%(Extent*SubvoxelsPerVoxel)==0
|
||||
&&Band>MIN_int32)
|
||||
CapBands.insert({XY[0],XY[1],static_cast<std::int32_t>(Band-1),Lod});
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t RequestedCapBands=0, EmittedCapBands=0, RequestedWithoutTriangles=0;
|
||||
std::uint64_t CulledCapBands=0;
|
||||
std::map<std::int32_t,std::array<std::uint64_t,4>> ByLod;
|
||||
VoxelForgeCore::TileGenerationOptions Options;
|
||||
std::string CoreError;
|
||||
struct FCapAgreementSample
|
||||
{ std::int64_t XSub=0,YSub=0;double FarZVoxel=0.0; };
|
||||
using FCapXYKey=std::pair<std::int64_t,std::int64_t>;
|
||||
std::set<FCapXYKey> CapAgreementSeen;
|
||||
std::vector<FCapAgreementSample> CapAgreementSamples;
|
||||
CapAgreementSamples.reserve(128);
|
||||
const double VoxelSizeCm=static_cast<double>(World.configuration().voxelSizeMicrons)/10000.0;
|
||||
constexpr std::int64_t CapGridPeriod=128*SubvoxelsPerVoxel;
|
||||
for(const FTileId& Id:CapBands)
|
||||
{
|
||||
const std::int32_t Lod=Id[3];
|
||||
auto& Counts=ByLod[Lod];
|
||||
++Counts[0];
|
||||
if(Desired.find(Id)==Desired.end())
|
||||
{
|
||||
++CulledCapBands;++Counts[1];continue;
|
||||
}
|
||||
++RequestedCapBands;
|
||||
VoxelForgeCore::TileKey Key{{Id[0],Id[1],Id[2]},Lod};
|
||||
const auto Mesh=World.generateTile(Key,Options,CoreError);
|
||||
if(!Mesh)
|
||||
{
|
||||
OutError=FString::Printf(TEXT("Far-cap census tile generation failed at (%d,%d,%d) LOD%d: %s"),
|
||||
Id[0],Id[1],Id[2],Lod,UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if(Mesh->diagnostics.horizonCapTriangles>0)
|
||||
{
|
||||
++EmittedCapBands;++Counts[2];
|
||||
const std::size_t CapIndexStart=Mesh->indices.size()
|
||||
-static_cast<std::size_t>(Mesh->diagnostics.horizonCapTriangles)*3u;
|
||||
for(std::size_t Index=CapIndexStart;Index<Mesh->indices.size()
|
||||
&&CapAgreementSamples.size()<128;++Index)
|
||||
{
|
||||
const std::uint32_t VertexIndex=Mesh->indices[Index];
|
||||
if(VertexIndex>=Mesh->vertices.size())continue;
|
||||
const auto& Vertex=Mesh->vertices[VertexIndex];
|
||||
if(Vertex.normal.z>=-0.35)continue;
|
||||
const std::int64_t XSub=static_cast<std::int64_t>(std::llround(
|
||||
Vertex.position.x/VoxelSizeCm*SubvoxelsPerVoxel));
|
||||
const std::int64_t YSub=static_cast<std::int64_t>(std::llround(
|
||||
Vertex.position.y/VoxelSizeCm*SubvoxelsPerVoxel));
|
||||
if(XSub%CapGridPeriod!=0||YSub%CapGridPeriod!=0)continue;
|
||||
if(!CapAgreementSeen.insert({XSub,YSub}).second)continue;
|
||||
CapAgreementSamples.push_back({XSub,YSub,
|
||||
Vertex.position.z/VoxelSizeCm});
|
||||
}
|
||||
}
|
||||
else
|
||||
{++RequestedWithoutTriangles;++Counts[3];}
|
||||
}
|
||||
|
||||
constexpr double CapAgreementToleranceCm=2.0;
|
||||
constexpr double CapNegativeDatumOffsetCm=100.0;
|
||||
const auto FindNearCapRoot=[&](std::int64_t XSub,std::int64_t YSub,
|
||||
double ProfileCapVoxel,double& OutRootVoxel)
|
||||
{
|
||||
const double X=static_cast<double>(XSub)/SubvoxelsPerVoxel;
|
||||
const double Y=static_cast<double>(YSub)/SubvoxelsPerVoxel;
|
||||
for(double Radius=4.0;Radius<=4096.0;Radius*=2.0)
|
||||
{
|
||||
double Lo=ProfileCapVoxel-Radius,Hi=ProfileCapVoxel+Radius;
|
||||
if(!(World.densityAt({X,Y,Lo})>=0.0f&&World.densityAt({X,Y,Hi})<0.0f))continue;
|
||||
for(int Iteration=0;Iteration<16;++Iteration)
|
||||
{
|
||||
const double Mid=(Lo+Hi)*0.5;
|
||||
if(World.densityAt({X,Y,Mid})>=0.0f)Lo=Mid;else Hi=Mid;
|
||||
}
|
||||
OutRootVoxel=(Lo+Hi)*0.5;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
double MaximumCapAgreementErrorCm=0.0,MaximumCapNegativeControlErrorCm=0.0;
|
||||
std::uint64_t CapAgreementCompared=0;
|
||||
for(const FCapAgreementSample& Sample:CapAgreementSamples)
|
||||
{
|
||||
const auto Column=Field.point(Sample.XSub,Sample.YSub,0);
|
||||
const std::int64_t FarCap=Field.farSurfaceCapHeightSubvoxels(
|
||||
Sample.XSub,Sample.YSub);
|
||||
if(FarCap<=Column.groundHeightSubvoxels)continue;
|
||||
double NearRootVoxel=0.0;
|
||||
if(!FindNearCapRoot(Sample.XSub,Sample.YSub,
|
||||
static_cast<double>(FarCap)/SubvoxelsPerVoxel,NearRootVoxel))continue;
|
||||
const double FarCapCm=Sample.FarZVoxel*VoxelSizeCm;
|
||||
const double NearCapCm=NearRootVoxel*VoxelSizeCm;
|
||||
MaximumCapAgreementErrorCm=std::max(MaximumCapAgreementErrorCm,
|
||||
std::abs(FarCapCm-NearCapCm));
|
||||
MaximumCapNegativeControlErrorCm=std::max(MaximumCapNegativeControlErrorCm,
|
||||
std::abs(FarCapCm+CapNegativeDatumOffsetCm-NearCapCm));
|
||||
++CapAgreementCompared;
|
||||
}
|
||||
const bool bCapNegativeControlDetected=CapAgreementCompared>0
|
||||
&&MaximumCapNegativeControlErrorCm>CapAgreementToleranceCm;
|
||||
UE_LOG(LogTemp,Display,
|
||||
TEXT("[VoxelForgeFarCapAgreement] status=%s shared_points=%llu max_far_near_cm=%.4f tolerance_cm=%.2f near_source=exact_density_root negative_control=100cm_datum_offset max_error_cm=%.4f detected=%d"),
|
||||
CapAgreementCompared>=32&&MaximumCapAgreementErrorCm<=CapAgreementToleranceCm
|
||||
&&bCapNegativeControlDetected?TEXT("PASS"):TEXT("FAIL"),
|
||||
static_cast<unsigned long long>(CapAgreementCompared),MaximumCapAgreementErrorCm,
|
||||
CapAgreementToleranceCm,MaximumCapNegativeControlErrorCm,bCapNegativeControlDetected?1:0);
|
||||
if(CapAgreementCompared<32||MaximumCapAgreementErrorCm>CapAgreementToleranceCm
|
||||
||!bCapNegativeControlDetected)
|
||||
{
|
||||
OutError=FString::Printf(TEXT("Far-cap agreement failed: points=%llu max_error_cm=%.4f tolerance_cm=%.2f negative_control_cm=%.4f detected=%d."),
|
||||
static_cast<unsigned long long>(CapAgreementCompared),MaximumCapAgreementErrorCm,
|
||||
CapAgreementToleranceCm,MaximumCapNegativeControlErrorCm,bCapNegativeControlDetected?1:0);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Les rayons couvrent l'hemisphere superieur depuis chaque point de marche.
|
||||
// Cast into the upper view hemisphere from every authored walk point.
|
||||
constexpr double Pi=3.14159265358979323846;
|
||||
constexpr std::array<double,8> AzimuthDegrees={0.0,45.0,90.0,135.0,180.0,225.0,270.0,315.0};
|
||||
constexpr std::array<double,6> ElevationDegrees={15.0,30.0,45.0,60.0,75.0,90.0};
|
||||
constexpr double RayStepVoxels=128.0;
|
||||
double ClipMinX=std::numeric_limits<double>::infinity();
|
||||
double ClipMinY=std::numeric_limits<double>::infinity();
|
||||
double ClipMaxX=-std::numeric_limits<double>::infinity();
|
||||
double ClipMaxY=-std::numeric_limits<double>::infinity();
|
||||
for(const FXYLod& XY:FarXYTiles)
|
||||
{
|
||||
const double Extent=static_cast<double>(32<<XY[2]);
|
||||
ClipMinX=std::min(ClipMinX,static_cast<double>(XY[0])*Extent);
|
||||
ClipMinY=std::min(ClipMinY,static_cast<double>(XY[1])*Extent);
|
||||
ClipMaxX=std::max(ClipMaxX,(static_cast<double>(XY[0])+1.0)*Extent);
|
||||
ClipMaxY=std::max(ClipMaxY,(static_cast<double>(XY[1])+1.0)*Extent);
|
||||
}
|
||||
enum class ECapRayTileResult : std::uint8_t
|
||||
{ EmittedHit, CulledByClipmap, RequestedWithoutTriangles, EmittedWithoutHit };
|
||||
struct FCapRayTileGeometry
|
||||
{
|
||||
std::vector<std::array<VoxelForgeCore::Vec3d,3>> Triangles;
|
||||
};
|
||||
std::uint64_t RayCount=0, RayHits=0, RayMissedClipEdge=0;
|
||||
std::uint64_t RayMissedNoRoof=0, RayMissedCulledByClipmap=0;
|
||||
std::uint64_t RayMissedRequestedWithoutTriangles=0, RayMissedEmittedWithoutHit=0;
|
||||
std::uint64_t RayStartedAboveCap=0;
|
||||
std::uint64_t RayProfileCrossings=0,RayProfileRoofCrossings=0,RayProfileRoofWithoutMeshHit=0;
|
||||
std::array<std::array<std::uint64_t,6>,6> RayResultsByElevation{};
|
||||
std::vector<double> RayHitDistancesMeters;
|
||||
RayHitDistancesMeters.reserve(Plan->route.size()*AzimuthDegrees.size()*ElevationDegrees.size());
|
||||
std::map<FTileId,FCapRayTileGeometry> RayTileGeometryCache;
|
||||
const auto GetCapRayTileGeometry=[&](const FTileId& Id)->const FCapRayTileGeometry*
|
||||
{
|
||||
auto Found=RayTileGeometryCache.find(Id);
|
||||
if(Found!=RayTileGeometryCache.end())return &Found->second;
|
||||
const VoxelForgeCore::TileKey Key{{Id[0],Id[1],Id[2]},Id[3]};
|
||||
const auto Mesh=World.generateTile(Key,Options,CoreError);
|
||||
if(!Mesh)
|
||||
{
|
||||
OutError=FString::Printf(TEXT("Far-cap ray tile generation failed at (%d,%d,%d) LOD%d: %s"),
|
||||
Id[0],Id[1],Id[2],Id[3],UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return nullptr;
|
||||
}
|
||||
FCapRayTileGeometry Geometry;
|
||||
Geometry.Triangles.reserve(Mesh->groups.ceilingTriangleCount);
|
||||
for(std::size_t Index=0;Index+2<Mesh->indices.size();Index+=3)
|
||||
{
|
||||
const std::uint32_t IA=Mesh->indices[Index],IB=Mesh->indices[Index+1],IC=Mesh->indices[Index+2];
|
||||
if(IA>=Mesh->vertices.size()||IB>=Mesh->vertices.size()||IC>=Mesh->vertices.size())continue;
|
||||
const double AverageNormalZ=(Mesh->vertices[IA].normal.z+Mesh->vertices[IB].normal.z
|
||||
+Mesh->vertices[IC].normal.z)/3.0;
|
||||
// Native marching-cubes roofs and refined far caps both face out of the solid
|
||||
// toward decreasing Z density. Include those ceiling faces at LOD0-8.
|
||||
// Les plafonds MC natifs et les caps lointains pointent hors du solide, vers -Z.
|
||||
if(AverageNormalZ>=-0.05)continue;
|
||||
Geometry.Triangles.push_back({
|
||||
VoxelForgeCore::Vec3d{Mesh->vertices[IA].position.x/VoxelSizeCm,
|
||||
Mesh->vertices[IA].position.y/VoxelSizeCm,Mesh->vertices[IA].position.z/VoxelSizeCm},
|
||||
VoxelForgeCore::Vec3d{Mesh->vertices[IB].position.x/VoxelSizeCm,
|
||||
Mesh->vertices[IB].position.y/VoxelSizeCm,Mesh->vertices[IB].position.z/VoxelSizeCm},
|
||||
VoxelForgeCore::Vec3d{Mesh->vertices[IC].position.x/VoxelSizeCm,
|
||||
Mesh->vertices[IC].position.y/VoxelSizeCm,Mesh->vertices[IC].position.z/VoxelSizeCm}});
|
||||
}
|
||||
return &RayTileGeometryCache.emplace(Id,std::move(Geometry)).first->second;
|
||||
};
|
||||
const auto Dot3=[](const VoxelForgeCore::Vec3d& A,const VoxelForgeCore::Vec3d& B)
|
||||
{return A.x*B.x+A.y*B.y+A.z*B.z;};
|
||||
const auto Sub3=[](const VoxelForgeCore::Vec3d& A,const VoxelForgeCore::Vec3d& B)
|
||||
{return VoxelForgeCore::Vec3d{A.x-B.x,A.y-B.y,A.z-B.z};};
|
||||
const auto Cross3=[](const VoxelForgeCore::Vec3d& A,const VoxelForgeCore::Vec3d& B)
|
||||
{return VoxelForgeCore::Vec3d{A.y*B.z-A.z*B.y,A.z*B.x-A.x*B.z,A.x*B.y-A.y*B.x};};
|
||||
const auto RayTriangleDistance=[&](const VoxelForgeCore::Vec3d& Origin,
|
||||
const VoxelForgeCore::Vec3d& Direction,const std::array<VoxelForgeCore::Vec3d,3>& Triangle,
|
||||
double MinimumDistance,double MaximumDistance,double& OutDistance)
|
||||
{
|
||||
const auto Edge1=Sub3(Triangle[1],Triangle[0]);
|
||||
const auto Edge2=Sub3(Triangle[2],Triangle[0]);
|
||||
const auto P=Cross3(Direction,Edge2);
|
||||
const double Determinant=Dot3(Edge1,P);
|
||||
if(std::abs(Determinant)<1.0e-12)return false;
|
||||
const double Inverse=1.0/Determinant;
|
||||
const auto T=Sub3(Origin,Triangle[0]);
|
||||
const double U=Dot3(T,P)*Inverse;
|
||||
if(U< -1.0e-8||U>1.0+1.0e-8)return false;
|
||||
const auto Q=Cross3(T,Edge1);
|
||||
const double V=Dot3(Direction,Q)*Inverse;
|
||||
if(V< -1.0e-8||U+V>1.0+1.0e-8)return false;
|
||||
const double Distance=Dot3(Edge2,Q)*Inverse;
|
||||
if(Distance<MinimumDistance-1.0e-6||Distance>MaximumDistance+1.0e-6)return false;
|
||||
OutDistance=Distance;
|
||||
return true;
|
||||
};
|
||||
const auto TraceEmittedCapRay=[&](const VoxelForgeCore::Vec3d& Origin,
|
||||
const VoxelForgeCore::Vec3d& Direction,double MaximumDistance,double& OutHitDistance)
|
||||
{
|
||||
bool bFoundRequestedKey=false,bFoundEmptyRequestedKey=false,bFoundGeometryWithoutHit=false;
|
||||
bool bFoundHit=false;
|
||||
for(const FTileId& Id:Desired)
|
||||
{
|
||||
const double Extent=static_cast<double>(32<<Id[3]);
|
||||
const std::array<double,3> BoundsMin={static_cast<double>(Id[0])*Extent,
|
||||
static_cast<double>(Id[1])*Extent,static_cast<double>(Id[2])*Extent};
|
||||
const std::array<double,3> BoundsMax={BoundsMin[0]+Extent,BoundsMin[1]+Extent,
|
||||
BoundsMin[2]+Extent};
|
||||
double TileEnter=0.0,TileExit=MaximumDistance;
|
||||
bool bIntersects=true;
|
||||
for(std::size_t Axis=0;Axis<3;++Axis)
|
||||
{
|
||||
const double RayOrigin=Axis==0?Origin.x:(Axis==1?Origin.y:Origin.z);
|
||||
const double RayDirection=Axis==0?Direction.x:(Axis==1?Direction.y:Direction.z);
|
||||
if(std::abs(RayDirection)<1.0e-12)
|
||||
{
|
||||
if(RayOrigin<BoundsMin[Axis]||RayOrigin>BoundsMax[Axis])
|
||||
{bIntersects=false;break;}
|
||||
continue;
|
||||
}
|
||||
double Near=(BoundsMin[Axis]-RayOrigin)/RayDirection;
|
||||
double Far=(BoundsMax[Axis]-RayOrigin)/RayDirection;
|
||||
if(Near>Far)std::swap(Near,Far);
|
||||
TileEnter=std::max(TileEnter,Near);
|
||||
TileExit=std::min(TileExit,Far);
|
||||
if(TileEnter>TileExit){bIntersects=false;break;}
|
||||
}
|
||||
if(!bIntersects||TileExit<0.0||TileEnter>MaximumDistance)continue;
|
||||
bFoundRequestedKey=true;
|
||||
const FCapRayTileGeometry* Geometry=GetCapRayTileGeometry(Id);
|
||||
if(Geometry==nullptr)continue;
|
||||
if(Geometry->Triangles.empty())
|
||||
{bFoundEmptyRequestedKey=true;continue;}
|
||||
bFoundGeometryWithoutHit=true;
|
||||
for(const auto& Triangle:Geometry->Triangles)
|
||||
{
|
||||
double HitDistance=0.0;
|
||||
if(!RayTriangleDistance(Origin,Direction,Triangle,
|
||||
std::max(0.0,TileEnter),std::min(MaximumDistance,TileExit),HitDistance))continue;
|
||||
if(!bFoundHit||HitDistance<OutHitDistance)OutHitDistance=HitDistance;
|
||||
bFoundHit=true;
|
||||
}
|
||||
if(!OutError.IsEmpty())return ECapRayTileResult::RequestedWithoutTriangles;
|
||||
}
|
||||
if(bFoundHit)return ECapRayTileResult::EmittedHit;
|
||||
if(bFoundRequestedKey&&bFoundGeometryWithoutHit)return ECapRayTileResult::EmittedWithoutHit;
|
||||
if(bFoundRequestedKey&&bFoundEmptyRequestedKey)return ECapRayTileResult::RequestedWithoutTriangles;
|
||||
return ECapRayTileResult::CulledByClipmap;
|
||||
};
|
||||
for(std::size_t RouteIndex=0;RouteIndex<Plan->route.size();++RouteIndex)
|
||||
{
|
||||
const auto& RoutePoint=Plan->route[RouteIndex];
|
||||
const double StartX=static_cast<double>(RoutePoint.positionSubvoxels[0])/SubvoxelsPerVoxel;
|
||||
const double StartY=static_cast<double>(RoutePoint.positionSubvoxels[1])/SubvoxelsPerVoxel;
|
||||
const double StartZ=static_cast<double>(RoutePoint.positionSubvoxels[2])/SubvoxelsPerVoxel;
|
||||
const auto StartColumn=Field.point(RoutePoint.positionSubvoxels[0],
|
||||
RoutePoint.positionSubvoxels[1],0);
|
||||
const double StartCap=static_cast<double>(Field.farSurfaceCapHeightSubvoxels(
|
||||
RoutePoint.positionSubvoxels[0],RoutePoint.positionSubvoxels[1]))/SubvoxelsPerVoxel;
|
||||
for(const double Elevation:ElevationDegrees)
|
||||
for(const double Azimuth:AzimuthDegrees)
|
||||
{
|
||||
++RayCount;
|
||||
const std::size_t ElevationIndex=static_cast<std::size_t>(
|
||||
std::find(ElevationDegrees.begin(),ElevationDegrees.end(),Elevation)
|
||||
-ElevationDegrees.begin());
|
||||
if(!(StartCap>StartZ))
|
||||
{++RayStartedAboveCap;++RayMissedNoRoof;++RayResultsByElevation[ElevationIndex][4];continue;}
|
||||
const double ElevationRadians=Elevation*Pi/180.0;
|
||||
const double AzimuthRadians=Azimuth*Pi/180.0;
|
||||
const double DX=std::cos(ElevationRadians)*std::cos(AzimuthRadians);
|
||||
const double DY=std::cos(ElevationRadians)*std::sin(AzimuthRadians);
|
||||
const double DZ=std::sin(ElevationRadians);
|
||||
double EdgeDistance=std::numeric_limits<double>::infinity();
|
||||
if(DX>1.0e-9)EdgeDistance=std::min(EdgeDistance,(ClipMaxX-StartX)/DX);
|
||||
else if(DX< -1.0e-9)EdgeDistance=std::min(EdgeDistance,(ClipMinX-StartX)/DX);
|
||||
if(DY>1.0e-9)EdgeDistance=std::min(EdgeDistance,(ClipMaxY-StartY)/DY);
|
||||
else if(DY< -1.0e-9)EdgeDistance=std::min(EdgeDistance,(ClipMinY-StartY)/DY);
|
||||
if(DZ>1.0e-9)
|
||||
EdgeDistance=std::min(EdgeDistance,(ClipMaxZ-StartZ)/DZ);
|
||||
if(!std::isfinite(EdgeDistance)||EdgeDistance<=0.0)
|
||||
{++RayMissedClipEdge;++RayResultsByElevation[ElevationIndex][5];continue;}
|
||||
bool bProfileCrossing=false,bProfileRoofAtCrossing=false;
|
||||
double ProfileHitDistance=std::numeric_limits<double>::infinity();
|
||||
double ProfileHitX=0.0,ProfileHitY=0.0,ProfileHitZ=0.0;
|
||||
std::int64_t ProfileHitCapSubvoxels=0;
|
||||
double PreviousDistance=0.0,PreviousDelta=StartZ-StartCap;
|
||||
for(double Distance=std::min(RayStepVoxels,EdgeDistance);;)
|
||||
{
|
||||
const double X=StartX+DX*Distance,Y=StartY+DY*Distance;
|
||||
const double Z=StartZ+DZ*Distance;
|
||||
const std::int64_t CapSubvoxels=Field.farSurfaceCapHeightSubvoxels(
|
||||
static_cast<std::int64_t>(std::llround(X*SubvoxelsPerVoxel)),
|
||||
static_cast<std::int64_t>(std::llround(Y*SubvoxelsPerVoxel)));
|
||||
const double Cap=static_cast<double>(CapSubvoxels)/SubvoxelsPerVoxel;
|
||||
const double Delta=Z-Cap;
|
||||
if(PreviousDelta<0.0&&Delta>=0.0)
|
||||
{
|
||||
const double Fraction=-PreviousDelta/(Delta-PreviousDelta);
|
||||
ProfileHitDistance=PreviousDistance+(Distance-PreviousDistance)*Fraction;
|
||||
const double HitX=StartX+DX*ProfileHitDistance;
|
||||
const double HitY=StartY+DY*ProfileHitDistance;
|
||||
const auto HitColumn=Field.point(
|
||||
static_cast<std::int64_t>(std::llround(HitX*SubvoxelsPerVoxel)),
|
||||
static_cast<std::int64_t>(std::llround(HitY*SubvoxelsPerVoxel)),0);
|
||||
const std::int64_t HitCapSubvoxels=Field.farSurfaceCapHeightSubvoxels(
|
||||
static_cast<std::int64_t>(std::llround(HitX*SubvoxelsPerVoxel)),
|
||||
static_cast<std::int64_t>(std::llround(HitY*SubvoxelsPerVoxel)));
|
||||
bProfileCrossing=true;
|
||||
bProfileRoofAtCrossing=HitCapSubvoxels>HitColumn.groundHeightSubvoxels;
|
||||
ProfileHitX=HitX;ProfileHitY=HitY;
|
||||
ProfileHitZ=StartZ+DZ*ProfileHitDistance;
|
||||
ProfileHitCapSubvoxels=HitCapSubvoxels;
|
||||
++RayProfileCrossings;
|
||||
if(bProfileRoofAtCrossing)++RayProfileRoofCrossings;
|
||||
break;
|
||||
}
|
||||
PreviousDistance=Distance;PreviousDelta=Delta;
|
||||
if(Distance>=EdgeDistance)break;
|
||||
Distance=std::min(Distance+RayStepVoxels,EdgeDistance);
|
||||
}
|
||||
const VoxelForgeCore::Vec3d RayOrigin{StartX,StartY,StartZ};
|
||||
const VoxelForgeCore::Vec3d RayDirection{DX,DY,DZ};
|
||||
double MeshHitDistance=std::numeric_limits<double>::infinity();
|
||||
const ECapRayTileResult TileResult=TraceEmittedCapRay(
|
||||
RayOrigin,RayDirection,EdgeDistance,MeshHitDistance);
|
||||
if(!OutError.IsEmpty())return false;
|
||||
switch(TileResult)
|
||||
{
|
||||
case ECapRayTileResult::EmittedHit:
|
||||
++RayHits;++RayResultsByElevation[ElevationIndex][0];
|
||||
RayHitDistancesMeters.push_back(MeshHitDistance*VoxelSizeCm/100.0);break;
|
||||
case ECapRayTileResult::CulledByClipmap:
|
||||
++RayMissedCulledByClipmap;++RayResultsByElevation[ElevationIndex][1];break;
|
||||
case ECapRayTileResult::RequestedWithoutTriangles:
|
||||
++RayMissedRequestedWithoutTriangles;++RayResultsByElevation[ElevationIndex][2];break;
|
||||
case ECapRayTileResult::EmittedWithoutHit:
|
||||
{
|
||||
++RayMissedEmittedWithoutHit;++RayResultsByElevation[ElevationIndex][3];
|
||||
if(bProfileCrossing&&bProfileRoofAtCrossing)++RayProfileRoofWithoutMeshHit;
|
||||
std::uint32_t FarRequestedMask=0,FarEmittedMask=0;
|
||||
std::uint32_t RootRequestedMask=0,RootEmittedMask=0;
|
||||
std::uint64_t RootDownwardTriangles=0;
|
||||
if(bProfileCrossing&&bProfileRoofAtCrossing)
|
||||
{
|
||||
for(std::int32_t Lod=0;Lod<=8;++Lod)
|
||||
{
|
||||
const std::int64_t Extent=static_cast<std::int64_t>(32)<<Lod;
|
||||
const std::int64_t TileX=FloorDiv64(static_cast<std::int64_t>(std::floor(ProfileHitX)),Extent);
|
||||
const std::int64_t TileY=FloorDiv64(static_cast<std::int64_t>(std::floor(ProfileHitY)),Extent);
|
||||
const std::int64_t TileZ=FloorDiv64(ProfileHitCapSubvoxels,Extent*SubvoxelsPerVoxel);
|
||||
const FTileId Id{static_cast<std::int32_t>(TileX),static_cast<std::int32_t>(TileY),
|
||||
static_cast<std::int32_t>(TileZ),Lod};
|
||||
if(Desired.find(Id)==Desired.end())continue;
|
||||
RootRequestedMask|=1u<<static_cast<std::uint32_t>(Lod);
|
||||
const FCapRayTileGeometry* Geometry=GetCapRayTileGeometry(Id);
|
||||
if(Geometry!=nullptr)
|
||||
{
|
||||
RootDownwardTriangles+=Geometry->Triangles.size();
|
||||
if(!Geometry->Triangles.empty())
|
||||
RootEmittedMask|=1u<<static_cast<std::uint32_t>(Lod);
|
||||
}
|
||||
if(Lod>=5)
|
||||
{
|
||||
FarRequestedMask|=1u<<static_cast<std::uint32_t>(Lod-5);
|
||||
if(Geometry!=nullptr&&!Geometry->Triangles.empty())
|
||||
FarEmittedMask|=1u<<static_cast<std::uint32_t>(Lod-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
UE_LOG(LogTemp,Display,
|
||||
TEXT("[VoxelForgeFarCapRayMiss] route_point=%llu elevation_deg=%.0f azimuth_deg=%.0f profile_crossing=%d profile_roof=%d profile_distance_vox=%.1f clip_distance_vox=%.1f profile_hit_vox=(%.1f,%.1f,%.1f) root_lod_requested_mask=0x%X root_lod_emitted_mask=0x%X root_downward_triangles=%llu far_requested_mask=0x%X far_emitted_mask=0x%X"),
|
||||
static_cast<unsigned long long>(RouteIndex),Elevation,Azimuth,
|
||||
bProfileCrossing?1:0,bProfileRoofAtCrossing?1:0,
|
||||
ProfileHitDistance,EdgeDistance,ProfileHitX,ProfileHitY,ProfileHitZ,
|
||||
RootRequestedMask,RootEmittedMask,
|
||||
static_cast<unsigned long long>(RootDownwardTriangles),
|
||||
FarRequestedMask,FarEmittedMask);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto WalkColumn=Field.point(WalkStart.positionSubvoxels[0],
|
||||
WalkStart.positionSubvoxels[1],0);
|
||||
const std::int64_t WalkFarCap=Field.farSurfaceCapHeightSubvoxels(
|
||||
WalkStart.positionSubvoxels[0],WalkStart.positionSubvoxels[1]);
|
||||
std::size_t RayTriangleCount=0;
|
||||
for(const auto& [Id,Geometry]:RayTileGeometryCache)
|
||||
{ (void)Id;RayTriangleCount+=Geometry.Triangles.size(); }
|
||||
std::sort(RayHitDistancesMeters.begin(),RayHitDistancesMeters.end());
|
||||
const double HitDistanceP50Meters=RayHitDistancesMeters.empty()?0.0:
|
||||
RayHitDistancesMeters[(RayHitDistancesMeters.size()-1)*50/100];
|
||||
const double HitDistanceP95Meters=RayHitDistancesMeters.empty()?0.0:
|
||||
RayHitDistancesMeters[(RayHitDistancesMeters.size()-1)*95/100];
|
||||
const double HitDistanceMaximumMeters=RayHitDistancesMeters.empty()?0.0:
|
||||
RayHitDistancesMeters.back();
|
||||
UE_LOG(LogTemp,Display,
|
||||
TEXT("[VoxelForgeFarCapCensus] source=surface_route_front route_points=%llu walk_keys=%llu far_xy_tiles=%llu cap_bands=%llu requested=%llu emitted=%llu requested_without_triangles=%llu culled_by_clipmap=%llu grid_samples=%llu samples_without_roof=%llu xy_tiles_with_roof_gaps=%llu walk_z_vox=%.3f ground_z_vox=%.3f cap_z_vox=%.3f"),
|
||||
static_cast<unsigned long long>(Plan->route.size()),
|
||||
static_cast<unsigned long long>(WalkKeys.size()),
|
||||
static_cast<unsigned long long>(FarXYTiles.size()),
|
||||
static_cast<unsigned long long>(CapBands.size()),
|
||||
static_cast<unsigned long long>(RequestedCapBands),
|
||||
static_cast<unsigned long long>(EmittedCapBands),
|
||||
static_cast<unsigned long long>(RequestedWithoutTriangles),
|
||||
static_cast<unsigned long long>(CulledCapBands),
|
||||
static_cast<unsigned long long>(ProfileGridSamples),
|
||||
static_cast<unsigned long long>(ProfileSamplesWithoutRoof),
|
||||
static_cast<unsigned long long>(FarXYTilesWithoutRoof.size()),
|
||||
Camera.z,static_cast<double>(WalkColumn.groundHeightSubvoxels)/SubvoxelsPerVoxel,
|
||||
static_cast<double>(WalkFarCap)/SubvoxelsPerVoxel);
|
||||
const double CoveragePercent=RayCount>0?100.0*static_cast<double>(RayHits)/RayCount:0.0;
|
||||
UE_LOG(LogTemp,Display,
|
||||
TEXT("[VoxelForgeFarCapRayCoverage] rays=%llu mesh_hits=%llu coverage_pct=%.3f missed_clip_edge=%llu missed_no_roof=%llu missed_cap_key_culled=%llu missed_requested_without_triangles=%llu missed_emitted_without_mesh_hit=%llu start_above_cap=%llu profile_crossings=%llu profile_roof_crossings=%llu profile_roof_without_mesh_hit=%llu traced_tiles=%llu traced_downward_triangles=%llu first_hit_distance_m_p50=%.2f_p95=%.2f_max=%.2f elevations_deg=15,30,45,60,75,90 azimuths=8 ray_method=all_downward_emitted_triangles_with_tile_aabb clip_edge=desired_far_xy_bounds lods_checked=0-8"),
|
||||
static_cast<unsigned long long>(RayCount),static_cast<unsigned long long>(RayHits),CoveragePercent,
|
||||
static_cast<unsigned long long>(RayMissedClipEdge),
|
||||
static_cast<unsigned long long>(RayMissedNoRoof),
|
||||
static_cast<unsigned long long>(RayMissedCulledByClipmap),
|
||||
static_cast<unsigned long long>(RayMissedRequestedWithoutTriangles),
|
||||
static_cast<unsigned long long>(RayMissedEmittedWithoutHit),
|
||||
static_cast<unsigned long long>(RayStartedAboveCap),
|
||||
static_cast<unsigned long long>(RayProfileCrossings),
|
||||
static_cast<unsigned long long>(RayProfileRoofCrossings),
|
||||
static_cast<unsigned long long>(RayProfileRoofWithoutMeshHit),
|
||||
static_cast<unsigned long long>(RayTileGeometryCache.size()),
|
||||
static_cast<unsigned long long>(RayTriangleCount),HitDistanceP50Meters,
|
||||
HitDistanceP95Meters,HitDistanceMaximumMeters);
|
||||
for(std::size_t Index=0;Index<ElevationDegrees.size();++Index)
|
||||
UE_LOG(LogTemp,Display,
|
||||
TEXT("[VoxelForgeFarCapRayElevation] degrees=%.0f rays=%llu mesh_hits=%llu key_culled=%llu requested_empty=%llu emitted_without_mesh_hit=%llu no_roof_or_above=%llu missed_clip_edge=%llu"),
|
||||
ElevationDegrees[Index],
|
||||
static_cast<unsigned long long>(AzimuthDegrees.size()*Plan->route.size()),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][0]),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][1]),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][2]),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][3]),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][4]),
|
||||
static_cast<unsigned long long>(RayResultsByElevation[Index][5]));
|
||||
if(RayHits!=RayCount)
|
||||
{
|
||||
OutError=FString::Printf(TEXT("Far-cap ray coverage failed: rays=%llu mesh_hits=%llu clip_edge=%llu no_roof=%llu culled_keys=%llu requested_empty=%llu emitted_without_hit=%llu."),
|
||||
static_cast<unsigned long long>(RayCount),static_cast<unsigned long long>(RayHits),
|
||||
static_cast<unsigned long long>(RayMissedClipEdge),
|
||||
static_cast<unsigned long long>(RayMissedNoRoof),
|
||||
static_cast<unsigned long long>(RayMissedCulledByClipmap),
|
||||
static_cast<unsigned long long>(RayMissedRequestedWithoutTriangles),
|
||||
static_cast<unsigned long long>(RayMissedEmittedWithoutHit));
|
||||
return false;
|
||||
}
|
||||
for(const auto& [Lod,Counts]:ByLod)
|
||||
UE_LOG(LogTemp,Display,
|
||||
TEXT("[VoxelForgeFarCapCensusLOD] lod=%d cap_bands=%llu culled=%llu emitted=%llu requested_without_triangles=%llu"),
|
||||
Lod,static_cast<unsigned long long>(Counts[0]),
|
||||
static_cast<unsigned long long>(Counts[1]),
|
||||
static_cast<unsigned long long>(Counts[2]),
|
||||
static_cast<unsigned long long>(Counts[3]));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Paired engine timings include preparation and derived shells, with canonical output hashes.
|
||||
// Mesures moteur appariees : preparation et coques derivees incluses, avec hashes canoniques.
|
||||
bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World,
|
||||
@@ -15441,8 +15992,12 @@ bool RunCoreSharedFeatureCostAudit(VoxelForgeCore::World& World,
|
||||
P95(ConnectorShellTimes),P95(ConnectorFieldCalls),P95(ShellTriangles),
|
||||
P95(GroundTriangles),P95(GroundRemoved),P95(CapTriangles));
|
||||
}
|
||||
if(Assembly!=nullptr&&!Assembly->plan.strates.empty()
|
||||
&&!RunCoreFarHorizonAgreementAudit(World,Keys,*Assembly,OutError))return false;
|
||||
if(Assembly!=nullptr&&!Assembly->plan.strates.empty())
|
||||
{
|
||||
if(FParse::Param(FCommandLine::Get(),TEXT("corefeatureauditonly"))
|
||||
&&!RunCoreFarCapTileCensus(World,*Assembly,OutError))return false;
|
||||
if(!RunCoreFarHorizonAgreementAudit(World,Keys,*Assembly,OutError))return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user