add Irregular Grid vertex jitter and Grid Step height snapping

This commit is contained in:
vld_voiculescu
2026-08-09 14:59:53 +03:00
parent b11baccb54
commit 1c14e4c12a
5 changed files with 217 additions and 127 deletions
+44 -49
View File
@@ -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;
+1 -1
View File
@@ -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,
+113 -18
View File
@@ -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 );
}
}
+13 -5
View File
@@ -9,6 +9,14 @@
using HeightMap = std::map<std::pair<double, double>, double>;
using WallMap = std::map<std::pair<double, double>, 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<std::pair<double, double>, 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 );
+46 -54
View File
@@ -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