From 1c14e4c12ab63f79bc398b6d43490aea6250f03c Mon Sep 17 00:00:00 2001 From: vld_voiculescu Date: Sun, 9 Aug 2026 14:59:53 +0300 Subject: [PATCH] add Irregular Grid vertex jitter and Grid Step height snapping --- contrib/terrain_generator/brush_builder.cpp | 93 ++++++------- contrib/terrain_generator/brush_builder.h | 2 +- contrib/terrain_generator/terrain_engine.cpp | 131 +++++++++++++++--- contrib/terrain_generator/terrain_engine.h | 18 ++- .../terrain_generator/terrain_generator.cpp | 100 ++++++------- 5 files changed, 217 insertions(+), 127 deletions(-) diff --git a/contrib/terrain_generator/brush_builder.cpp b/contrib/terrain_generator/brush_builder.cpp index d5f19912..96e49c3f 100644 --- a/contrib/terrain_generator/brush_builder.cpp +++ b/contrib/terrain_generator/brush_builder.cpp @@ -52,26 +52,27 @@ static void select_generated( scene::Node& node ){ Entity_setSelected( *instance, true ); } -static void insert_brush_into( scene::Node& entity, - double x, double y, double min_z, - double mx, double my, double base_max_z, - double z_bl, double z_tl, double z_br, double z_tr, +// bl/tl/br/tr carry each corner's own X/Y position along with its sampled +// height: bl.x==x, tl.x==x, br.x==mx, tr.x==mx etc. +static void insert_brush_into( scene::Node& entity, double min_z, double base_max_z, + const GridPoint& bl, const GridPoint& tl, + const GridPoint& br, const GridPoint& tr, const char* top_tex, const char* caulk, bool split_diagonally, bool alt_dir ){ if ( !split_diagonally ) { NodeSmartReference brush( GlobalBrushCreator().createBrush() ); _QERFaceData face; - fill_face( face, x, y, base_max_z, x, my, base_max_z, mx, y, base_max_z, top_tex ); + fill_face( face, bl.x, bl.y, base_max_z, tl.x, tl.y, base_max_z, br.x, br.y, base_max_z, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, y, min_z, mx, y, min_z, x, my, min_z, caulk ); + fill_face( face, bl.x, bl.y, min_z, br.x, br.y, min_z, tl.x, tl.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, mx, y, min_z, mx, y, base_max_z, mx, my, min_z, caulk ); + fill_face( face, br.x, br.y, min_z, br.x, br.y, base_max_z, tr.x, tr.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, y, min_z, x, my, min_z, x, y, base_max_z, caulk ); + fill_face( face, bl.x, bl.y, min_z, tl.x, tl.y, min_z, bl.x, bl.y, base_max_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, my, min_z, mx, my, min_z, x, my, base_max_z, caulk ); + fill_face( face, tl.x, tl.y, min_z, tr.x, tr.y, min_z, tl.x, tl.y, base_max_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, y, min_z, x, y, base_max_z, mx, y, min_z, caulk ); + fill_face( face, bl.x, bl.y, min_z, bl.x, bl.y, base_max_z, br.x, br.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); Node_getTraversable( entity )->insert( brush ); } @@ -81,15 +82,15 @@ static void insert_brush_into( scene::Node& entity, { NodeSmartReference brush( GlobalBrushCreator().createBrush() ); _QERFaceData face; - fill_face( face, x, y, z_bl, x, my, z_tl, mx, y, z_br, top_tex ); + fill_face( face, bl.x, bl.y, bl.z, tl.x, tl.y, tl.z, br.x, br.y, br.z, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, y, min_z, mx, y, min_z, x, my, min_z, caulk ); + fill_face( face, bl.x, bl.y, min_z, br.x, br.y, min_z, tl.x, tl.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, y, min_z, x, my, min_z, x, y, base_max_z, caulk ); + fill_face( face, bl.x, bl.y, min_z, tl.x, tl.y, min_z, bl.x, bl.y, base_max_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, y, min_z, x, y, base_max_z, mx, y, min_z, caulk ); + fill_face( face, bl.x, bl.y, min_z, bl.x, bl.y, base_max_z, br.x, br.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, mx, y, min_z, mx, y, base_max_z, x, my, min_z, caulk ); + fill_face( face, br.x, br.y, min_z, br.x, br.y, base_max_z, tl.x, tl.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); Node_getTraversable( entity )->insert( brush ); } @@ -97,15 +98,15 @@ static void insert_brush_into( scene::Node& entity, { NodeSmartReference brush( GlobalBrushCreator().createBrush() ); _QERFaceData face; - fill_face( face, mx, my, z_tr, mx, y, z_br, x, my, z_tl, top_tex ); + fill_face( face, tr.x, tr.y, tr.z, br.x, br.y, br.z, tl.x, tl.y, tl.z, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, mx, my, min_z, x, my, min_z, mx, y, min_z, caulk ); + fill_face( face, tr.x, tr.y, min_z, tl.x, tl.y, min_z, br.x, br.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, mx, y, min_z, mx, y, base_max_z, mx, my, min_z, caulk ); + fill_face( face, br.x, br.y, min_z, br.x, br.y, base_max_z, tr.x, tr.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, my, min_z, mx, my, min_z, x, my, base_max_z, caulk ); + fill_face( face, tl.x, tl.y, min_z, tr.x, tr.y, min_z, tl.x, tl.y, base_max_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, my, min_z, x, my, base_max_z, mx, y, min_z, caulk ); + fill_face( face, tl.x, tl.y, min_z, tl.x, tl.y, base_max_z, br.x, br.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); Node_getTraversable( entity )->insert( brush ); } @@ -116,15 +117,15 @@ static void insert_brush_into( scene::Node& entity, { NodeSmartReference brush( GlobalBrushCreator().createBrush() ); _QERFaceData face; - fill_face( face, x, my, z_tl, mx, my, z_tr, x, y, z_bl, top_tex ); + fill_face( face, tl.x, tl.y, tl.z, tr.x, tr.y, tr.z, bl.x, bl.y, bl.z, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, my, min_z, x, y, min_z, mx, my, min_z, caulk ); + fill_face( face, tl.x, tl.y, min_z, bl.x, bl.y, min_z, tr.x, tr.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, y, min_z, x, my, min_z, x, y, base_max_z, caulk ); + fill_face( face, bl.x, bl.y, min_z, tl.x, tl.y, min_z, bl.x, bl.y, base_max_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, my, min_z, mx, my, min_z, x, my, base_max_z, caulk ); + fill_face( face, tl.x, tl.y, min_z, tr.x, tr.y, min_z, tl.x, tl.y, base_max_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, mx, my, min_z, x, y, min_z, x, y, base_max_z, caulk ); + fill_face( face, tr.x, tr.y, min_z, bl.x, bl.y, min_z, bl.x, bl.y, base_max_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); Node_getTraversable( entity )->insert( brush ); } @@ -132,15 +133,15 @@ static void insert_brush_into( scene::Node& entity, { NodeSmartReference brush( GlobalBrushCreator().createBrush() ); _QERFaceData face; - fill_face( face, mx, my, z_tr, mx, y, z_br, x, y, z_bl, top_tex ); + fill_face( face, tr.x, tr.y, tr.z, br.x, br.y, br.z, bl.x, bl.y, bl.z, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, mx, my, min_z, x, y, min_z, mx, y, min_z, caulk ); + fill_face( face, tr.x, tr.y, min_z, bl.x, bl.y, min_z, br.x, br.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, mx, y, min_z, mx, y, base_max_z, mx, my, min_z, caulk ); + fill_face( face, br.x, br.y, min_z, br.x, br.y, base_max_z, tr.x, tr.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, y, min_z, x, y, base_max_z, mx, y, min_z, caulk ); + fill_face( face, bl.x, bl.y, min_z, bl.x, bl.y, base_max_z, br.x, br.y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); - fill_face( face, x, y, min_z, mx, my, min_z, x, y, base_max_z, caulk ); + fill_face( face, bl.x, bl.y, min_z, tr.x, tr.y, min_z, bl.x, bl.y, base_max_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face ); Node_getTraversable( entity )->insert( brush ); } @@ -152,7 +153,7 @@ static void insert_brush_into( scene::Node& entity, // --------------------------------------------------------------------------- void build_terrain_brushes( const BrushData& target, double step_x, double step_y, - const HeightMap& height_map, const char* top_texture, + const TerrainMap& height_map, const char* top_texture, bool split_diagonally ){ // Undo is started by the caller so deletion + generation form one step. scene::Node& entity = create_func_group(); @@ -170,20 +171,20 @@ void build_terrain_brushes( const BrushData& target, double step_x, double step_ double mx = x + step_x < target.max_x ? x + step_x : target.max_x; double my = y + step_y < target.max_y ? y + step_y : target.max_y; - auto lookup = [&]( double kx, double ky ) -> double { + auto lookup = [&]( double kx, double ky ) -> GridPoint { auto it = height_map.find({ r2( kx ), r2( ky ) }); - return it != height_map.end() ? it->second : min_z; + return it != height_map.end() ? it->second : GridPoint{ kx, ky, min_z }; }; - double z_bl = lookup( x, y ); - double z_tl = lookup( x, my ); - double z_br = lookup( mx, y ); - double z_tr = lookup( mx, my ); + GridPoint bl = lookup( x, y ); + GridPoint tl = lookup( x, my ); + GridPoint br = lookup( mx, y ); + GridPoint tr = lookup( mx, my ); bool alt_dir = ( ( x_index + y_index ) % 2 ) != 0; - insert_brush_into( entity, x, y, min_z, mx, my, base_max_z, - z_bl, z_tl, z_br, z_tr, + insert_brush_into( entity, min_z, base_max_z, + bl, tl, br, tr, top_texture, caulk, split_diagonally, alt_dir ); } } @@ -278,16 +279,10 @@ static void insert_wall_brush( scene::Node& entity, double outer, double limit, const char* top_tex, const char* caulk, bool is_left, bool alt_dir, Axis axis ){ - // Wall geometry is floor/ceiling rotated 90°. - // For a Y-running tunnel (walls at fixed X, left/right), coordinate mapping: - // floor(fx,fy,fz) → world(fz, fx, fy) — gu→fx, gz→fy, gmx_u→fmx, gmx_z→fmy, - // x_*→fz_*, outer/limit→fmin_z/fsolid_top. This is a proper 90° rotation, so - // face winding carries over unchanged. - // For an X-running tunnel (walls at fixed Y, near/far), the same swap - // (world(fx, fz, fy)) is a mirror image of the above rather than a rotation, - // so v1/v2 are swapped per face to keep normals pointing outward. - // Left/near wall: inner surface faces away from outer (min_z side). - // Right/far wall: inner surface faces away from outer (solid_top side). + // Wall geometry is floor/ceiling rotated 90°: world(fz,fx,fy) for a + // Y-running tunnel. That's a proper rotation, so winding is unchanged; for + // an X-running tunnel the same axis swap is a mirror image instead, so + // v1/v2 are swapped per face below to keep normals pointing outward. _QERFaceData face; const bool is_ceiling = !is_left; const double min_z = is_left ? outer : limit; diff --git a/contrib/terrain_generator/brush_builder.h b/contrib/terrain_generator/brush_builder.h index d2fad3d4..e6450b57 100644 --- a/contrib/terrain_generator/brush_builder.h +++ b/contrib/terrain_generator/brush_builder.h @@ -6,7 +6,7 @@ // the scene graph. For standard terrain, one func_group is created. // For tunnel terrain, four func_groups are created (floor, ceiling, left wall, right wall). void build_terrain_brushes( const BrushData& target, double step_x, double step_y, - const HeightMap& height_map, const char* top_texture, + const TerrainMap& height_map, const char* top_texture, bool split_diagonally ); void build_tunnel_brushes( const BrushData& target, double step_x, double step_y, diff --git a/contrib/terrain_generator/terrain_engine.cpp b/contrib/terrain_generator/terrain_engine.cpp index f60b12f6..503f495c 100644 --- a/contrib/terrain_generator/terrain_engine.cpp +++ b/contrib/terrain_generator/terrain_engine.cpp @@ -26,6 +26,58 @@ static double sample_noise( NoiseType noise_type, double x, double y ){ } } +// A too-thin jittered triangle makes the engine's corner-plane-intersection +// math ill-conditioned, occasionally producing a wildly-off vertex that +// corrupts selection bounds on regeneration. Fix: snap any cell whose +// triangles fall under a minimum area back to its exact grid positions — +// always safe. One forward pass suffices since a revert only ever moves a +// vertex toward its nominal (safer) position, never away from it. +static void repair_thin_triangles( TerrainMap& height_map, const BrushData& target, + double step_x, double step_y ){ + constexpr double MIN_AREA_FRACTION = 0.15; // of a normal half-cell triangle + const double min_area = MIN_AREA_FRACTION * 0.5 * step_x * step_y; + + auto tri_area = []( const GridPoint& a, const GridPoint& b, const GridPoint& c ) -> double { + return 0.5 * std::abs( ( b.x - a.x ) * ( c.y - a.y ) - ( c.x - a.x ) * ( b.y - a.y ) ); + }; + auto lookup = [&]( double kx, double ky ) -> GridPoint* { + auto it = height_map.find( { round2( kx ), round2( ky ) } ); + return it != height_map.end() ? &it->second : nullptr; + }; + auto revert = [&]( double gx, double gy ){ + if ( GridPoint* p = lookup( gx, gy ) ) { + p->x = gx; + p->y = gy; + } + }; + + int x_index = 0; + for ( double x = target.min_x; x < target.max_x - 0.01; x += step_x, ++x_index ) { + int y_index = 0; + for ( double y = target.min_y; y < target.max_y - 0.01; y += step_y, ++y_index ) { + double mx = x + step_x < target.max_x ? x + step_x : target.max_x; + double my = y + step_y < target.max_y ? y + step_y : target.max_y; + + GridPoint* bl = lookup( x, y ); + GridPoint* tl = lookup( x, my ); + GridPoint* br = lookup( mx, y ); + GridPoint* tr = lookup( mx, my ); + if ( !bl || !tl || !br || !tr ) + continue; + + // Matches the triangle split insert_brush_into actually builds. + bool alt_dir = ( ( x_index + y_index ) % 2 ) != 0; + bool bad = !alt_dir + ? ( tri_area( *bl, *tl, *br ) < min_area || tri_area( *tr, *br, *tl ) < min_area ) + : ( tri_area( *tl, *tr, *bl ) < min_area || tri_area( *tr, *br, *bl ) < min_area ); + + if ( bad ) { + revert( x, y ); revert( x, my ); revert( mx, y ); revert( mx, my ); + } + } + } +} + // --------------------------------------------------------------------------- BrushData make_manual_brush_data( double width, double length, double height ){ @@ -64,16 +116,20 @@ void adjust_bounds_to_fit_grid( BrushData& target, double step_x, double step_y // Standard heightmap // --------------------------------------------------------------------------- -HeightMap generate_height_map( const BrushData& target, double step_x, double step_y, +TerrainMap generate_height_map( const BrushData& target, double step_x, double step_y, ShapeType shape_type, double shape_height, double variance, double frequency, NoiseType noise_type, double terrace_step, - Axis axis ){ - HeightMap height_map; + Axis axis, bool jitter_grid, double grid_step ){ + TerrainMap height_map; double seed_x = random_double() * 10000.0; double seed_y = random_double() * 10000.0; + // Max fraction of a cell a vertex may drift by. Kept under 0.5 so two + // neighbors jittering toward each other can never cross. + constexpr double JITTER_FRACTION = 0.35; + for ( double x = target.min_x; x <= target.max_x + 0.01; x += step_x ) { for ( double y = target.min_y; y <= target.max_y + 0.01; y += step_y ) { double nx = target.width_x > 0 ? ( x - target.min_x ) / target.width_x : 0.0; @@ -120,10 +176,8 @@ HeightMap generate_height_map( const BrushData& target, double step_x, double st break; } - // Terrace the clean shape height first, before noise is mixed in, so - // terrace bands stay flat strips instead of being broken up by - // per-vertex noise jitter. Noise is then layered on top as surface - // detail on the (possibly stepped) base height. + // Terrace before noise is added, so terrace bands stay flat strips + // instead of being broken up by per-vertex noise. double shape_z = target.max_z + base_z; if ( shape_type != ShapeType::Flat && terrace_step > 0.0 ) shape_z = std::floor( shape_z / terrace_step ) * terrace_step; @@ -139,12 +193,51 @@ HeightMap generate_height_map( const BrushData& target, double step_x, double st } } - double final_z = shape_z + noise_z; + // Snap only the noise to grid_step, anchored at shape_z rather than + // absolute world Z — anchoring at world Z made a large step's effect + // depend on where the brush happened to sit in the map (sometimes + // flattening it, sometimes shifting its whole height). + double snapped_noise = std::round( noise_z / grid_step ) * grid_step; + double final_z = std::round( shape_z + snapped_noise ); - height_map[{ round2( x ), round2( y ) }] = std::round( final_z ); + // High variance on a thin target can push the surface at/below the + // floor. Clamping to exactly min_z isn't enough — a whole clamped + // neighborhood would give a cell zero thickness (still a degenerate, + // hole-causing brush) — so clamp to a small positive margin instead. + constexpr double MIN_THICKNESS = 4.0; + final_z = std::max( final_z, target.min_z + MIN_THICKNESS ); + + // Jitter the vertex's world position (not its grid key) so triangles + // vary in size instead of tiling identical rectangles. Boundary + // vertices are left unjittered so the footprint matches target bounds. + // Offset is a random whole number of grid_step increments, picked + // directly within [-max_steps, max_steps] so it can never overshoot + // JITTER_FRACTION's safety margin the way round-then-clamp could. + auto grid_jitter = [&]( double step ) -> double { + int max_steps = (int)std::floor( ( JITTER_FRACTION * step ) / grid_step ); + if ( max_steps <= 0 ) + return 0.0; + int steps = (int)( random_double() * ( 2 * max_steps + 1 ) ) - max_steps; + return std::clamp( steps, -max_steps, max_steps ) * grid_step; // guards random_double()==1.0 + }; + + double jx = x, jy = y; + if ( jitter_grid ) { + const bool interior_x = ( x > target.min_x + 0.01 && x < target.max_x - 0.01 ); + const bool interior_y = ( y > target.min_y + 0.01 && y < target.max_y - 0.01 ); + if ( interior_x ) + jx = std::round( x + grid_jitter( step_x ) ); + if ( interior_y ) + jy = std::round( y + grid_jitter( step_y ) ); + } + + height_map[{ round2( x ), round2( y ) }] = GridPoint{ jx, jy, final_z }; } } + if ( jitter_grid ) + repair_thin_triangles( height_map, target, step_x, step_y ); + return height_map; } @@ -156,7 +249,7 @@ TunnelMaps generate_tunnel_height_maps( const BrushData& target, double step_x, double cave_height, double slope_height, double variance, double frequency, NoiseType noise_type, double terrace_step, - Axis axis ){ + Axis axis, double grid_step ){ TunnelMaps result; double seed_floor_x = random_double() * 10000.0; @@ -188,10 +281,7 @@ TunnelMaps generate_tunnel_height_maps( const BrushData& target, double step_x, double floor_base = base_z + blend * ( cave_height * 0.25 ); double ceil_base = base_z + cave_height - blend * ( cave_height * 0.25 ); - // Terrace the clean floor/ceiling first, before noise is mixed in, so - // terrace bands stay flat strips instead of being broken up by - // per-vertex noise jitter (see the standard-terrain heightmap for the - // same reasoning). + // Terrace before noise, as above, so bands stay flat. if ( terrace_step > 0.0 ) { floor_base = std::floor( floor_base / terrace_step ) * terrace_step; ceil_base = std::ceil( ceil_base / terrace_step ) * terrace_step; @@ -210,8 +300,12 @@ TunnelMaps generate_tunnel_height_maps( const BrushData& target, double step_x, } } - double floor_z = floor_base + floor_noise; - double ceil_z = ceil_base - ceil_noise; + // Snap only the noise, anchored at floor_base/ceil_base, as above. + double snapped_floor_noise = std::round( floor_noise / grid_step ) * grid_step; + double snapped_ceil_noise = std::round( ceil_noise / grid_step ) * grid_step; + + double floor_z = floor_base + snapped_floor_noise; + double ceil_z = ceil_base - snapped_ceil_noise; if ( floor_z > ceil_z ) { double mid = ( floor_z + ceil_z ) / 2.0; @@ -256,7 +350,8 @@ TunnelMaps generate_tunnel_height_maps( const BrushData& target, double step_x, ( u + seed_wall_l ) * frequency, ( z + seed_wall_l ) * frequency ) ) * variance; } } - result.left_wall_map[{ ru, rz }] = std::round( wall_lo + wall_noise ); + // Snap only the noise, anchored at wall_lo/wall_hi, as above. + result.left_wall_map[{ ru, rz }] = std::round( wall_lo + std::round( wall_noise / grid_step ) * grid_step ); wall_noise = 0.0; if ( variance > 0.0 ) { @@ -267,7 +362,7 @@ TunnelMaps generate_tunnel_height_maps( const BrushData& target, double step_x, ( u + seed_wall_r ) * frequency, ( z + seed_wall_r ) * frequency ) ) * variance; } } - result.right_wall_map[{ ru, rz }] = std::round( wall_hi - wall_noise ); + result.right_wall_map[{ ru, rz }] = std::round( wall_hi - std::round( wall_noise / grid_step ) * grid_step ); } } diff --git a/contrib/terrain_generator/terrain_engine.h b/contrib/terrain_generator/terrain_engine.h index 3a9161ab..1270a6b6 100644 --- a/contrib/terrain_generator/terrain_engine.h +++ b/contrib/terrain_generator/terrain_engine.h @@ -9,6 +9,14 @@ using HeightMap = std::map, double>; using WallMap = std::map, double>; +// A standard-terrain grid vertex. x/y may be jittered away from the grid's +// exact (rounded) key position when jitter_grid is enabled — z is the height. +struct GridPoint +{ + double x, y, z; +}; +using TerrainMap = std::map, GridPoint>; + struct TunnelMaps { HeightMap floor_map; @@ -36,8 +44,8 @@ enum class NoiseType { Random = 2 }; -// Direction Slope / Ridge / Valley run along. Ignored by radially symmetric -// shapes (Hill, Crater, Volcano) and by Tunnel/SlopeTunnel, which run along Y. +// Direction Slope / Ridge / Valley / Tunnel / SlopeTunnel run along. Ignored +// by radially symmetric shapes (Hill, Crater, Volcano). enum class Axis { X = 0, Y = 1 @@ -47,14 +55,14 @@ BrushData make_manual_brush_data( double width, double length, double height ); void adjust_bounds_to_fit_grid( BrushData& target, double step_x, double step_y ); -HeightMap generate_height_map( const BrushData& target, double step_x, double step_y, +TerrainMap generate_height_map( const BrushData& target, double step_x, double step_y, ShapeType shape_type, double shape_height, double variance, double frequency, NoiseType noise_type, double terrace_step, - Axis axis ); + Axis axis, bool jitter_grid, double grid_step ); TunnelMaps generate_tunnel_height_maps( const BrushData& target, double step_x, double step_y, double cave_height, double slope_height, double variance, double frequency, NoiseType noise_type, double terrace_step, - Axis axis ); + Axis axis, double grid_step ); diff --git a/contrib/terrain_generator/terrain_generator.cpp b/contrib/terrain_generator/terrain_generator.cpp index 41202a07..e9853743 100644 --- a/contrib/terrain_generator/terrain_generator.cpp +++ b/contrib/terrain_generator/terrain_generator.cpp @@ -181,6 +181,14 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush form->addRow( "Step X:", step_x_spin ); form->addRow( "Step Y:", step_y_spin ); + // Jitters each vertex's X/Y position (standard terrain only) so + // triangles vary in size instead of tiling identical rectangles. + auto *jitter_check = new QCheckBox( "Irregular Grid" ); + jitter_check->setToolTip( "Varies triangle sizes instead of a uniform grid.\n" + "Occasionally produces an oddly steep or spiky triangle —\n" + "if that happens, just regenerate." ); + form->addRow( "", jitter_check ); + // Base shape auto *shape_combo = new ComboBox; shape_combo->addItem( "Flat (None)", (int)ShapeType::Flat ); @@ -195,10 +203,8 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush shape_combo->setCurrentIndex( 0 ); // Flat form->addRow( "Base Shape:", shape_combo ); - // Axis — meaningful for the directional shapes (Slope, Ridge, Valley) and - // for tunnels (which way the tunnel runs). Radially symmetric shapes - // (Hill, Crater, Volcano) ignore it. Default differs per shape (see - // update_shape below) to match each shape's original hardcoded axis. + // Direction for Slope/Ridge/Valley and tunnels; ignored by radially + // symmetric shapes. Default differs per shape (see update_shape below). auto *axis_combo = new ComboBox; axis_combo->addItem( "X Axis", (int)Axis::X ); axis_combo->addItem( "Y Axis", (int)Axis::Y ); @@ -219,6 +225,14 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush auto *terrace_spin = new DoubleSpinBox( 0, 512, 0, 2, 8 ); form->addRow( "Terrace Step:", terrace_spin ); + // Every generated vertex height snaps to a multiple of this, so it lands + // cleanly on the editor's grid for manual editing later. + auto *grid_step_combo = new ComboBox; + for ( int v : { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 } ) + grid_step_combo->addItem( QString::number( v ), v ); + grid_step_combo->setCurrentIndex( 0 ); // 1 + form->addRow( "Grid Step:", grid_step_combo ); + // Noise type auto *noise_combo = new ComboBox; noise_combo->addItem( "Perlin Noise", (int)NoiseType::Perlin ); @@ -267,10 +281,7 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush } ); pick_timer->start(); - // Surface the texture browser and leave it open so the user can keep - // picking. Closing it automatically proved fragile and is poor UX. - // A tooltip covers the embedded-layout case, where there is no separate - // browser window to raise (it's always visible in the main window). + // Leave the browser open after picking (auto-closing it was fragile/poor UX). tex_pick->setToolTip( "Open the texture browser to pick a shader.\n" "If the browser is docked in the main window layout,\n" "select a texture there and it fills in here automatically." ); @@ -279,9 +290,7 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush } ); form->addRow( "Texture:", tex_widget ); - // Generate (generate without closing) + Close - // Buttons: Generate (left) — Close (right), explicit layout so the - // order is platform-independent. + // Explicit layout (Generate left, Close right) so order is platform-independent. auto *btn_widget = new QWidget; auto *btn_layout = new QHBoxLayout( btn_widget ); btn_layout->setContentsMargins( 0, 12, 0, 0 ); // top spacing from fields @@ -301,12 +310,9 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush lbl->setVisible( visible ); }; - // --- Per-entity persisted footprint size ------------------------------ - // Generated terrain records the footprint SIZE it was built from as a key - // on its func_group(s). On regeneration we reuse that stored size (so the - // terrain stops growing) but take the position from the live selection (so - // it follows wherever the terrain was moved). The size lives on each piece - // of terrain, so a second brush never picks up the first one's size. + // Generated terrain stores the footprint size it was built from as a key + // on its func_group(s), so regeneration reuses that size (stops growth) + // while taking position from the live selection (follows if moved). const char* const TERRAINGEN_KEY = "_terraingen_size"; // Fills w/l/h and returns true if any selected node (or its parent entity) @@ -395,10 +401,12 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush const ShapeType shape = (ShapeType)shape_combo->currentData().toInt(); const Axis axis = (Axis)axis_combo->currentData().toInt(); const NoiseType noise = (NoiseType)noise_combo->currentData().toInt(); + const bool jitter_grid = jitter_check->isChecked(); const double tun_height = tunnel_height_spin->value(); const double variance = variance_spin->value(); const double frequency = frequency_spin->value(); const double terrace = terrace_spin->value(); + const double grid_step = grid_step_combo->currentData().toInt(); const std::string texture_str = texture_edit->text().toStdString(); const char* texture = texture_str.c_str(); @@ -414,12 +422,9 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush target = make_manual_brush_data( manual_w_spin->value(), manual_l_spin->value(), manual_h_spin->value() ); } else { - // Reuse the stored footprint size (so regeneration doesn't grow), - // but take the position from the live selection (so it follows the - // terrain if it was moved). Peaks and walls extend symmetrically, - // so the live center equals the footprint center; the floor sits at - // the live minimum Z. A fresh brush has no stored size and uses its - // own bounds directly. + // Reuse the stored size but recenter on the live selection (peaks/ + // walls extend symmetrically, so live center == footprint center). + // A fresh brush has no stored size and uses its own bounds directly. double sw, sl, sh; SelBounds s{}; if ( read_stored_size( sw, sl, sh ) ) { @@ -442,12 +447,9 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush target.min_y = s.y0; target.max_y = s.y1; target.min_z = s.z0; - // Base level the terrain builds up from. Slopes use the slope - // height so the ramp's top rises with it while its low edge - // stays at the floor (z0 + 64); otherwise a steep slope sinks - // the low edge below the floor, clips away geometry and makes - // regeneration drift sideways. Other shapes use the selection - // height. + // Slopes use the slope height (so the low edge stays at z0+64 + // instead of sinking below the floor on a steep drop); other + // shapes use the selection height. const bool is_slope = ( shape == ShapeType::Slope || shape == ShapeType::SlopeTunnel ); const double top_h = is_slope ? std::max( shape_height_spin->value(), 64.0 ) : std::max( s.z1 - s.z0, 64.0 ); @@ -466,19 +468,12 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush } } - // For Slope / Slope Tunnel in Use Selection mode, derive the slope - // height from the brush's Z extent so the terrain descends from the - // top of the brush down to a minimum height of 64 units. - // A negative value flips the engine's formula (base_z = height * nx) - // so it slopes downward instead of upward. - // For Slope/SlopeTunnel in Use Selection mode the spinbox holds the - // drop amount (auto-filled from brush Z − 64). Negate it so the - // engine formula (base_z = shape_height * nx) slopes downward. + // For Slope/SlopeTunnel in Use Selection mode, the spinbox holds the + // full brush Z height; negate the drop amount (Z − 64) so the engine + // formula (base_z = shape_height * nx) descends to min_z + 64 instead + // of ascending. const bool slope_from_sel = !use_manual && ( shape == ShapeType::Slope || shape == ShapeType::SlopeTunnel ); - // Spinbox shows the full brush Z height. For the downward slope the - // engine needs the drop amount (full_z − 64), negated so the - // formula base_z = shape_height*nx descends to min_z + 64. const double shape_height = slope_from_sel ? -( shape_height_spin->value() - 64.0 ) : shape_height_spin->value(); @@ -532,12 +527,12 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush const double cave_height = ( shape == ShapeType::SlopeTunnel ) ? tun_height : shape_height; const double slope_height = ( shape == ShapeType::SlopeTunnel ) ? shape_height : 0; const double tunnel_terrace = ( shape == ShapeType::SlopeTunnel ) ? terrace : 0.0; - auto maps = generate_tunnel_height_maps( target, step_x, step_y, cave_height, slope_height, variance, frequency, noise, tunnel_terrace, axis ); + auto maps = generate_tunnel_height_maps( target, step_x, step_y, cave_height, slope_height, variance, frequency, noise, tunnel_terrace, axis, grid_step ); build_tunnel_brushes( target, step_x, step_y, maps, texture, cave_height, slope_height, axis ); } else { - bool split_diagonally = ( variance > 0 || shape != ShapeType::Flat ); - auto height_map = generate_height_map( target, step_x, step_y, shape, shape_height, variance, frequency, noise, terrace, axis ); + bool split_diagonally = ( variance > 0 || shape != ShapeType::Flat || jitter_grid ); + auto height_map = generate_height_map( target, step_x, step_y, shape, shape_height, variance, frequency, noise, terrace, axis, jitter_grid, grid_step ); build_terrain_brushes( target, step_x, step_y, height_map, texture, split_diagonally ); } @@ -583,10 +578,8 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush if ( use_sel ) { refresh_sel_labels(); if ( slope_derived() ) { - // Derive the slope height from the stored footprint height when - // the selection has one, so it stays consistent with the size - // used for generation. Using the live (grown) selection here - // over-steepens the slope and pushes the floor below the base. + // Prefer the stored footprint height over the live (grown) + // selection, which would over-steepen the slope. double sw, sl, sh; if ( read_stored_size( sw, sl, sh ) ) { shape_height_spin->setValue( sh ); @@ -625,9 +618,7 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush set_row_visible( axis_combo, has_axis ); if ( has_axis ) { - // Match each shape's original hardcoded axis: Slope/Ridge/Valley - // ran along X, tunnels ran along Y. Re-applied on every shape - // switch, same as the slope-height auto-fill below. + // Matches each shape's original hardcoded axis (Slope/Ridge/Valley: X, tunnels: Y). axis_combo->setCurrentIndex( is_tunnel_shape ? (int)Axis::Y : (int)Axis::X ); } set_row_visible( shape_height_spin, !is_flat ); @@ -636,10 +627,8 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush lbl->setText( shape_height_label[idx] ); // Auto-fill slope height from selection when applicable if ( slope_derived() ) { - // Derive the slope height from the stored footprint height when - // the selection has one, so it stays consistent with the size - // used for generation. Using the live (grown) selection here - // over-steepens the slope and pushes the floor below the base. + // Prefer the stored footprint height over the live (grown) + // selection, which would over-steepen the slope. double sw, sl, sh; if ( read_stored_size( sw, sl, sh ) ) { shape_height_spin->setValue( sh ); @@ -655,6 +644,9 @@ void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush // Terrace not applicable to flat tunnels (no slope to step), // but valid for slope tunnels where the floor descends along Y set_row_visible( terrace_spin, !is_flat && st != ShapeType::Tunnel ); + // Irregular grid only applies to standard terrain (generate_height_map), + // not tunnels. + set_row_visible( jitter_check, !is_tunnel_shape ); }; // Wire signals