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

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

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

No files matched your search

@@ -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;
+49 -2
View File
@@ -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;
+15
View File
@@ -306,6 +306,21 @@ Impacts to carry into the mesher proof:
corner invites snagging and makes capsule clearance disagree with what the player sees. Mesh atoms
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
+137 -4
View File
@@ -89,9 +89,12 @@ These timings are **not comparable to the optimized production MC mesher**. The
The [updated offline contact sheet](results/captures/contact_sheet.jpg) is 864 × 1,968 pixels. It contains the five analytic fixtures, the sampled SurfaceWorld cliff, and both LOD seam views, with MC/Hybrid/DC columns. The 24 individual JPEGs are 288 × 246 pixels; all 25 files including the sheet are below 300 KB. No viewer or GPU path was used.
## 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:
+44
View File
@@ -482,3 +482,47 @@ fix, the actor test passed. The final rerun after gating the manifest hash on th
passed in 9m20s with 3,880,357,888 bytes peak sampled private memory (3.61 GiB). No `0xC0000142`
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`.