wip(assembler): M2 round 4 -- WorldAssemblerHybridV2: exact contacts, tagged tiles 1.18-1.27x MC
The round-3 'field contact error' measured hit-to-anchor distance; V2 contacts are 0 cm and on the zero isosurface. Tagged-tile cost 5.7-6.7x -> 1.175-1.270x MC; organic 0.998x. Bound, shelf-thickness and route audits rerun with negative controls; W1/W8/W16 stable; V1 and MarchingCubesV1 unchanged. 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
22a6b30494
commit
4661a445be
48 files changed
+106493
-71
No files matched your search
@@ -43,6 +43,11 @@ namespace VoxelHybridSharpMesher
|
||||
else V.Z = Value;
|
||||
}
|
||||
|
||||
inline int32 IntAxisValue(const FIntVector& V, int32 Axis)
|
||||
{
|
||||
return Axis == 0 ? V.X : (Axis == 1 ? V.Y : V.Z);
|
||||
}
|
||||
|
||||
struct FPlane
|
||||
{
|
||||
uint64 Id = 0;
|
||||
@@ -229,6 +234,109 @@ namespace VoxelHybridSharpMesher
|
||||
return SDF;
|
||||
}
|
||||
|
||||
struct FPreparedFeatureBox
|
||||
{
|
||||
uint64 StableId = 0;
|
||||
double Minimum[3]{};
|
||||
double Maximum[3]{};
|
||||
};
|
||||
|
||||
inline std::vector<FPreparedFeatureBox> PrepareFeatureBoxes(
|
||||
const TArray<FVoxelHybridSharpFeatureBox>& Features)
|
||||
{
|
||||
std::vector<FPreparedFeatureBox> Prepared;
|
||||
Prepared.reserve(static_cast<std::size_t>(Features.Num()));
|
||||
for (const FVoxelHybridSharpFeatureBox& Feature : Features)
|
||||
{
|
||||
FPreparedFeatureBox Box;
|
||||
Box.StableId = Feature.StableId;
|
||||
for (int32 Axis = 0; Axis < 3; ++Axis)
|
||||
{
|
||||
Box.Minimum[Axis] = static_cast<double>(Feature.MinimumSubvoxels[Axis])
|
||||
/ SubvoxelsPerVoxel;
|
||||
Box.Maximum[Axis] = static_cast<double>(Feature.MaximumSubvoxels[Axis])
|
||||
/ SubvoxelsPerVoxel;
|
||||
}
|
||||
Prepared.push_back(Box);
|
||||
}
|
||||
return Prepared;
|
||||
}
|
||||
|
||||
// MC interpolates the sampled field. A finite hard box is only included by the field at
|
||||
// samples inside it, so an edge can interpolate away from the box plane when the box face
|
||||
// lies between lattice points. For V2, keep the MC connectivity and move only roots whose
|
||||
// lattice edge crosses a tagged analytic face. This is deterministic and leaves tile edges
|
||||
// to the unchanged MC/skirt path.
|
||||
inline bool TrySnapFeatureEdge(const std::vector<FPreparedFeatureBox>& Features,
|
||||
const FIntVector& OriginVoxels, const FIntVector& LocalStart, int32 Axis,
|
||||
const FVector& ChunkWorldPos, float GeometryVoxelSize,
|
||||
FVector& InOutWorldPosition, FVector& OutNormal, uint64& OutSplitKey)
|
||||
{
|
||||
const double EdgeStart = static_cast<double>(IntAxisValue(OriginVoxels, Axis)
|
||||
+ IntAxisValue(LocalStart, Axis));
|
||||
const double EdgeEnd = EdgeStart + 1.0;
|
||||
const double EdgeLow = FMath::Min(EdgeStart, EdgeEnd);
|
||||
const double EdgeHigh = FMath::Max(EdgeStart, EdgeEnd);
|
||||
const double RootVoxel = static_cast<double>(IntAxisValue(OriginVoxels, Axis))
|
||||
+ (AxisValue(InOutWorldPosition, Axis) - AxisValue(ChunkWorldPos, Axis))
|
||||
/ GeometryVoxelSize;
|
||||
constexpr double Tolerance = 1.0e-9;
|
||||
double BestDistance = std::numeric_limits<double>::infinity();
|
||||
uint64 BestStableId = std::numeric_limits<uint64>::max();
|
||||
uint64 BestFaceId = 0;
|
||||
double BestPlane = 0.0;
|
||||
int32 BestSide = 0;
|
||||
|
||||
for (const FPreparedFeatureBox& Feature : Features)
|
||||
{
|
||||
bool bWithinFaceSpan = true;
|
||||
for (int32 OtherAxis = 0; OtherAxis < 3; ++OtherAxis)
|
||||
{
|
||||
if (OtherAxis == Axis) continue;
|
||||
const double Coordinate = static_cast<double>(IntAxisValue(OriginVoxels, OtherAxis)
|
||||
+ IntAxisValue(LocalStart, OtherAxis));
|
||||
if (Coordinate < Feature.Minimum[OtherAxis] - Tolerance
|
||||
|| Coordinate > Feature.Maximum[OtherAxis] + Tolerance)
|
||||
{
|
||||
bWithinFaceSpan = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bWithinFaceSpan) continue;
|
||||
|
||||
for (int32 Side = 0; Side < 2; ++Side)
|
||||
{
|
||||
const double Plane = Side == 0
|
||||
? Feature.Minimum[Axis] : Feature.Maximum[Axis];
|
||||
if (Plane < EdgeLow - Tolerance || Plane > EdgeHigh + Tolerance) continue;
|
||||
const double Distance = FMath::Abs(Plane - RootVoxel);
|
||||
const uint64 FaceId = Feature.StableId ^ (0x9e3779b97f4a7c15ull
|
||||
+ static_cast<uint64>(Axis * 2 + Side + 1));
|
||||
if (Distance > BestDistance + Tolerance
|
||||
|| (FMath::Abs(Distance - BestDistance) <= Tolerance
|
||||
&& std::tie(Feature.StableId, FaceId)
|
||||
>= std::tie(BestStableId, BestFaceId)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BestDistance = Distance;
|
||||
BestStableId = Feature.StableId;
|
||||
BestFaceId = FaceId != 0 ? FaceId : static_cast<uint64>(Axis + 1);
|
||||
BestPlane = Plane;
|
||||
BestSide = Side;
|
||||
}
|
||||
}
|
||||
|
||||
if (BestFaceId == 0) return false;
|
||||
SetAxisValue(InOutWorldPosition, Axis, AxisValue(ChunkWorldPos, Axis)
|
||||
+ (BestPlane - static_cast<double>(IntAxisValue(OriginVoxels, Axis)))
|
||||
* GeometryVoxelSize);
|
||||
OutNormal = FVector::ZeroVector;
|
||||
SetAxisValue(OutNormal, Axis, BestSide == 0 ? -1.0 : 1.0);
|
||||
OutSplitKey = BestFaceId;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline uint64 SelectFeature(const FRoot& Root, int32 Axis)
|
||||
{
|
||||
if (Root.Planes.empty()) return 0;
|
||||
|
||||
@@ -1073,8 +1073,9 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
// Le DC reste limite aux surfaces LOD0 balisees; les patches connectes au bord restent MC.
|
||||
VoxelHybridSharpMesher::FResult HybridResult;
|
||||
MeshData.HybridFeatureCount = HybridSharpFeatures.Num();
|
||||
if (bUseHybridSharpMesher && Step == 1 && CellsPerAxis == CHUNK_SIZE
|
||||
&& CzLo == 0 && CzHi == CellsPerAxis - 1 && !HybridSharpFeatures.IsEmpty())
|
||||
const bool bHybridLod0Interior = Step == 1 && CellsPerAxis == CHUNK_SIZE
|
||||
&& CzLo == 0 && CzHi == CellsPerAxis - 1 && !HybridSharpFeatures.IsEmpty();
|
||||
if (bUseHybridSharpMesher && bHybridLod0Interior)
|
||||
{
|
||||
MeshData.bHybridMesherInvoked = true;
|
||||
HybridResult = VoxelHybridSharpMesher::Generate(
|
||||
@@ -1083,6 +1084,12 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
[&](int32 X, int32 Y, int32 Z) { return SampleG(X, Y, Z); },
|
||||
[&](int32 X, int32 Y, int32 Z) { return GradAt(X, Y, Z); });
|
||||
}
|
||||
const bool bHybridSharpSnap = bUseHybridSharpFeatureSnap && bHybridLod0Interior;
|
||||
const std::vector<VoxelHybridSharpMesher::FPreparedFeatureBox> PreparedSharpFeatures =
|
||||
bHybridSharpSnap
|
||||
? VoxelHybridSharpMesher::PrepareFeatureBoxes(HybridSharpFeatures)
|
||||
: std::vector<VoxelHybridSharpMesher::FPreparedFeatureBox>{};
|
||||
if (bHybridSharpSnap) MeshData.bHybridMesherInvoked = true;
|
||||
MeshData.HybridSharpCellCount = HybridResult.SharpCells;
|
||||
MeshData.HybridDualCellCount = HybridResult.DualCells;
|
||||
MeshData.HybridActiveEdgeRootCount = HybridResult.ActiveEdgeRoots;
|
||||
@@ -1182,6 +1189,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
FVector EdgeVertices[12];
|
||||
FVector EdgeNormals[12];
|
||||
uint64 EdgeSplitKeys[12]{};
|
||||
for (int32 i = 0; i < 12; i++)
|
||||
{
|
||||
if (MCEdgeTable[CaseIndex] & (1 << i))
|
||||
@@ -1194,6 +1202,28 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
: FMath::Clamp((IsoLevel - D1) / (D2 - D1), 0.0f, 1.0f);
|
||||
EdgeVertices[i] = Positions[A] + T * (Positions[B] - Positions[A]);
|
||||
EdgeNormals[i] = Gradients[A] + T * (Gradients[B] - Gradients[A]);
|
||||
if (bHybridSharpSnap)
|
||||
{
|
||||
const int32 Axis = CornerOffsets[A].X != CornerOffsets[B].X ? 0
|
||||
: (CornerOffsets[A].Y != CornerOffsets[B].Y ? 1 : 2);
|
||||
const FIntVector LocalStart(cx + CornerOffsets[A].X,
|
||||
cy + CornerOffsets[A].Y, cz + CornerOffsets[A].Z);
|
||||
// Every cell incident to this lattice edge must be interior so all
|
||||
// copies of the shared root take the same exact or MC path.
|
||||
const bool bCanSnapEdge = bHybridSharpSnap
|
||||
&& LocalStart.X >= 2 && LocalStart.Y >= 2 && LocalStart.Z >= 2
|
||||
&& LocalStart.X <= CellsPerAxis - 2
|
||||
&& LocalStart.Y <= CellsPerAxis - 2
|
||||
&& LocalStart.Z <= CellsPerAxis - 2;
|
||||
FVector AnalyticNormal;
|
||||
if (bCanSnapEdge && VoxelHybridSharpMesher::TrySnapFeatureEdge(
|
||||
PreparedSharpFeatures, OriginVoxels, LocalStart, Axis,
|
||||
ChunkWorldPos, GeometryVoxelSize, EdgeVertices[i],
|
||||
AnalyticNormal, EdgeSplitKeys[i]))
|
||||
{
|
||||
EdgeNormals[i] = AnalyticNormal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bSampleCell)
|
||||
@@ -1216,9 +1246,12 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const int32 Idx0 = GetOrCreateVertex(EdgeVertices[E0], EdgeNormals[E0]);
|
||||
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
|
||||
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
|
||||
const int32 Idx0 = GetOrCreateVertex(
|
||||
EdgeVertices[E0], EdgeNormals[E0], EdgeSplitKeys[E0]);
|
||||
const int32 Idx1 = GetOrCreateVertex(
|
||||
EdgeVertices[E1], EdgeNormals[E1], EdgeSplitKeys[E1]);
|
||||
const int32 Idx2 = GetOrCreateVertex(
|
||||
EdgeVertices[E2], EdgeNormals[E2], EdgeSplitKeys[E2]);
|
||||
|
||||
// F17 — vote majoritaire (≥ 2 vertex sky-cap ⇒ triangle sky-cap).
|
||||
const int32 CapVotes = (int32)VertexClasses[Idx0]
|
||||
|
||||
@@ -11449,9 +11449,18 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
|
||||
const VoxelForgeCore::TileKey CoreKey{{Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z}, Tile.Level};
|
||||
VoxelForgeCore::TileGenerationOptions Options;
|
||||
Options.mesherRecipe = CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1
|
||||
? VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1
|
||||
: VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||
switch (CoreMesherRecipe)
|
||||
{
|
||||
case EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1:
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
||||
break;
|
||||
case EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV2:
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV2;
|
||||
break;
|
||||
default:
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||
break;
|
||||
}
|
||||
Options.hashDensity = bCaptureHashes;
|
||||
std::string CoreError;
|
||||
std::optional<VoxelForgeCore::TileMesh> CoreMesh;
|
||||
@@ -13290,7 +13299,7 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
const bool bHybridMesher = CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1;
|
||||
const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
if (AssemblyHash.IsEmpty() && !bHybridMesher)
|
||||
{
|
||||
return SeasonHash;
|
||||
@@ -13316,10 +13325,14 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
Payload += TEXT("|world_assembly=");
|
||||
Payload += AssemblyHash;
|
||||
}
|
||||
if (bHybridMesher)
|
||||
if (CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1)
|
||||
{
|
||||
Payload += TEXT("|core_mesher=WorldAssemblerHybridV1");
|
||||
}
|
||||
else if (CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV2)
|
||||
{
|
||||
Payload += TEXT("|core_mesher=WorldAssemblerHybridV2");
|
||||
}
|
||||
FTCHARToUTF8 Utf8Payload(*Payload);
|
||||
uint8 Digest[FSHA1::DigestSize];
|
||||
FSHA1::HashBuffer(Utf8Payload.Get(), Utf8Payload.Length(), Digest);
|
||||
|
||||
@@ -120,6 +120,15 @@ public:
|
||||
bUseHybridSharpMesher = true;
|
||||
}
|
||||
|
||||
// Version 2 keeps MC topology and projects eligible LOD0 edge roots onto the exact
|
||||
// analytic box planes. It avoids the tile-wide dual-contouring reconstruction in V1.
|
||||
// La version 2 conserve la topologie MC et projette les racines LOD0 sur les plans exacts.
|
||||
void SetHybridSharpFeatureSnap(TArray<FVoxelHybridSharpFeatureBox>&& InFeatures)
|
||||
{
|
||||
HybridSharpFeatures = MoveTemp(InFeatures);
|
||||
bUseHybridSharpFeatureSnap = true;
|
||||
}
|
||||
|
||||
private:
|
||||
FORCEINLINE bool ShouldAbortWork() const
|
||||
{
|
||||
@@ -130,6 +139,7 @@ private:
|
||||
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
||||
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
|
||||
bool bUseHybridSharpMesher = false;
|
||||
bool bUseHybridSharpFeatureSnap = false;
|
||||
float GeometryVoxelSizeCm = VOXEL_SIZE;
|
||||
|
||||
public:
|
||||
|
||||
@@ -78,7 +78,8 @@ UENUM(BlueprintType)
|
||||
enum class EVoxelForgeCoreMesherRecipe : uint8
|
||||
{
|
||||
MarchingCubesV1 UMETA(DisplayName = "Marching Cubes V1"),
|
||||
WorldAssemblerHybridV1 UMETA(DisplayName = "World Assembler Hybrid V1")
|
||||
WorldAssemblerHybridV1 UMETA(DisplayName = "World Assembler Hybrid V1"),
|
||||
WorldAssemblerHybridV2 UMETA(DisplayName = "World Assembler Hybrid V2")
|
||||
};
|
||||
|
||||
/** A non-player streaming center: keep a small box of level-0 tiles loaded around Actor so it has
|
||||
|
||||
@@ -792,7 +792,9 @@ struct FExploreArguments
|
||||
FString StrateReference;
|
||||
FString SettingsReference;
|
||||
FString CoreWorldAssemblyManifestPath;
|
||||
int32 CoreHybridMesherVersion = 0;
|
||||
bool bCoreHybridMesher = false;
|
||||
bool bCoreHybridMesherV2 = false;
|
||||
bool bCoreHybridFixturePlacement = false;
|
||||
bool bCoreHybridCliffOnly = false;
|
||||
bool bCoreHybridFixturesOnly = false;
|
||||
@@ -910,7 +912,14 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
||||
int32 CoreHybridMesher = 0;
|
||||
FParse::Value(*Params, TEXT("corehybridmesher="), CoreHybridMesher);
|
||||
if (CoreHybridMesher < 0 || CoreHybridMesher > 2)
|
||||
{
|
||||
OutError = TEXT("corehybridmesher supports only 0 (MC), 1 (hybrid V1), or 2 (hybrid V2).");
|
||||
return false;
|
||||
}
|
||||
OutArguments.CoreHybridMesherVersion = CoreHybridMesher;
|
||||
OutArguments.bCoreHybridMesher = CoreHybridMesher != 0;
|
||||
OutArguments.bCoreHybridMesherV2 = CoreHybridMesher == 2;
|
||||
int32 CoreHybridFixturePlacement = 0;
|
||||
FParse::Value(*Params, TEXT("corehybridfixtureplacement="), CoreHybridFixturePlacement);
|
||||
OutArguments.bCoreHybridFixturePlacement = CoreHybridFixturePlacement != 0;
|
||||
@@ -14026,7 +14035,7 @@ bool SameHybridMeshArrays(const FVoxelMeshData& A, const FVoxelMeshData& B)
|
||||
|
||||
bool GenerateHybridFixtureMesh(UVoxelGenerator* Generator, const FHybridAnalyticFixture& Fixture,
|
||||
const FVector& Phase, const FIntVector& Origin, int32 Step, int32 Cells,
|
||||
bool bHybrid, FVoxelMeshData& OutMesh, double* OutMeshingMilliseconds,
|
||||
int32 HybridRecipe, FVoxelMeshData& OutMesh, double* OutMeshingMilliseconds,
|
||||
FString& OutError)
|
||||
{
|
||||
if (Generator == nullptr || Step <= 0 || Cells <= 0)
|
||||
@@ -14049,7 +14058,7 @@ bool GenerateHybridFixtureMesh(UVoxelGenerator* Generator, const FHybridAnalytic
|
||||
}
|
||||
|
||||
TArray<FVoxelHybridSharpFeatureBox> SharpFeatures;
|
||||
if (bHybrid)
|
||||
if (HybridRecipe != 0)
|
||||
{
|
||||
for (const FHybridAnalyticBox& Feature : Fixture.Boxes)
|
||||
{
|
||||
@@ -14078,7 +14087,10 @@ bool GenerateHybridFixtureMesh(UVoxelGenerator* Generator, const FHybridAnalytic
|
||||
Mesher->SetGenerator(Generator);
|
||||
Mesher->SetGeometryVoxelSizeCm(25.0f);
|
||||
Mesher->bGenerateSkirts = false;
|
||||
if (bHybrid) Mesher->SetHybridSharpFeatures(MoveTemp(SharpFeatures));
|
||||
if (HybridRecipe == 2)
|
||||
Mesher->SetHybridSharpFeatureSnap(MoveTemp(SharpFeatures));
|
||||
else if (HybridRecipe == 1)
|
||||
Mesher->SetHybridSharpFeatures(MoveTemp(SharpFeatures));
|
||||
const double StartSeconds = FPlatformTime::Seconds();
|
||||
OutMesh = Mesher->GenerateMesh(Origin, Step, Cells, nullptr,
|
||||
INT32_MIN, INT32_MAX, nullptr, nullptr, &Grid);
|
||||
@@ -14207,7 +14219,8 @@ double HybridContourSetMismatchCm(const FHybridContourSet& A, const FHybridConto
|
||||
return Maximum;
|
||||
}
|
||||
|
||||
bool RunHybridPluginMesherFixtures(UVoxelGenerator* Generator, FString& OutError)
|
||||
bool RunHybridPluginMesherFixtures(UVoxelGenerator* Generator, bool bIncludeV2,
|
||||
FString& OutError)
|
||||
{
|
||||
const FVector Phase(0.27, 0.41, 0.33);
|
||||
const TCHAR* FixtureNames[] = {
|
||||
@@ -14268,6 +14281,58 @@ bool RunHybridPluginMesherFixtures(UVoxelGenerator* Generator, FString& OutError
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bIncludeV2)
|
||||
{
|
||||
int32 SnapSharpDifferences = 0;
|
||||
for (const TCHAR* FixtureName : FixtureNames)
|
||||
{
|
||||
const FHybridAnalyticFixture Fixture = MakeHybridAnalyticFixture(FixtureName, Phase);
|
||||
std::vector<double> McTimes, SnapTimes;
|
||||
FVoxelMeshData McMesh, SnapMesh, RepeatedSnapMesh;
|
||||
for (int32 Repeat = 0; Repeat < 3; ++Repeat)
|
||||
{
|
||||
double ElapsedMs = 0.0;
|
||||
if (!GenerateHybridFixtureMesh(Generator, Fixture, Phase, FIntVector::ZeroValue,
|
||||
1, CHUNK_SIZE, 0, McMesh, &ElapsedMs, OutError)) return false;
|
||||
McTimes.push_back(ElapsedMs);
|
||||
if (!GenerateHybridFixtureMesh(Generator, Fixture, Phase, FIntVector::ZeroValue,
|
||||
1, CHUNK_SIZE, 2, SnapMesh, &ElapsedMs, OutError)) return false;
|
||||
SnapTimes.push_back(ElapsedMs);
|
||||
if (Repeat == 0) RepeatedSnapMesh = SnapMesh;
|
||||
}
|
||||
const bool bExpectedEqual = Fixture.Boxes.empty();
|
||||
const bool bStable = SameHybridMeshArrays(RepeatedSnapMesh, SnapMesh);
|
||||
const bool bMatchesExpected = bExpectedEqual
|
||||
? SameHybridMeshArrays(McMesh, SnapMesh)
|
||||
: !SameHybridMeshArrays(McMesh, SnapMesh);
|
||||
const bool bInvocationMatchesExpected = bExpectedEqual
|
||||
? !SnapMesh.bHybridMesherInvoked : SnapMesh.bHybridMesherInvoked;
|
||||
if (!bStable || !bMatchesExpected || !bInvocationMatchesExpected)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Production hybrid V2 fixture failed: %s stable=%d expected_mc_identity=%d invoked=%d."),
|
||||
FixtureName, bStable ? 1 : 0, bExpectedEqual ? 1 : 0,
|
||||
SnapMesh.bHybridMesherInvoked ? 1 : 0);
|
||||
return false;
|
||||
}
|
||||
if (!bExpectedEqual) ++SnapSharpDifferences;
|
||||
std::sort(McTimes.begin(), McTimes.end());
|
||||
std::sort(SnapTimes.begin(), SnapTimes.end());
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[WorldAssemblerHybridPluginFixtureV2] name=%s lod=0 cells=%d mc_ms=%.3f hybrid_ms=%.3f ratio=%.3f mc_triangles=%d hybrid_triangles=%d exact_repeat=1"),
|
||||
FixtureName, CHUNK_SIZE, McTimes[1], SnapTimes[1],
|
||||
McTimes[1] > 0.0 ? SnapTimes[1] / McTimes[1] : 0.0,
|
||||
McMesh.Triangles.Num() / 3, SnapMesh.Triangles.Num() / 3);
|
||||
}
|
||||
if (SnapSharpDifferences != 4)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Expected four analytic sharp fixtures to exercise hybrid V2; got %d."),
|
||||
SnapSharpDifferences);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Coarse LOD uses MC regardless of the selected recipe, so its full arrays must match.
|
||||
// Le LOD grossier reste sur MC, meme si la recette hybride est activee.
|
||||
const FHybridAnalyticFixture CoarseFixture = MakeHybridAnalyticFixture(TEXT("doorway"), Phase);
|
||||
@@ -14281,6 +14346,17 @@ bool RunHybridPluginMesherFixtures(UVoxelGenerator* Generator, FString& OutError
|
||||
OutError = TEXT("Hybrid altered a coarse LOD mesh; DC must remain LOD0-only.");
|
||||
return false;
|
||||
}
|
||||
if (bIncludeV2)
|
||||
{
|
||||
FVoxelMeshData CoarseSnap;
|
||||
if (!GenerateHybridFixtureMesh(Generator, CoarseFixture, Phase, FIntVector::ZeroValue,
|
||||
2, CHUNK_SIZE, 2, CoarseSnap, nullptr, OutError)
|
||||
|| !SameHybridMeshArrays(CoarseMc, CoarseSnap))
|
||||
{
|
||||
OutError = TEXT("Hybrid V2 altered a coarse LOD mesh; snapping must remain LOD0-only.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Independent same-LOD neighbors share the exact sampled contour at their common face.
|
||||
// Des voisins LOD identique independants doivent partager exactement leur contour commun.
|
||||
@@ -14314,6 +14390,24 @@ bool RunHybridPluginMesherFixtures(UVoxelGenerator* Generator, FString& OutError
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[WorldAssemblerHybridPluginBorder] same_lod=PASS independent=1 mc_segments=%d hybrid_segments=%d border_fallback_mc_identity=1"),
|
||||
static_cast<int32>(McLeftBorder.size()), static_cast<int32>(HybridLeftBorder.size()));
|
||||
if (bIncludeV2)
|
||||
{
|
||||
FVoxelMeshData SnapLeft, SnapRight;
|
||||
if (!GenerateHybridFixtureMesh(Generator, BorderFixture, BorderPhase, FIntVector(0, 0, 0),
|
||||
1, CHUNK_SIZE, 2, SnapLeft, nullptr, OutError)
|
||||
|| !GenerateHybridFixtureMesh(Generator, BorderFixture, BorderPhase, FIntVector(32, 0, 0),
|
||||
1, CHUNK_SIZE, 2, SnapRight, nullptr, OutError)
|
||||
|| ExtractHybridBoundaryContour(SnapLeft, 800.0) != McLeftBorder
|
||||
|| ExtractHybridBoundaryContour(SnapRight, 800.0) != McRightBorder
|
||||
|| ExtractHybridBoundaryContour(SnapLeft, 800.0)
|
||||
!= ExtractHybridBoundaryContour(SnapRight, 800.0))
|
||||
{
|
||||
OutError = TEXT("Hybrid V2 changed an interior-only same-LOD border fixture.");
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[WorldAssemblerHybridPluginBorderV2] same_lod=PASS independent=1 mc_identity=1"));
|
||||
}
|
||||
|
||||
// Compare the raw doorway contours at the 2:1 seam over all 125 sub-cell phases.
|
||||
// Comparer les contours bruts de la porte sur la couture 2:1 pour 125 phases.
|
||||
@@ -14339,7 +14433,7 @@ bool RunHybridPluginMesherFixtures(UVoxelGenerator* Generator, FString& OutError
|
||||
};
|
||||
for (double PhaseX : Phases)
|
||||
for (double PhaseY : Phases)
|
||||
for (double PhaseZ : Phases)
|
||||
for (double PhaseZ : Phases)
|
||||
{
|
||||
const FVector SweepPhase(PhaseX, PhaseY, PhaseZ);
|
||||
const FHybridAnalyticFixture Doorway =
|
||||
@@ -14364,6 +14458,17 @@ bool RunHybridPluginMesherFixtures(UVoxelGenerator* Generator, FString& OutError
|
||||
OutError = TEXT("The production doorway border probe did not produce a contour on both LODs.");
|
||||
return false;
|
||||
}
|
||||
if (bIncludeV2)
|
||||
{
|
||||
FVoxelMeshData FineSnap;
|
||||
if (!GenerateHybridFixtureMesh(Generator, Doorway, SweepPhase, FIntVector(0, 0, 0),
|
||||
1, CHUNK_SIZE, 2, FineSnap, nullptr, OutError)
|
||||
|| ExtractHybridBoundaryContour(FineSnap, 800.0) != FineContour)
|
||||
{
|
||||
OutError = TEXT("Hybrid V2 changed a raw LOD border contour in the 125-phase doorway sweep.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
int32 OverBudgetProbes = 0;
|
||||
const double MismatchCm = HybridContourSetMismatchCm(
|
||||
FineContour, CoarseContour, OverBudgetProbes);
|
||||
@@ -14426,7 +14531,8 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
return false;
|
||||
}
|
||||
FixtureGenerator->InitializeSettings(AuthoredSettings);
|
||||
if (!RunHybridPluginMesherFixtures(FixtureGenerator, OutError)) return false;
|
||||
if (!RunHybridPluginMesherFixtures(
|
||||
FixtureGenerator, Arguments.bCoreHybridMesherV2, OutError)) return false;
|
||||
UE_LOG(LogTemp, Display, TEXT("[WorldAssemblerHybridPluginFixture] fixture_only=PASS"));
|
||||
return true;
|
||||
}
|
||||
@@ -14728,7 +14834,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
McOptions.hashDensity = true;
|
||||
McOptions.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||
VoxelForgeCore::TileGenerationOptions HybridOptions = McOptions;
|
||||
HybridOptions.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
||||
HybridOptions.mesherRecipe = Arguments.bCoreHybridMesherV2
|
||||
? VoxelForgeCore::MesherRecipe::worldAssemblerHybridV2
|
||||
: VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
||||
const auto McMesh = ReferenceWorld.generateTile(CliffKey, McOptions, CoreError);
|
||||
const auto HybridMesh = ReferenceWorld.generateTile(CliffKey, HybridOptions, CoreError);
|
||||
if (!McMesh || !HybridMesh || !McMesh->hasMeshHash || !HybridMesh->hasMeshHash)
|
||||
@@ -14768,9 +14876,12 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
|
||||
VoxelForgeCore::TileGenerationOptions Options;
|
||||
Options.hashDensity = true;
|
||||
Options.mesherRecipe = Arguments.bCoreHybridMesher
|
||||
? VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1
|
||||
: VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||
if (Arguments.CoreHybridMesherVersion == 2)
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV2;
|
||||
else if (Arguments.CoreHybridMesherVersion == 1)
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1;
|
||||
else
|
||||
Options.mesherRecipe = VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
|
||||
AggregateRows.reserve(Desired.size());
|
||||
bool bFoundCarveVertex = false;
|
||||
@@ -15594,9 +15705,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
Actor->Settings = AuthoredSettings;
|
||||
Actor->bManageAtmosphere = false;
|
||||
Actor->bUseVoxelForgeCore = true;
|
||||
Actor->CoreMesherRecipe = Arguments.bCoreHybridMesher
|
||||
? EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1
|
||||
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
Actor->CoreMesherRecipe = Arguments.CoreHybridMesherVersion == 2
|
||||
? EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV2
|
||||
: (Arguments.CoreHybridMesherVersion == 1
|
||||
? EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1
|
||||
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
|
||||
Actor->CoreClipRadius = 3;
|
||||
Actor->CoreMaxClipLevel = 8;
|
||||
Actor->bCaptureVoxelForgeCoreHashesForValidation = true;
|
||||
@@ -15611,11 +15724,14 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
Actor->SetActorTickEnabled(true);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] world_initialized=1 recipe=%s world_begun=%d actor_initialized=%d actor_beginplay=%d"),
|
||||
Arguments.bCoreHybridMesher ? TEXT("WorldAssemblerHybridV1") : TEXT("MarchingCubesV1"),
|
||||
Arguments.CoreHybridMesherVersion == 2 ? TEXT("WorldAssemblerHybridV2")
|
||||
: (Arguments.CoreHybridMesherVersion == 1 ? TEXT("WorldAssemblerHybridV1")
|
||||
: TEXT("MarchingCubesV1")),
|
||||
TestWorld->HasBegunPlay() ? 1 : 0, Actor->IsActorInitialized() ? 1 : 0,
|
||||
Actor->HasActorBegunPlay() ? 1 : 0);
|
||||
if (Arguments.bCoreHybridMesher
|
||||
&& !RunHybridPluginMesherFixtures(Actor->Generator, OutError))
|
||||
&& !RunHybridPluginMesherFixtures(
|
||||
Actor->Generator, Arguments.bCoreHybridMesherV2, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -16255,9 +16371,14 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
&& PawnCapsuleHit.GetComponent() == FixtureComponent;
|
||||
const FHitResult* ContactHit = bPawnLineContact ? &PawnHit
|
||||
: (bPawnCapsuleContact ? &PawnCapsuleHit : nullptr);
|
||||
const double ContactErrorCm = ContactHit != nullptr
|
||||
const FVector ContactDeltaCm = ContactHit != nullptr
|
||||
? ContactHit->ImpactPoint - Probe.Expected : FVector::ZeroVector;
|
||||
const double ExpectedPointErrorCm = ContactHit != nullptr
|
||||
? FVector::Dist(ContactHit->ImpactPoint, Probe.Expected)
|
||||
: TNumericLimits<double>::Max();
|
||||
float DensityAtContact = TNumericLimits<float>::Max();
|
||||
const bool bCoreDensityAtContact = ContactHit != nullptr
|
||||
&& Actor->QueryVoxelForgeCoreDensityAt(ContactHit->ImpactPoint, DensityAtContact);
|
||||
FHitResult ComponentHit;
|
||||
const bool bComponentHit = IsValid(FixtureComponent)
|
||||
&& FixtureComponent->LineTraceComponent(
|
||||
@@ -16278,9 +16399,10 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
&& bRenderCollisionBoundsMatch;
|
||||
// Le trace direct du composant et le BodySetup cuisine doivent suivre la meme section RMC.
|
||||
// The direct component trace and cooked BodySetup must agree with the same RMC render section.
|
||||
constexpr double AnalyticContactToleranceCm = CoreVoxelSizeCm;
|
||||
const double AnalyticContactToleranceCm = Arguments.bCoreHybridMesherV2
|
||||
? 1.0 : CoreVoxelSizeCm;
|
||||
constexpr double ComponentTraceToleranceCm = 2.0;
|
||||
if (ContactHit == nullptr || ContactErrorCm > AnalyticContactToleranceCm
|
||||
if (ContactHit == nullptr || ExpectedPointErrorCm > AnalyticContactToleranceCm
|
||||
|| !bVisualHit || VisualContactErrorCm > ComponentTraceToleranceCm)
|
||||
{
|
||||
const FTransform ComponentTransform = IsValid(FixtureComponent)
|
||||
@@ -16400,12 +16522,14 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
bCoreDensityEnd ? 1 : 0,
|
||||
DensityStart, DensityExpected, DensityEnd);
|
||||
HybridCollisionContactFailures += FString::Printf(
|
||||
TEXT("Hybrid cooked Pawn collision contact failed for %s: pawn_hit=%d pawn_capsule_hit=%d capsule_actor_match=%d capsule_component_match=%d component_trace_hit=%d component_trace_error_cm=%.3f bounds_match=%d field_contact_error_cm=%.3f visibility_hit=%d world_static_hit=%d expected=(%.2f,%.2f,%.2f) pawn_impact=(%.2f,%.2f,%.2f) capsule_impact=(%.2f,%.2f,%.2f)."),
|
||||
TEXT("Hybrid cooked Pawn collision contact failed for %s: pawn_hit=%d pawn_capsule_hit=%d capsule_actor_match=%d capsule_component_match=%d component_trace_hit=%d component_trace_error_cm=%.3f bounds_match=%d expected_point_error_cm=%.3f expected_delta_cm=(%.3f,%.3f,%.3f) density_at_contact=%.6f density_query=%d visibility_hit=%d world_static_hit=%d expected=(%.2f,%.2f,%.2f) pawn_impact=(%.2f,%.2f,%.2f) capsule_impact=(%.2f,%.2f,%.2f)."),
|
||||
*Probe.Name, bPawnHit ? 1 : 0, bPawnCapsuleHit ? 1 : 0,
|
||||
bPawnCapsuleHit && PawnCapsuleHit.GetActor() == Actor ? 1 : 0,
|
||||
bPawnCapsuleHit && PawnCapsuleHit.GetComponent() == FixtureComponent ? 1 : 0,
|
||||
bComponentHit ? 1 : 0, VisualContactErrorCm,
|
||||
bRenderCollisionBoundsMatch ? 1 : 0, ContactErrorCm,
|
||||
bRenderCollisionBoundsMatch ? 1 : 0, ExpectedPointErrorCm,
|
||||
ContactDeltaCm.X, ContactDeltaCm.Y, ContactDeltaCm.Z,
|
||||
DensityAtContact, bCoreDensityAtContact ? 1 : 0,
|
||||
bVisibilityHit ? 1 : 0, bWorldStaticHit ? 1 : 0,
|
||||
Probe.Expected.X, Probe.Expected.Y, Probe.Expected.Z,
|
||||
bPawnHit ? PawnHit.ImpactPoint.X : 0.0,
|
||||
@@ -16419,8 +16543,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
}
|
||||
const TCHAR* ContactMethod = bPawnLineContact ? TEXT("PawnLine") : TEXT("PawnCapsule");
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeHybridCollisionContact] fixture=%s method=%s channel=Pawn hit=1 actor_match=1 component_match=1 actor_core_hash_match=1 render_collision_bounds_match=1 component_trace_error_cm=%.3f field_contact_error_cm=%.3f impact=(%.2f,%.2f,%.2f) component_impact=(%.2f,%.2f,%.2f)"),
|
||||
*Probe.Name, ContactMethod, VisualContactErrorCm, ContactErrorCm,
|
||||
TEXT("[VoxelForgeHybridCollisionContact] fixture=%s method=%s channel=Pawn hit=1 actor_match=1 component_match=1 actor_core_hash_match=1 render_collision_bounds_match=1 component_trace_error_cm=%.3f expected_point_error_cm=%.3f expected_delta_cm=(%.3f,%.3f,%.3f) density_at_contact=%.6f density_query=%d expected=(%.2f,%.2f,%.2f) impact=(%.2f,%.2f,%.2f) component_impact=(%.2f,%.2f,%.2f)"),
|
||||
*Probe.Name, ContactMethod, VisualContactErrorCm, ExpectedPointErrorCm,
|
||||
ContactDeltaCm.X, ContactDeltaCm.Y, ContactDeltaCm.Z,
|
||||
DensityAtContact, bCoreDensityAtContact ? 1 : 0,
|
||||
Probe.Expected.X, Probe.Expected.Y, Probe.Expected.Z,
|
||||
ContactHit->ImpactPoint.X, ContactHit->ImpactPoint.Y, ContactHit->ImpactPoint.Z,
|
||||
VisualImpact.X, VisualImpact.Y, VisualImpact.Z);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user