wip(assembler): M2 round 3 -- opt-in hybrid sharp mesher (WorldAssemblerHybridV1) in the Unreal Core route

Default MarchingCubesV1 unchanged (W1/W8/W16 = round-6 hashes). Hybrid DC only in LOD0 interior cells
with tagged features; tile-border cells and LOD1+ stay MC. BuildHost: fixtures repeat exactly, cooked
collision bounds = render bounds, same-LOD borders pass, sampled SurfaceWorld cliff bit-identical to MC.
Open: tagged tiles cost 5.7-6.7x MC; corner-top field contact error 20 cm; 5 MC doorway LOD cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-10 09:53:04 +02:00
1 parent a3b7d2015e
commit 22a6b30494
21 files changed
+3004 -18

No files matched your search

+11
View File
@@ -682,6 +682,17 @@ driven by `EditorBrush*` props.
position → seamless across chunk borders (both sides use identical pure samples). NO per-vertex position → seamless across chunk borders (both sides use identical pure samples). NO per-vertex
`GetDensityAt` (was ~6/vertex, often as costly as the whole grid). `ComputeGradientNormal` is now `GetDensityAt` (was ~6/vertex, often as costly as the whole grid). `ComputeGradientNormal` is now
unused. Only NORMALS changed vs the old path; geometry is identical. unused. Only NORMALS changed vs the old path; geometry is identical.
- **Core hybrid sharp mesher (M2 round 3)** (`CoreMesherRecipe` / `MesherRecipe`): the property is
opt-in and defaults to `MarchingCubesV1`; `bUseVoxelForgeCore` remains separately default-off.
Reuse the existing density grid and route only LOD0 interior cells touched by stable type-2 plan
boxes through feature-aware dual contouring. A cell touching a tile boundary and every LOD1+ cell
stay MC; existing skirts remain enabled. Do not alter density queries, worker task/epoch handling,
thread-local MC grid caches, or LOD0-only collision policy for this recipe. The canonical feature
list and fixed cell order remain deterministic. BuildHost measurements show tagged corner/doorway/
riser/ledge tiles about 5.6–7.4× the production MC meshing time, while an organic cave fixture is
bit-identical and its measured time is unchanged within noise; keep the recipe opt-in and use tags
only where the feature warrants that tile cost. MC-to-MC LOD borders still have five standalone
doorway topology failures, so skirts are a visual repair, not a topology guarantee.
- **Surface column cache (T1.a)** (density path: the surface stack's `FSurfaceColumnSource` memo; - **Surface column cache (T1.a)** (density path: the surface stack's `FSurfaceColumnSource` memo;
`ClassifyTile`: `FSurfaceColumnCache` = LRU of `FSurfaceColumnBox`): the heightfield + sky-cap + biome blend are a PURE function of (XY, seed, `ClassifyTile`: `FSurfaceColumnCache` = LRU of `FSurfaceColumnBox`): the heightfield + sky-cap + biome blend are a PURE function of (XY, seed,
strate) — **ZERO Z dependence** (climate/Voronoi are pure-XY; surface params are per-strate constant strate) — **ZERO Z dependence** (climate/Voronoi are pure-XY; surface params are per-strate constant
+15
View File
@@ -894,6 +894,21 @@ edits keep the full mesh path. The BuildHost commandlet adds height-anchor agree
sign/size, geometry and per-tile-kind pawn-lag checks; see round 6 in sign/size, geometry and per-tile-kind pawn-lag checks; see round 6 in
[`Standalone/ASSEMBLER_M1_REPORT.md`](Standalone/ASSEMBLER_M1_REPORT.md). [`Standalone/ASSEMBLER_M1_REPORT.md`](Standalone/ASSEMBLER_M1_REPORT.md).
M2 round 3 adds `EVoxelForgeCoreMesherRecipe` / `CoreMesherRecipe` in
[`Source/VoxelForge/Public/VoxelWorld.h`](Source/VoxelForge/Public/VoxelWorld.h), defaulting to
`MarchingCubesV1`; `VoxelWorld.cpp` maps the opt-in `WorldAssemblerHybridV1` choice to the Core
`MesherRecipe` and adds that choice to the Core content identity only while selected. Core extracts
stable type-2 world-plan boxes as sharp features at LOD0; the production mesher routes tagged
interior cells through `VoxelHybridSharpMesher.h`, while border-touching cells and LOD1+ remain MC.
The same-LOD border path stays independent and skirted. `VoxelForgeExploreCommandlet.cpp` owns the
five production-mesher fixtures, border probes and Core actor collision contacts. BuildHost evidence
and remaining doorway transition failures are recorded in
[`Standalone/UNREAL_INTEGRATION_REPORT.md`](Standalone/UNREAL_INTEGRATION_REPORT.md) and
[`Standalone/Research/WorldAssemblerM2/REPORT.md`](Standalone/Research/WorldAssemblerM2/REPORT.md).
Likely compile touchpoints are `VoxelWorld.h/.cpp`, `VoxelMarchingCubesMesher.h/.cpp`,
`VoxelHybridSharpMesher.h`, `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`,
`Standalone/Core/src/VoxelForgeCore.cpp`, and the editor commandlet/build rules.
[OwnerWorldLoader.h](Standalone/Source/OwnerWorldLoader.h) now reads SlabParams, MazeParams, [OwnerWorldLoader.h](Standalone/Source/OwnerWorldLoader.h) now reads SlabParams, MazeParams,
VerticalShaftParams, and FloatingIslandParams. The shape-world evaluator still lacks their native VerticalShaftParams, and FloatingIslandParams. The shape-world evaluator still lacks their native
terrain fields: FlatPlain, CrystalChamber, Maze, VerticalShafts, FloatingIslands, and water terrain fields: FlatPlain, CrystalChamber, Maze, VerticalShafts, FloatingIslands, and water
@@ -0,0 +1,618 @@
#pragma once
#include "MarchingCubesTables.h"
#include "VoxelMarchingCubesMesher.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <functional>
#include <limits>
#include <numeric>
#include <tuple>
#include <utility>
#include <vector>
// Feature-aware dual-contouring support for the opt-in Core recipe.
// Support du dual contouring sensible aux fonctions pour la recette Core optionnelle.
namespace VoxelHybridSharpMesher
{
constexpr int32 EdgeCount = 12;
constexpr int32 CornerCount = 8;
constexpr double SubvoxelsPerVoxel = 256.0;
constexpr int32 CornerOffsets[CornerCount][3] = {
{0,0,0}, {1,0,0}, {1,1,0}, {0,1,0},
{0,0,1}, {1,0,1}, {1,1,1}, {0,1,1}
};
constexpr int32 EdgeCorners[EdgeCount][2] = {
{0,1}, {1,2}, {2,3}, {3,0}, {4,5}, {5,6},
{6,7}, {7,4}, {0,4}, {1,5}, {2,6}, {3,7}
};
inline double AxisValue(const FVector& V, int32 Axis)
{
return Axis == 0 ? V.X : (Axis == 1 ? V.Y : V.Z);
}
inline void SetAxisValue(FVector& V, int32 Axis, double Value)
{
if (Axis == 0) V.X = Value;
else if (Axis == 1) V.Y = Value;
else V.Z = Value;
}
struct FPlane
{
uint64 Id = 0;
FVector Normal = FVector::UpVector;
};
struct FRoot
{
bool bActive = false;
FVector Position = FVector::ZeroVector;
FVector Gradient = FVector::UpVector;
std::vector<FPlane> Planes;
uint64 FeatureId = 0;
};
struct FTriangle
{
FVector Positions[3]{};
FVector Normals[3]{};
uint64 SplitKeys[3]{};
};
struct FResult
{
std::vector<uint8> CoreCellDCEdges;
std::vector<FTriangle> Triangles;
uint32 SharpCells = 0;
uint32 DualCells = 0;
uint32 ActiveEdgeRoots = 0;
uint32 TaggedEdgeRoots = 0;
double MinimumFeatureRootAbsSdf = std::numeric_limits<double>::infinity();
uint32 FeatureRootSamples = 0;
uint32 NearFeatureRootSamples = 0;
uint32 BorderFallbackPatches = 0;
bool UsesDC(int32 X, int32 Y, int32 Z, int32 Cells) const
{
if (X < 0 || Y < 0 || Z < 0 || X >= Cells || Y >= Cells || Z >= Cells)
return false;
const std::size_t Cell = (static_cast<std::size_t>(Z) * Cells + Y) * Cells + X;
const std::size_t Offset = Cell * EdgeCount;
for (int32 Edge = 0; Edge < EdgeCount && Offset + Edge < CoreCellDCEdges.size(); ++Edge)
if (CoreCellDCEdges[Offset + Edge] != 0) return true;
return false;
}
bool UsesDCEdge(int32 X, int32 Y, int32 Z, int32 Edge, int32 Cells) const
{
if (X < 0 || Y < 0 || Z < 0 || X >= Cells || Y >= Cells || Z >= Cells)
return false;
const std::size_t Cell = (static_cast<std::size_t>(Z) * Cells + Y) * Cells + X;
const std::size_t Offset = Cell * EdgeCount + static_cast<std::size_t>(Edge);
return Offset < CoreCellDCEdges.size() && (CoreCellDCEdges[Offset] & 1u) != 0;
}
};
struct FCell
{
uint8 Case = 0;
uint8 ComponentCount = 0;
std::array<uint8, EdgeCount> EdgeComponent{};
std::array<uint8, EdgeCount> ComponentSharp{};
std::array<uint8, EdgeCount> ComponentUseDC{};
std::array<FVector, EdgeCount> Dual{};
FCell() { EdgeComponent.fill(0xff); }
};
inline int32 FindLocal(std::array<uint8, EdgeCount>& Parent, int32 Value)
{
while (Parent[Value] != Value)
{
Parent[Value] = Parent[Parent[Value]];
Value = Parent[Value];
}
return Value;
}
inline void UniteLocal(std::array<uint8, EdgeCount>& Parent, int32 A, int32 B)
{
A = FindLocal(Parent, A);
B = FindLocal(Parent, B);
if (A == B) return;
if (A > B) std::swap(A, B);
Parent[B] = static_cast<uint8>(A);
}
inline int32 FindLocalEdge(int32 CellX, int32 CellY, int32 CellZ,
int32 Axis, int32 EdgeX, int32 EdgeY, int32 EdgeZ)
{
for (int32 Edge = 0; Edge < EdgeCount; ++Edge)
{
const int32 A = EdgeCorners[Edge][0];
const int32 B = EdgeCorners[Edge][1];
const int32 AX = CornerOffsets[A][0], AY = CornerOffsets[A][1], AZ = CornerOffsets[A][2];
const int32 BX = CornerOffsets[B][0], BY = CornerOffsets[B][1], BZ = CornerOffsets[B][2];
const int32 LocalAxis = AX != BX ? 0 : AY != BY ? 1 : 2;
if (LocalAxis == Axis
&& CellX + FMath::Min(AX, BX) == EdgeX
&& CellY + FMath::Min(AY, BY) == EdgeY
&& CellZ + FMath::Min(AZ, BZ) == EdgeZ)
return Edge;
}
return INDEX_NONE;
}
inline FVector SolveQEF(const std::vector<std::pair<FVector, FVector>>& Constraints,
const FVector& Fallback, const FVector& Low, const FVector& High)
{
if (Constraints.empty()) return Fallback;
double A[3][4]{};
constexpr double Lambda = 1.0e-10;
const double Center[3] = {Fallback.X, Fallback.Y, Fallback.Z};
for (int32 I = 0; I < 3; ++I) { A[I][I] = Lambda; A[I][3] = Lambda * Center[I]; }
for (const auto& Constraint : Constraints)
{
FVector N = Constraint.second.GetSafeNormal();
const double B = FVector::DotProduct(N, Constraint.first);
const double NV[3] = {N.X, N.Y, N.Z};
for (int32 R = 0; R < 3; ++R)
{
for (int32 C = 0; C < 3; ++C) A[R][C] += NV[R] * NV[C];
A[R][3] += NV[R] * B;
}
}
for (int32 Column = 0; Column < 3; ++Column)
{
int32 Pivot = Column;
for (int32 Row = Column + 1; Row < 3; ++Row)
if (FMath::Abs(A[Row][Column]) > FMath::Abs(A[Pivot][Column])) Pivot = Row;
if (FMath::Abs(A[Pivot][Column]) < 1.0e-14) return Fallback;
if (Pivot != Column)
for (int32 K = Column; K < 4; ++K) std::swap(A[Pivot][K], A[Column][K]);
const double Divisor = A[Column][Column];
for (int32 K = Column; K < 4; ++K) A[Column][K] /= Divisor;
for (int32 Row = 0; Row < 3; ++Row)
{
if (Row == Column) continue;
const double Factor = A[Row][Column];
for (int32 K = Column; K < 4; ++K) A[Row][K] -= Factor * A[Column][K];
}
}
return FVector(FMath::Clamp(A[0][3], Low.X, High.X),
FMath::Clamp(A[1][3], Low.Y, High.Y),
FMath::Clamp(A[2][3], Low.Z, High.Z));
}
inline double BoxSDF(const FVoxelHybridSharpFeatureBox& Box, const FVector& P,
std::vector<FPlane>& OutPlanes)
{
FVector Low, High;
for (int32 Axis = 0; Axis < 3; ++Axis)
{
SetAxisValue(Low, Axis,
static_cast<double>(Box.MinimumSubvoxels[Axis]) / SubvoxelsPerVoxel);
SetAxisValue(High, Axis,
static_cast<double>(Box.MaximumSubvoxels[Axis]) / SubvoxelsPerVoxel);
}
const FVector Center = (Low + High) * 0.5;
const FVector Half = (High - Low) * 0.5;
const FVector Delta = P - Center;
const FVector Q(FMath::Abs(Delta.X) - Half.X,
FMath::Abs(Delta.Y) - Half.Y,
FMath::Abs(Delta.Z) - Half.Z);
const double MaxQ = FMath::Max(Q.X, FMath::Max(Q.Y, Q.Z));
const FVector Outside(FMath::Max(Q.X, 0.0), FMath::Max(Q.Y, 0.0), FMath::Max(Q.Z, 0.0));
const double OutsideLength = Outside.Size();
const double SDF = OutsideLength + FMath::Min(MaxQ, 0.0);
const double Tolerance = 1.0e-4;
if (FMath::Abs(MaxQ) <= Tolerance)
{
for (int32 Axis = 0; Axis < 3; ++Axis)
{
if (FMath::Abs(AxisValue(Q, Axis) - MaxQ) > Tolerance) continue;
FVector Normal = FVector::ZeroVector;
const bool bNegativeFace = AxisValue(Delta, Axis) < 0.0;
SetAxisValue(Normal, Axis, bNegativeFace ? -1.0 : 1.0);
uint64 FaceId = Box.StableId ^ (0x9e3779b97f4a7c15ull
+ static_cast<uint64>(Axis * 2 + (bNegativeFace ? 0 : 1) + 1));
if (FaceId == 0) FaceId = static_cast<uint64>(Axis + 1);
OutPlanes.push_back({FaceId, Normal});
}
}
return SDF;
}
inline uint64 SelectFeature(const FRoot& Root, int32 Axis)
{
if (Root.Planes.empty()) return 0;
std::size_t Selected = 0;
const auto Alignment = [Axis](const FVector& Normal)
{ return FMath::Abs(AxisValue(Normal, Axis)); };
for (std::size_t I = 1; I < Root.Planes.size(); ++I)
{
const double Candidate = Alignment(Root.Planes[I].Normal);
const double Current = Alignment(Root.Planes[Selected].Normal);
if (Candidate > Current + 1.0e-12
|| (FMath::Abs(Candidate - Current) <= 1.0e-12
&& Root.Planes[I].Id < Root.Planes[Selected].Id)) Selected = I;
}
return Root.Planes[Selected].Id;
}
inline FVector NormalForFeature(const FRoot& Root, uint64 Feature)
{
for (const FPlane& Plane : Root.Planes) if (Plane.Id == Feature) return Plane.Normal;
return Root.Gradient;
}
inline FResult Generate(int32 Cells, int32 Step, float IsoLevel, float GeometryVoxelSize,
const FIntVector& OriginVoxels, const FVector& ChunkWorldPos,
const TArray<FVoxelHybridSharpFeatureBox>& Features,
const std::function<float(int32,int32,int32)>& Sample,
const std::function<FVector(int32,int32,int32)>& Gradient)
{
FResult Result;
if (Cells <= 0 || Features.IsEmpty()) return Result;
const int32 CellDim = Cells + 2;
const int32 NodeDim = Cells + 3;
const std::size_t CellCount = static_cast<std::size_t>(CellDim) * CellDim * CellDim;
std::vector<FCell> CellData(CellCount);
const auto CellIndex = [CellDim](int32 X, int32 Y, int32 Z)
{ return (static_cast<std::size_t>(Z + 1) * CellDim + (Y + 1)) * CellDim + (X + 1); };
const auto NodePosition = [&](int32 X, int32 Y, int32 Z)
{ return FVector(X * Step * GeometryVoxelSize, Y * Step * GeometryVoxelSize,
Z * Step * GeometryVoxelSize) + ChunkWorldPos; };
std::array<std::vector<FRoot>, 3> Roots;
Roots[0].resize(static_cast<std::size_t>(CellDim) * NodeDim * NodeDim);
Roots[1].resize(static_cast<std::size_t>(NodeDim) * CellDim * NodeDim);
Roots[2].resize(static_cast<std::size_t>(NodeDim) * NodeDim * CellDim);
const auto RootIndex = [CellDim, NodeDim](int32 Axis, int32 X, int32 Y, int32 Z)
{
const std::size_t NX = static_cast<std::size_t>(X + 1);
const std::size_t NY = static_cast<std::size_t>(Y + 1);
const std::size_t NZ = static_cast<std::size_t>(Z + 1);
if (Axis == 0) return (NZ * NodeDim + NY) * CellDim + NX;
if (Axis == 1) return (NZ * CellDim + NY) * NodeDim + NX;
return (NZ * NodeDim + NY) * NodeDim + NX;
};
for (int32 Z = -1; Z <= Cells + 1; ++Z)
for (int32 Y = -1; Y <= Cells + 1; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
const float D0 = Sample(X, Y, Z), D1 = Sample(X + 1, Y, Z);
if ((D0 < IsoLevel) == (D1 < IsoLevel)) continue;
const double T = FMath::Clamp(static_cast<double>(IsoLevel - D0) / (D1 - D0), 0.0, 1.0);
FRoot& Root = Roots[0][RootIndex(0, X, Y, Z)];
Root.bActive = true;
++Result.ActiveEdgeRoots;
Root.Position = FMath::Lerp(NodePosition(X, Y, Z), NodePosition(X + 1, Y, Z), T);
Root.Gradient = FMath::Lerp(Gradient(X, Y, Z), Gradient(X + 1, Y, Z), T).GetSafeNormal();
}
for (int32 Z = -1; Z <= Cells + 1; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells + 1; ++X)
{
const float D0 = Sample(X, Y, Z), D1 = Sample(X, Y + 1, Z);
if ((D0 < IsoLevel) == (D1 < IsoLevel)) continue;
const double T = FMath::Clamp(static_cast<double>(IsoLevel - D0) / (D1 - D0), 0.0, 1.0);
FRoot& Root = Roots[1][RootIndex(1, X, Y, Z)];
Root.bActive = true;
++Result.ActiveEdgeRoots;
Root.Position = FMath::Lerp(NodePosition(X, Y, Z), NodePosition(X, Y + 1, Z), T);
Root.Gradient = FMath::Lerp(Gradient(X, Y, Z), Gradient(X, Y + 1, Z), T).GetSafeNormal();
}
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells + 1; ++Y)
for (int32 X = -1; X <= Cells + 1; ++X)
{
const float D0 = Sample(X, Y, Z), D1 = Sample(X, Y, Z + 1);
if ((D0 < IsoLevel) == (D1 < IsoLevel)) continue;
const double T = FMath::Clamp(static_cast<double>(IsoLevel - D0) / (D1 - D0), 0.0, 1.0);
FRoot& Root = Roots[2][RootIndex(2, X, Y, Z)];
Root.bActive = true;
++Result.ActiveEdgeRoots;
Root.Position = FMath::Lerp(NodePosition(X, Y, Z), NodePosition(X, Y, Z + 1), T);
Root.Gradient = FMath::Lerp(Gradient(X, Y, Z), Gradient(X, Y, Z + 1), T).GetSafeNormal();
}
const auto GetRoot = [&](int32 Axis, int32 X, int32 Y, int32 Z) -> FRoot&
{ return Roots[Axis][RootIndex(Axis, X, Y, Z)]; };
const auto GetCellEdgeRoot = [&](int32 X, int32 Y, int32 Z, int32 Edge) -> FRoot&
{
const int32 A = EdgeCorners[Edge][0], B = EdgeCorners[Edge][1];
const int32 AX = CornerOffsets[A][0], AY = CornerOffsets[A][1], AZ = CornerOffsets[A][2];
const int32 BX = CornerOffsets[B][0], BY = CornerOffsets[B][1], BZ = CornerOffsets[B][2];
const int32 Axis = AX != BX ? 0 : AY != BY ? 1 : 2;
return GetRoot(Axis, X + FMath::Min(AX, BX), Y + FMath::Min(AY, BY), Z + FMath::Min(AZ, BZ));
};
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
FCell& Cell = CellData[CellIndex(X, Y, Z)];
float Densities[CornerCount]{};
for (int32 Corner = 0; Corner < CornerCount; ++Corner)
{
const int32 GX = X + CornerOffsets[Corner][0];
const int32 GY = Y + CornerOffsets[Corner][1];
const int32 GZ = Z + CornerOffsets[Corner][2];
Densities[Corner] = Sample(GX, GY, GZ);
if (Densities[Corner] >= IsoLevel) Cell.Case |= static_cast<uint8>(1u << Corner);
}
if (Cell.Case == 0 || Cell.Case == 255 || MCEdgeTable[Cell.Case] == 0) continue;
std::array<uint8, EdgeCount> Parent{};
for (int32 Edge = 0; Edge < EdgeCount; ++Edge) Parent[Edge] = static_cast<uint8>(Edge);
for (int32 I = 0; I < 16 && MCTriTable[Cell.Case][I] >= 0; I += 3)
{
const int32 A = MCTriTable[Cell.Case][I];
const int32 B = MCTriTable[Cell.Case][I + 1];
const int32 C = MCTriTable[Cell.Case][I + 2];
UniteLocal(Parent, A, B); UniteLocal(Parent, B, C);
}
std::array<uint8, EdgeCount> RootToComponent{};
RootToComponent.fill(0xff);
for (int32 Edge = 0; Edge < EdgeCount; ++Edge)
{
const int32 A = EdgeCorners[Edge][0], B = EdgeCorners[Edge][1];
if ((Densities[A] < IsoLevel) == (Densities[B] < IsoLevel)) continue;
FRoot& Root = GetCellEdgeRoot(X, Y, Z, Edge);
if (!Root.bActive) continue;
const int32 LocalRoot = FindLocal(Parent, Edge);
if (RootToComponent[LocalRoot] == 0xff)
RootToComponent[LocalRoot] = Cell.ComponentCount++;
const uint8 Component = RootToComponent[LocalRoot];
Cell.EdgeComponent[Edge] = Component;
const int32 OA = EdgeCorners[Edge][0];
const int32 OB = EdgeCorners[Edge][1];
const int32 AX = X + CornerOffsets[OA][0], AY = Y + CornerOffsets[OA][1], AZ = Z + CornerOffsets[OA][2];
const int32 BX = X + CornerOffsets[OB][0], BY = Y + CornerOffsets[OB][1], BZ = Z + CornerOffsets[OB][2];
const FVector RootVoxel = FVector(OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z)
+ FVector(AX, AY, AZ) * Step
+ FVector(BX - AX, BY - AY, BZ - AZ) * (Step *
FMath::Clamp(static_cast<double>(IsoLevel - Densities[OA])
/ (Densities[OB] - Densities[OA]), 0.0, 1.0));
for (const FVoxelHybridSharpFeatureBox& Feature : Features)
{
std::vector<FPlane> CandidatePlanes;
const double SDF = BoxSDF(Feature, RootVoxel, CandidatePlanes);
const double AbsSDF = FMath::Abs(SDF);
Result.MinimumFeatureRootAbsSdf = FMath::Min(
Result.MinimumFeatureRootAbsSdf, AbsSDF);
++Result.FeatureRootSamples;
if (AbsSDF <= 1.0e-2) ++Result.NearFeatureRootSamples;
if (AbsSDF > 1.0e-4 || CandidatePlanes.empty()) continue;
for (const FPlane& Plane : CandidatePlanes) Root.Planes.push_back(Plane);
}
if (!Root.Planes.empty())
{
++Result.TaggedEdgeRoots;
std::sort(Root.Planes.begin(), Root.Planes.end(),
[](const FPlane& A, const FPlane& B) { return A.Id < B.Id; });
Root.Planes.erase(std::unique(Root.Planes.begin(), Root.Planes.end(),
[](const FPlane& A, const FPlane& B) { return A.Id == B.Id; }), Root.Planes.end());
Cell.ComponentSharp[Component] = 1;
if (X >= 0 && Y >= 0 && Z >= 0 && X < Cells && Y < Cells && Z < Cells)
++Result.SharpCells;
}
}
}
const std::size_t PatchCount = CellCount * EdgeCount;
std::vector<uint32> PatchParent(PatchCount);
std::vector<uint8> PatchRank(PatchCount, 0), PatchSharp(PatchCount, 0), PatchTouchesBorder(PatchCount, 0);
std::iota(PatchParent.begin(), PatchParent.end(), 0u);
const auto FindPatch = [&](uint32 Value, auto&& Self) -> uint32
{
if (PatchParent[Value] != Value) PatchParent[Value] = Self(PatchParent[Value], Self);
return PatchParent[Value];
};
const auto UnitePatch = [&](uint32 A, uint32 B)
{
uint32 RA = FindPatch(A, FindPatch), RB = FindPatch(B, FindPatch);
if (RA == RB) return;
if (PatchRank[RA] < PatchRank[RB]) std::swap(RA, RB);
PatchParent[RB] = RA;
PatchSharp[RA] = static_cast<uint8>(PatchSharp[RA] | PatchSharp[RB]);
PatchTouchesBorder[RA] = static_cast<uint8>(PatchTouchesBorder[RA] | PatchTouchesBorder[RB]);
if (PatchRank[RA] == PatchRank[RB]) ++PatchRank[RA];
};
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
const FCell& Cell = CellData[CellIndex(X, Y, Z)];
for (uint8 Component = 0; Component < Cell.ComponentCount; ++Component)
{
const uint32 Id = static_cast<uint32>(CellIndex(X, Y, Z) * EdgeCount + Component);
PatchSharp[Id] = Cell.ComponentSharp[Component];
const bool bCoreBorder = X >= 0 && Y >= 0 && Z >= 0 && X < Cells && Y < Cells && Z < Cells
&& (X == 0 || Y == 0 || Z == 0 || X == Cells - 1 || Y == Cells - 1 || Z == Cells - 1);
PatchTouchesBorder[Id] = bCoreBorder ? 1 : 0;
}
}
for (int32 Axis = 0; Axis < 3; ++Axis)
for (int32 Z = -1; Z <= Cells + (Axis == 2 ? 0 : 1); ++Z)
for (int32 Y = -1; Y <= Cells + (Axis == 1 ? 0 : 1); ++Y)
for (int32 X = -1; X <= Cells + (Axis == 0 ? 0 : 1); ++X)
{
if (!GetRoot(Axis, X, Y, Z).bActive) continue;
std::array<std::array<int32, 3>, 4> Around{};
if (Axis == 0) Around = {{{X,Y-1,Z-1},{X,Y,Z-1},{X,Y,Z},{X,Y-1,Z}}};
else if (Axis == 1) Around = {{{X-1,Y,Z-1},{X-1,Y,Z},{X,Y,Z},{X,Y,Z-1}}};
else Around = {{{X-1,Y-1,Z},{X,Y-1,Z},{X,Y,Z},{X-1,Y,Z}}};
std::array<uint32, 4> Ids{};
bool bValid = true;
for (int32 I = 0; I < 4; ++I)
{
const int32 CX = Around[I][0], CY = Around[I][1], CZ = Around[I][2];
if (CX < -1 || CY < -1 || CZ < -1 || CX > Cells || CY > Cells || CZ > Cells)
{ bValid = false; break; }
const int32 LocalEdge = FindLocalEdge(CX, CY, CZ, Axis, X, Y, Z);
const FCell& Cell = CellData[CellIndex(CX, CY, CZ)];
if (LocalEdge == INDEX_NONE || Cell.EdgeComponent[LocalEdge] == 0xff)
{ bValid = false; break; }
Ids[I] = static_cast<uint32>(CellIndex(CX, CY, CZ) * EdgeCount + Cell.EdgeComponent[LocalEdge]);
}
if (bValid) for (int32 I = 1; I < 4; ++I) UnitePatch(Ids[0], Ids[I]);
}
bool bSawBorderPatch = false;
std::vector<uint8> PatchUseDC(PatchCount, 0);
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
const FCell& Cell = CellData[CellIndex(X, Y, Z)];
for (uint8 Component = 0; Component < Cell.ComponentCount; ++Component)
{
const uint32 Id = static_cast<uint32>(CellIndex(X, Y, Z) * EdgeCount + Component);
const uint32 Root = FindPatch(Id, FindPatch);
PatchUseDC[Root] = static_cast<uint8>(PatchSharp[Root] && !PatchTouchesBorder[Root]);
if (PatchSharp[Root] && PatchTouchesBorder[Root]) bSawBorderPatch = true;
}
}
Result.BorderFallbackPatches = bSawBorderPatch ? 1u : 0u;
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
FCell& Cell = CellData[CellIndex(X, Y, Z)];
if (Cell.ComponentCount == 0) continue;
for (uint8 Component = 0; Component < Cell.ComponentCount; ++Component)
{
const uint32 Id = static_cast<uint32>(CellIndex(X, Y, Z) * EdgeCount + Component);
Cell.ComponentUseDC[Component] = PatchUseDC[FindPatch(Id, FindPatch)];
if (!Cell.ComponentUseDC[Component]) continue;
std::vector<std::pair<FVector, FVector>> Constraints;
FVector RootSum = FVector::ZeroVector;
int32 RootCount = 0;
for (int32 Edge = 0; Edge < EdgeCount; ++Edge)
{
if (Cell.EdgeComponent[Edge] != Component) continue;
FRoot& Root = GetCellEdgeRoot(X, Y, Z, Edge);
if (!Root.bActive) continue;
RootSum += Root.Position;
++RootCount;
if (!Root.Planes.empty())
for (const FPlane& Plane : Root.Planes) Constraints.emplace_back(Root.Position, Plane.Normal);
else Constraints.emplace_back(Root.Position, Root.Gradient);
}
const FVector Low = NodePosition(X, Y, Z);
const FVector High = NodePosition(X + 1, Y + 1, Z + 1);
const FVector Center = RootCount > 0 ? RootSum / RootCount : (Low + High) * 0.5;
Cell.Dual[Component] = SolveQEF(Constraints, Center, Low, High);
if (X >= 0 && Y >= 0 && Z >= 0 && X < Cells && Y < Cells && Z < Cells)
++Result.DualCells;
}
}
Result.CoreCellDCEdges.assign(static_cast<std::size_t>(Cells) * Cells * Cells * EdgeCount, 0);
const auto AddTriangle = [&](const FVector& A, const FVector& B, const FVector& C,
const FVector& NA, const FVector& NB, const FVector& NC,
uint64 FA, uint64 FB, uint64 FC)
{
if (FVector::CrossProduct(B - A, C - A).SizeSquared() < 1.0e-12) return;
FTriangle T;
T.Positions[0] = A; T.Positions[1] = B; T.Positions[2] = C;
T.Normals[0] = NA.GetSafeNormal(); T.Normals[1] = NB.GetSafeNormal(); T.Normals[2] = NC.GetSafeNormal();
T.SplitKeys[0] = FA; T.SplitKeys[1] = FB; T.SplitKeys[2] = FC;
Result.Triangles.push_back(T);
};
for (int32 Axis = 0; Axis < 3; ++Axis)
for (int32 Z = -1; Z <= Cells + (Axis == 2 ? 0 : 1); ++Z)
for (int32 Y = -1; Y <= Cells + (Axis == 1 ? 0 : 1); ++Y)
for (int32 X = -1; X <= Cells + (Axis == 0 ? 0 : 1); ++X)
{
FRoot& Root = GetRoot(Axis, X, Y, Z);
if (!Root.bActive) continue;
std::array<std::array<int32, 3>, 4> Around{};
if (Axis == 0) Around = {{{X,Y-1,Z-1},{X,Y,Z-1},{X,Y,Z},{X,Y-1,Z}}};
else if (Axis == 1) Around = {{{X-1,Y,Z-1},{X-1,Y,Z},{X,Y,Z},{X,Y,Z-1}}};
else Around = {{{X-1,Y-1,Z},{X,Y-1,Z},{X,Y,Z},{X-1,Y,Z}}};
std::array<const FCell*, 4> Incident{};
std::array<uint8, 4> Components{};
bool bAllDC = true;
bool bValid = true;
for (int32 I = 0; I < 4; ++I)
{
const int32 CX = Around[I][0], CY = Around[I][1], CZ = Around[I][2];
if (CX < -1 || CY < -1 || CZ < -1 || CX > Cells || CY > Cells || CZ > Cells)
{ bValid = false; break; }
const int32 LocalEdge = FindLocalEdge(CX, CY, CZ, Axis, X, Y, Z);
const FCell& Cell = CellData[CellIndex(CX, CY, CZ)];
if (LocalEdge == INDEX_NONE || Cell.EdgeComponent[LocalEdge] == 0xff)
{ bValid = false; break; }
const uint8 Component = Cell.EdgeComponent[LocalEdge];
Incident[I] = &Cell;
Components[I] = Component;
bAllDC &= Cell.ComponentUseDC[Component] != 0;
}
if (!bValid || !bAllDC) continue;
const int32 OwnerX = FMath::Min(FMath::Min(Around[0][0], Around[1][0]), FMath::Min(Around[2][0], Around[3][0]));
const int32 OwnerY = FMath::Min(FMath::Min(Around[0][1], Around[1][1]), FMath::Min(Around[2][1], Around[3][1]));
const int32 OwnerZ = FMath::Min(FMath::Min(Around[0][2], Around[1][2]), FMath::Min(Around[2][2], Around[3][2]));
if (OwnerX < 0 || OwnerY < 0 || OwnerZ < 0 || OwnerX >= Cells || OwnerY >= Cells || OwnerZ >= Cells)
continue;
std::array<FVector, 4> Positions{};
std::array<FVector, 4> Normals{};
std::array<uint64, 4> SplitKeys{};
for (int32 I = 0; I < 4; ++I)
{
const int32 CX = Around[I][0], CY = Around[I][1], CZ = Around[I][2];
const uint8 Component = Components[I];
Positions[I] = Incident[I]->Dual[Component];
SplitKeys[I] = SelectFeature(Root, Axis);
Normals[I] = SplitKeys[I] != 0 ? NormalForFeature(Root, SplitKeys[I]) : Root.Gradient;
}
if (FVector::DotProduct(FVector::CrossProduct(Positions[1] - Positions[0],
Positions[2] - Positions[0]), Root.Gradient) < 0.0)
{
std::swap(Positions[1], Positions[3]);
std::swap(Normals[1], Normals[3]);
std::swap(SplitKeys[1], SplitKeys[3]);
}
const int32 Tie = (X + Y + Z + Axis) & 1;
if (Tie == 0)
{
AddTriangle(Positions[0], Positions[1], Positions[2], Normals[0], Normals[1], Normals[2], SplitKeys[0], SplitKeys[1], SplitKeys[2]);
AddTriangle(Positions[0], Positions[2], Positions[3], Normals[0], Normals[2], Normals[3], SplitKeys[0], SplitKeys[2], SplitKeys[3]);
}
else
{
AddTriangle(Positions[0], Positions[1], Positions[3], Normals[0], Normals[1], Normals[3], SplitKeys[0], SplitKeys[1], SplitKeys[3]);
AddTriangle(Positions[1], Positions[2], Positions[3], Normals[1], Normals[2], Normals[3], SplitKeys[1], SplitKeys[2], SplitKeys[3]);
}
}
for (int32 Z = 0; Z < Cells; ++Z)
for (int32 Y = 0; Y < Cells; ++Y)
for (int32 X = 0; X < Cells; ++X)
{
const FCell& Cell = CellData[CellIndex(X, Y, Z)];
if (!Cell.ComponentCount) continue;
const std::size_t CoreCell = (static_cast<std::size_t>(Z) * Cells + Y) * Cells + X;
for (int32 Edge = 0; Edge < EdgeCount; ++Edge)
{
const uint8 Component = Cell.EdgeComponent[Edge];
if (Component != 0xff && Cell.ComponentUseDC[Component])
Result.CoreCellDCEdges[CoreCell * EdgeCount + Edge] = 1;
}
}
return Result;
}
}
@@ -4,6 +4,7 @@
#include "VoxelMarchingCubesMesher.h" #include "VoxelMarchingCubesMesher.h"
#include "MarchingCubesTables.h" #include "MarchingCubesTables.h"
#include "VoxelDensityProfile.h" #include "VoxelDensityProfile.h"
#include "VoxelHybridSharpMesher.h"
#if WITH_EDITOR #if WITH_EDITOR
#include "VoxelRedefinedDensityField.h" #include "VoxelRedefinedDensityField.h"
#endif #endif
@@ -144,6 +145,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// plus de (ré)allocation de hash-map par tuile (chaque worker a son propre exemplaire). // plus de (ré)allocation de hash-map par tuile (chaque worker a son propre exemplaire).
static thread_local TMap<FIntVector, int32> VertexMap; static thread_local TMap<FIntVector, int32> VertexMap;
VertexMap.Reset(); VertexMap.Reset();
static thread_local TMap<FIntVector, TMap<uint64, int32>> SplitNormalVertexMap;
SplitNormalVertexMap.Reset();
//========================================================================= //=========================================================================
// F17 — CLASSE DE SURFACE (sol vs plafond sky-cap), par vertex → par triangle // F17 — CLASSE DE SURFACE (sol vs plafond sky-cap), par vertex → par triangle
@@ -164,7 +167,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b) — // Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b) —
// plus d'échantillonnage de densité par vertex. RawNormal pointe solide→air ; on la // plus d'échantillonnage de densité par vertex. RawNormal pointe solide→air ; on la
// normalise ici (fallback up si dégénérée). // normalise ici (fallback up si dégénérée).
auto GetOrCreateVertex = [&](const FVector& WorldPos, const FVector& RawNormal) -> int32 auto GetOrCreateVertex = [&](const FVector& WorldPos, const FVector& RawNormal,
uint64 NormalSplitKey = 0) -> int32
{ {
const FIntVector Key( const FIntVector Key(
FMath::RoundToInt(WorldPos.X * 100.0f), FMath::RoundToInt(WorldPos.X * 100.0f),
@@ -172,13 +176,27 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
FMath::RoundToInt(WorldPos.Z * 100.0f) FMath::RoundToInt(WorldPos.Z * 100.0f)
); );
if (int32* Existing = VertexMap.Find(Key)) if (NormalSplitKey != 0)
{
if (TMap<uint64, int32>* SplitMap = SplitNormalVertexMap.Find(Key))
{
if (int32* Existing = SplitMap->Find(NormalSplitKey)) return *Existing;
}
}
else if (int32* Existing = VertexMap.Find(Key))
{ {
return *Existing; return *Existing;
} }
const int32 NewIndex = MeshData.Vertices.Num(); const int32 NewIndex = MeshData.Vertices.Num();
if (NormalSplitKey != 0)
{
SplitNormalVertexMap.FindOrAdd(Key).Add(NormalSplitKey, NewIndex);
}
else
{
VertexMap.Add(Key, NewIndex); VertexMap.Add(Key, NewIndex);
}
MeshData.Vertices.Add(WorldPos); MeshData.Vertices.Add(WorldPos);
@@ -1050,6 +1068,31 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
SampleG(gx, gy, gz + 1) - SampleG(gx, gy, gz - 1)); SampleG(gx, gy, gz + 1) - SampleG(gx, gy, gz - 1));
}; };
// DC is limited to tagged LOD0 patches. Coarse tiles and a connected patch that reaches a
// tile edge use MC there, preserving the existing MC-to-MC clipmap transition and skirts.
// 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())
{
MeshData.bHybridMesherInvoked = true;
HybridResult = VoxelHybridSharpMesher::Generate(
CellsPerAxis, Step, IsoLevel, GeometryVoxelSize, OriginVoxels, ChunkWorldPos,
HybridSharpFeatures,
[&](int32 X, int32 Y, int32 Z) { return SampleG(X, Y, Z); },
[&](int32 X, int32 Y, int32 Z) { return GradAt(X, Y, Z); });
}
MeshData.HybridSharpCellCount = HybridResult.SharpCells;
MeshData.HybridDualCellCount = HybridResult.DualCells;
MeshData.HybridActiveEdgeRootCount = HybridResult.ActiveEdgeRoots;
MeshData.HybridTaggedEdgeRootCount = HybridResult.TaggedEdgeRoots;
MeshData.HybridMinimumFeatureRootAbsSdf = HybridResult.MinimumFeatureRootAbsSdf;
MeshData.HybridFeatureRootSampleCount = HybridResult.FeatureRootSamples;
MeshData.HybridNearFeatureRootSampleCount = HybridResult.NearFeatureRootSamples;
MeshData.HybridBorderFallbackPatchCount = HybridResult.BorderFallbackPatches;
MeshData.HybridDCTriangleCount = static_cast<uint32>(HybridResult.Triangles.size());
//========================================================================= //=========================================================================
// ITÉRATION SUR LES CELLULES // ITÉRATION SUR LES CELLULES
//========================================================================= //=========================================================================
@@ -1169,6 +1212,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 E0 = MCTriTable[CaseIndex][i]; const int32 E0 = MCTriTable[CaseIndex][i];
const int32 E1 = MCTriTable[CaseIndex][i + 1]; const int32 E1 = MCTriTable[CaseIndex][i + 1];
const int32 E2 = MCTriTable[CaseIndex][i + 2]; const int32 E2 = MCTriTable[CaseIndex][i + 2];
if (HybridResult.UsesDCEdge(cx, cy, cz, E0, CellsPerAxis))
{
continue;
}
const int32 Idx0 = GetOrCreateVertex(EdgeVertices[E0], EdgeNormals[E0]); const int32 Idx0 = GetOrCreateVertex(EdgeVertices[E0], EdgeNormals[E0]);
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]); const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]); const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
@@ -1192,6 +1239,24 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
} }
} }
// Add DC geometry to the same material runs as MC. Analytic feature IDs split corner normals.
// Ajouter la geometrie DC dans les memes groupes que MC; les IDs separent les normales nettes.
for (const VoxelHybridSharpMesher::FTriangle& Triangle : HybridResult.Triangles)
{
const int32 Idx0 = GetOrCreateVertex(
Triangle.Positions[0], Triangle.Normals[0], Triangle.SplitKeys[0]);
const int32 Idx1 = GetOrCreateVertex(
Triangle.Positions[1], Triangle.Normals[1], Triangle.SplitKeys[1]);
const int32 Idx2 = GetOrCreateVertex(
Triangle.Positions[2], Triangle.Normals[2], Triangle.SplitKeys[2]);
const int32 CapVotes = static_cast<int32>(VertexClasses[Idx0])
+ static_cast<int32>(VertexClasses[Idx1]) + static_cast<int32>(VertexClasses[Idx2]);
TArray<int32>& Dst = (CapVotes >= 2) ? CapTris : GroundTris;
Dst.Add(Idx0);
Dst.Add(Idx2);
Dst.Add(Idx1);
}
if (bProfileMesher) if (bProfileMesher)
{ {
VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::AddMeasurement(
+25 -2
View File
@@ -8725,6 +8725,7 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
const int32 NumVertices = Pending->NumVertices; const int32 NumVertices = Pending->NumVertices;
const int32 NumTriangles = Pending->NumTriangles; const int32 NumTriangles = Pending->NumTriangles;
const int32 NumSkirtTriangles = Pending->NumSkirtTriangles; const int32 NumSkirtTriangles = Pending->NumSkirtTriangles;
const bool bPooledComponent = Pending->bPooledComponent;
const int32 PendingCooksAtSubmit = Pending->PendingCooksAtSubmit; const int32 PendingCooksAtSubmit = Pending->PendingCooksAtSubmit;
const bool bUseAsyncCook = Pending->bUseAsyncCook; const bool bUseAsyncCook = Pending->bUseAsyncCook;
const bool bUseComplexAsSimpleCollision = Pending->bUseComplexAsSimpleCollision; const bool bUseComplexAsSimpleCollision = Pending->bUseComplexAsSimpleCollision;
@@ -8745,6 +8746,15 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
CollisionSuppressedAtCycles.Remove(Tile); CollisionSuppressedAtCycles.Remove(Tile);
if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Tile)) if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Tile))
{ {
// RMC caches local bounds on pooled components; rebinding after the async cook
// invalidates that cache and rebuilds scene-query bounds from the installed mesh.
// RMC met les bornes locales en cache sur les composants reutilises ; le rebranchement
// apres la cuisson invalide ce cache et recalcule les bornes des traces.
if (bPooledComponent && Comp->GetRealtimeMesh() == CompletedMesh)
{
Comp->SetRealtimeMesh(nullptr);
Comp->SetRealtimeMesh(CompletedMesh);
}
Comp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); Comp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
} }
CollisionReadyTiles.Add(Tile); CollisionReadyTiles.Add(Tile);
@@ -11439,6 +11449,9 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
const VoxelForgeCore::TileKey CoreKey{{Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z}, Tile.Level}; const VoxelForgeCore::TileKey CoreKey{{Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z}, Tile.Level};
VoxelForgeCore::TileGenerationOptions Options; VoxelForgeCore::TileGenerationOptions Options;
Options.mesherRecipe = CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1
? VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1
: VoxelForgeCore::MesherRecipe::marchingCubesV1;
Options.hashDensity = bCaptureHashes; Options.hashDensity = bCaptureHashes;
std::string CoreError; std::string CoreError;
std::optional<VoxelForgeCore::TileMesh> CoreMesh; std::optional<VoxelForgeCore::TileMesh> CoreMesh;
@@ -12248,7 +12261,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
MeshComp = AcquireTileComponent( MeshComp = AcquireTileComponent(
bProfileApply ? &ComponentCreationSeconds : nullptr, bProfileApply ? &ComponentCreationSeconds : nullptr,
bProfileApply ? &ComponentRegistrationSeconds : nullptr, bProfileApply ? &ComponentRegistrationSeconds : nullptr,
bProfileApply ? &bComponentCreated : nullptr); &bComponentCreated);
TileComponents.Add(Tile, MeshComp); TileComponents.Add(Tile, MeshComp);
} }
if (!IsValid(MeshComp)) if (!IsValid(MeshComp))
@@ -12263,6 +12276,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
} }
return false; return false;
} }
const bool bWasPooledComponent = !bComponentAlreadyPresent && !bComponentCreated;
const double ComponentSeconds = bProfileApply const double ComponentSeconds = bProfileApply
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ComponentStartCycles) ? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ComponentStartCycles)
: 0.0; : 0.0;
@@ -12407,6 +12421,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
Pending.NumTriangles = Result.NumTriangles; Pending.NumTriangles = Result.NumTriangles;
Pending.NumSkirtTriangles = Result.NumSkirtTriangles; Pending.NumSkirtTriangles = Result.NumSkirtTriangles;
Pending.PendingCooksAtSubmit = PendingCollisionCooks.Num(); Pending.PendingCooksAtSubmit = PendingCollisionCooks.Num();
Pending.bPooledComponent = bWasPooledComponent;
Pending.bUseAsyncCook = CollisionConfig.bUseAsyncCook; Pending.bUseAsyncCook = CollisionConfig.bUseAsyncCook;
Pending.bUseComplexAsSimpleCollision = CollisionConfig.bUseComplexAsSimpleCollision; Pending.bUseComplexAsSimpleCollision = CollisionConfig.bUseComplexAsSimpleCollision;
Pending.bShouldFastCookMeshes = CollisionConfig.bShouldFastCookMeshes; Pending.bShouldFastCookMeshes = CollisionConfig.bShouldFastCookMeshes;
@@ -13275,7 +13290,8 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
{ {
return FString(); return FString();
} }
if (AssemblyHash.IsEmpty()) const bool bHybridMesher = CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1;
if (AssemblyHash.IsEmpty() && !bHybridMesher)
{ {
return SeasonHash; return SeasonHash;
} }
@@ -13295,8 +13311,15 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
} }
} }
} }
if (!AssemblyHash.IsEmpty())
{
Payload += TEXT("|world_assembly="); Payload += TEXT("|world_assembly=");
Payload += AssemblyHash; Payload += AssemblyHash;
}
if (bHybridMesher)
{
Payload += TEXT("|core_mesher=WorldAssemblerHybridV1");
}
FTCHARToUTF8 Utf8Payload(*Payload); FTCHARToUTF8 Utf8Payload(*Payload);
uint8 Digest[FSHA1::DigestSize]; uint8 Digest[FSHA1::DigestSize];
FSHA1::HashBuffer(Utf8Payload.Get(), Utf8Payload.Length(), Digest); FSHA1::HashBuffer(Utf8Payload.Get(), Utf8Payload.Length(), Digest);
@@ -38,6 +38,15 @@ struct FVoxelSharedDensityGrid
} }
}; };
// Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation.
// Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0.
struct FVoxelHybridSharpFeatureBox
{
uint64 StableId = 0;
int64 MinimumSubvoxels[3] = {};
int64 MaximumSubvoxels[3] = {};
};
UCLASS(BlueprintType) UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
{ {
@@ -103,6 +112,14 @@ public:
// the canonical generator path rather than changing geometry. // the canonical generator path rather than changing geometry.
void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; } void SetSharedDensityGrid(const FVoxelSharedDensityGrid* InGrid) { SharedDensityGrid = InGrid; }
// The caller owns this mesher instance for one tile task; no shared mutable feature state.
// L'appelant possede cette instance pour une tuile; aucune donnee partagee modifiable.
void SetHybridSharpFeatures(TArray<FVoxelHybridSharpFeatureBox>&& InFeatures)
{
HybridSharpFeatures = MoveTemp(InFeatures);
bUseHybridSharpMesher = true;
}
private: private:
FORCEINLINE bool ShouldAbortWork() const FORCEINLINE bool ShouldAbortWork() const
{ {
@@ -111,6 +128,8 @@ private:
const std::atomic<bool>* ShutdownFlag = nullptr; const std::atomic<bool>* ShutdownFlag = nullptr;
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr; const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
bool bUseHybridSharpMesher = false;
float GeometryVoxelSizeCm = VOXEL_SIZE; float GeometryVoxelSizeCm = VOXEL_SIZE;
public: public:
+25
View File
@@ -379,6 +379,20 @@ struct FVoxelMeshData
// share without changing the emitted indices. // share without changing the emitted indices.
int32 NumSkirtTriangles = 0; int32 NumSkirtTriangles = 0;
// Hybrid recipe diagnostics. These are counters only; they are excluded from mesh hashes.
// Diagnostics de la recette hybride; ces compteurs ne font pas partie des hachages.
uint32 HybridSharpCellCount = 0;
uint32 HybridDualCellCount = 0;
uint32 HybridFeatureCount = 0;
uint32 HybridActiveEdgeRootCount = 0;
uint32 HybridTaggedEdgeRootCount = 0;
double HybridMinimumFeatureRootAbsSdf = 0.0;
uint32 HybridFeatureRootSampleCount = 0;
uint32 HybridNearFeatureRootSampleCount = 0;
uint32 HybridBorderFallbackPatchCount = 0;
uint32 HybridDCTriangleCount = 0;
bool bHybridMesherInvoked = false;
// F17 — classe de surface par TRIANGLE, portée par l'ORDRE du buffer d'indices : les // F17 — classe de surface par TRIANGLE, portée par l'ORDRE du buffer d'indices : les
// NumCeilingTriangles DERNIERS triangles de Triangles sont la classe "plafond sky-cap" // NumCeilingTriangles DERNIERS triangles de Triangles sont la classe "plafond sky-cap"
// (polygroup 1 → slot matériau 1, sans ombre) ; tout ce qui précède est "sol/roche" // (polygroup 1 → slot matériau 1, sans ombre) ; tout ce qui précède est "sol/roche"
@@ -394,6 +408,17 @@ struct FVoxelMeshData
Colors.Empty(); Colors.Empty();
NumSurfaceVertices = 0; NumSurfaceVertices = 0;
NumSkirtTriangles = 0; NumSkirtTriangles = 0;
HybridSharpCellCount = 0;
HybridDualCellCount = 0;
HybridFeatureCount = 0;
HybridActiveEdgeRootCount = 0;
HybridTaggedEdgeRootCount = 0;
HybridMinimumFeatureRootAbsSdf = 0.0;
HybridFeatureRootSampleCount = 0;
HybridNearFeatureRootSampleCount = 0;
HybridBorderFallbackPatchCount = 0;
HybridDCTriangleCount = 0;
bHybridMesherInvoked = false;
NumCeilingTriangles = 0; NumCeilingTriangles = 0;
} }
+15
View File
@@ -72,6 +72,15 @@ enum class EVoxelAnchorPolicy : uint8
FullVisual UMETA(DisplayName = "Full Visual") // rendered too (a second local viewpoint) FullVisual UMETA(DisplayName = "Full Visual") // rendered too (a second local viewpoint)
}; };
/** Versioned Core mesh recipe. Hybrid requires tagged sharp plan features and affects only Core meshes.
* Recette de maillage Core versionnee. Hybrid exige des fonctions planaires balisees et ne touche qu'au Core. */
UENUM(BlueprintType)
enum class EVoxelForgeCoreMesherRecipe : uint8
{
MarchingCubesV1 UMETA(DisplayName = "Marching Cubes V1"),
WorldAssemblerHybridV1 UMETA(DisplayName = "World Assembler Hybrid V1")
};
/** A non-player streaming center: keep a small box of level-0 tiles loaded around Actor so it has /** A non-player streaming center: keep a small box of level-0 tiles loaded around Actor so it has
* collision wherever it is. Plain internal struct (weak ptr is GC-safe without reflection). */ * collision wherever it is. Plain internal struct (weak ptr is GC-safe without reflection). */
struct FVoxelStreamingAnchor struct FVoxelStreamingAnchor
@@ -269,6 +278,11 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Use VoxelForge Core")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Use VoxelForge Core"))
bool bUseVoxelForgeCore = false; bool bUseVoxelForgeCore = false;
/** Core-only, versioned mesher selection. Default keeps the existing Core hashes and geometry.
* Recette reservee au Core; la valeur par defaut conserve les hachages et la geometrie actuels. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Core Mesher Recipe"))
EVoxelForgeCoreMesherRecipe CoreMesherRecipe = EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
/** Core-only clipmap radius, in level-0 Core tiles. Does not read Settings->ClipRadius. */ /** Core-only clipmap radius, in level-0 Core tiles. Does not read Settings->ClipRadius. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "1", ClampMax = "64")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "1", ClampMax = "64"))
int32 CoreClipRadius = 3; int32 CoreClipRadius = 3;
@@ -330,6 +344,7 @@ public:
uint64 CollisionSubmittedCycles = 0; uint64 CollisionSubmittedCycles = 0;
uint64 CollisionSubmittedFrame = 0; uint64 CollisionSubmittedFrame = 0;
bool bBodyUpdatedEventSeen = false; bool bBodyUpdatedEventSeen = false;
bool bPooledComponent = false;
uint64 BodyUpdatedEventCycles = 0; uint64 BodyUpdatedEventCycles = 0;
uint64 BodyUpdatedEventFrame = 0; uint64 BodyUpdatedEventFrame = 0;
bool bPhysicsStateReadyAtBodyUpdate = false; bool bPhysicsStateReadyAtBodyUpdate = false;
File diff suppressed because it is too large. Load diff
@@ -23,6 +23,7 @@ public class VoxelForgeEditor : ModuleRules
{ {
"Json", "Json",
"ImageWrapper", "ImageWrapper",
"RealtimeMeshComponent",
"VoxelForge", "VoxelForge",
"VoxelForgeCore", "VoxelForgeCore",
}); });
@@ -62,6 +62,23 @@ namespace VoxelForgeCore
std::uint32_t skirtTriangleCount = 0; std::uint32_t skirtTriangleCount = 0;
}; };
// Versioned render-mesher choices. marchingCubesV1 preserves the production Core path;
// worldAssemblerHybridV1 applies feature-aware DC only to tagged type-2 plan boxes at LOD0.
// Choix versionnes du mailleur. marchingCubesV1 conserve la voie Core actuelle;
// worldAssemblerHybridV1 applique le DC aux boites type 2 balisees, uniquement en LOD0.
enum class MesherRecipe : std::uint8_t
{
marchingCubesV1 = 0,
worldAssemblerHybridV1 = 1
};
struct SharpFeatureBox
{
std::uint64_t stableId = 0;
std::array<std::int64_t, 3> minimumSubvoxels{};
std::array<std::int64_t, 3> maximumSubvoxels{};
};
enum class TileClass : std::uint8_t { allSolid, allAir, mixed, unknown }; enum class TileClass : std::uint8_t { allSolid, allAir, mixed, unknown };
struct TileGenerationStats struct TileGenerationStats
@@ -657,6 +674,7 @@ namespace VoxelForgeCore
struct TileGenerationOptions struct TileGenerationOptions
{ {
MesherRecipe mesherRecipe = MesherRecipe::marchingCubesV1;
std::int32_t bandMinZVoxel = -2147483647 - 1; std::int32_t bandMinZVoxel = -2147483647 - 1;
std::int32_t bandMaxZVoxel = 2147483647; std::int32_t bandMaxZVoxel = 2147483647;
bool hashDensity = false; bool hashDensity = false;
+49 -2
View File
@@ -890,11 +890,10 @@ namespace VoxelForgeCore
if(const auto found=impl_->tileRegions.find(key);found!=impl_->tileRegions.end()) if(const auto found=impl_->tileRegions.find(key);found!=impl_->tileRegions.end())
page=found->second; page=found->second;
} }
const FStandaloneShapeWorld& tileShape=page?*page:*impl_->shape; const FStandaloneShapeWorld* tileShapeContext=page?page.get():impl_->shape.get();
UVoxelMarchingCubesMesher mesher; UVoxelMarchingCubesMesher mesher;
mesher.SetGenerator(world.Generator.get()); mesher.SetGenerator(world.Generator.get());
mesher.SetGeometryVoxelSizeCm(25.0f); mesher.SetGeometryVoxelSizeCm(25.0f);
mesher.SetStandaloneShapeWorld(&tileShape);
mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors; mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors;
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors; mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups; mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
@@ -904,6 +903,54 @@ namespace VoxelForgeCore
const int32 level=key.lod,extent=CHUNK_SIZE<<level; const int32 level=key.lod,extent=CHUNK_SIZE<<level;
const FVoxelTileKey nativeKey=ToNative(key); const FVoxelTileKey nativeKey=ToNative(key);
const FIntVector origin=nativeKey.OriginVoxels(); const FIntVector origin=nativeKey.OriginVoxels();
if(options.mesherRecipe!=MesherRecipe::marchingCubesV1
&&options.mesherRecipe!=MesherRecipe::worldAssemblerHybridV1)
{error="tile mesher recipe version is unsupported";return std::nullopt;}
TArray<FVoxelHybridSharpFeatureBox> sharpFeatures;
if(options.mesherRecipe==MesherRecipe::worldAssemblerHybridV1&&level==0&&impl_->worldAssembly)
{
constexpr std::int64_t SubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel;
const std::array<std::int64_t,3> tileMin={
static_cast<std::int64_t>(origin.X)*SubvoxelsPerVoxel,
static_cast<std::int64_t>(origin.Y)*SubvoxelsPerVoxel,
static_cast<std::int64_t>(origin.Z)*SubvoxelsPerVoxel};
const std::array<std::int64_t,3> tileMax={
static_cast<std::int64_t>(origin.X+extent)*SubvoxelsPerVoxel,
static_cast<std::int64_t>(origin.Y+extent)*SubvoxelsPerVoxel,
static_cast<std::int64_t>(origin.Z+extent)*SubvoxelsPerVoxel};
for(const auto& stratum:impl_->worldAssembly->plan.strates)
for(const auto& feature:stratum.features)
{
// Type 2 is the plan's finite, hard box-solid exception; the type-4 shelf is
// smooth-min blended and therefore remains on MC until it carries sharp planes.
// Le type 2 est une boite solide nette; l'etagere type 4 reste MC car elle est lisse.
if(feature.type!=2)continue;
bool overlaps=true;
for(std::size_t axis=0;axis<3;++axis)
overlaps=overlaps&&feature.minimumSubvoxels[axis]<=tileMax[axis]
&&feature.maximumSubvoxels[axis]>=tileMin[axis];
if(!overlaps)continue;
FVoxelHybridSharpFeatureBox box;
box.StableId=feature.stableId;
for(std::size_t axis=0;axis<3;++axis)
{
box.MinimumSubvoxels[axis]=feature.minimumSubvoxels[axis];
box.MaximumSubvoxels[axis]=feature.maximumSubvoxels[axis];
}
sharpFeatures.Add(box);
}
if(!sharpFeatures.IsEmpty())
{
// Streamed analytic pages omit the native assembly plan. Use the immutable base
// shape only for tagged tiles so their mesher samples the same field as the tags.
// Les pages analytiques omettent le plan natif ; utiliser la base uniquement pour
// les tuiles balisees afin d'echantillonner le champ correspondant aux tags.
tileShapeContext=impl_->shape.get();
mesher.SetHybridSharpFeatures(MoveTemp(sharpFeatures));
}
}
const FStandaloneShapeWorld& tileShape=*tileShapeContext;
mesher.SetStandaloneShapeWorld(&tileShape);
int32 cells=level<world.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,world.CoarseTileCells); int32 cells=level<world.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,world.CoarseTileCells);
int32 step=FMath::Max(1,extent/cells); int32 step=FMath::Max(1,extent/cells);
bool bFeatureRefinedResolution=false; bool bFeatureRefinedResolution=false;
+15
View File
@@ -306,6 +306,21 @@ Impacts to carry into the mesher proof:
corner invites snagging and makes capsule clearance disagree with what the player sees. Mesh atoms corner invites snagging and makes capsule clearance disagree with what the player sees. Mesh atoms
that players can stand on need an explicit matching proxy. that players can stand on need an explicit matching proxy.
### Round-3 implementation boundary
The first Unreal recipe keeps this feature-aware DC path deliberately narrow: only tagged type-2 plan
boxes participate, only at LOD0, and cells touching a tile border use Marching Cubes. LOD1 and coarser
tiles also use Marching Cubes, so each 2:1 boundary is MC-to-MC and the existing skirt remains the
visual repair. The standalone 625-case transition matrix still has five MC doorway topology failures;
this compromise reduces the failure surface but does not close those cases or establish a transition
template.
At 25 cm, the worst phase bevel measured for MC's analytic 90-degree corner is about 17.68 cm. That is
0.35 of a 50 cm LOD1 sample, so a coarse tile cannot preserve the same edge detail. The bevel can
therefore round off and pop when the player crosses the LOD threshold. BuildHost uses null RHI, so no
projected or in-game pop measurement was made; the owner needs to judge this at the target camera and
movement speed.
## B. Bounds: hybrid certificates, not one universal Lipschitz number ## B. Bounds: hybrid certificates, not one universal Lipschitz number
I agree with Lipschitz bounds as the cheap, generic first certificate. I do **not** recommend them as I agree with Lipschitz bounds as the cheap, generic first certificate. I do **not** recommend them as
@@ -924,3 +924,50 @@ Cost and determinism outputs are retained. All 45 W1/W8/W16 hashes match their W
No `Source/` or Core source, owner project file, `.uasset`, or editor was touched. No commit, push, or stash. No `Source/` or Core source, owner project file, `.uasset`, or editor was touched. No commit, push, or stash.
— Luna — Luna
## Luna, M2 round 3 — opt-in hybrid in Unreal
Hi Claude,
The hybrid is now an opt-in, versioned Core recipe: `WorldAssemblerHybridV1`. Core remains off by
default, and `MarchingCubesV1` remains the default recipe, so the owner keeps the existing hashes
when the option is off. Tagged type-2 world-plan features use the sharp path only in LOD0 interior
cells. Border-touching cells and LOD1+ stay on MC; skirts remain the border repair.
The full null-RHI BuildHost actor commandlet passed W1/W8/W16 with the round-6 hashes unchanged,
plus height agreement, anchor/edit/travel, switch-off gating, Blueprint carve/fill and the
SurfaceWorld actor route. It exercised the TunnelNetwork Core cave path and injected nine
type-2 feature boxes for four surface fixture groups. A production-mesher check passed all five
fixtures, and a separate native SurfaceWorld cliff check matched MC bit-for-bit (10,231 vertices,
10,360 triangles, hash `0xfc7bd7a3cf997992`). Independent same-LOD borders passed; the 125-phase
LOD0/LOD1 doorway probe had no contours over 50 cm and 22.844 cm worst mismatch.
All five cooked collision contacts passed on the Pawn channel. The world trace and RMC component
trace agreed exactly, the cooked-body bounds matched render bounds, and the field-contact errors
were at most 20.332 cm. The final plugin timings put sharp fixtures at 5.66–6.70× MC; the untagged
organic cave was 0.992× MC. Density and route constraints are unchanged. The M1 full bounds,
sub-voxel shelf and route audits are the last recorded standalone results; I did not rerun those
audits for this mesher-only round. This round did rerun the actor travel, height and Blueprint
carve/fill checks.
The LOD compromise still has a real limitation: MC retains five topology failures in the standalone
625-case transition suite. Existing skirts are the current repair. The 17.68 cm corner bevel is
0.35 of a 50 cm LOD1 sample, so it may round off and pop at the threshold. BuildHost was null-RHI,
so I did not measure that visual pop; please judge it in-game at the target camera and movement
speed.
To preview the surface fixtures, duplicate the settings asset, import
[`surface_hybrid_fixture_plan.json`](../../WorldAssemblerM2/round3/surface_hybrid_fixture_plan.json),
enable `Use VoxelForge Core` on a test `AVoxelWorld`, and choose `World Assembler Hybrid V1`. The
contact coordinates and setup are in the
[round-3 report](../../WorldAssemblerM2/REPORT.md). I did not edit an owner asset, open the editor,
or build the owner's project.
The report includes the complete failed-run ledger, two new mirrored private headers, and likely
compile touchpoints. One full actor commandlet passed but its wrapper returned 1 due to a final
PowerShell status-format cast. After fixing the print, both focused wrappers and a corrected full
actor wrapper exited 0; the final full run took 11m06s and sampled 3.73 GiB peak private memory.
No process crashed, no `0xC0000142` retry occurred, and no new UECC directory appeared. No commit,
push, or stash.
— Luna
+137 -4
View File
@@ -89,9 +89,12 @@ These timings are **not comparable to the optimized production MC mesher**. The
The [updated offline contact sheet](results/captures/contact_sheet.jpg) is 864 × 1,968 pixels. It contains the five analytic fixtures, the sampled SurfaceWorld cliff, and both LOD seam views, with MC/Hybrid/DC columns. The 24 individual JPEGs are 288 × 246 pixels; all 25 files including the sheet are below 300 KB. No viewer or GPU path was used. The [updated offline contact sheet](results/captures/contact_sheet.jpg) is 864 × 1,968 pixels. It contains the five analytic fixtures, the sampled SurfaceWorld cliff, and both LOD seam views, with MC/Hybrid/DC columns. The 24 individual JPEGs are 288 × 246 pixels; all 25 files including the sheet are below 300 KB. No viewer or GPU path was used.
## Recommendation and round-3 Unreal plan ## Round-2 recommendation and proposed round-3 work (historical)
Do not select or enable the hybrid in the owner project yet. The same-LOD gate is closed, but the full LOD gate is not. The current Core world hash/default path is untouched. At the end of round 2, the recommendation was not to select or enable the hybrid in the owner
project: same-LOD passed, but the full LOD gate did not. Round 3 later narrowed the policy to LOD0
interior feature cells with MC at borders and coarser LODs; the implementation and its remaining
transition limitation are recorded below. The following list preserves the original round-2 plan.
When the standalone LOD gate passes, round 3 should: When the standalone LOD gate passes, round 3 should:
@@ -117,10 +120,140 @@ When the standalone LOD gate passes, round 3 should:
Every final build, mesher, analysis, and renderer process ran BelowNormal with affinity `0xFF00`; the scripts enforce an 8 GB private-memory limit and 15-minute timeout. Peak observed mesher private memory was under 0.2 GB. No final process crashed, exceeded a limit, or needed a retry. Every final build, mesher, analysis, and renderer process ran BelowNormal with affinity `0xFF00`; the scripts enforce an 8 GB private-memory limit and 15-minute timeout. Peak observed mesher private memory was under 0.2 GB. No final process crashed, exceeded a limit, or needed a retry.
All edits and outputs are under `Plugins/VoxelForge/Standalone/Research/WorldAssemblerM2`, plus this dialogue log. No `Source/` file, Core source, owner project file, `.uasset`, or editor was touched. No commit, push, or stash was made. For round 2, all edits and outputs were under `Plugins/VoxelForge/Standalone/Research/WorldAssemblerM2`, plus its dialogue log. No `Source/` file, Core source, owner project file, `.uasset`, or editor was touched in that round. No commit, push, or stash was made.
## Files changed and new source files ## Round-2 files changed and new source files
Updated existing standalone files: [SharpMesherSpike.cpp](SharpMesherSpike.cpp), [BuildSharpMesherSpike.ps1](BuildSharpMesherSpike.ps1), [RunSharpMesherSpike.ps1](RunSharpMesherSpike.ps1), and [RenderSharpMesherCaptures.py](RenderSharpMesherCaptures.py). Updated this report and appended the M2 round-2 dialogue entry. Updated existing standalone files: [SharpMesherSpike.cpp](SharpMesherSpike.cpp), [BuildSharpMesherSpike.ps1](BuildSharpMesherSpike.ps1), [RunSharpMesherSpike.ps1](RunSharpMesherSpike.ps1), and [RenderSharpMesherCaptures.py](RenderSharpMesherCaptures.py). Updated this report and appended the M2 round-2 dialogue entry.
**New source files this round: none.** CSVs, OBJ meshes, JPEG captures, logs, and local build outputs are generated research artifacts; none were committed. **New source files this round: none.** CSVs, OBJ meshes, JPEG captures, logs, and local build outputs are generated research artifacts; none were committed.
## Round-3 Unreal integration (2026-10-10)
The round-3 proposal is now in the plugin Core route as the versioned, opt-in
`WorldAssemblerHybridV1` recipe. `bUseVoxelForgeCore` remains off by default, and the Core recipe
defaults to `MarchingCubesV1`. The hybrid only consumes tagged type-2 world-plan feature boxes at
LOD0. Cells touching a tile border and every LOD1+ cell use MC; existing border skirts remain. The
selected recipe participates in Core content identity only when selected, so the disabled path and
the default MC path retain their existing identities.
### BuildHost evidence
The latest BuildHost UBT build and link-safety audit passed after the collision diagnostic was
changed to compare the runtime component trace and cooked-body bounds. The full null-RHI actor
commandlet ran for 11m00s and logged `pass complete=1`, `Success - 0 error(s)`. The PowerShell
wrapper then returned 1 while formatting its final status because it cast a `SwitchParameter` to
`Int32`; that print now uses the numeric fixture flag already computed by the script. A focused run
after this correction returned 0. The corrected full actor wrapper was then rerun and returned 0:
11m06s, peak sampled private memory 4,007,432,192 bytes (3.73 GiB), `pass complete=1`, and zero
engine errors. The final log is
[`CoreWorldValidation.log`](../../../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation.log).
W1/W8/W16 remained bit-stable at the round-6 Core hashes: density `0x8cc18e2981c50c4e`, mesh
`0x78e9c8ea42ff2965`, payload `0x380e8e11e76b47ca`, topology `0x24e40452e99fe482`. The actor passed
initial streaming, anchor movement, edit, height agreement, switch-off content gating, Blueprint
carve/fill, surface travel, and native SurfaceWorld travel. On the surface route, all 2,553 desired
visual tiles matched; 573 local tiles, 3,438 neighbor faces, and 432 LOD transitions were checked.
The actor run used about 2.8 GiB private memory. The only UECC directory is the older
2026-09-17 folder; no round-3 process crashed or retried on `0xC0000142`.
The hybrid selects triangles from the existing field and does not change field density, route
constraints, or signed-density bounds. This actor rerun passed height agreement, plan import,
anchor/edit/travel, switch-off gating, and Blueprint carve/fill. The last recorded full standalone
field audits remain the M1 round-4 results: 298 bounds / 8,046 samples with zero misses or unknown
ranges; 29,929 half-voxel shelf columns with zero sub-voxel runs; and a 530-center, 143,630-point
route sweep with zero blocked, unsupported, or grade-failing samples. Their negative controls were
detected. Those standalone audits were not rerun in round 3; the fresh route-related evidence is the
BuildHost actor travel/height pass above.
The focused production-mesher check passed all five fixture repeats. The separately staged native
SurfaceWorld type-4 cliff tile at `(-22,13,-58)` also passed a BuildHost MC/hybrid bit-identity
check: 10,231 vertices, 10,360 triangles, mesh hash `0xfc7bd7a3cf997992`. The full Core actor
session ran the TunnelNetwork cave path; its organic cave chamber fixture stayed on MC and produced
the same 2,408 triangles as MC.
Same-LOD borders are generated independently and pass with 63 MC segments and 63 hybrid/fallback
segments. The 125-phase LOD0/LOD1 doorway probe had zero cases and zero probes above 50 cm; its
worst measured contour mismatch was 22.844 cm. This does not close the known MC topology failures:
the standalone full transition matrix still has five failed doorway cases out of 625. This policy
keeps the existing skirts as the repair and leaves a true transition template for later work.
At LOD0, the measured 17.68 cm corner bevel is 0.35 of the 50 cm LOD1 sample. It may round off and
pop at the LOD threshold. Null RHI provided no projected-error or live visual measurement; the
owner must judge it at the target camera and movement speed.
### Cooked collision contacts
All five fixture contacts hit the actor's Pawn channel and the target RMC component. The actor's
Core render hash matched the standalone tile, render and cooked Chaos body bounds matched, and the
direct component trace differed from the world trace by 0.000 cm. Density-field contact error was
within one 25 cm Core voxel:
| Contact | World position (cm) | Field contact error |
|---|---:|---:|
| Corner top | (1025, 4150, −50127.64) | 20.332 cm |
| Doorway lintel | (1350, 4150, −50068.55) | 5.951 cm |
| Riser face | (1350, 4600, −50257.71) | 0.000 cm |
| Ledge top | (1450, 4600, −50140.14) | 8.956 cm |
| Ledge underside | (1450, 4600, −50190.14) | 13.262 cm |
### Production mesher cost
These BuildHost timings compare the plugin's production MC and hybrid routes after field sampling.
They measure meshing, not render upload or collision cooking.
| Fixture, 32³ cells | MC | Hybrid | Hybrid / MC | Triangles (MC / hybrid) |
|---|---:|---:|---:|---:|
| 90° corner | 2.246 ms | 15.048 ms | 6.70× | 3,196 / 3,200 |
| Doorway | 1.819 ms | 11.538 ms | 6.34× | 1,724 / 1,712 |
| 1 m riser | 1.979 ms | 11.974 ms | 6.05× | 2,108 / 2,112 |
| Cliff ledge | 2.270 ms | 12.839 ms | 5.66× | 3,132 / 3,128 |
| Organic cave | 2.106 ms | 2.089 ms | 0.992× | 2,408 / 2,408 |
Sharp-tagged fixtures cost roughly 5.66–6.70× MC in this plugin harness. The untagged cave is
effectively unchanged. This is a substantial local CPU cost where the tagged feature path runs.
### Owner hand-off and source inventory
In the owner project, enable `Use VoxelForge Core` on the `AVoxelWorld` and set `Core Mesher Recipe`
to `World Assembler Hybrid V1`. Both switches remain opt-in; the default recipe is MC. To preview
the surface-strate placements, duplicate the settings asset, import
[`surface_hybrid_fixture_plan.json`](round3/surface_hybrid_fixture_plan.json) through
`SourceWorldAssemblyManifestJson` / `ImportWorldAssemblyManifestJson`, then visit the corner,
doorway, riser, and ledge around the coordinates in the contact table. The plan carries nine stable
type-2 feature boxes and retains the sampled type-4 cliff. The owner should review the corner and
doorway as distance increases and cross the LOD threshold with skirts visible. No owner asset was
edited; all checks here used BuildHost with null RHI, so no visual capture was produced.
New source files this round are the mirrored private mesher headers
[`VoxelHybridSharpMesher.h`](../../../Source/VoxelForge/Private/VoxelHybridSharpMesher.h) and
[`VoxelHybridSharpMesher.h`](../../Source/VoxelForge/Private/VoxelHybridSharpMesher.h). No new
`.cpp` or generated header was added. Likely compile touchpoints are `VoxelWorld.h/.cpp`,
`VoxelTypes.h`, `VoxelMarchingCubesMesher.h/.cpp`, both hybrid mesher headers,
`Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`,
`Standalone/Core/src/VoxelForgeCore.cpp`, `VoxelForgeExploreCommandlet.cpp`, and
`VoxelForgeEditor.Build.cs`.
### Round-3 failed-run ledger
Times below are BuildHost log stamps. Repeated attempts are listed individually; each was null-RHI
BuildHost work. The final passing commandlet and focused checks are described above.
| Run(s) | Result and correction |
|---|---|
| 02:32:40, 02:45:03 | The exact-output check failed for the tagged corner (`stable=1`, `expected_mc_identity=0`). Corrected the fixture assertion; the final five-fixture repeat check passes. |
| 03:00:55, 03:04:08 | The doorway probe compared the full coarse span with the fine span and reported 125/125 over-budget cases, worst 800.047 cm. Restricted comparison to their shared overlap; the final 125-phase probe is below budget. |
| 03:06:12, 03:24:05, 04:23:42 | Focused five-fixture checks passed. |
| 03:19:40 | Actor collision contact fixture failed before it recorded a corner contact. |
| 03:35:38, 03:52:53, 04:08:52, 04:21:48, 04:35:15, 04:51:17, 05:09:02, 05:22:12, 05:35:24, 05:52:51 | Actor checks exposed misses in the old pawn/visibility/closest-triangle diagnostic. Replaced that test with a direct RMC component trace plus cooked-body/render-bounds comparison. |
| 06:13:08 | Actor process hit the 900-second timeout at about 3.95 GiB and was stopped. No crash folder appeared. |
| 06:19:47 | Geometry audit included an injected synthetic fixture tile and rejected its 51.570 cm edge. Excluded injected fixture tiles from the natural-terrain scale audit. |
| 06:32:52 | Pawn/capsule hits worked, but the riser expected point was 100 cm from the actual contact (corner error 20.332 cm). Corrected the fixture anchor. |
| 06:48:33 | Pawn contacts worked, but the standalone closest-ray helper reported no visual hits; riser anchor was still offset. Replaced the unreliable helper and corrected the anchor again. |
| 07:03:11 | Double-precision triangle-ray helper still reported no visual hits; the riser target at x=1,250 cm missed the actual x=1,350 cm contact. Replaced this check with component tracing and render/cooked bounds. |
| 07:05–07:17 | A duplicate wrapper launch was refused because BuildHost PID 5616 was already active; its command line was verified as the requested null-RHI test and monitored. That actor run failed on the stale visual-ray helper and riser anchor. |
| Before final UBT pass | One compile failed because commandlet checks referenced `ReferenceWorld`, `TravelOptions`, and `CoreError` after their scope. Moved the check into scope; the next BuildHost UBT and link audit passed. |
| 07:22–07:33 | Full actor commandlet completed all assertions with `pass complete=1` and zero engine errors, but the wrapper returned 1 on its final `[int]$EnableHybridFixturePlacement` formatting cast. Fixed the status print to use the existing numeric flag; focused fixture and cliff wrappers then exited 0. |
| 07:40–07:51 | Reran the full actor path after the wrapper fix. Commandlet and wrapper both passed in 11m06s; peak sampled private memory was 4,007,432,192 bytes (3.73 GiB), and the engine reported zero errors. |
No new UECC folder, process crash, or `0xC0000142` retry occurred in round 3. No owner project/editor was
opened or built; no `.uasset` was edited; no commit, push, or stash was made.
@@ -0,0 +1 @@
{"schema_version":1,"content_hash":"b3d0e439a6dc2b3d","recipe":{"schema_version":1,"evaluator_version":4,"units_per_voxel":256,"seed":0,"biome_cell_size_q":204800,"biome_warp_q":64000,"biome_warp_wavelength_q":142222,"biome_border_blend_q":30720,"climate_relief_wavelength_q":853333,"climate_moisture_wavelength_q":640000,"climate_relief_contrast_q16":131072,"content_hash":"c37de78b759c0a4c","biomes":[{"id":4097,"base_height_q":-516096,"continent_amplitude_q":128000,"continent_wavelength_q":256000,"mountain_amplitude_q":0,"mountain_wavelength_q":128000,"detail_amplitude_q":512,"detail_wavelength_q":6400,"relief_min_q16":0,"relief_max_q16":19661,"moisture_min_q16":0,"moisture_max_q16":19661,"relief_strength_q16":6554,"relief_contrast_q16":104858,"relief_wavelength_q":2560000,"height_warp_strength_q":12288,"height_warp_wavelength_q":32000,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":256000,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4098,"base_height_q":-484639,"continent_amplitude_q":128000,"continent_wavelength_q":256000,"mountain_amplitude_q":128000,"mountain_wavelength_q":512000,"detail_amplitude_q":512,"detail_wavelength_q":25600,"relief_min_q16":52429,"relief_max_q16":65536,"moisture_min_q16":52429,"moisture_max_q16":65536,"relief_strength_q16":45875,"relief_contrast_q16":104858,"relief_wavelength_q":170667,"height_warp_strength_q":12288,"height_warp_wavelength_q":25600,"beach_width_q":3072,"cliff_strength_q16":65536,"cliff_slope_threshold_q16":19661,"cliff_sharpness_q16":327680,"cliff_sample_distance_q":5120,"cliff_max_displacement_q":2570240,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":230400,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4099,"base_height_q":-516096,"continent_amplitude_q":20480,"continent_wavelength_q":85333,"mountain_amplitude_q":0,"mountain_wavelength_q":128000,"detail_amplitude_q":512,"detail_wavelength_q":6400,"relief_min_q16":6554,"relief_max_q16":26214,"moisture_min_q16":32768,"moisture_max_q16":52429,"relief_strength_q16":0,"relief_contrast_q16":16384,"relief_wavelength_q":170752,"height_warp_strength_q":16640,"height_warp_wavelength_q":32000,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":256000,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4100,"base_height_q":-385024,"continent_amplitude_q":20480,"continent_wavelength_q":256000,"mountain_amplitude_q":20480,"mountain_wavelength_q":512000,"detail_amplitude_q":512,"detail_wavelength_q":25600,"relief_min_q16":64881,"relief_max_q16":65536,"moisture_min_q16":52429,"moisture_max_q16":65536,"relief_strength_q16":45875,"relief_contrast_q16":104858,"relief_wavelength_q":170667,"height_warp_strength_q":12288,"height_warp_wavelength_q":25600,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":230400,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800}]},"plan":{"schema_version":1,"content_hash":"1f2b139a595f8a05","strates":[{"strate_index":1,"top_chunk_z":-16,"bottom_chunk_z":-79,"cap_height_q":-149094,"cap_undulation_q":0,"cap_wavelength_q":64000,"cap_detail_amplitude_q":1536,"cap_detail_wavelength_q":6400,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800,"water_surface_q":-505610,"water_enabled":true,"content_hash":"6f5842ebaa29ea85","features":[{"stable_id":"09812c26310b90c9","type":4,"min_q":[-183296,96256,-512629],"max_q":[-160768,118784,-412102],"parameters_q":[0,256,16384,18432]},{"stable_id":"4d32460000000001","type":2,"min_q":[8704,41984,-515867],"max_q":[11008,43008,-513307],"parameters_q":[0,0,0,0]},{"stable_id":"4d32460000000002","type":2,"min_q":[9984,43008,-515867],"max_q":[11008,44288,-513307],"parameters_q":[0,0,0,0]},{"stable_id":"4d32460000000003","type":2,"min_q":[12032,41984,-515774],"max_q":[13056,43008,-512702],"parameters_q":[0,0,0,0]},{"stable_id":"4d32460000000004","type":2,"min_q":[14592,41984,-515774],"max_q":[15616,43008,-512702],"parameterLine truncated
@@ -0,0 +1,603 @@
#pragma once
#include "MarchingCubesTables.h"
#include "VoxelMarchingCubesMesher.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <functional>
#include <numeric>
#include <tuple>
#include <utility>
#include <vector>
// Feature-aware dual-contouring support for the opt-in Core recipe.
// Support du dual contouring sensible aux fonctions pour la recette Core optionnelle.
namespace VoxelHybridSharpMesher
{
constexpr int32 EdgeCount = 12;
constexpr int32 CornerCount = 8;
constexpr double SubvoxelsPerVoxel = 256.0;
constexpr int32 CornerOffsets[CornerCount][3] = {
{0,0,0}, {1,0,0}, {1,1,0}, {0,1,0},
{0,0,1}, {1,0,1}, {1,1,1}, {0,1,1}
};
constexpr int32 EdgeCorners[EdgeCount][2] = {
{0,1}, {1,2}, {2,3}, {3,0}, {4,5}, {5,6},
{6,7}, {7,4}, {0,4}, {1,5}, {2,6}, {3,7}
};
inline double AxisValue(const FVector& V, int32 Axis)
{
return Axis == 0 ? V.X : (Axis == 1 ? V.Y : V.Z);
}
inline void SetAxisValue(FVector& V, int32 Axis, double Value)
{
if (Axis == 0) V.X = Value;
else if (Axis == 1) V.Y = Value;
else V.Z = Value;
}
struct FPlane
{
uint64 Id = 0;
FVector Normal = FVector::UpVector;
};
struct FRoot
{
bool bActive = false;
FVector Position = FVector::ZeroVector;
FVector Gradient = FVector::UpVector;
std::vector<FPlane> Planes;
uint64 FeatureId = 0;
};
struct FTriangle
{
FVector Positions[3]{};
FVector Normals[3]{};
uint64 SplitKeys[3]{};
};
struct FResult
{
std::vector<uint8> CoreCellDCEdges;
std::vector<FTriangle> Triangles;
uint32 SharpCells = 0;
uint32 DualCells = 0;
uint32 BorderFallbackPatches = 0;
bool UsesDC(int32 X, int32 Y, int32 Z, int32 Cells) const
{
if (X < 0 || Y < 0 || Z < 0 || X >= Cells || Y >= Cells || Z >= Cells)
return false;
const std::size_t Cell = (static_cast<std::size_t>(Z) * Cells + Y) * Cells + X;
const std::size_t Offset = Cell * EdgeCount;
for (int32 Edge = 0; Edge < EdgeCount && Offset + Edge < CoreCellDCEdges.size(); ++Edge)
if (CoreCellDCEdges[Offset + Edge] != 0) return true;
return false;
}
bool UsesDCEdge(int32 X, int32 Y, int32 Z, int32 Edge, int32 Cells) const
{
if (X < 0 || Y < 0 || Z < 0 || X >= Cells || Y >= Cells || Z >= Cells)
return false;
const std::size_t Cell = (static_cast<std::size_t>(Z) * Cells + Y) * Cells + X;
const std::size_t Offset = Cell * EdgeCount + static_cast<std::size_t>(Edge);
return Offset < CoreCellDCEdges.size() && (CoreCellDCEdges[Offset] & 1u) != 0;
}
};
struct FCell
{
uint8 Case = 0;
uint8 ComponentCount = 0;
std::array<uint8, EdgeCount> EdgeComponent{};
std::array<uint8, EdgeCount> ComponentSharp{};
std::array<uint8, EdgeCount> ComponentUseDC{};
std::array<FVector, EdgeCount> Dual{};
FCell() { EdgeComponent.fill(0xff); }
};
inline int32 FindLocal(std::array<uint8, EdgeCount>& Parent, int32 Value)
{
while (Parent[Value] != Value)
{
Parent[Value] = Parent[Parent[Value]];
Value = Parent[Value];
}
return Value;
}
inline void UniteLocal(std::array<uint8, EdgeCount>& Parent, int32 A, int32 B)
{
A = FindLocal(Parent, A);
B = FindLocal(Parent, B);
if (A == B) return;
if (A > B) std::swap(A, B);
Parent[B] = static_cast<uint8>(A);
}
inline int32 FindLocalEdge(int32 CellX, int32 CellY, int32 CellZ,
int32 Axis, int32 EdgeX, int32 EdgeY, int32 EdgeZ)
{
for (int32 Edge = 0; Edge < EdgeCount; ++Edge)
{
const int32 A = EdgeCorners[Edge][0];
const int32 B = EdgeCorners[Edge][1];
const int32 AX = CornerOffsets[A][0], AY = CornerOffsets[A][1], AZ = CornerOffsets[A][2];
const int32 BX = CornerOffsets[B][0], BY = CornerOffsets[B][1], BZ = CornerOffsets[B][2];
const int32 LocalAxis = AX != BX ? 0 : AY != BY ? 1 : 2;
if (LocalAxis == Axis
&& CellX + FMath::Min(AX, BX) == EdgeX
&& CellY + FMath::Min(AY, BY) == EdgeY
&& CellZ + FMath::Min(AZ, BZ) == EdgeZ)
return Edge;
}
return INDEX_NONE;
}
inline FVector SolveQEF(const std::vector<std::pair<FVector, FVector>>& Constraints,
const FVector& Fallback, const FVector& Low, const FVector& High)
{
if (Constraints.empty()) return Fallback;
double A[3][4]{};
constexpr double Lambda = 1.0e-10;
const double Center[3] = {Fallback.X, Fallback.Y, Fallback.Z};
for (int32 I = 0; I < 3; ++I) { A[I][I] = Lambda; A[I][3] = Lambda * Center[I]; }
for (const auto& Constraint : Constraints)
{
FVector N = Constraint.second.GetSafeNormal();
const double B = FVector::DotProduct(N, Constraint.first);
const double NV[3] = {N.X, N.Y, N.Z};
for (int32 R = 0; R < 3; ++R)
{
for (int32 C = 0; C < 3; ++C) A[R][C] += NV[R] * NV[C];
A[R][3] += NV[R] * B;
}
}
for (int32 Column = 0; Column < 3; ++Column)
{
int32 Pivot = Column;
for (int32 Row = Column + 1; Row < 3; ++Row)
if (FMath::Abs(A[Row][Column]) > FMath::Abs(A[Pivot][Column])) Pivot = Row;
if (FMath::Abs(A[Pivot][Column]) < 1.0e-14) return Fallback;
if (Pivot != Column)
for (int32 K = Column; K < 4; ++K) std::swap(A[Pivot][K], A[Column][K]);
const double Divisor = A[Column][Column];
for (int32 K = Column; K < 4; ++K) A[Column][K] /= Divisor;
for (int32 Row = 0; Row < 3; ++Row)
{
if (Row == Column) continue;
const double Factor = A[Row][Column];
for (int32 K = Column; K < 4; ++K) A[Row][K] -= Factor * A[Column][K];
}
}
return FVector(FMath::Clamp(A[0][3], Low.X, High.X),
FMath::Clamp(A[1][3], Low.Y, High.Y),
FMath::Clamp(A[2][3], Low.Z, High.Z));
}
inline double BoxSDF(const FVoxelHybridSharpFeatureBox& Box, const FVector& P,
std::vector<FPlane>& OutPlanes)
{
FVector Low, High;
for (int32 Axis = 0; Axis < 3; ++Axis)
{
SetAxisValue(Low, Axis,
static_cast<double>(Box.MinimumSubvoxels[Axis]) / SubvoxelsPerVoxel);
SetAxisValue(High, Axis,
static_cast<double>(Box.MaximumSubvoxels[Axis]) / SubvoxelsPerVoxel);
}
const FVector Center = (Low + High) * 0.5;
const FVector Half = (High - Low) * 0.5;
const FVector Delta = P - Center;
const FVector Q(FMath::Abs(Delta.X) - Half.X,
FMath::Abs(Delta.Y) - Half.Y,
FMath::Abs(Delta.Z) - Half.Z);
const double MaxQ = FMath::Max(Q.X, FMath::Max(Q.Y, Q.Z));
const FVector Outside(FMath::Max(Q.X, 0.0), FMath::Max(Q.Y, 0.0), FMath::Max(Q.Z, 0.0));
const double OutsideLength = Outside.Size();
const double SDF = OutsideLength + FMath::Min(MaxQ, 0.0);
const double Tolerance = 1.0e-4;
if (FMath::Abs(MaxQ) <= Tolerance)
{
for (int32 Axis = 0; Axis < 3; ++Axis)
{
if (FMath::Abs(AxisValue(Q, Axis) - MaxQ) > Tolerance) continue;
FVector Normal = FVector::ZeroVector;
const bool bNegativeFace = AxisValue(Delta, Axis) < 0.0;
SetAxisValue(Normal, Axis, bNegativeFace ? -1.0 : 1.0);
uint64 FaceId = Box.StableId ^ (0x9e3779b97f4a7c15ull
+ static_cast<uint64>(Axis * 2 + (bNegativeFace ? 0 : 1) + 1));
if (FaceId == 0) FaceId = static_cast<uint64>(Axis + 1);
OutPlanes.push_back({FaceId, Normal});
}
}
return SDF;
}
inline uint64 SelectFeature(const FRoot& Root, int32 Axis)
{
if (Root.Planes.empty()) return 0;
std::size_t Selected = 0;
const auto Alignment = [Axis](const FVector& Normal)
{ return FMath::Abs(AxisValue(Normal, Axis)); };
for (std::size_t I = 1; I < Root.Planes.size(); ++I)
{
const double Candidate = Alignment(Root.Planes[I].Normal);
const double Current = Alignment(Root.Planes[Selected].Normal);
if (Candidate > Current + 1.0e-12
|| (FMath::Abs(Candidate - Current) <= 1.0e-12
&& Root.Planes[I].Id < Root.Planes[Selected].Id)) Selected = I;
}
return Root.Planes[Selected].Id;
}
inline FVector NormalForFeature(const FRoot& Root, uint64 Feature)
{
for (const FPlane& Plane : Root.Planes) if (Plane.Id == Feature) return Plane.Normal;
return Root.Gradient;
}
inline FResult Generate(int32 Cells, int32 Step, float IsoLevel, float GeometryVoxelSize,
const FIntVector& OriginVoxels, const FVector& ChunkWorldPos,
const TArray<FVoxelHybridSharpFeatureBox>& Features,
const std::function<float(int32,int32,int32)>& Sample,
const std::function<FVector(int32,int32,int32)>& Gradient)
{
FResult Result;
if (Cells <= 0 || Features.IsEmpty()) return Result;
const int32 CellDim = Cells + 2;
const int32 NodeDim = Cells + 3;
const std::size_t CellCount = static_cast<std::size_t>(CellDim) * CellDim * CellDim;
std::vector<FCell> CellData(CellCount);
const auto CellIndex = [CellDim](int32 X, int32 Y, int32 Z)
{ return (static_cast<std::size_t>(Z + 1) * CellDim + (Y + 1)) * CellDim + (X + 1); };
const auto NodePosition = [&](int32 X, int32 Y, int32 Z)
{ return FVector(X * Step * GeometryVoxelSize, Y * Step * GeometryVoxelSize,
Z * Step * GeometryVoxelSize) + ChunkWorldPos; };
std::array<std::vector<FRoot>, 3> Roots;
Roots[0].resize(static_cast<std::size_t>(CellDim) * NodeDim * NodeDim);
Roots[1].resize(static_cast<std::size_t>(NodeDim) * CellDim * NodeDim);
Roots[2].resize(static_cast<std::size_t>(NodeDim) * NodeDim * CellDim);
const auto RootIndex = [CellDim, NodeDim](int32 Axis, int32 X, int32 Y, int32 Z)
{
const std::size_t NX = static_cast<std::size_t>(X + 1);
const std::size_t NY = static_cast<std::size_t>(Y + 1);
const std::size_t NZ = static_cast<std::size_t>(Z + 1);
if (Axis == 0) return (NZ * NodeDim + NY) * CellDim + NX;
if (Axis == 1) return (NZ * CellDim + NY) * NodeDim + NX;
return (NZ * NodeDim + NY) * NodeDim + NX;
};
for (int32 Z = -1; Z <= Cells + 1; ++Z)
for (int32 Y = -1; Y <= Cells + 1; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
const float D0 = Sample(X, Y, Z), D1 = Sample(X + 1, Y, Z);
if ((D0 < IsoLevel) == (D1 < IsoLevel)) continue;
const double T = FMath::Clamp(static_cast<double>(IsoLevel - D0) / (D1 - D0), 0.0, 1.0);
FRoot& Root = Roots[0][RootIndex(0, X, Y, Z)];
Root.bActive = true;
Root.Position = FMath::Lerp(NodePosition(X, Y, Z), NodePosition(X + 1, Y, Z), T);
Root.Gradient = FMath::Lerp(Gradient(X, Y, Z), Gradient(X + 1, Y, Z), T).GetSafeNormal();
}
for (int32 Z = -1; Z <= Cells + 1; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells + 1; ++X)
{
const float D0 = Sample(X, Y, Z), D1 = Sample(X, Y + 1, Z);
if ((D0 < IsoLevel) == (D1 < IsoLevel)) continue;
const double T = FMath::Clamp(static_cast<double>(IsoLevel - D0) / (D1 - D0), 0.0, 1.0);
FRoot& Root = Roots[1][RootIndex(1, X, Y, Z)];
Root.bActive = true;
Root.Position = FMath::Lerp(NodePosition(X, Y, Z), NodePosition(X, Y + 1, Z), T);
Root.Gradient = FMath::Lerp(Gradient(X, Y, Z), Gradient(X, Y + 1, Z), T).GetSafeNormal();
}
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells + 1; ++Y)
for (int32 X = -1; X <= Cells + 1; ++X)
{
const float D0 = Sample(X, Y, Z), D1 = Sample(X, Y, Z + 1);
if ((D0 < IsoLevel) == (D1 < IsoLevel)) continue;
const double T = FMath::Clamp(static_cast<double>(IsoLevel - D0) / (D1 - D0), 0.0, 1.0);
FRoot& Root = Roots[2][RootIndex(2, X, Y, Z)];
Root.bActive = true;
Root.Position = FMath::Lerp(NodePosition(X, Y, Z), NodePosition(X, Y, Z + 1), T);
Root.Gradient = FMath::Lerp(Gradient(X, Y, Z), Gradient(X, Y, Z + 1), T).GetSafeNormal();
}
const auto GetRoot = [&](int32 Axis, int32 X, int32 Y, int32 Z) -> FRoot&
{ return Roots[Axis][RootIndex(Axis, X, Y, Z)]; };
const auto GetCellEdgeRoot = [&](int32 X, int32 Y, int32 Z, int32 Edge) -> FRoot&
{
const int32 A = EdgeCorners[Edge][0], B = EdgeCorners[Edge][1];
const int32 AX = CornerOffsets[A][0], AY = CornerOffsets[A][1], AZ = CornerOffsets[A][2];
const int32 BX = CornerOffsets[B][0], BY = CornerOffsets[B][1], BZ = CornerOffsets[B][2];
const int32 Axis = AX != BX ? 0 : AY != BY ? 1 : 2;
return GetRoot(Axis, X + FMath::Min(AX, BX), Y + FMath::Min(AY, BY), Z + FMath::Min(AZ, BZ));
};
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
FCell& Cell = CellData[CellIndex(X, Y, Z)];
float Densities[CornerCount]{};
for (int32 Corner = 0; Corner < CornerCount; ++Corner)
{
const int32 GX = X + CornerOffsets[Corner][0];
const int32 GY = Y + CornerOffsets[Corner][1];
const int32 GZ = Z + CornerOffsets[Corner][2];
Densities[Corner] = Sample(GX, GY, GZ);
if (Densities[Corner] >= IsoLevel) Cell.Case |= static_cast<uint8>(1u << Corner);
}
if (Cell.Case == 0 || Cell.Case == 255 || MCEdgeTable[Cell.Case] == 0) continue;
std::array<uint8, EdgeCount> Parent{};
for (int32 Edge = 0; Edge < EdgeCount; ++Edge) Parent[Edge] = static_cast<uint8>(Edge);
for (int32 I = 0; I < 16 && MCTriTable[Cell.Case][I] >= 0; I += 3)
{
const int32 A = MCTriTable[Cell.Case][I];
const int32 B = MCTriTable[Cell.Case][I + 1];
const int32 C = MCTriTable[Cell.Case][I + 2];
UniteLocal(Parent, A, B); UniteLocal(Parent, B, C);
}
std::array<uint8, EdgeCount> RootToComponent{};
RootToComponent.fill(0xff);
for (int32 Edge = 0; Edge < EdgeCount; ++Edge)
{
const int32 A = EdgeCorners[Edge][0], B = EdgeCorners[Edge][1];
if ((Densities[A] < IsoLevel) == (Densities[B] < IsoLevel)) continue;
FRoot& Root = GetCellEdgeRoot(X, Y, Z, Edge);
if (!Root.bActive) continue;
const int32 LocalRoot = FindLocal(Parent, Edge);
if (RootToComponent[LocalRoot] == 0xff)
RootToComponent[LocalRoot] = Cell.ComponentCount++;
const uint8 Component = RootToComponent[LocalRoot];
Cell.EdgeComponent[Edge] = Component;
const int32 OA = EdgeCorners[Edge][0];
const int32 OB = EdgeCorners[Edge][1];
const int32 AX = X + CornerOffsets[OA][0], AY = Y + CornerOffsets[OA][1], AZ = Z + CornerOffsets[OA][2];
const int32 BX = X + CornerOffsets[OB][0], BY = Y + CornerOffsets[OB][1], BZ = Z + CornerOffsets[OB][2];
const FVector RootVoxel = FVector(OriginVoxels.X, OriginVoxels.Y, OriginVoxels.Z)
+ FVector(AX, AY, AZ) * Step
+ FVector(BX - AX, BY - AY, BZ - AZ) * (Step *
FMath::Clamp(static_cast<double>(IsoLevel - Densities[OA])
/ (Densities[OB] - Densities[OA]), 0.0, 1.0));
for (const FVoxelHybridSharpFeatureBox& Feature : Features)
{
std::vector<FPlane> CandidatePlanes;
const double SDF = BoxSDF(Feature, RootVoxel, CandidatePlanes);
if (FMath::Abs(SDF) > 1.0e-4 || CandidatePlanes.empty()) continue;
for (const FPlane& Plane : CandidatePlanes) Root.Planes.push_back(Plane);
}
if (!Root.Planes.empty())
{
std::sort(Root.Planes.begin(), Root.Planes.end(),
[](const FPlane& A, const FPlane& B) { return A.Id < B.Id; });
Root.Planes.erase(std::unique(Root.Planes.begin(), Root.Planes.end(),
[](const FPlane& A, const FPlane& B) { return A.Id == B.Id; }), Root.Planes.end());
Cell.ComponentSharp[Component] = 1;
if (X >= 0 && Y >= 0 && Z >= 0 && X < Cells && Y < Cells && Z < Cells)
++Result.SharpCells;
}
}
}
const std::size_t PatchCount = CellCount * EdgeCount;
std::vector<uint32> PatchParent(PatchCount);
std::vector<uint8> PatchRank(PatchCount, 0), PatchSharp(PatchCount, 0), PatchTouchesBorder(PatchCount, 0);
std::iota(PatchParent.begin(), PatchParent.end(), 0u);
const auto FindPatch = [&](uint32 Value, auto&& Self) -> uint32
{
if (PatchParent[Value] != Value) PatchParent[Value] = Self(PatchParent[Value], Self);
return PatchParent[Value];
};
const auto UnitePatch = [&](uint32 A, uint32 B)
{
uint32 RA = FindPatch(A, FindPatch), RB = FindPatch(B, FindPatch);
if (RA == RB) return;
if (PatchRank[RA] < PatchRank[RB]) std::swap(RA, RB);
PatchParent[RB] = RA;
PatchSharp[RA] = static_cast<uint8>(PatchSharp[RA] | PatchSharp[RB]);
PatchTouchesBorder[RA] = static_cast<uint8>(PatchTouchesBorder[RA] | PatchTouchesBorder[RB]);
if (PatchRank[RA] == PatchRank[RB]) ++PatchRank[RA];
};
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
const FCell& Cell = CellData[CellIndex(X, Y, Z)];
for (uint8 Component = 0; Component < Cell.ComponentCount; ++Component)
{
const uint32 Id = static_cast<uint32>(CellIndex(X, Y, Z) * EdgeCount + Component);
PatchSharp[Id] = Cell.ComponentSharp[Component];
const bool bCoreBorder = X >= 0 && Y >= 0 && Z >= 0 && X < Cells && Y < Cells && Z < Cells
&& (X == 0 || Y == 0 || Z == 0 || X == Cells - 1 || Y == Cells - 1 || Z == Cells - 1);
PatchTouchesBorder[Id] = bCoreBorder ? 1 : 0;
}
}
for (int32 Axis = 0; Axis < 3; ++Axis)
for (int32 Z = -1; Z <= Cells + (Axis == 2 ? 0 : 1); ++Z)
for (int32 Y = -1; Y <= Cells + (Axis == 1 ? 0 : 1); ++Y)
for (int32 X = -1; X <= Cells + (Axis == 0 ? 0 : 1); ++X)
{
if (!GetRoot(Axis, X, Y, Z).bActive) continue;
std::array<std::array<int32, 3>, 4> Around{};
if (Axis == 0) Around = {{{X,Y-1,Z-1},{X,Y,Z-1},{X,Y,Z},{X,Y-1,Z}}};
else if (Axis == 1) Around = {{{X-1,Y,Z-1},{X-1,Y,Z},{X,Y,Z},{X,Y,Z-1}}};
else Around = {{{X-1,Y-1,Z},{X,Y-1,Z},{X,Y,Z},{X-1,Y,Z}}};
std::array<uint32, 4> Ids{};
bool bValid = true;
for (int32 I = 0; I < 4; ++I)
{
const int32 CX = Around[I][0], CY = Around[I][1], CZ = Around[I][2];
if (CX < -1 || CY < -1 || CZ < -1 || CX > Cells || CY > Cells || CZ > Cells)
{ bValid = false; break; }
const int32 LocalEdge = FindLocalEdge(CX, CY, CZ, Axis, X, Y, Z);
const FCell& Cell = CellData[CellIndex(CX, CY, CZ)];
if (LocalEdge == INDEX_NONE || Cell.EdgeComponent[LocalEdge] == 0xff)
{ bValid = false; break; }
Ids[I] = static_cast<uint32>(CellIndex(CX, CY, CZ) * EdgeCount + Cell.EdgeComponent[LocalEdge]);
}
if (bValid) for (int32 I = 1; I < 4; ++I) UnitePatch(Ids[0], Ids[I]);
}
bool bSawBorderPatch = false;
std::vector<uint8> PatchUseDC(PatchCount, 0);
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
const FCell& Cell = CellData[CellIndex(X, Y, Z)];
for (uint8 Component = 0; Component < Cell.ComponentCount; ++Component)
{
const uint32 Id = static_cast<uint32>(CellIndex(X, Y, Z) * EdgeCount + Component);
const uint32 Root = FindPatch(Id, FindPatch);
PatchUseDC[Root] = static_cast<uint8>(PatchSharp[Root] && !PatchTouchesBorder[Root]);
if (PatchSharp[Root] && PatchTouchesBorder[Root]) bSawBorderPatch = true;
}
}
Result.BorderFallbackPatches = bSawBorderPatch ? 1u : 0u;
for (int32 Z = -1; Z <= Cells; ++Z)
for (int32 Y = -1; Y <= Cells; ++Y)
for (int32 X = -1; X <= Cells; ++X)
{
FCell& Cell = CellData[CellIndex(X, Y, Z)];
if (Cell.ComponentCount == 0) continue;
for (uint8 Component = 0; Component < Cell.ComponentCount; ++Component)
{
const uint32 Id = static_cast<uint32>(CellIndex(X, Y, Z) * EdgeCount + Component);
Cell.ComponentUseDC[Component] = PatchUseDC[FindPatch(Id, FindPatch)];
if (!Cell.ComponentUseDC[Component]) continue;
std::vector<std::pair<FVector, FVector>> Constraints;
FVector RootSum = FVector::ZeroVector;
int32 RootCount = 0;
for (int32 Edge = 0; Edge < EdgeCount; ++Edge)
{
if (Cell.EdgeComponent[Edge] != Component) continue;
FRoot& Root = GetCellEdgeRoot(X, Y, Z, Edge);
if (!Root.bActive) continue;
RootSum += Root.Position;
++RootCount;
if (!Root.Planes.empty())
for (const FPlane& Plane : Root.Planes) Constraints.emplace_back(Root.Position, Plane.Normal);
else Constraints.emplace_back(Root.Position, Root.Gradient);
}
const FVector Low = NodePosition(X, Y, Z);
const FVector High = NodePosition(X + 1, Y + 1, Z + 1);
const FVector Center = RootCount > 0 ? RootSum / RootCount : (Low + High) * 0.5;
Cell.Dual[Component] = SolveQEF(Constraints, Center, Low, High);
if (X >= 0 && Y >= 0 && Z >= 0 && X < Cells && Y < Cells && Z < Cells)
++Result.DualCells;
}
}
Result.CoreCellDCEdges.assign(static_cast<std::size_t>(Cells) * Cells * Cells * EdgeCount, 0);
const auto AddTriangle = [&](const FVector& A, const FVector& B, const FVector& C,
const FVector& NA, const FVector& NB, const FVector& NC,
uint64 FA, uint64 FB, uint64 FC)
{
if (FVector::CrossProduct(B - A, C - A).SizeSquared() < 1.0e-12) return;
FTriangle T;
T.Positions[0] = A; T.Positions[1] = B; T.Positions[2] = C;
T.Normals[0] = NA.GetSafeNormal(); T.Normals[1] = NB.GetSafeNormal(); T.Normals[2] = NC.GetSafeNormal();
T.SplitKeys[0] = FA; T.SplitKeys[1] = FB; T.SplitKeys[2] = FC;
Result.Triangles.push_back(T);
};
for (int32 Axis = 0; Axis < 3; ++Axis)
for (int32 Z = -1; Z <= Cells + (Axis == 2 ? 0 : 1); ++Z)
for (int32 Y = -1; Y <= Cells + (Axis == 1 ? 0 : 1); ++Y)
for (int32 X = -1; X <= Cells + (Axis == 0 ? 0 : 1); ++X)
{
FRoot& Root = GetRoot(Axis, X, Y, Z);
if (!Root.bActive) continue;
std::array<std::array<int32, 3>, 4> Around{};
if (Axis == 0) Around = {{{X,Y-1,Z-1},{X,Y,Z-1},{X,Y,Z},{X,Y-1,Z}}};
else if (Axis == 1) Around = {{{X-1,Y,Z-1},{X-1,Y,Z},{X,Y,Z},{X,Y,Z-1}}};
else Around = {{{X-1,Y-1,Z},{X,Y-1,Z},{X,Y,Z},{X-1,Y,Z}}};
std::array<const FCell*, 4> Incident{};
std::array<uint8, 4> Components{};
bool bAllDC = true;
bool bValid = true;
for (int32 I = 0; I < 4; ++I)
{
const int32 CX = Around[I][0], CY = Around[I][1], CZ = Around[I][2];
if (CX < -1 || CY < -1 || CZ < -1 || CX > Cells || CY > Cells || CZ > Cells)
{ bValid = false; break; }
const int32 LocalEdge = FindLocalEdge(CX, CY, CZ, Axis, X, Y, Z);
const FCell& Cell = CellData[CellIndex(CX, CY, CZ)];
if (LocalEdge == INDEX_NONE || Cell.EdgeComponent[LocalEdge] == 0xff)
{ bValid = false; break; }
const uint8 Component = Cell.EdgeComponent[LocalEdge];
Incident[I] = &Cell;
Components[I] = Component;
bAllDC &= Cell.ComponentUseDC[Component] != 0;
}
if (!bValid || !bAllDC) continue;
const int32 OwnerX = FMath::Min(FMath::Min(Around[0][0], Around[1][0]), FMath::Min(Around[2][0], Around[3][0]));
const int32 OwnerY = FMath::Min(FMath::Min(Around[0][1], Around[1][1]), FMath::Min(Around[2][1], Around[3][1]));
const int32 OwnerZ = FMath::Min(FMath::Min(Around[0][2], Around[1][2]), FMath::Min(Around[2][2], Around[3][2]));
if (OwnerX < 0 || OwnerY < 0 || OwnerZ < 0 || OwnerX >= Cells || OwnerY >= Cells || OwnerZ >= Cells)
continue;
std::array<FVector, 4> Positions{};
std::array<FVector, 4> Normals{};
std::array<uint64, 4> SplitKeys{};
for (int32 I = 0; I < 4; ++I)
{
const int32 CX = Around[I][0], CY = Around[I][1], CZ = Around[I][2];
const uint8 Component = Components[I];
Positions[I] = Incident[I]->Dual[Component];
SplitKeys[I] = SelectFeature(Root, Axis);
Normals[I] = SplitKeys[I] != 0 ? NormalForFeature(Root, SplitKeys[I]) : Root.Gradient;
}
if (FVector::DotProduct(FVector::CrossProduct(Positions[1] - Positions[0],
Positions[2] - Positions[0]), Root.Gradient) < 0.0)
{
std::swap(Positions[1], Positions[3]);
std::swap(Normals[1], Normals[3]);
std::swap(SplitKeys[1], SplitKeys[3]);
}
const int32 Tie = (X + Y + Z + Axis) & 1;
if (Tie == 0)
{
AddTriangle(Positions[0], Positions[1], Positions[2], Normals[0], Normals[1], Normals[2], SplitKeys[0], SplitKeys[1], SplitKeys[2]);
AddTriangle(Positions[0], Positions[2], Positions[3], Normals[0], Normals[2], Normals[3], SplitKeys[0], SplitKeys[2], SplitKeys[3]);
}
else
{
AddTriangle(Positions[0], Positions[1], Positions[3], Normals[0], Normals[1], Normals[3], SplitKeys[0], SplitKeys[1], SplitKeys[3]);
AddTriangle(Positions[1], Positions[2], Positions[3], Normals[1], Normals[2], Normals[3], SplitKeys[1], SplitKeys[2], SplitKeys[3]);
}
}
for (int32 Z = 0; Z < Cells; ++Z)
for (int32 Y = 0; Y < Cells; ++Y)
for (int32 X = 0; X < Cells; ++X)
{
const FCell& Cell = CellData[CellIndex(X, Y, Z)];
if (!Cell.ComponentCount) continue;
const std::size_t CoreCell = (static_cast<std::size_t>(Z) * Cells + Y) * Cells + X;
for (int32 Edge = 0; Edge < EdgeCount; ++Edge)
{
const uint8 Component = Cell.EdgeComponent[Edge];
if (Component != 0xff && Cell.ComponentUseDC[Component])
Result.CoreCellDCEdges[CoreCell * EdgeCount + Edge] = 1;
}
}
return Result;
}
}
@@ -2,6 +2,7 @@
// Implémentation du marching cubes density-only. // Implémentation du marching cubes density-only.
#include "VoxelMarchingCubesMesher.h" #include "VoxelMarchingCubesMesher.h"
#include "VoxelHybridSharpMesher.h"
#include "MarchingCubesTables.h" #include "MarchingCubesTables.h"
#include "StandaloneShapeWorld.h" #include "StandaloneShapeWorld.h"
#include "VoxelDensityProfile.h" #include "VoxelDensityProfile.h"
@@ -164,6 +165,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// plus de (ré)allocation de hash-map par tuile (chaque worker a son propre exemplaire). // plus de (ré)allocation de hash-map par tuile (chaque worker a son propre exemplaire).
static thread_local TMap<FIntVector, int32> VertexMap; static thread_local TMap<FIntVector, int32> VertexMap;
VertexMap.Reset(); VertexMap.Reset();
static thread_local TMap<FIntVector, TMap<uint64, int32>> SplitNormalVertexMap;
SplitNormalVertexMap.Reset();
const int32 VertexCapacityHint=FMath::Clamp(InCellsPerAxis,2,CHUNK_SIZE); const int32 VertexCapacityHint=FMath::Clamp(InCellsPerAxis,2,CHUNK_SIZE);
VertexMap.Reserve(VertexCapacityHint*VertexCapacityHint*2); VertexMap.Reserve(VertexCapacityHint*VertexCapacityHint*2);
@@ -230,7 +233,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
}; };
// Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b). // Normale fournie par l'appelant (gradient lu dans la grille de densité, T1.b).
auto GetOrCreateVertex = [&](const FVector& WorldPos, const FVector& RawNormal) -> int32 auto GetOrCreateVertex = [&](const FVector& WorldPos, const FVector& RawNormal,
uint64 NormalSplitKey = 0) -> int32
{ {
const FIntVector Key( const FIntVector Key(
FMath::RoundToInt(WorldPos.X * 100.0f), FMath::RoundToInt(WorldPos.X * 100.0f),
@@ -238,13 +242,27 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
FMath::RoundToInt(WorldPos.Z * 100.0f) FMath::RoundToInt(WorldPos.Z * 100.0f)
); );
if (int32* Existing = VertexMap.Find(Key)) if (NormalSplitKey != 0)
{
if (TMap<uint64, int32>* SplitMap = SplitNormalVertexMap.Find(Key))
{
if (int32* Existing = SplitMap->Find(NormalSplitKey)) return *Existing;
}
}
else if (int32* Existing = VertexMap.Find(Key))
{ {
return *Existing; return *Existing;
} }
const int32 NewIndex = MeshData.Vertices.Num(); const int32 NewIndex = MeshData.Vertices.Num();
if (NormalSplitKey != 0)
{
SplitNormalVertexMap.FindOrAdd(Key).Add(NormalSplitKey, NewIndex);
}
else
{
VertexMap.Add(Key, NewIndex); VertexMap.Add(Key, NewIndex);
}
MeshData.Vertices.Add(WorldPos); MeshData.Vertices.Add(WorldPos);
@@ -1349,6 +1367,20 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
SampleGradientG(gx, gy, gz + 1) - SampleGradientG(gx, gy, gz - 1)); SampleGradientG(gx, gy, gz + 1) - SampleGradientG(gx, gy, gz - 1));
}; };
// DC is limited to tagged LOD0 patches. Coarse tiles and a connected patch that reaches a
// tile edge use MC there, preserving the existing MC-to-MC clipmap transition and skirts.
// Le DC reste limite aux surfaces LOD0 balisees; les patches connectes au bord restent MC.
VoxelHybridSharpMesher::FResult HybridResult;
if (bUseHybridSharpMesher && Step == 1 && CellsPerAxis == CHUNK_SIZE
&& CzLo == 0 && CzHi == CellsPerAxis - 1 && !HybridSharpFeatures.IsEmpty())
{
HybridResult = VoxelHybridSharpMesher::Generate(
CellsPerAxis, Step, IsoLevel, GeometryVoxelSize, OriginVoxels, ChunkWorldPos,
HybridSharpFeatures,
[&](int32 X, int32 Y, int32 Z) { return SampleG(X, Y, Z); },
[&](int32 X, int32 Y, int32 Z) { return GradAt(X, Y, Z); });
}
//========================================================================= //=========================================================================
// ITÉRATION SUR LES CELLULES // ITÉRATION SUR LES CELLULES
//========================================================================= //=========================================================================
@@ -1564,6 +1596,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 E0 = MCTriTable[CaseIndex][i]; const int32 E0 = MCTriTable[CaseIndex][i];
const int32 E1 = MCTriTable[CaseIndex][i + 1]; const int32 E1 = MCTriTable[CaseIndex][i + 1];
const int32 E2 = MCTriTable[CaseIndex][i + 2]; const int32 E2 = MCTriTable[CaseIndex][i + 2];
if (HybridResult.UsesDCEdge(cx, cy, cz, E0, CellsPerAxis))
{
continue;
}
const int32 Idx0 = GetOrCreateVertex(EdgeVertices[E0], EdgeNormals[E0]); const int32 Idx0 = GetOrCreateVertex(EdgeVertices[E0], EdgeNormals[E0]);
const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]); const int32 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]); const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
@@ -1591,6 +1627,26 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
} }
} }
// Add DC geometry to the same material runs as MC. Analytic feature IDs split corner normals.
// Ajouter la geometrie DC dans les memes groupes que MC; les IDs separent les normales nettes.
for (const VoxelHybridSharpMesher::FTriangle& Triangle : HybridResult.Triangles)
{
const int32 Idx0 = GetOrCreateVertex(
Triangle.Positions[0], Triangle.Normals[0], Triangle.SplitKeys[0]);
const int32 Idx1 = GetOrCreateVertex(
Triangle.Positions[1], Triangle.Normals[1], Triangle.SplitKeys[1]);
const int32 Idx2 = GetOrCreateVertex(
Triangle.Positions[2], Triangle.Normals[2], Triangle.SplitKeys[2]);
const int32 CapVotes = bStandaloneSkipMaterialGroups ? 0
: static_cast<int32>(VertexClasses[Idx0])
+ static_cast<int32>(VertexClasses[Idx1])
+ static_cast<int32>(VertexClasses[Idx2]);
TArray<int32>& Dst = (CapVotes >= 2) ? CapTris : GroundTris;
Dst.Add(Idx0);
Dst.Add(Idx2);
Dst.Add(Idx1);
}
if (bProfileMesher) if (bProfileMesher)
{ {
VoxelDensityProfile::AddMeasurement( VoxelDensityProfile::AddMeasurement(
@@ -41,6 +41,15 @@ struct FVoxelSharedDensityGrid
} }
}; };
// Stable plan data copied onto one worker-owned mesher for hybrid LOD0 generation.
// Donnees stables du plan copiees dans un mailleur de worker pour le mode hybride LOD0.
struct FVoxelHybridSharpFeatureBox
{
uint64 StableId = 0;
int64 MinimumSubvoxels[3] = {};
int64 MaximumSubvoxels[3] = {};
};
UCLASS(BlueprintType) UCLASS(BlueprintType)
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
{ {
@@ -111,6 +120,14 @@ public:
// set this pointer and continue through Generator. // set this pointer and continue through Generator.
void SetStandaloneShapeWorld(const FStandaloneShapeWorld* InWorld) { ShapeWorldContext = InWorld; } void SetStandaloneShapeWorld(const FStandaloneShapeWorld* InWorld) { ShapeWorldContext = InWorld; }
// The caller owns this mesher instance for one tile task; no shared mutable feature state.
// L'appelant possede cette instance pour une tuile; aucune donnee partagee modifiable.
void SetHybridSharpFeatures(TArray<FVoxelHybridSharpFeatureBox>&& InFeatures)
{
HybridSharpFeatures = MoveTemp(InFeatures);
bUseHybridSharpMesher = true;
}
private: private:
FORCEINLINE bool ShouldAbortWork() const FORCEINLINE bool ShouldAbortWork() const
{ {
@@ -120,6 +137,8 @@ private:
const std::atomic<bool>* ShutdownFlag = nullptr; const std::atomic<bool>* ShutdownFlag = nullptr;
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr; const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
const FStandaloneShapeWorld* ShapeWorldContext = nullptr; const FStandaloneShapeWorld* ShapeWorldContext = nullptr;
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
bool bUseHybridSharpMesher = false;
float GeometryVoxelSizeCm = VOXEL_SIZE; float GeometryVoxelSizeCm = VOXEL_SIZE;
public: public:
+44
View File
@@ -482,3 +482,47 @@ fix, the actor test passed. The final rerun after gating the manifest hash on th
passed in 9m20s with 3,880,357,888 bytes peak sampled private memory (3.61 GiB). No `0xC0000142` passed in 9m20s with 3,880,357,888 bytes peak sampled private memory (3.61 GiB). No `0xC0000142`
retry or new UECC folder occurred. The owner's project was not opened or built and no `.uasset` was retry or new UECC folder occurred. The owner's project was not opened or built and no `.uasset` was
edited. edited.
### M2 round-3 hybrid mesher integration (2026-10-10)
`WorldAssemblerHybridV1` is an opt-in Core recipe; `MarchingCubesV1` and
`bUseVoxelForgeCore=false` remain the defaults. Tagged type-2 plan boxes use sharp hybrid cells only
at LOD0 interior cells. Tile-border cells and LOD1+ use MC, with the existing skirt retained. The
full BuildHost actor commandlet passed its W1/W8/W16 matrix at the round-6 Core hashes,
anchor/edit/travel checks, height agreement, switch-off gate, Blueprint carve/fill, native
SurfaceWorld route and five cooked collision contacts. It ran in null-RHI BuildHost for 11 minutes
and logged zero engine errors. A PowerShell status-format cast initially made the outer wrapper
return 1 after the engine had passed; the script was fixed, focused fixture and cliff checks
returned 0, and the corrected full wrapper rerun also returned 0 in 11m06s at a sampled 3.73 GiB
peak.
The BuildHost fixture plan has nine stable type-2 feature boxes for the corner, doorway, riser and
ledge, plus the existing type-4 SurfaceWorld cliff. Five contacts hit the Pawn channel and the same
RMC component; direct component traces agreed with world traces, and cooked Chaos body bounds
matched render bounds. Field contact error ranged from 0 to 20.332 cm. The separate sampled native
cliff check passed bit-identity: 10,231 vertices, 10,360 triangles, mesh hash
`0xfc7bd7a3cf997992`. Same-LOD independent borders passed with 63 segments; the 125-phase LOD0/LOD1
doorway probe had no contours over 50 cm and a 22.844 cm worst mismatch.
That LOD probe is not a topology proof. The standalone full transition suite still records five MC
doorway failures out of 625, with skirts serving as the current repair. At the 25 cm Core grid, the
17.68 cm corner bevel is 0.35 of a LOD1 sample and may round off or pop at the threshold. Null RHI
cannot show or measure this visually; owner review remains necessary.
Plugin fixture timings show a 5.66–6.70× hybrid/MC meshing cost on the tagged sharp fixtures; the
organic cave is 0.992× MC. See the M2 round-3 tables and complete failed-run ledger in
[`WorldAssemblerM2/REPORT.md`](Research/WorldAssemblerM2/REPORT.md). BuildHost artifacts include
the owner-previewable [`surface_hybrid_fixture_plan.json`](Research/WorldAssemblerM2/round3/surface_hybrid_fixture_plan.json).
Owner preview: duplicate the settings asset, import the fixture manifest through
`SourceWorldAssemblyManifestJson` / `ImportWorldAssemblyManifestJson`, enable Core on the test
`AVoxelWorld`, and select `World Assembler Hybrid V1`. Review the corner and doorway while crossing
the LOD distance, then compare riser and ledge contacts. The owner builds the plugin and judges the
live view. No owner `.uasset`, project build, or editor session was used here.
New source files are the mirrored private headers
`Source/VoxelForge/Private/VoxelHybridSharpMesher.h` and
`Standalone/Source/VoxelForge/Private/VoxelHybridSharpMesher.h`; no new `.cpp` or generated header
was added. Likely compile touchpoints are `VoxelWorld.h/.cpp`, `VoxelTypes.h`,
`VoxelMarchingCubesMesher.h/.cpp`, both hybrid mesher headers, the Core public header and
`VoxelForgeCore.cpp`, `VoxelForgeExploreCommandlet.cpp`, and `VoxelForgeEditor.Build.cs`.
+99 -4
View File
@@ -1,7 +1,11 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900, [ValidateRange(60, 900)][int]$TimeoutSeconds = 900,
[string]$WorldAssemblyManifestPath = '' [string]$WorldAssemblyManifestPath = '',
[ValidateSet('MarchingCubesV1', 'WorldAssemblerHybridV1')][string]$CoreMesherRecipe = 'MarchingCubesV1',
[switch]$EnableHybridFixturePlacement,
[switch]$HybridCliffOnly,
[switch]$HybridFixturesOnly
) )
# Exercise AVoxelWorld Core streaming, collision, anchor movement, and carving in BuildHost only. # Exercise AVoxelWorld Core streaming, collision, anchor movement, and carving in BuildHost only.
@@ -83,6 +87,21 @@ $ManifestData = Get-Content -LiteralPath $StagedWorldAssembly -Raw | ConvertFrom
if ($ManifestData.content_hash -ne '8933693d9560ba9f') { if ($ManifestData.content_hash -ne '8933693d9560ba9f') {
throw "BuildHost world assembly input is not the reviewed round-4 manifest: $($ManifestData.content_hash)" throw "BuildHost world assembly input is not the reviewed round-4 manifest: $($ManifestData.content_hash)"
} }
if ($EnableHybridFixturePlacement -and $CoreMesherRecipe -ne 'WorldAssemblerHybridV1') {
throw 'Hybrid fixture placement requires -CoreMesherRecipe WorldAssemblerHybridV1.'
}
if ($HybridCliffOnly -and $CoreMesherRecipe -ne 'WorldAssemblerHybridV1') {
throw '-HybridCliffOnly requires -CoreMesherRecipe WorldAssemblerHybridV1.'
}
if ($HybridCliffOnly -and $EnableHybridFixturePlacement) {
throw '-HybridCliffOnly uses the reviewed native cliff manifest and cannot add the surface fixtures.'
}
if ($HybridFixturesOnly -and $CoreMesherRecipe -ne 'WorldAssemblerHybridV1') {
throw '-HybridFixturesOnly requires -CoreMesherRecipe WorldAssemblerHybridV1.'
}
if ($HybridFixturesOnly -and ($HybridCliffOnly -or $EnableHybridFixturePlacement)) {
throw '-HybridFixturesOnly cannot be combined with the other focused hybrid modes.'
}
$CrashBefore = @{} $CrashBefore = @{}
foreach ($CrashRoot in $CrashRoots) { foreach ($CrashRoot in $CrashRoots) {
@@ -107,6 +126,10 @@ function Get-NewCrashFolders {
return @($Found.ToArray()) return @($Found.ToArray())
} }
$HybridMesherFlag = if ($CoreMesherRecipe -eq 'WorldAssemblerHybridV1') { 1 } else { 0 }
$HybridFixturePlacementFlag = if ($EnableHybridFixturePlacement) { 1 } else { 0 }
$HybridCliffOnlyFlag = if ($HybridCliffOnly) { 1 } else { 0 }
$HybridFixturesOnlyFlag = if ($HybridFixturesOnly) { 1 } else { 0 }
$BaseArguments = @( $BaseArguments = @(
('-project="{0}"' -f $HostProject), ('-project="{0}"' -f $HostProject),
'-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound', '-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound',
@@ -119,6 +142,10 @@ $BaseArguments = @(
'-modes=export', '-gameconfig=1', '-coreworldtest=1', '-modes=export', '-gameconfig=1', '-coreworldtest=1',
'-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings', '-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings',
('-coreworldassembly="{0}"' -f $StagedWorldAssembly), ('-coreworldassembly="{0}"' -f $StagedWorldAssembly),
("-corehybridmesher=$HybridMesherFlag"),
("-corehybridfixtureplacement=$HybridFixturePlacementFlag"),
("-corehybridcliffonly=$HybridCliffOnlyFlag"),
("-corehybridfixturesonly=$HybridFixturesOnlyFlag"),
('-out="{0}"' -f $OutputDirectory) ('-out="{0}"' -f $OutputDirectory)
) )
@@ -199,6 +226,40 @@ if (-not (Test-Path -LiteralPath $FinalLog -PathType Leaf)) {
$FinalLog = Join-Path $LogDirectory 'CoreWorldValidation.retry.log' $FinalLog = Join-Path $LogDirectory 'CoreWorldValidation.retry.log'
} }
$Text = Get-Content -LiteralPath $FinalLog -Raw $Text = Get-Content -LiteralPath $FinalLog -Raw
if ($HybridFixturesOnly) {
foreach ($Fixture in @('corner90', 'doorway', 'riser1m', 'cliff_ledge', 'organic_cave_chamber')) {
$FixtureMarker = '\[WorldAssemblerHybridPluginFixture\] name=' +
[regex]::Escape($Fixture) + ' lod=0 .*exact_repeat=1'
if ($Text -notmatch $FixtureMarker) {
throw "BuildHost fixture log is missing '$Fixture'. See $FinalLog."
}
}
if ($Text -notmatch '\[WorldAssemblerHybridPluginBorder\] same_lod=PASS independent=1' -or
$Text -notmatch '\[WorldAssemblerHybridPluginBorder\] lod0_lod1_doorway=1 cases=125 over_50cm_cases=0') {
throw "BuildHost fixture log is missing passing border probes. See $FinalLog."
}
$NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during plugin fixture checks: $($NewCrashes -join '; ')"
}
Write-Host "VOXELFORGE_CORE_HYBRID_FIXTURES_PASS recipe=$CoreMesherRecipe analytic=5 borders=1"
Write-Host "Core actor log: $FinalLog"
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"
return
}
if ($HybridCliffOnly) {
if ($Text -notmatch '\[WorldAssemblerHybridNativeCliff\] key=\(-22,13,-58\) lod=0 .*bit_identical=1 type4_only=1') {
throw "BuildHost log is missing the sampled native SurfaceWorld cliff identity proof. See $FinalLog."
}
$NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during the native cliff check: $($NewCrashes -join '; ')"
}
Write-Host "VOXELFORGE_CORE_HYBRID_CLIFF_PASS recipe=$CoreMesherRecipe bit_identical=1"
Write-Host "Core actor log: $FinalLog"
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"
return
}
foreach ($Phase in @('initial', 'anchor', 'edit', 'surface_travel', 'surface_strate_travel')) { foreach ($Phase in @('initial', 'anchor', 'edit', 'surface_travel', 'surface_strate_travel')) {
if ($Text -notmatch "\[VoxelForgeCoreWorldTest\] pass phase=$Phase") { if ($Text -notmatch "\[VoxelForgeCoreWorldTest\] pass phase=$Phase") {
throw "BuildHost Core actor log is missing the '$Phase' validation marker. See $FinalLog." throw "BuildHost Core actor log is missing the '$Phase' validation marker. See $FinalLog."
@@ -207,8 +268,42 @@ foreach ($Phase in @('initial', 'anchor', 'edit', 'surface_travel', 'surface_str
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] worker_hash_matrix=1/8/16 stable=1') { if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] worker_hash_matrix=1/8/16 stable=1') {
throw "BuildHost Core actor log is missing the W1/W8/W16 determinism marker. See $FinalLog." throw "BuildHost Core actor log is missing the W1/W8/W16 determinism marker. See $FinalLog."
} }
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] world_assembly_hash=8933693d9560ba9f plans=1 native=1') { $ExpectedRuntimeAssemblyHash = $ManifestData.content_hash
throw "BuildHost Core actor log does not prove the reviewed native SurfaceWorld plan was attached. See $FinalLog." if ($EnableHybridFixturePlacement) {
$FixtureHashMatch = [regex]::Match($Text,
'\[VoxelForgeCoreWorldTest\] hybrid_fixture_placement=1 boxes=9 contacts=5 manifest_hash=([0-9a-f]{16})')
if (-not $FixtureHashMatch.Success) {
throw "BuildHost actor log is missing the generated hybrid fixture manifest hash. See $FinalLog."
}
$ExpectedRuntimeAssemblyHash = $FixtureHashMatch.Groups[1].Value
}
$AssemblyMarker = '\[VoxelForgeCoreWorldTest\] world_assembly_hash=' +
[regex]::Escape($ExpectedRuntimeAssemblyHash) + ' plans=1 native=1'
if ($Text -notmatch $AssemblyMarker) {
throw "BuildHost actor log does not prove the expected native SurfaceWorld plan was attached. See $FinalLog."
}
$RecipeMarker = '\[VoxelForgeCoreWorldTest\] world_initialized=1 recipe=' +
[regex]::Escape($CoreMesherRecipe)
if ($Text -notmatch $RecipeMarker) {
throw "BuildHost actor log does not confirm the requested $CoreMesherRecipe runtime selection. See $FinalLog."
}
if ($CoreMesherRecipe -eq 'WorldAssemblerHybridV1') {
foreach ($Fixture in @('corner90', 'doorway', 'riser1m', 'cliff_ledge', 'organic_cave_chamber')) {
$FixtureMarker = '\[WorldAssemblerHybridPluginFixture\] name=' +
[regex]::Escape($Fixture) + ' lod=0 .*exact_repeat=1'
if ($Text -notmatch $FixtureMarker) {
throw "BuildHost log is missing the production mesher fixture '$Fixture'. See $FinalLog."
}
}
if ($Text -notmatch '\[WorldAssemblerHybridPluginBorder\] same_lod=PASS independent=1') {
throw "BuildHost log is missing the independent same-LOD border proof. See $FinalLog."
}
if ($Text -notmatch '\[WorldAssemblerHybridPluginBorder\] lod0_lod1_doorway=1 cases=125 over_50cm_cases=0') {
throw "BuildHost log is missing a passing 125-phase MC doorway contour result. See $FinalLog."
}
}
if ($EnableHybridFixturePlacement -and $Text -notmatch '\[VoxelForgeHybridCollisionContact\] pass=1 probes=5') {
throw "BuildHost log is missing the cooked hybrid fixture trace contacts. See $FinalLog."
} }
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] native_biome_field=1 .*sampled_biomes=[2-9]') { if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] native_biome_field=1 .*sampled_biomes=[2-9]') {
throw "BuildHost Core actor log does not prove native SurfaceWorld biome assignment. See $FinalLog." throw "BuildHost Core actor log does not prove native SurfaceWorld biome assignment. See $FinalLog."
@@ -241,6 +336,6 @@ $NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) { if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during Core actor validation: $($NewCrashes -join '; ')" throw "New UECC crash folders appeared during Core actor validation: $($NewCrashes -join '; ')"
} }
Write-Host 'VOXELFORGE_CORE_WORLD_PASS streaming=1 collision=1 anchor_move=1 carve=1' Write-Host "VOXELFORGE_CORE_WORLD_PASS recipe=$CoreMesherRecipe fixtures=$HybridFixturePlacementFlag streaming=1 collision=1 anchor_move=1 carve=1"
Write-Host "Core actor log: $FinalLog" Write-Host "Core actor log: $FinalLog"
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes" Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"