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:
1 parent
a3b7d2015e
commit
22a6b30494
21 files changed
+3009
-23
No files matched your search
@@ -682,6 +682,17 @@ driven by `EditorBrush*` props.
|
||||
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
|
||||
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;
|
||||
`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
|
||||
|
||||
+15
@@ -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
|
||||
[`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,
|
||||
VerticalShaftParams, and FloatingIslandParams. The shape-world evaluator still lacks their native
|
||||
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 "MarchingCubesTables.h"
|
||||
#include "VoxelDensityProfile.h"
|
||||
#include "VoxelHybridSharpMesher.h"
|
||||
#if WITH_EDITOR
|
||||
#include "VoxelRedefinedDensityField.h"
|
||||
#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).
|
||||
static thread_local TMap<FIntVector, int32> VertexMap;
|
||||
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
|
||||
@@ -164,7 +167,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
// 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
|
||||
// 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(
|
||||
FMath::RoundToInt(WorldPos.X * 100.0f),
|
||||
@@ -172,13 +176,27 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
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;
|
||||
}
|
||||
|
||||
const int32 NewIndex = MeshData.Vertices.Num();
|
||||
VertexMap.Add(Key, NewIndex);
|
||||
if (NormalSplitKey != 0)
|
||||
{
|
||||
SplitNormalVertexMap.FindOrAdd(Key).Add(NormalSplitKey, NewIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
VertexMap.Add(Key, NewIndex);
|
||||
}
|
||||
|
||||
MeshData.Vertices.Add(WorldPos);
|
||||
|
||||
@@ -1050,6 +1068,31 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
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
|
||||
//=========================================================================
|
||||
@@ -1169,6 +1212,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
const int32 E0 = MCTriTable[CaseIndex][i];
|
||||
const int32 E1 = MCTriTable[CaseIndex][i + 1];
|
||||
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 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
|
||||
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)
|
||||
{
|
||||
VoxelDensityProfile::AddMeasurement(
|
||||
|
||||
@@ -8725,6 +8725,7 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
|
||||
const int32 NumVertices = Pending->NumVertices;
|
||||
const int32 NumTriangles = Pending->NumTriangles;
|
||||
const int32 NumSkirtTriangles = Pending->NumSkirtTriangles;
|
||||
const bool bPooledComponent = Pending->bPooledComponent;
|
||||
const int32 PendingCooksAtSubmit = Pending->PendingCooksAtSubmit;
|
||||
const bool bUseAsyncCook = Pending->bUseAsyncCook;
|
||||
const bool bUseComplexAsSimpleCollision = Pending->bUseComplexAsSimpleCollision;
|
||||
@@ -8745,6 +8746,15 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
|
||||
CollisionSuppressedAtCycles.Remove(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);
|
||||
}
|
||||
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};
|
||||
VoxelForgeCore::TileGenerationOptions Options;
|
||||
Options.mesherRecipe = CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1
|
||||
? VoxelForgeCore::MesherRecipe::worldAssemblerHybridV1
|
||||
: VoxelForgeCore::MesherRecipe::marchingCubesV1;
|
||||
Options.hashDensity = bCaptureHashes;
|
||||
std::string CoreError;
|
||||
std::optional<VoxelForgeCore::TileMesh> CoreMesh;
|
||||
@@ -12248,7 +12261,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
MeshComp = AcquireTileComponent(
|
||||
bProfileApply ? &ComponentCreationSeconds : nullptr,
|
||||
bProfileApply ? &ComponentRegistrationSeconds : nullptr,
|
||||
bProfileApply ? &bComponentCreated : nullptr);
|
||||
&bComponentCreated);
|
||||
TileComponents.Add(Tile, MeshComp);
|
||||
}
|
||||
if (!IsValid(MeshComp))
|
||||
@@ -12263,6 +12276,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const bool bWasPooledComponent = !bComponentAlreadyPresent && !bComponentCreated;
|
||||
const double ComponentSeconds = bProfileApply
|
||||
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - ComponentStartCycles)
|
||||
: 0.0;
|
||||
@@ -12407,6 +12421,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
||||
Pending.NumTriangles = Result.NumTriangles;
|
||||
Pending.NumSkirtTriangles = Result.NumSkirtTriangles;
|
||||
Pending.PendingCooksAtSubmit = PendingCollisionCooks.Num();
|
||||
Pending.bPooledComponent = bWasPooledComponent;
|
||||
Pending.bUseAsyncCook = CollisionConfig.bUseAsyncCook;
|
||||
Pending.bUseComplexAsSimpleCollision = CollisionConfig.bUseComplexAsSimpleCollision;
|
||||
Pending.bShouldFastCookMeshes = CollisionConfig.bShouldFastCookMeshes;
|
||||
@@ -13275,7 +13290,8 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
if (AssemblyHash.IsEmpty())
|
||||
const bool bHybridMesher = CoreMesherRecipe == EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1;
|
||||
if (AssemblyHash.IsEmpty() && !bHybridMesher)
|
||||
{
|
||||
return SeasonHash;
|
||||
}
|
||||
@@ -13295,8 +13311,15 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
}
|
||||
}
|
||||
}
|
||||
Payload += TEXT("|world_assembly=");
|
||||
Payload += AssemblyHash;
|
||||
if (!AssemblyHash.IsEmpty())
|
||||
{
|
||||
Payload += TEXT("|world_assembly=");
|
||||
Payload += AssemblyHash;
|
||||
}
|
||||
if (bHybridMesher)
|
||||
{
|
||||
Payload += TEXT("|core_mesher=WorldAssemblerHybridV1");
|
||||
}
|
||||
FTCHARToUTF8 Utf8Payload(*Payload);
|
||||
uint8 Digest[FSHA1::DigestSize];
|
||||
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)
|
||||
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
|
||||
{
|
||||
@@ -103,6 +112,14 @@ public:
|
||||
// the canonical generator path rather than changing geometry.
|
||||
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:
|
||||
FORCEINLINE bool ShouldAbortWork() const
|
||||
{
|
||||
@@ -111,6 +128,8 @@ private:
|
||||
|
||||
const std::atomic<bool>* ShutdownFlag = nullptr;
|
||||
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
||||
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
|
||||
bool bUseHybridSharpMesher = false;
|
||||
float GeometryVoxelSizeCm = VOXEL_SIZE;
|
||||
|
||||
public:
|
||||
|
||||
@@ -379,6 +379,20 @@ struct FVoxelMeshData
|
||||
// share without changing the emitted indices.
|
||||
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
|
||||
// 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"
|
||||
@@ -394,6 +408,17 @@ struct FVoxelMeshData
|
||||
Colors.Empty();
|
||||
NumSurfaceVertices = 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,15 @@ enum class EVoxelAnchorPolicy : uint8
|
||||
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
|
||||
* collision wherever it is. Plain internal struct (weak ptr is GC-safe without reflection). */
|
||||
struct FVoxelStreamingAnchor
|
||||
@@ -269,6 +278,11 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Use VoxelForge Core"))
|
||||
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. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "1", ClampMax = "64"))
|
||||
int32 CoreClipRadius = 3;
|
||||
@@ -330,6 +344,7 @@ public:
|
||||
uint64 CollisionSubmittedCycles = 0;
|
||||
uint64 CollisionSubmittedFrame = 0;
|
||||
bool bBodyUpdatedEventSeen = false;
|
||||
bool bPooledComponent = false;
|
||||
uint64 BodyUpdatedEventCycles = 0;
|
||||
uint64 BodyUpdatedEventFrame = 0;
|
||||
bool bPhysicsStateReadyAtBodyUpdate = false;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -23,6 +23,7 @@ public class VoxelForgeEditor : ModuleRules
|
||||
{
|
||||
"Json",
|
||||
"ImageWrapper",
|
||||
"RealtimeMeshComponent",
|
||||
"VoxelForge",
|
||||
"VoxelForgeCore",
|
||||
});
|
||||
|
||||
@@ -62,6 +62,23 @@ namespace VoxelForgeCore
|
||||
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 };
|
||||
|
||||
struct TileGenerationStats
|
||||
@@ -657,6 +674,7 @@ namespace VoxelForgeCore
|
||||
|
||||
struct TileGenerationOptions
|
||||
{
|
||||
MesherRecipe mesherRecipe = MesherRecipe::marchingCubesV1;
|
||||
std::int32_t bandMinZVoxel = -2147483647 - 1;
|
||||
std::int32_t bandMaxZVoxel = 2147483647;
|
||||
bool hashDensity = false;
|
||||
|
||||
@@ -890,11 +890,10 @@ namespace VoxelForgeCore
|
||||
if(const auto found=impl_->tileRegions.find(key);found!=impl_->tileRegions.end())
|
||||
page=found->second;
|
||||
}
|
||||
const FStandaloneShapeWorld& tileShape=page?*page:*impl_->shape;
|
||||
const FStandaloneShapeWorld* tileShapeContext=page?page.get():impl_->shape.get();
|
||||
UVoxelMarchingCubesMesher mesher;
|
||||
mesher.SetGenerator(world.Generator.get());
|
||||
mesher.SetGeometryVoxelSizeCm(25.0f);
|
||||
mesher.SetStandaloneShapeWorld(&tileShape);
|
||||
mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors;
|
||||
mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors;
|
||||
mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups;
|
||||
@@ -904,6 +903,54 @@ namespace VoxelForgeCore
|
||||
const int32 level=key.lod,extent=CHUNK_SIZE<<level;
|
||||
const FVoxelTileKey nativeKey=ToNative(key);
|
||||
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 step=FMath::Max(1,extent/cells);
|
||||
bool bFeatureRefinedResolution=false;
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
I agree with Lipschitz bounds as the cheap, generic first certificate. I do **not** recommend them as
|
||||
|
||||
@@ -923,4 +923,51 @@ 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.
|
||||
|
||||
— 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
|
||||
@@ -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.
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
#include "VoxelHybridSharpMesher.h"
|
||||
#include "MarchingCubesTables.h"
|
||||
#include "StandaloneShapeWorld.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).
|
||||
static thread_local TMap<FIntVector, int32> VertexMap;
|
||||
VertexMap.Reset();
|
||||
static thread_local TMap<FIntVector, TMap<uint64, int32>> SplitNormalVertexMap;
|
||||
SplitNormalVertexMap.Reset();
|
||||
const int32 VertexCapacityHint=FMath::Clamp(InCellsPerAxis,2,CHUNK_SIZE);
|
||||
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).
|
||||
auto GetOrCreateVertex = [&](const FVector& WorldPos, const FVector& RawNormal) -> int32
|
||||
auto GetOrCreateVertex = [&](const FVector& WorldPos, const FVector& RawNormal,
|
||||
uint64 NormalSplitKey = 0) -> int32
|
||||
{
|
||||
const FIntVector Key(
|
||||
FMath::RoundToInt(WorldPos.X * 100.0f),
|
||||
@@ -238,13 +242,27 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
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;
|
||||
}
|
||||
|
||||
const int32 NewIndex = MeshData.Vertices.Num();
|
||||
VertexMap.Add(Key, NewIndex);
|
||||
if (NormalSplitKey != 0)
|
||||
{
|
||||
SplitNormalVertexMap.FindOrAdd(Key).Add(NormalSplitKey, NewIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
VertexMap.Add(Key, NewIndex);
|
||||
}
|
||||
|
||||
MeshData.Vertices.Add(WorldPos);
|
||||
|
||||
@@ -1349,6 +1367,20 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
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
|
||||
//=========================================================================
|
||||
@@ -1564,6 +1596,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
const int32 E0 = MCTriTable[CaseIndex][i];
|
||||
const int32 E1 = MCTriTable[CaseIndex][i + 1];
|
||||
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 Idx1 = GetOrCreateVertex(EdgeVertices[E1], EdgeNormals[E1]);
|
||||
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)
|
||||
{
|
||||
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)
|
||||
class VOXELFORGE_API UVoxelMarchingCubesMesher : public UObject
|
||||
{
|
||||
@@ -111,6 +120,14 @@ public:
|
||||
// set this pointer and continue through Generator.
|
||||
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:
|
||||
FORCEINLINE bool ShouldAbortWork() const
|
||||
{
|
||||
@@ -120,6 +137,8 @@ private:
|
||||
const std::atomic<bool>* ShutdownFlag = nullptr;
|
||||
const FVoxelSharedDensityGrid* SharedDensityGrid = nullptr;
|
||||
const FStandaloneShapeWorld* ShapeWorldContext = nullptr;
|
||||
TArray<FVoxelHybridSharpFeatureBox> HybridSharpFeatures;
|
||||
bool bUseHybridSharpMesher = false;
|
||||
float GeometryVoxelSizeCm = VOXEL_SIZE;
|
||||
|
||||
public:
|
||||
|
||||
@@ -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`
|
||||
retry or new UECC folder occurred. The owner's project was not opened or built and no `.uasset` was
|
||||
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`.
|
||||
@@ -1,7 +1,11 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[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.
|
||||
@@ -83,6 +87,21 @@ $ManifestData = Get-Content -LiteralPath $StagedWorldAssembly -Raw | ConvertFrom
|
||||
if ($ManifestData.content_hash -ne '8933693d9560ba9f') {
|
||||
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 = @{}
|
||||
foreach ($CrashRoot in $CrashRoots) {
|
||||
@@ -107,6 +126,10 @@ function Get-NewCrashFolders {
|
||||
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 = @(
|
||||
('-project="{0}"' -f $HostProject),
|
||||
'-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound',
|
||||
@@ -119,6 +142,10 @@ $BaseArguments = @(
|
||||
'-modes=export', '-gameconfig=1', '-coreworldtest=1',
|
||||
'-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings',
|
||||
('-coreworldassembly="{0}"' -f $StagedWorldAssembly),
|
||||
("-corehybridmesher=$HybridMesherFlag"),
|
||||
("-corehybridfixtureplacement=$HybridFixturePlacementFlag"),
|
||||
("-corehybridcliffonly=$HybridCliffOnlyFlag"),
|
||||
("-corehybridfixturesonly=$HybridFixturesOnlyFlag"),
|
||||
('-out="{0}"' -f $OutputDirectory)
|
||||
)
|
||||
|
||||
@@ -199,6 +226,40 @@ if (-not (Test-Path -LiteralPath $FinalLog -PathType Leaf)) {
|
||||
$FinalLog = Join-Path $LogDirectory 'CoreWorldValidation.retry.log'
|
||||
}
|
||||
$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')) {
|
||||
if ($Text -notmatch "\[VoxelForgeCoreWorldTest\] pass phase=$Phase") {
|
||||
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') {
|
||||
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') {
|
||||
throw "BuildHost Core actor log does not prove the reviewed native SurfaceWorld plan was attached. See $FinalLog."
|
||||
$ExpectedRuntimeAssemblyHash = $ManifestData.content_hash
|
||||
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]') {
|
||||
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) {
|
||||
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 "Peak commandlet private bytes sampled: $PeakPrivateBytes"
|
||||
Reference in new issue
Block a user