mirror of
https://github.com/Garux/netradiant-custom.git
synced 2026-09-28 00:00:03 +02:00
add Irregular Grid vertex jitter and Grid Step height snapping
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user