diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 4e49fa6..f928630 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -249,8 +249,8 @@ namespace VoxelForgeCore // L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles. inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2; - // Version 14 adds shared passage columns for long native open-air crossings. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 15; + // Version 14 adds shared passage columns; version 16 smooths owner Gradient faces. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 16; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 581d7e0..7536a32 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -53,6 +53,10 @@ public: :shape_(&shape),minimumSubvoxels_(minimumSubvoxels), maximumSubvoxels_(maximumSubvoxels),previous_(ActivePassageTileQueries) { + mayContainResolvedBoundaryBand_=shape.ResolvedBoundaryBandsIntersectSubvoxelRange( + minimumSubvoxels_[2],maximumSubvoxels_[2]); + mayContainTransitionPassageFade_=shape.ResolvedTransitionPassageFadeIntersectsTileXY( + minimumSubvoxels_,maximumSubvoxels_); featureIndices_=shape.BuildPassageFeatureTileList( minimumSubvoxels_,maximumSubvoxels_); passageRoutes_.assign(shape.ResolvedLayout?shape.ResolvedLayout->passages.size():0,0); @@ -92,6 +96,13 @@ public: {return passageIndexResolvedBoundaryBandMayAffectAtZMicrometres(zMicrometres); + } std::size_t featureCount() const{return featureIndices_.size();} private: @@ -100,6 +111,8 @@ public: std::vector featureIndices_; std::vector passageRoutes_,passageColumns_,passageMouthSupports_; bool routeFeatureOverlap_=false,columnZOverlap_=false; + bool mayContainResolvedBoundaryBand_=true; + bool mayContainTransitionPassageFade_=true; const FScopedPassageTileQueries* previous_; }; @@ -174,6 +187,83 @@ public: &&Upper.transitionHonouredInCore; } + bool ResolvedBoundaryBandsIntersectSubvoxelRange(std::int64_t minimumZSubvoxels, + std::int64_t maximumZSubvoxels) const + { + if(!ResolvedLayout||!Owner||Owner->CoreVoxelSizeMicrons==0 + ||minimumZSubvoxels>maximumZSubvoxels)return false; + constexpr double Subvoxels=static_cast( + VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + const double queryMinimumMicrometres=static_cast(minimumZSubvoxels) + *Owner->CoreVoxelSizeMicrons/Subvoxels; + const double queryMaximumMicrometres=static_cast(maximumZSubvoxels) + *Owner->CoreVoxelSizeMicrons/Subvoxels; + const double sampleMarginMicrometres=static_cast(Owner->CoreVoxelSizeMicrons); + for(const auto& interval:ResolvedBoundaryZIntervalsMicrometres) + { + if(queryMaximumMicrometres+sampleMarginMicrometres>=interval[0]-1.0 + &&queryMinimumMicrometres-sampleMarginMicrometresat(1); + } + + void BuildResolvedBoundaryZIntervals() + { + ResolvedBoundaryZIntervalsMicrometres.clear(); + if(!ResolvedLayout)return; + for(const auto& stratum:ResolvedLayout->strates) + { + if(stratum.ceiling.presentInCurrentField + &&stratum.ceiling.bottomMicrometres(stratum.ceiling.bottomMicrometres), + static_cast(stratum.ceiling.topMicrometres)}); + if(stratum.bedrock.presentInCurrentField + &&stratum.bedrock.bottomMicrometres(stratum.bedrock.bottomMicrometres), + static_cast(stratum.bedrock.topMicrometres)}); + } + for(std::int32_t order=0;order+1(ResolvedLayout->strates.size()); + ++order) + { + const auto* upper=FindResolvedLayoutStratumByOrder(order); + const auto* lower=FindResolvedLayoutStratumByOrder(order+1); + if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))continue; + const auto displacement=ResolvedTransitionMaximumFaceDisplacementMicrometres( + *upper,*lower); + ResolvedBoundaryZIntervalsMicrometres.push_back({ + static_cast(std::min(upper->bedrock.bottomMicrometres, + lower->ceiling.bottomMicrometres)-displacement), + static_cast(std::max(upper->bedrock.topMicrometres, + lower->ceiling.topMicrometres)+displacement)}); + } + std::sort(ResolvedBoundaryZIntervalsMicrometres.begin(), + ResolvedBoundaryZIntervalsMicrometres.end(), + [](const auto& a,const auto& b){return a[0]0&&interval[0]<=ResolvedBoundaryZIntervalsMicrometres[write-1][1]) + ResolvedBoundaryZIntervalsMicrometres[write-1][1]=std::max( + ResolvedBoundaryZIntervalsMicrometres[write-1][1],interval[1]); + else + ResolvedBoundaryZIntervalsMicrometres[write++]=interval; + } + ResolvedBoundaryZIntervalsMicrometres.resize(write); + } + std::int64_t ResolvedTransitionMaximumFaceDisplacementMicrometres( const VoxelForgeCore::ResolvedLayoutStratumV2& Upper, const VoxelForgeCore::ResolvedLayoutStratumV2& Lower) const @@ -189,11 +279,68 @@ public: return (maximumCrossing+1)/2; } + bool ResolvedTransitionPassageFadeIntersectsTileXY( + const std::array& minimumSubvoxels, + const std::array& maximumSubvoxels) const + { + if(!Owner||!ResolvedLayout||Owner->CoreVoxelSizeMicrons==0)return true; + for(std::size_t axis=0;axis<2;++axis) + if(minimumSubvoxels[axis]>maximumSubvoxels[axis])return true; + constexpr std::int64_t Subvoxels=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + constexpr std::int64_t SupportMarginMicrometres=1000000; + constexpr std::int64_t FadeWidthMicrometres=2000000; + const double voxelMicrometres=static_cast(Owner->CoreVoxelSizeMicrons); + const double minimumX=static_cast(minimumSubvoxels[0])*voxelMicrometres + /Subvoxels-voxelMicrometres; + const double maximumX=static_cast(maximumSubvoxels[0])*voxelMicrometres + /Subvoxels+voxelMicrometres; + const double minimumY=static_cast(minimumSubvoxels[1])*voxelMicrometres + /Subvoxels-voxelMicrometres; + const double maximumY=static_cast(maximumSubvoxels[1])*voxelMicrometres + /Subvoxels+voxelMicrometres; + for(std::int32_t order=0;order+1(ResolvedLayout->strates.size()); + ++order) + { + const auto* upper=FindResolvedLayoutStratumByOrder(order); + const auto* lower=FindResolvedLayoutStratumByOrder(order+1); + if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))continue; + const std::int64_t seam=upper->bedrock.bottomMicrometres; + for(const auto& passage:ResolvedLayout->passages) + { + if(passage.upperStrateIndex!=upper->strateIndex + ||passage.lowerStrateIndex!=lower->strateIndex)continue; + const std::int64_t mouthRadius=std::max(0, + passage.mouthRadiusSubvoxels*Owner->CoreVoxelSizeMicrons/Subvoxels); + const std::int64_t protectedRadius=mouthRadius+SupportMarginMicrometres; + const double fadeEnd=static_cast(protectedRadius+FadeWidthMicrometres); + for(const auto* mouth:{&passage.upperMouthSubvoxels,&passage.lowerMouthSubvoxels}) + { + const std::int64_t mouthZ=mouth->at(2)*Owner->CoreVoxelSizeMicrons/Subvoxels; + if(std::abs(mouthZ-seam) + >upper->transitionWidthMicrometres/2+mouthRadius)continue; + const double mouthX=static_cast(mouth->at(0) + *Owner->CoreVoxelSizeMicrons/Subvoxels); + const double mouthY=static_cast(mouth->at(1) + *Owner->CoreVoxelSizeMicrons/Subvoxels); + const double dx=mouthXmaximumX?mouthX-maximumX:0.0; + const double dy=mouthYmaximumY?mouthY-maximumY:0.0; + if(dx*dx+dy*dyat(2)*coreMicrometres /subvoxelsPerVoxel; if(std::abs(mouthZMicrometres-seam) @@ -236,6 +386,31 @@ public: bool IsInsideResolvedStrateBoundaryBand(float Z) const { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().BoundaryBandCalls; +#endif + const auto* tileQueries=ActivePassageTileQueries; + if(tileQueries&&tileQueries->matches(*this)) + { + if(!tileQueries->mayContainResolvedBoundaryBand()) + { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().BoundaryTileCullRejects; +#endif + return false; + } + if(Owner&&!tileQueries->mayContainResolvedBoundaryBandAtZ( + static_cast(Z)*Owner->CoreVoxelSizeMicrons)) + { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().BoundaryPointCullRejects; +#endif + return false; + } + // This predicate only selects the scalar path for boundary-capable Z + // samples. The merged intervals are conservative, so a positive is safe. + return true; + } if(!ResolvedLayout||!Owner||!Owner->Manager||!std::isfinite(Z))return false; const double worldZCentimetres=static_cast(Z)*GetCoreVoxelSizeCm(); const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast(worldZCentimetres)); @@ -248,6 +423,9 @@ public: const auto mayTouchTransition=[&](const VoxelForgeCore::ResolvedLayoutStratumV2* Upper, const VoxelForgeCore::ResolvedLayoutStratumV2* Lower) { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().TransitionWindowChecks; +#endif if(!Upper||!Lower||!IsResolvedLayoutTransitionActive(*Upper))return false; const auto maxDisplacement=ResolvedTransitionMaximumFaceDisplacementMicrometres( *Upper,*Lower); @@ -255,7 +433,11 @@ public: Lower->ceiling.bottomMicrometres)-maxDisplacement; const double maximum=std::max(Upper->bedrock.topMicrometres, Lower->ceiling.topMicrometres)+maxDisplacement; - return zMicrometres>=minimum&&zMicrometres=minimum&&zMicrometresorder+1); const auto* upper=stratum->order>0?FindResolvedLayoutStratumByOrder(stratum->order-1):nullptr; @@ -265,7 +447,29 @@ public: float ApplyResolvedStrateBoundaryBand(float density,float X,float Y,float Z) const { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().BoundaryBandCalls; +#endif (void)X;(void)Y; + const auto* tileQueries=ActivePassageTileQueries; + if(tileQueries&&tileQueries->matches(*this)) + { + if(!tileQueries->mayContainResolvedBoundaryBand()) + { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().BoundaryTileCullRejects; +#endif + return density; + } + if(Owner&&!tileQueries->mayContainResolvedBoundaryBandAtZ( + static_cast(Z)*Owner->CoreVoxelSizeMicrons)) + { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().BoundaryPointCullRejects; +#endif + return density; + } + } if(!ResolvedLayout||!Owner||!Owner->Manager||!std::isfinite(Z))return density; const double worldZCentimetres=static_cast(Z)*GetCoreVoxelSizeCm(); const int32 strateIndex=Owner->Manager->GetStrateIndex(static_cast(worldZCentimetres)); @@ -280,6 +484,9 @@ public: const auto applyInterface=[&](const VoxelForgeCore::ResolvedLayoutStratumV2* upper, const VoxelForgeCore::ResolvedLayoutStratumV2* lower) { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().TransitionWindowChecks; +#endif if(!upper||!lower||!IsResolvedLayoutTransitionActive(*upper))return false; const auto maxDisplacement=ResolvedTransitionMaximumFaceDisplacementMicrometres( *upper,*lower); @@ -288,6 +495,10 @@ public: const double maximum=std::max(upper->bedrock.topMicrometres, lower->ceiling.topMicrometres)+maxDisplacement; if(zMicrometres=maximum)return false; +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().TransitionWindowHits; + ++ThreadClassificationStats().TransitionEvaluatorCalls; +#endif if(upper==stratum)bBedrockHandledByTransition=true; if(lower==stratum)bCeilingHandledByTransition=true; const std::int64_t xMicrometres=static_cast(std::llround( @@ -298,9 +509,15 @@ public: Owner->Seed,ResolvedLayout->strateLayoutChunkSizeMicrometres, xMicrometres,yMicrometres); auto resolvedBands=bands; - const std::int64_t transitionWeight=ResolvedTransitionPassageWeightQ16( - *upper,*lower,xMicrometres,yMicrometres); constexpr std::int64_t TransitionQ16=1ll<<16; + const auto* tileQueries=ActivePassageTileQueries; + const bool skipPassageFade=tileQueries&&tileQueries->matches(*this) + &&!tileQueries->mayContainTransitionPassageFade(); + const std::int64_t transitionWeight=skipPassageFade?TransitionQ16 + :ResolvedTransitionPassageWeightQ16(*upper,*lower,xMicrometres,yMicrometres); +#if defined(VF_L30_ENABLE_COST_COUNTERS) + if(skipPassageFade)++ThreadClassificationStats().TransitionPassageTileSkips; +#endif if(transitionWeightbedrock.bottomMicrometres; @@ -325,9 +542,36 @@ public: &&zMicrometres=resolvedBands.lowerCeilingBottomMicrometres &&zMicrometresbedrock.bottomMicrometres; + if(upper->generatorKind=="FloatingIslands" + &&upper->transitionMode=="Gradient" + &&resolvedBands.upperBedrockTopMicrometres=resolvedBands.upperBedrockTopMicrometres + &&zMicrometres(zMicrometres + -resolvedBands.upperBedrockTopMicrometres) + /std::max(1,Owner->CoreVoxelSizeMicrons); + density=std::max(0.05f,static_cast(distanceVoxels)); + return true; + } + return false; + } const auto& band=upperBedrock?upper->bedrock:lower->ceiling; - density=-std::max(0.05f,static_cast(band.solidDensityQ16)/65536.0f); + if(upperBedrock&&upper->generatorKind=="FloatingIslands" + &&upper->transitionMode=="Gradient") + { + const double distanceVoxels=static_cast( + resolvedBands.upperBedrockTopMicrometres-zMicrometres) + /std::max(1,Owner->CoreVoxelSizeMicrons); + density=-std::max(0.05f,static_cast(distanceVoxels)); + } + else density=-std::max(0.05f,static_cast(band.solidDensityQ16)/65536.0f); return true; }; @@ -435,23 +679,49 @@ public: { if(!crossing.needsColumn||crossing.passageIndex!=passageIndex ||qz>crossing.topZSubvoxels||qzhasFeature(passageIndex,true); if(tileProvesColumnExterior) { - if(composed<0.0f)continue; +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().ColumnGuardCalls; +#endif + if(composed<0.0f) + { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().ColumnGuardNonpositiveSkips; +#endif + continue; + } const double dx=qx-crossing.centerXSubvoxels; const double dy=qy-crossing.centerYSubvoxels; const double horizontalSquared=dx*dx+dy*dy; const double maximumRadius=static_cast(crossing.maximumRadiusSubvoxels); const double exteriorLimit=maximumRadius +static_cast(composed)*Q+1.0e-4; +#if defined(VF_L30_ENABLE_COST_COUNTERS) + if(horizontalSquared>exteriorLimit*exteriorLimit) + { + ++ThreadClassificationStats().ColumnGuardRadialRejects; + continue; + } + ++ThreadClassificationStats().ColumnSignedDistanceCalls; +#else if(horizontalSquared>exteriorLimit*exteriorLimit)continue; +#endif columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); } else + { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + ++ThreadClassificationStats().ColumnSignedDistanceCalls; +#endif columnDistance=PassageColumnSignedDistance(crossing,X,Y,Z); + } composed=std::min(composed,static_cast(columnDistance/Q)); } const bool tilePassageFree=passageTileScopeMatches @@ -867,6 +1137,14 @@ public: uint64 SolidBoxes=0,AirBoxes=0,MixedBoxes=0,UnknownBoxes=0; uint64 NativeFloatFieldCalls=0,NativeLatticeFieldCalls=0,NativeUnclampedFieldCalls=0; double Milliseconds=0.0; +#if defined(VF_L30_ENABLE_COST_COUNTERS) + uint64 BoundaryBandCalls=0,TransitionWindowChecks=0,TransitionWindowHits=0; + uint64 TransitionEvaluatorCalls=0,TransitionPassageFadeCalls=0,TransitionPassageMouthTests=0; + uint64 BoundaryTileCullRejects=0,BoundaryPointCullRejects=0; + uint64 ColumnZOverlapCalls=0,ColumnGuardCalls=0,ColumnGuardNonpositiveSkips=0; + uint64 ColumnGuardRadialRejects=0,ColumnSignedDistanceCalls=0; + uint64 TransitionPassageTileSkips=0; +#endif }; static void ResetThreadClassificationStats() { ThreadClassificationStats()={}; } static FClassificationThreadStats GetThreadClassificationStats() @@ -2793,6 +3071,7 @@ private: void BuildPassageVoidCrossingPlan() { + BuildResolvedBoundaryZIntervals(); PassageVoidCrossings.clear(); if(!Owner||!ResolvedLayout||ResolvedLayout->passages.empty())return; if(!Owner->PassageVoidCrossingCache) @@ -7231,6 +7510,7 @@ private: FIntVector NativeOnlyMaximum=FIntVector::ZeroValue; mutable std::atomic NativeOnlyViolations{0}; bool FarBuilt=false; + std::vector> ResolvedBoundaryZIntervalsMicrometres; std::vector LayerRanges; std::vector LayerFields; std::vector Primitives; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index a0a00c1..c9408c6 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -40,6 +40,34 @@ namespace VoxelForgeCore namespace { +#if defined(VF_L30_ENABLE_COST_COUNTERS) + void PrintL30CostCounters(const FStandaloneShapeWorld::FClassificationThreadStats& stats) + { + if(stats.BoundaryBandCalls==0&&stats.ColumnZOverlapCalls==0)return; + std::printf("[L30CostTile] boundary_band_calls=%llu transition_window_checks=%llu " + "transition_window_hits=%llu transition_evaluations=%llu passage_fade_calls=%llu " + "passage_fade_mouth_tests=%llu column_z_overlap_calls=%llu column_guard_calls=%llu " + "column_guard_nonpositive_skips=%llu column_guard_radial_rejects=%llu " + "boundary_tile_cull_rejects=%llu boundary_point_cull_rejects=%llu " + "transition_passage_tile_skips=%llu " + "column_sdf_calls=%llu\n", + static_cast(stats.BoundaryBandCalls), + static_cast(stats.TransitionWindowChecks), + static_cast(stats.TransitionWindowHits), + static_cast(stats.TransitionEvaluatorCalls), + static_cast(stats.TransitionPassageFadeCalls), + static_cast(stats.TransitionPassageMouthTests), + static_cast(stats.ColumnZOverlapCalls), + static_cast(stats.ColumnGuardCalls), + static_cast(stats.ColumnGuardNonpositiveSkips), + static_cast(stats.ColumnGuardRadialRejects), + static_cast(stats.BoundaryTileCullRejects), + static_cast(stats.BoundaryPointCullRejects), + static_cast(stats.TransitionPassageTileSkips), + static_cast(stats.ColumnSignedDistanceCalls)); + } +#endif + FIntVector ToNative(Vec3i value) { return {value.x,value.y,value.z}; } FVector ToNative(Vec3d value) { return {value.x,value.y,value.z}; } Vec3d FromNative(const FVector& value) { return {value.X,value.Y,value.Z}; } @@ -1995,6 +2023,9 @@ namespace VoxelForgeCore out.diagnostics.classificationExclusiveMilliseconds=std::max(0.0, Counts.Milliseconds-classificationColumnComputeMilliseconds); out.diagnostics.columnComputeMilliseconds=Columns.columnComputeMilliseconds; +#if defined(VF_L30_ENABLE_COST_COUNTERS) + PrintL30CostCounters(Counts); +#endif if(options.captureColumnCoordinates) { out.diagnostics.columnEvaluationWorkerHash=static_cast( @@ -2356,6 +2387,9 @@ namespace VoxelForgeCore if(bHasWorldAssembly) { const auto Counts=FStandaloneShapeWorld::GetThreadClassificationStats(); +#if defined(VF_L30_ENABLE_COST_COUNTERS) + PrintL30CostCounters(Counts); +#endif const auto& Columns=VoxelForgeCore::Heightfield2D::threadColumnCacheStats(); out.diagnostics.fieldCalls=Columns.pointCalls; out.diagnostics.floatFieldCalls=Counts.NativeFloatFieldCalls; diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/MeasureRing.py b/Standalone/Research/WorldAssemblerM3/Linux30/MeasureRing.py new file mode 100644 index 0000000..9a41a07 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/MeasureRing.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""Rasterize upward mesh triangles into a top-down field height map.""" +import argparse +import array +import json +import math +import struct +import sys + +Q16 = 1 << 16 +MASK64 = (1 << 64) - 1 + + +def trunc_div(numerator, denominator): + return numerator // denominator if numerator >= 0 else -((-numerator) // denominator) + + +def transition_hash(seed, interface, x, y, z, salt): + value = 14695981039346656037 + for word in (seed & 0xffffffff, interface & 0xffffffff, + x & MASK64, y & MASK64, z & MASK64, salt): + for byte in range(8): + value ^= (word >> (byte * 8)) & 0xff + value = (value * 1099511628211) & MASK64 + value ^= value >> 30 + value = (value * 0xbf58476d1ce4e5b9) & MASK64 + value ^= value >> 27 + value = (value * 0x94d049bb133111eb) & MASK64 + value ^= value >> 31 + return ((value >> 32) >> 15) - Q16 + + +def transition_fade(value): + value = max(0, min(Q16, value)) + square = value * value // Q16 + return square * (3 * Q16 - 2 * value) // Q16 + + +def gradient_height_um(layout, x_um, y_um): + upper = next(item for item in layout["strates"] if item["order"] == 0) + lower = next(item for item in layout["strates"] if item["order"] == 1) + width = upper["transition_width_um"] + cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4) + ix = x_um // cell + iy = y_um // cell + tx = transition_fade(((x_um - ix * cell) * Q16) // cell) + ty = transition_fade(((y_um - iy * cell) * Q16) // cell) + + def at(dx, dy): + noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0, + 0x47524144) + return noise + + def lerp(a, b, t): + return a + trunc_div((b - a) * t, Q16) + + low = lerp(lerp(at(0, 0), at(1, 0), tx), + lerp(at(0, 1), at(1, 1), tx), ty) + displacement = trunc_div(low * (width // 8), Q16) + seam = upper["bedrock"]["bottom_um"] + thickness = upper["bedrock"]["top_um"] - seam + return seam + displacement + thickness + + +def read_scene(path): + triangles = [] + with open(path, "rb") as stream: + if stream.read(8) != b"VFMSH001": + raise SystemExit(f"{path}: invalid scene header") + version, voxel_um, count = struct.unpack("<3I", stream.read(12)) + stream.read(40) + if version != 1 or count > 100000: + raise SystemExit("unsupported scene") + for _ in range(count): + key = struct.unpack("<4i", stream.read(16)) + surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack( + "<5I", stream.read(20)) + positions = array.array("f") + positions.frombytes(stream.read(vertex_count * 12)) + normals = array.array("f") + normals.frombytes(stream.read(vertex_count * 12)) + indices = array.array("I") + indices.frombytes(stream.read(index_count * 4)) + masks = stream.read(mask_count) + if sys.byteorder != "little": + positions.byteswap() + normals.byteswap() + indices.byteswap() + for index in range(0, index_count, 3): + if masks and masks[index // 3]: + continue + a, b, c = indices[index:index + 3] + if max(a, b, c) >= surface_vertices: + continue + nz = (normals[a * 3 + 2] + normals[b * 3 + 2] + + normals[c * 3 + 2]) / 3.0 + if nz < 0.35: + continue + vertices = tuple((positions[v * 3], positions[v * 3 + 1], + positions[v * 3 + 2]) for v in (a, b, c)) + triangles.append(vertices) + return voxel_um, triangles + + +def rasterize(triangles, bounds, width, height): + x0, x1, y0, y1 = bounds + values = [None] * (width * height) + sx, sy = width / (x1 - x0), height / (y1 - y0) + for tri in triangles: + (ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri + den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(den) < 1.0e-12: + continue + left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx))) + right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx))) + top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy))) + bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy))) + for py in range(top, bottom + 1): + y = y1 - (py + 0.5) / sy + row = py * width + for px in range(left, right + 1): + x = x0 + (px + 0.5) / sx + u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den + v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den + w = 1.0 - u - v + if min(u, v, w) < -1.0e-6: + continue + z = u * az + v * bz + w * cz + idx = row + px + if values[idx] is None or z > values[idx]: + values[idx] = z + return values + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("scene") + parser.add_argument("output_ppm") + parser.add_argument("--bounds", nargs=4, type=float, required=True, + metavar=("XMIN", "XMAX", "YMIN", "YMAX")) + parser.add_argument("--size", nargs=2, type=int, default=(640, 640)) + parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0), + metavar=("ZMIN", "ZMAX")) + parser.add_argument("--ring-center", nargs=2, type=float, + metavar=("X", "Y")) + parser.add_argument("--layout") + args = parser.parse_args() + voxel_um, triangles = read_scene(args.scene) + width, height = args.size + sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2]) + values = rasterize(triangles, args.bounds, width, height) + finite = sorted(value for value in values if value is not None) + if not finite: + raise SystemExit("no upward-facing field samples in bounds") + low, high = args.range + if high <= low: + raise SystemExit("height range must increase") + stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)), + (0.50, (85, 177, 143)), (0.73, (226, 207, 105)), + (1.00, (181, 71, 49))) + with open(args.output_ppm, "wb") as output: + output.write(f"P6\n{width} {height}\n255\n".encode()) + for value in values: + if value is None: + output.write(bytes((18, 22, 30))) + continue + t = max(0.0, min(1.0, (value - low) / (high - low))) + for index in range(len(stops) - 1): + if t <= stops[index + 1][0]: + ta, ca = stops[index] + tb, cb = stops[index + 1] + blend = (t - ta) / (tb - ta) + rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c])) + for c in range(3)) + break + else: + rgb = stops[-1][1] + output.write(bytes(rgb)) + def quantile(p): + return finite[round((len(finite) - 1) * p)] + print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} " + f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} " + f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} " + f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} " + f"height_range_m={finite[-1]-finite[0]:.6f} " + f"map={args.output_ppm}") + if args.ring_center: + cx, cy = args.ring_center + def at_world(x, y): + fx = (x - args.bounds[0]) * sx - 0.5 + fy = (args.bounds[3] - y) * sy - 0.5 + ix, iy = math.floor(fx), math.floor(fy) + tx, ty = fx - ix, fy - iy + samples = [] + for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)): + if not (0 <= px < width and 0 <= py < height): + return None + value = values[py * width + px] + if value is None: + return None + samples.append(value) + return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty) + + (samples[2] * (1 - tx) + samples[3] * tx) * ty) + print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) " + "radius_m mean_m angular_sd_m min_m max_m samples") + for radius in range(1, 17): + row = [at_world(cx + radius * math.cos(a * math.tau / 32), + cy + radius * math.sin(a * math.tau / 32)) + for a in range(32)] + row = [value for value in row if value is not None] + if row: + mean = sum(row) / len(row) + sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row)) + print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} " + f"{max(row):.6f} {len(row)}") + if args.layout: + layout = json.load(open(args.layout, encoding="utf-8")) + if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient": + raise SystemExit("--layout must describe a Gradient owner interface") + errors = [] + for index, actual in enumerate(values): + if actual is None: + continue + px, py = index % width, index // width + x = args.bounds[0] + (px + 0.5) / sx + y = args.bounds[3] - (py + 0.5) / sy + expected = gradient_height_um(layout, round(x * 1_000_000), + round(y * 1_000_000)) / 1_000_000.0 + errors.append(actual - expected) + absolute = sorted(abs(error) for error in errors) + rms = math.sqrt(sum(error * error for error in errors) / len(errors)) + print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} " + f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} " + f"max_abs_m={absolute[-1]:.6f} " + "expected=owner-resolved-2d-hash-value-noise") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/ProbeTransitionNoise.cpp b/Standalone/Research/WorldAssemblerM3/Linux30/ProbeTransitionNoise.cpp new file mode 100644 index 0000000..087bb9a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/ProbeTransitionNoise.cpp @@ -0,0 +1,28 @@ +#include "CoreNamespacePreamble.h" +#include "ResolvedLayoutInternal.h" + +int main() +{ + VoxelForgeCore::ResolvedLayoutStratumV2 upper, lower; + upper.strateIndex=0;upper.order=0;upper.transitionMode="Gradient"; + upper.transitionWidthMicrometres=16000000; + upper.bedrock.bottomMicrometres=-120000000; + upper.bedrock.topMicrometres=-119500000; + upper.bedrock.presentInCurrentField=true; + lower.strateIndex=1;lower.order=1; + lower.ceiling.bottomMicrometres=-126250000; + lower.ceiling.topMicrometres=-120000000; + lower.ceiling.presentInCurrentField=true; + for(const auto& point:std::array,5>{ + std::array{64000000,-32000000}, + {65000000,-32000000},{64000000,-31000000},{70000000,-32000000}, + {80000000,-35000000}}) + { + const auto bands=VoxelForgeCore::EvaluateResolvedLayoutTransitionBandsV1( + upper,lower,0,8000000,point[0],point[1]); + std::printf("x_m=%.3f y_m=%.3f top_m=%.6f bottom_m=%.6f\n", + point[0]/1000000.0,point[1]/1000000.0, + bands.upperBedrockTopMicrometres/1000000.0, + bands.upperBedrockBottomMicrometres/1000000.0); + } +} diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux30/REPORT.md new file mode 100644 index 0000000..ae1106c --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/REPORT.md @@ -0,0 +1,121 @@ +# VoxelForge L30 + +Work is on `assembler/world`. The speed and look patches are saved separately under +`Saved/Linux/l30/`; evaluator version is now 16 for the field change. No Unreal files were edited. +Nothing was committed, pushed, or stashed. + +## Ring measurement and fix + +I rasterized upward-facing triangles from the L29 mesh (skirt triangles excluded) over the same +409,600 pixels as the after field. At `(64, -32)`, the L29 field stayed almost flat at `-120.063 m` from +radii 1–11 m, then broke at 12 m. The expected Gradient at radius 1 m is `-121.360 m`. This is a +field defect, not shading: the probe is about 67 m from the passage mouth, and the raster has no +skirts or readable-shading tint. The passage column flare is at that distant mouth as well. + +The upper FloatingIslands generator could leave native solid density in the air-side gap above a +negatively displaced Gradient face. The constant band densities also snapped the face to Core +voxel levels. The owner Gradient path now clears that gap with positive signed distance and writes +negative signed distance below the face, allowing the mesh to interpolate its height. The after +profile follows the resolved hash surface at every sampled radius. Match error fell from RMS +`0.361765 m`, p95 `1.016178 m`, max `1.304169 m` to RMS `0.110357 m`, p95 `0.218531 m`, max +`0.248818 m` (one Core voxel is `0.25 m`). + +Before/after captures use the same color ramp and bounds: + +- [L29 height map](captures/fi-floor-gradient-before-heightmap.jpg), [fixed height map](captures/fi-floor-gradient-after-heightmap.jpg) +- [L29 readable render](captures/fi-floor-gradient-before.jpg), [fixed readable render](captures/fi-floor-gradient-after.jpg) + +## Transition captures + +The maps cover the full 640×640 surface and are all below 300 KB. Gradient is the owner world +without a mode override. Hard and Interleaved use fixture overrides; the resolved layouts record +those modes. + +- [Hard](captures/transition-hard-heightmap.jpg) — flat interface. +- [Gradient](captures/transition-gradient-heightmap.jpg) — owner field. +- [Interleaved](captures/transition-interleaved-heightmap.jpg) — fixture override. + +## Cost + +Five alternating W12 pairs, hashing off and `cache_hits=0`. First table is the separable speed-only +build, whose hashes remain L29-identical. Second table is the final version-16 build. + +| Recipe | L24 | L29 | L30 speed-only | vs L24 | L30 final | vs L24 | +|---|---:|---:|---:|---:|---:|---:| +| SurfaceWorld | 1.566 s | 1.653 s | 1.606 s | +2.55% | 1.530 s | +2.68% | +| FloatingIslands | 16.625 s | 16.779 s | 16.470 s | −0.93% | 16.543 s | +0.21% | + +Counter-enabled runs on the same plain W12 workload locate the saved work: + +| Recipe | Boundary calls | Window checks → evaluations | Fade calls | Boundary tile / point culls | Column Z tests → guards → radial rejects | SDF calls | +|---|---:|---:|---:|---:|---:|---:| +| SurfaceWorld | 19.95 M | 3.54 M → 739 k | 8.2 k | 8.99 M / 8.96 M | 1.87 M → 1.85 M → 1.58 M | 18.4 k | +| FloatingIslands | 66.33 M | 8.00 M → 2.68 M | 8.6 k | 38.25 M / 23.63 M | 8.17 M → 8.14 M → 6.82 M | 83.4 k | + +The fade-tile cull skips 731 k SurfaceWorld and 2.67 M FloatingIslands fade evaluations; the +column guard exits on nonpositive field values 272 k and 1.27 M times. Timing comes from +uninstrumented binaries; the counts above use counter logging. + +## Hashes and audits + +The speed-only v15 build matches all 12 L29 hash records for each recipe/compiler/worker matrix +(W1/W8/W12), and all 12 records for each 24 m crossing matrix. That is 24 exact comparisons; +all pass. The final v16 build is deterministic across both compilers and all worker counts, in the +normal and 24 m crossing runs. Representative W12 hashes (`density / mesh / payload / topology`): + +| Recipe | Full detail | Whole view | +|---|---|---| +| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` | +| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `4ee19ea6b6406ca6 / be648f94c899b47e / 310b525f31604f98 / 10366eb6dfce2384` | + +Exact-query hashes also match across v16 runs: SurfaceWorld +`c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849`; FloatingIslands +`7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf`. + +- Interface audit: all 12 compiler/recipe/mode combinations pass; 4,221 columns each, zero holes + and air samples, both negative controls reject. +- The 16 L29 fixture-matrix replays pass passage safety/open-air, validator controls (10/10), L13 + route parity, L9 proof checks (zero violations), and walkability. `FloorProfileBlend` still + reports 2.544 vox in style 1 and 3.645 vox in style 3, unchanged from L29. +- Audit scope caveat: the L29 files named `floating-islands` were run with + `--recipe=surface-world`. With the actual `--recipe=floating-islands` option, the rock-column + audit fails on all four styles on both L29 and L30: connected ends are 1/2, 3/4, 1/2, 3/4. + The open-air audit passes. This is inherited, but means the L29 report's “two recipes” matrix did + not exercise the FloatingIslands CLI recipe. + +## Failed and retried runs + +- Broad air-gap experiments first lost a SurfaceWorld style-2 support sample. Route/radius masks + and support restoration then caused open-air violations. Widening the fade restored that case, + but measurement placed the ring far outside the fade, so those variants were discarded. The + final owner-only gate and signed-distance face pass the targeted support audit. +- Early speed variants missed the target: tile-only `+4.04%`, Z-point `+3.11%`, scalar path + `+4.33%` vs L24. The final band and fade-tile culls meet the target. +- The first counter-instrumentation compile failed; the counter hook was corrected and rebuilt + with Clang and GCC. One duplicated timing sequence was discarded; only the clean five-pair runs + are reported. +- The first 24 m crossing comparison supplied an extra camera override and mismatched. Rerunning + with the L29 camera setup and only `--camera-step=24,0,0` matches all hashes. +- An early Hard export hit `std::bad_alloc`; retry passed. The first EGL render used an unwritable + cache; retry with `XDG_CACHE_HOME` under `Saved/Linux/l30` passed. The first map conversion also + stopped before the Hard/Interleaved maps; both were rerun individually and have full raster + coverage. +- The first passage-matrix summary script assumed a different marker order and reported failure; + checking each audit marker by name confirms the core matrix passes. A `check=True` true-recipe + control runner stopped on the expected L29 exit code 10; rerunning while recording return codes + confirmed the same four inherited rock-column failures on L29 and L30. + +No build or run exceeded 30 minutes or remained stuck for 15 minutes. Builds and runs used CPUs +0–11, at most 12 workers, normal priority, and the 6 GiB address-space cap. No Unreal build was +attempted. + +## Orchestrator review (Claude) + +- Merged: the ring was a real field defect (air-side gap left solid, face snapped to voxel levels), now fixed + (RMS 0.36 → 0.11 m). Cost back to SW +2.7% / FI +0.2% vs L24. The speed part is bit-identical; final v16 is + deterministic. +- Still visible: `fi-floor-gradient-after.jpg` shows faint concentric lines. Unknown whether that is residual + geometry (≤0.25 m steps) or the readable shader's height-tint banding; L31 measures it. +- **Important finding (thanks to Luna's caveat):** L29's "both recipes" passage matrix ran SurfaceWorld twice. On + the real `--recipe=floating-islands`, the rock-column connected-ends audit fails in all 4 styles (1/2 or 3/4). + Since L27, FI column results were never really tested. L31 priority. diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/RunCost.py b/Standalone/Research/WorldAssemblerM3/Linux30/RunCost.py new file mode 100644 index 0000000..6f596e7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/RunCost.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Interleave five W12 hash-off whole-view cost triplets for L24/L29/L30.""" +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l30" +OUT.mkdir(parents=True, exist_ok=True) +BINARIES = { + "l24": ROOT / "Saved/Linux/l24-source/bin/linux/VoxelForgeCoreHeadless-linux24-clang19", + "l29": ROOT / "Saved/Linux/bin/VoxelForgeCoreHeadless-linux29-clang", + "l30": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade", +} +ORDERS = ( + ("l24", "l29", "l30"), + ("l29", "l30", "l24"), + ("l30", "l24", "l29"), + ("l24", "l30", "l29"), + ("l29", "l24", "l30"), +) +WHOLE_READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s") +rows = [] +for recipe in ("surface-world", "floating-islands"): + for pair, order in enumerate(ORDERS, 1): + for build in order: + name = f"cost-speedfade-{recipe}-pair{pair}-{build}" + logfile = OUT / f"{name}.log" + command = [ + "prlimit", "--as=6442450944", "--", str(BINARIES[build]), + str(ROOT / "Standalone/data/owner_world.json"), + "--workers=12", "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", + ] + started = time.monotonic() + with logfile.open("w", encoding="utf-8") as stream: + subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + check=True, timeout=1800) + elapsed = time.monotonic() - started + text = logfile.read_text(encoding="utf-8", errors="replace") + matches = WHOLE_READY.findall(text) + if len(matches) != 1 or "hash=off" not in text or "cache_hits=0" not in text: + raise SystemExit(f"invalid cost run: {name}; see {logfile}") + ready = float(matches[0]) + rows.append((recipe, pair, build, ready, elapsed)) + print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s " + f"process_elapsed={elapsed:.3f}s", flush=True) + +summary = OUT / "cost-runs-speedfade.csv" +summary.write_text("recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds\n" + + "".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.6f}\n" + for recipe, pair, build, ready, elapsed in rows), encoding="utf-8") +print(f"wrote {summary}", flush=True) diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/RunCrossing30.py b/Standalone/Research/WorldAssemblerM3/Linux30/RunCrossing30.py new file mode 100644 index 0000000..8659973 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux30/RunCrossing30.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Verify speed-only L30 hashes on the 24 m crossing against L29.""" +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l30" +OUT.mkdir(parents=True, exist_ok=True) +BINARIES = { + "clang": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade", + "gcc": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade-gcc", +} +PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +passed = True +for compiler, binary in BINARIES.items(): + for recipe in ("surface-world", "floating-islands"): + reference_path = ROOT / f"Saved/Linux/linux29-final-crossing24-{compiler}-{recipe}.log" + reference = PATTERN.findall(reference_path.read_text(encoding="utf-8")) + for workers in (1, 8, 12): + name = f"crossing-speedonly-default-{compiler}-{recipe}-w{workers}" + logfile = OUT / f"{name}.log" + command = [ + "prlimit", "--as=6442450944", "--", str(binary), + str(ROOT / "Standalone/data/owner_world.json"), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--stream-hash", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", f"--workers={workers}", + "--camera-step=24,0,0", + "--region-crossing", f"--recipe={recipe}", + ] + started = time.monotonic() + with logfile.open("w", encoding="utf-8") as stream: + subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + check=True, timeout=1800) + elapsed = time.monotonic() - started + rows = PATTERN.findall(logfile.read_text(encoding="utf-8", errors="replace")) + ok = len(reference) == 12 and rows == reference + passed &= ok + print(f"{compiler} {recipe} W{workers} elapsed={elapsed:.3f}s " + f"hash_rows={len(rows)} {'MATCH' if ok else 'MISMATCH'}", flush=True) +print(f"[CrossingSpeedOnly] runs=12 status={'PASS' if passed else 'FAIL'}", flush=True) +raise SystemExit(0 if passed else 1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after-heightmap.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after-heightmap.jpg new file mode 100644 index 0000000..45284b5 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after-heightmap.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after.jpg new file mode 100644 index 0000000..4d2059c Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before-heightmap.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before-heightmap.jpg new file mode 100644 index 0000000..727918f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before-heightmap.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before.jpg new file mode 100644 index 0000000..65bebf6 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux30/captures/fi-floor-gradient-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-gradient-heightmap.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-gradient-heightmap.jpg new file mode 100644 index 0000000..45284b5 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-gradient-heightmap.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-hard-heightmap.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-hard-heightmap.jpg new file mode 100644 index 0000000..4c98a90 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-hard-heightmap.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-interleaved-heightmap.jpg b/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-interleaved-heightmap.jpg new file mode 100644 index 0000000..13e9af1 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux30/captures/transition-interleaved-heightmap.jpg differ