mirror of
https://github.com/Garux/netradiant-custom.git
synced 2026-09-28 00:00:03 +02:00
Add terrain generation plugin
This commit is contained in:
@@ -446,6 +446,7 @@ binaries-radiant-plugins: \
|
||||
$(INSTALLDIR)/plugins/sunplug.$(DLL) \
|
||||
$(INSTALLDIR)/plugins/ufoaiplug.$(DLL) \
|
||||
$(INSTALLDIR)/plugins/meshtex.$(DLL) \
|
||||
$(INSTALLDIR)/plugins/terrain_generator.$(DLL) \
|
||||
|
||||
.PHONY: binaries-radiant
|
||||
binaries-radiant-core: \
|
||||
@@ -1186,6 +1187,14 @@ $(INSTALLDIR)/plugins/sunplug.$(DLL): CPPFLAGS_EXTRA := $(CPPFLAGS_GLIB) $(CPPFL
|
||||
$(INSTALLDIR)/plugins/sunplug.$(DLL): \
|
||||
contrib/sunplug/sunplug.o \
|
||||
|
||||
$(INSTALLDIR)/plugins/terrain_generator.$(DLL): LIBS_EXTRA := $(LIBS_GLIB) $(LIBS_QTWIDGETS)
|
||||
$(INSTALLDIR)/plugins/terrain_generator.$(DLL): CPPFLAGS_EXTRA := $(CPPFLAGS_GLIB) $(CPPFLAGS_QTWIDGETS) -Ilibs -Iinclude
|
||||
$(INSTALLDIR)/plugins/terrain_generator.$(DLL): \
|
||||
contrib/terrain_generator/terrain_generator.o \
|
||||
contrib/terrain_generator/noise.o \
|
||||
contrib/terrain_generator/terrain_engine.o \
|
||||
contrib/terrain_generator/brush_builder.o \
|
||||
|
||||
$(INSTALLDIR)/qdata3.$(EXE): LIBS_EXTRA := $(LIBS_XML)
|
||||
$(INSTALLDIR)/qdata3.$(EXE): CPPFLAGS_EXTRA := $(CPPFLAGS_XML) -Itools/quake2/common -Ilibs -Iinclude -Wno-format-overflow
|
||||
$(INSTALLDIR)/qdata3.$(EXE): \
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
#include "brush_builder.h"
|
||||
|
||||
#include "debugging/debugging.h"
|
||||
#include "ibrush.h"
|
||||
#include "ientity.h"
|
||||
#include "ieclass.h"
|
||||
#include "iscenegraph.h"
|
||||
#include "iundo.h"
|
||||
#include "qerplugin.h"
|
||||
#include "scenelib.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void fill_face( _QERFaceData& face,
|
||||
double x0, double y0, double z0,
|
||||
double x1, double y1, double z1,
|
||||
double x2, double y2, double z2,
|
||||
const char* shader ){
|
||||
face.m_p0[0] = x0; face.m_p0[1] = y0; face.m_p0[2] = z0;
|
||||
face.m_p1[0] = x1; face.m_p1[1] = y1; face.m_p1[2] = z1;
|
||||
face.m_p2[0] = x2; face.m_p2[1] = y2; face.m_p2[2] = z2;
|
||||
face.m_shader = shader;
|
||||
face.m_texdef.scale[0] = 0.03125f; // matches ( ( 0.03125 0 0 ) ( 0 0.03125 0 ) ) in .map
|
||||
face.m_texdef.scale[1] = 0.03125f;
|
||||
face.m_texdef.shift[0] = 0;
|
||||
face.m_texdef.shift[1] = 0;
|
||||
face.m_texdef.rotate = 0;
|
||||
face.contents = 0;
|
||||
face.flags = 0;
|
||||
face.value = 0;
|
||||
}
|
||||
|
||||
static scene::Node& create_func_group(){
|
||||
EntityClass* ec = GlobalEntityClassManager().findOrInsert( "func_group", true );
|
||||
NodeSmartReference entity( GlobalEntityCreator().createEntity( ec ) );
|
||||
Node_getTraversable( GlobalSceneGraph().root() )->insert( entity );
|
||||
return entity;
|
||||
}
|
||||
|
||||
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,
|
||||
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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, mx, y, min_z, x, my, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, x, my, min_z, x, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, x, y, base_max_z, mx, y, min_z, caulk );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
else if ( !alt_dir ) {
|
||||
// Standard diagonal: BL→TR split
|
||||
// Triangle 1: BL, TL, BR
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
_QERFaceData face;
|
||||
fill_face( face, x, y, z_bl, x, my, z_tl, mx, y, z_br, top_tex );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, mx, y, min_z, x, my, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, x, y, base_max_z, mx, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
// Triangle 2: TR, BR, TL
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
_QERFaceData face;
|
||||
fill_face( face, mx, my, z_tr, mx, y, z_br, x, my, z_tl, top_tex );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, mx, my, min_z, x, my, min_z, mx, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, my, min_z, mx, my, min_z, x, my, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Alternate diagonal: TL→BR split (checkerboard pattern)
|
||||
// Triangle 1: TL, TR, BL
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
_QERFaceData face;
|
||||
fill_face( face, x, my, z_tl, mx, my, z_tr, x, y, z_bl, top_tex );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, my, min_z, x, y, min_z, mx, my, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, my, min_z, mx, my, min_z, x, my, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
// Triangle 2: TR, BR, BL
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
_QERFaceData face;
|
||||
fill_face( face, mx, my, z_tr, mx, y, z_br, x, y, z_bl, top_tex );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, mx, my, min_z, x, y, min_z, mx, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, x, y, base_max_z, mx, 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 );
|
||||
GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Standard terrain
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void build_terrain_brushes( const BrushData& target, double step_x, double step_y,
|
||||
const HeightMap& height_map, const char* top_texture,
|
||||
bool split_diagonally ){
|
||||
UndoableCommand undo( "terrainGenerator.generateTerrain" );
|
||||
|
||||
scene::Node& entity = create_func_group();
|
||||
|
||||
const char* caulk = "textures/common/caulk";
|
||||
double min_z = target.min_z;
|
||||
double base_max_z = target.max_z;
|
||||
|
||||
auto r2 = []( double v ) { return std::round( v * 100.0 ) / 100.0; };
|
||||
|
||||
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;
|
||||
|
||||
auto lookup = [&]( double kx, double ky ) -> double {
|
||||
auto it = height_map.find({ r2( kx ), r2( ky ) });
|
||||
return it != height_map.end() ? it->second : 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 );
|
||||
|
||||
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,
|
||||
top_texture, caulk, split_diagonally, alt_dir );
|
||||
}
|
||||
}
|
||||
|
||||
SceneChangeNotify();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tunnel terrain
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void insert_floor_ceil_brush( scene::Node& entity,
|
||||
double x, double y, double mx, double my,
|
||||
double z_bl, double z_tl, double z_br, double z_tr,
|
||||
double min_z, double solid_top,
|
||||
const char* top_tex, const char* caulk,
|
||||
bool is_ceiling, bool alt_dir ){
|
||||
_QERFaceData face;
|
||||
|
||||
if ( !alt_dir ) {
|
||||
// BL→TR split
|
||||
// Triangle 1: BL, TL, BR
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
if ( !is_ceiling ) {
|
||||
fill_face( face, x, y, z_bl, x, my, z_tl, mx, y, z_br, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, mx, y, min_z, x, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
} else {
|
||||
fill_face( face, x, y, z_bl, mx, y, z_br, x, my, z_tl, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, solid_top, x, my, solid_top, mx, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
}
|
||||
fill_face( face, x, y, min_z, x, my, min_z, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, x, y, solid_top, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, mx, y, min_z, mx, y, solid_top, x, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
// Triangle 2: TR, BR, TL
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
if ( !is_ceiling ) {
|
||||
fill_face( face, mx, my, z_tr, mx, y, z_br, x, my, z_tl, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, mx, my, min_z, x, my, min_z, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
} else {
|
||||
fill_face( face, mx, my, z_tr, x, my, z_tl, mx, y, z_br, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, mx, my, solid_top, mx, y, solid_top, x, my, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
}
|
||||
fill_face( face, mx, y, min_z, mx, y, solid_top, mx, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, my, min_z, mx, my, min_z, x, my, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, my, min_z, x, my, solid_top, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TL→BR split
|
||||
// Triangle 1: TL, TR, BL
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
if ( !is_ceiling ) {
|
||||
fill_face( face, x, my, z_tl, mx, my, z_tr, x, y, z_bl, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, my, min_z, x, y, min_z, mx, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
} else {
|
||||
fill_face( face, x, my, z_tl, x, y, z_bl, mx, my, z_tr, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, my, solid_top, mx, my, solid_top, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
}
|
||||
fill_face( face, x, y, min_z, x, my, min_z, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, my, min_z, mx, my, min_z, x, my, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, mx, my, min_z, x, y, min_z, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
// Triangle 2: TR, BR, BL
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
if ( !is_ceiling ) {
|
||||
fill_face( face, mx, my, z_tr, mx, y, z_br, x, y, z_bl, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, mx, my, min_z, x, y, min_z, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
} else {
|
||||
fill_face( face, mx, my, z_tr, x, y, z_bl, mx, y, z_br, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, mx, my, solid_top, mx, y, solid_top, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
}
|
||||
fill_face( face, mx, y, min_z, mx, y, solid_top, mx, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, x, y, solid_top, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fill_face( face, x, y, min_z, mx, my, min_z, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void insert_wall_brush( scene::Node& entity,
|
||||
double x_bl, double x_tl, double x_br, double x_tr,
|
||||
double gy, double gmx_y, double gz, double gmx_z,
|
||||
double outer_x, double limit_x,
|
||||
const char* top_tex, const char* caulk,
|
||||
bool is_left, bool alt_dir ){
|
||||
// Wall geometry is floor/ceiling rotated 90°.
|
||||
// Coordinate mapping: floor(fx,fy,fz) → world(fz, fx, fy)
|
||||
// gy→fx, gz→fy, gmx_y→fmx, gmx_z→fmy, x_*→fz_*, outer_x/limit_x→fmin_z/fsolid_top
|
||||
// Left wall: inner surface faces +X (like floor), cap at outer_x (min_z side).
|
||||
// Right wall: inner surface faces −X (like ceiling), cap at outer_x (solid_top side).
|
||||
_QERFaceData face;
|
||||
const bool is_ceiling = !is_left;
|
||||
const double min_z = is_left ? outer_x : limit_x;
|
||||
const double solid_top = is_left ? limit_x : outer_x;
|
||||
|
||||
// Aliases to match insert_floor_ceil_brush variable names exactly.
|
||||
const double x = gy, y = gz, mx = gmx_y, my = gmx_z;
|
||||
const double z_bl = x_bl, z_tl = x_tl, z_br = x_br, z_tr = x_tr;
|
||||
|
||||
// fw: emit a face using floor coordinate order but with world axes permuted.
|
||||
auto fw = [&]( double fx0, double fy0, double fz0,
|
||||
double fx1, double fy1, double fz1,
|
||||
double fx2, double fy2, double fz2,
|
||||
const char* shader ){
|
||||
fill_face( face, fz0, fx0, fy0, fz1, fx1, fy1, fz2, fx2, fy2, shader );
|
||||
};
|
||||
|
||||
if ( !alt_dir ) {
|
||||
// BL→TR split
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
if ( !is_ceiling ) {
|
||||
fw( x, y, z_bl, x, my, z_tl, mx, y, z_br, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, y, min_z, mx, y, min_z, x, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
} else {
|
||||
fw( x, y, z_bl, mx, y, z_br, x, my, z_tl, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, y, solid_top, x, my, solid_top, mx, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
}
|
||||
fw( x, y, min_z, x, my, min_z, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, y, min_z, x, y, solid_top, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( mx, y, min_z, mx, y, solid_top, x, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
if ( !is_ceiling ) {
|
||||
fw( mx, my, z_tr, mx, y, z_br, x, my, z_tl, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( mx, my, min_z, x, my, min_z, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
} else {
|
||||
fw( mx, my, z_tr, x, my, z_tl, mx, y, z_br, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( mx, my, solid_top, mx, y, solid_top, x, my, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
}
|
||||
fw( mx, y, min_z, mx, y, solid_top, mx, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, my, min_z, mx, my, min_z, x, my, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, my, min_z, x, my, solid_top, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
}
|
||||
else {
|
||||
// TL→BR split
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
if ( !is_ceiling ) {
|
||||
fw( x, my, z_tl, mx, my, z_tr, x, y, z_bl, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, my, min_z, x, y, min_z, mx, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
} else {
|
||||
fw( x, my, z_tl, x, y, z_bl, mx, my, z_tr, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, my, solid_top, mx, my, solid_top, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
}
|
||||
fw( x, y, min_z, x, my, min_z, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, my, min_z, mx, my, min_z, x, my, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( mx, my, min_z, x, y, min_z, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
{
|
||||
NodeSmartReference brush( GlobalBrushCreator().createBrush() );
|
||||
if ( !is_ceiling ) {
|
||||
fw( mx, my, z_tr, mx, y, z_br, x, y, z_bl, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( mx, my, min_z, x, y, min_z, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
} else {
|
||||
fw( mx, my, z_tr, x, y, z_bl, mx, y, z_br, top_tex ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( mx, my, solid_top, mx, y, solid_top, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
}
|
||||
fw( mx, y, min_z, mx, y, solid_top, mx, my, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, y, min_z, x, y, solid_top, mx, y, min_z, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
fw( x, y, min_z, mx, my, min_z, x, y, solid_top, caulk ); GlobalBrushCreator().Brush_addFace( brush, face );
|
||||
Node_getTraversable( entity )->insert( brush );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void build_tunnel_brushes( const BrushData& target, double step_x, double step_y,
|
||||
const TunnelMaps& maps, const char* top_texture,
|
||||
double cave_height, double slope_height ){
|
||||
UndoableCommand undo( "terrainGenerator.generateTunnel" );
|
||||
|
||||
scene::Node& floor_entity = create_func_group();
|
||||
scene::Node& ceil_entity = create_func_group();
|
||||
scene::Node& lwall_entity = create_func_group();
|
||||
scene::Node& rwall_entity = create_func_group();
|
||||
|
||||
const char* caulk = "textures/common/caulk";
|
||||
double min_z = target.min_z;
|
||||
// Highest ceiling point — slope_height may be negative (downward slope),
|
||||
// so the ceiling peak is always at the high end of the slope.
|
||||
double max_ceil_z = target.max_z + cave_height + std::max( 0.0, slope_height );
|
||||
double ceil_solid_top = max_ceil_z + target.height_z;
|
||||
|
||||
auto r2 = []( double v ) { return std::round( v * 100.0 ) / 100.0; };
|
||||
|
||||
// Floor and ceiling
|
||||
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;
|
||||
|
||||
auto safe_at = [&]( const HeightMap& m, double kx, double ky, double fallback ) -> double {
|
||||
auto it = m.find( { r2( kx ), r2( ky ) } );
|
||||
if ( it == m.end() ) {
|
||||
globalErrorStream() << "TerrainGenerator: height map missing key ("
|
||||
<< kx << ", " << ky << ") — using fallback\n";
|
||||
return fallback;
|
||||
}
|
||||
return it->second;
|
||||
};
|
||||
|
||||
double f_bl = safe_at( maps.floor_map, x, y, min_z );
|
||||
double f_tl = safe_at( maps.floor_map, x, my, min_z );
|
||||
double f_br = safe_at( maps.floor_map, mx, y, min_z );
|
||||
double f_tr = safe_at( maps.floor_map, mx, my, min_z );
|
||||
double c_bl = safe_at( maps.ceiling_map, x, y, max_ceil_z );
|
||||
double c_tl = safe_at( maps.ceiling_map, x, my, max_ceil_z );
|
||||
double c_br = safe_at( maps.ceiling_map, mx, y, max_ceil_z );
|
||||
double c_tr = safe_at( maps.ceiling_map, mx, my, max_ceil_z );
|
||||
|
||||
bool alt_dir = ( ( x_index + y_index ) % 2 ) != 0;
|
||||
|
||||
insert_floor_ceil_brush( floor_entity, x, y, mx, my,
|
||||
f_bl, f_tl, f_br, f_tr,
|
||||
min_z, max_ceil_z, top_texture, caulk, false, alt_dir );
|
||||
insert_floor_ceil_brush( ceil_entity, x, y, mx, my,
|
||||
c_bl, c_tl, c_br, c_tr,
|
||||
min_z, ceil_solid_top, top_texture, caulk, true, alt_dir );
|
||||
}
|
||||
}
|
||||
|
||||
// Walls — must match the range generated by terrain_engine.
|
||||
// slope_height may be negative (downward slope), so clamp accordingly.
|
||||
double wall_min_z = target.max_z + std::min( 0.0, slope_height );
|
||||
double wall_max_z = wall_min_z + cave_height + std::abs( slope_height );
|
||||
double step_z = maps.step_z;
|
||||
double outer_left = target.min_x - step_x;
|
||||
double outer_right = target.max_x + step_x;
|
||||
double limit_x = ( target.min_x + target.max_x ) / 2.0;
|
||||
|
||||
int gy_index = 0;
|
||||
for ( double gy = target.min_y; gy < target.max_y - 0.01; gy += step_y, ++gy_index ) {
|
||||
int gz_index = 0;
|
||||
for ( double gz = wall_min_z; gz < wall_max_z - 0.01; gz += step_z, ++gz_index ) {
|
||||
double gmx_y = gy + step_y < target.max_y ? gy + step_y : target.max_y;
|
||||
double gmx_z = gz + step_z < wall_max_z ? gz + step_z : wall_max_z;
|
||||
|
||||
auto safe_wall = [&]( const HeightMap& m, double kx, double ky, double fallback ) -> double {
|
||||
auto it = m.find( { r2( kx ), r2( ky ) } );
|
||||
if ( it == m.end() ) {
|
||||
globalErrorStream() << "TerrainGenerator: wall map missing key ("
|
||||
<< kx << ", " << ky << ") — using fallback\n";
|
||||
return fallback;
|
||||
}
|
||||
return it->second;
|
||||
};
|
||||
|
||||
double lx_bl = safe_wall( maps.left_wall_map, gy, gz, limit_x );
|
||||
double lx_tl = safe_wall( maps.left_wall_map, gy, gmx_z, limit_x );
|
||||
double lx_br = safe_wall( maps.left_wall_map, gmx_y, gz, limit_x );
|
||||
double lx_tr = safe_wall( maps.left_wall_map, gmx_y, gmx_z, limit_x );
|
||||
|
||||
double rx_bl = safe_wall( maps.right_wall_map, gy, gz, limit_x );
|
||||
double rx_tl = safe_wall( maps.right_wall_map, gy, gmx_z, limit_x );
|
||||
double rx_br = safe_wall( maps.right_wall_map, gmx_y, gz, limit_x );
|
||||
double rx_tr = safe_wall( maps.right_wall_map, gmx_y, gmx_z, limit_x );
|
||||
|
||||
bool alt_dir = ( ( gy_index + gz_index ) % 2 ) != 0;
|
||||
|
||||
insert_wall_brush( lwall_entity, lx_bl, lx_tl, lx_br, lx_tr,
|
||||
gy, gmx_y, gz, gmx_z, outer_left, limit_x, top_texture, caulk, true, alt_dir );
|
||||
insert_wall_brush( rwall_entity, rx_bl, rx_tl, rx_br, rx_tr,
|
||||
gy, gmx_y, gz, gmx_z, outer_right, limit_x, top_texture, caulk, false, alt_dir );
|
||||
}
|
||||
}
|
||||
|
||||
SceneChangeNotify();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "terrain_engine.h"
|
||||
|
||||
// Generates terrain brushes into func_group entities and inserts them into
|
||||
// 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,
|
||||
bool split_diagonally );
|
||||
|
||||
void build_tunnel_brushes( const BrushData& target, double step_x, double step_y,
|
||||
const TunnelMaps& maps, const char* top_texture,
|
||||
double cave_height, double slope_height );
|
||||
@@ -0,0 +1,157 @@
|
||||
#include "noise.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Perlin noise
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const int perlin_perm_src[256] = {
|
||||
151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233, 7,225,
|
||||
140, 36,103, 30, 69,142, 8, 99, 37,240, 21, 10, 23,190, 6,148,
|
||||
247,120,234, 75, 0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
|
||||
57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
|
||||
74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
|
||||
60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
|
||||
65, 25, 63,161, 1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
|
||||
200,196,135,130,116,188,159, 86,164,100,109,198,173,186, 3, 64,
|
||||
52,217,226,250,124,123, 5,202, 38,147,118,126,255, 82, 85,212,
|
||||
207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
|
||||
119,248,152, 2, 44,154,163, 70,221,153,101,155,167, 43,172, 9,
|
||||
129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
|
||||
218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
|
||||
81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
|
||||
184, 84,204,176,115,121, 50, 45,127, 4,150,254,138,236,205, 93,
|
||||
222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180
|
||||
};
|
||||
|
||||
static int perlin_p[512];
|
||||
|
||||
static bool perlin_initialised = false;
|
||||
|
||||
static void perlin_init(){
|
||||
if ( perlin_initialised ) return;
|
||||
for ( int i = 0; i < 256; ++i )
|
||||
perlin_p[i] = perlin_p[i + 256] = perlin_perm_src[i];
|
||||
perlin_initialised = true;
|
||||
}
|
||||
|
||||
static double perlin_fade( double t ){
|
||||
return t * t * t * ( t * ( t * 6.0 - 15.0 ) + 10.0 );
|
||||
}
|
||||
|
||||
static double perlin_lerp( double t, double a, double b ){
|
||||
return a + t * ( b - a );
|
||||
}
|
||||
|
||||
static double perlin_grad( int hash, double x, double y ){
|
||||
int h = hash & 15;
|
||||
double u = h < 8 ? x : y;
|
||||
double v = h < 4 ? y : ( h == 12 || h == 14 ? x : 0.0 );
|
||||
return ( ( h & 1 ) == 0 ? u : -u ) + ( ( h & 2 ) == 0 ? v : -v );
|
||||
}
|
||||
|
||||
double Perlin::noise( double x, double y ){
|
||||
perlin_init();
|
||||
int X = (int)std::floor( x ) & 255;
|
||||
int Y = (int)std::floor( y ) & 255;
|
||||
x -= std::floor( x );
|
||||
y -= std::floor( y );
|
||||
double u = perlin_fade( x );
|
||||
double v = perlin_fade( y );
|
||||
int A = perlin_p[X] + Y;
|
||||
int B = perlin_p[X + 1] + Y;
|
||||
return perlin_lerp( v,
|
||||
perlin_lerp( u, perlin_grad( perlin_p[A], x, y ),
|
||||
perlin_grad( perlin_p[B], x - 1.0, y ) ),
|
||||
perlin_lerp( u, perlin_grad( perlin_p[A + 1], x, y - 1.0 ),
|
||||
perlin_grad( perlin_p[B + 1], x - 1.0, y - 1.0 ) ) );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Simplex noise
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const int simplex_perm_src[256] = {
|
||||
151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233, 7,225,
|
||||
140, 36,103, 30, 69,142, 8, 99, 37,240, 21, 10, 23,190, 6,148,
|
||||
247,120,234, 75, 0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
|
||||
57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
|
||||
74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
|
||||
60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
|
||||
65, 25, 63,161, 1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
|
||||
200,196,135,130,116,188,159, 86,164,100,109,198,173,186, 3, 64,
|
||||
52,217,226,250,124,123, 5,202, 38,147,118,126,255, 82, 85,212,
|
||||
207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
|
||||
119,248,152, 2, 44,154,163, 70,221,153,101,155,167, 43,172, 9,
|
||||
129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
|
||||
218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
|
||||
81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
|
||||
184, 84,204,176,115,121, 50, 45,127, 4,150,254,138,236,205, 93,
|
||||
222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180
|
||||
};
|
||||
|
||||
static int simplex_perm[512];
|
||||
static bool simplex_initialised = false;
|
||||
|
||||
static void simplex_init(){
|
||||
if ( simplex_initialised ) return;
|
||||
for ( int i = 0; i < 256; ++i )
|
||||
simplex_perm[i] = simplex_perm[i + 256] = simplex_perm_src[i];
|
||||
simplex_initialised = true;
|
||||
}
|
||||
|
||||
static const int grad3[12][2] = {
|
||||
{ 1, 1},{ -1, 1},{ 1,-1},{ -1,-1},
|
||||
{ 1, 0},{ -1, 0},{ 1, 0},{ -1, 0},
|
||||
{ 0, 1},{ 0,-1},{ 0, 1},{ 0,-1}
|
||||
};
|
||||
|
||||
static int simplex_fast_floor( double x ){
|
||||
int xi = (int)x;
|
||||
return x < xi ? xi - 1 : xi;
|
||||
}
|
||||
|
||||
static double simplex_dot( const int g[2], double x, double y ){
|
||||
return g[0] * x + g[1] * y;
|
||||
}
|
||||
|
||||
double Simplex::noise( double x, double y ){
|
||||
simplex_init();
|
||||
|
||||
const double F2 = 0.5 * ( std::sqrt( 3.0 ) - 1.0 );
|
||||
const double G2 = ( 3.0 - std::sqrt( 3.0 ) ) / 6.0;
|
||||
|
||||
double s = ( x + y ) * F2;
|
||||
int i = simplex_fast_floor( x + s );
|
||||
int j = simplex_fast_floor( y + s );
|
||||
|
||||
double t = ( i + j ) * G2;
|
||||
double x0 = x - ( i - t );
|
||||
double y0 = y - ( j - t );
|
||||
|
||||
int i1 = x0 > y0 ? 1 : 0;
|
||||
int j1 = x0 > y0 ? 0 : 1;
|
||||
|
||||
double x1 = x0 - i1 + G2;
|
||||
double y1 = y0 - j1 + G2;
|
||||
double x2 = x0 - 1.0 + 2.0 * G2;
|
||||
double y2 = y0 - 1.0 + 2.0 * G2;
|
||||
|
||||
int ii = i & 255;
|
||||
int jj = j & 255;
|
||||
int gi0 = simplex_perm[ii + simplex_perm[jj ]] % 12;
|
||||
int gi1 = simplex_perm[ii + i1 + simplex_perm[jj + j1]] % 12;
|
||||
int gi2 = simplex_perm[ii + 1 + simplex_perm[jj + 1 ]] % 12;
|
||||
|
||||
double t0 = 0.5 - x0*x0 - y0*y0;
|
||||
double n0 = t0 < 0.0 ? 0.0 : ( t0*t0*t0*t0 ) * simplex_dot( grad3[gi0], x0, y0 );
|
||||
|
||||
double t1 = 0.5 - x1*x1 - y1*y1;
|
||||
double n1 = t1 < 0.0 ? 0.0 : ( t1*t1*t1*t1 ) * simplex_dot( grad3[gi1], x1, y1 );
|
||||
|
||||
double t2 = 0.5 - x2*x2 - y2*y2;
|
||||
double n2 = t2 < 0.0 ? 0.0 : ( t2*t2*t2*t2 ) * simplex_dot( grad3[gi2], x2, y2 );
|
||||
|
||||
return 70.0 * ( n0 + n1 + n2 );
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace Perlin
|
||||
{
|
||||
double noise( double x, double y );
|
||||
}
|
||||
|
||||
namespace Simplex
|
||||
{
|
||||
double noise( double x, double y );
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
#include "terrain_engine.h"
|
||||
#include "noise.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
#include <numbers>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static double round2( double v ){
|
||||
return std::round( v * 100.0 ) / 100.0;
|
||||
}
|
||||
|
||||
static double random_double(){
|
||||
return (double)std::rand() / (double)RAND_MAX;
|
||||
}
|
||||
|
||||
static double sample_noise( NoiseType noise_type, double x, double y ){
|
||||
switch ( noise_type ) {
|
||||
case NoiseType::Perlin: return Perlin::noise( x, y );
|
||||
case NoiseType::Simplex: return Simplex::noise( x, y );
|
||||
default: return random_double() * 2.0 - 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
BrushData make_manual_brush_data( double width, double length, double height ){
|
||||
BrushData b;
|
||||
b.min_x = -width / 2.0;
|
||||
b.max_x = width / 2.0;
|
||||
b.min_y = -length / 2.0;
|
||||
b.max_y = length / 2.0;
|
||||
b.min_z = 0.0;
|
||||
b.max_z = height;
|
||||
b.width_x = width;
|
||||
b.length_y = length;
|
||||
b.height_z = height;
|
||||
return b;
|
||||
}
|
||||
|
||||
void adjust_bounds_to_fit_grid( BrushData& target, double step_x, double step_y ){
|
||||
double new_width = std::max( step_x, std::round( target.width_x / step_x ) * step_x );
|
||||
double new_length = std::max( step_y, std::round( target.length_y / step_y ) * step_y );
|
||||
|
||||
if ( std::abs( target.width_x - new_width ) > 0.001 ||
|
||||
std::abs( target.length_y - new_length ) > 0.001 ) {
|
||||
double diff_x = new_width - target.width_x;
|
||||
double diff_y = new_length - target.length_y;
|
||||
|
||||
target.min_x = std::round( target.min_x - diff_x / 2.0 );
|
||||
target.max_x = target.min_x + new_width;
|
||||
target.min_y = std::round( target.min_y - diff_y / 2.0 );
|
||||
target.max_y = target.min_y + new_length;
|
||||
target.width_x = new_width;
|
||||
target.length_y = new_length;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Standard heightmap
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
HeightMap 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 ){
|
||||
HeightMap height_map;
|
||||
|
||||
double seed_x = random_double() * 10000.0;
|
||||
double seed_y = random_double() * 10000.0;
|
||||
|
||||
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;
|
||||
double ny = target.length_y > 0 ? ( y - target.min_y ) / target.length_y : 0.0;
|
||||
|
||||
double center_dist = std::min( 1.0, std::sqrt(
|
||||
( nx - 0.5 ) * ( nx - 0.5 ) + ( ny - 0.5 ) * ( ny - 0.5 ) ) / 0.5 );
|
||||
|
||||
double base_z = 0.0;
|
||||
switch ( shape_type ) {
|
||||
case ShapeType::Hill: {
|
||||
base_z = shape_height * 0.5 * ( 1.0 + std::cos( center_dist * std::numbers::pi ) );
|
||||
break;
|
||||
}
|
||||
case ShapeType::Crater: {
|
||||
base_z = shape_height * 0.5 * ( 1.0 - std::cos( center_dist * std::numbers::pi ) );
|
||||
break;
|
||||
}
|
||||
case ShapeType::Ridge: {
|
||||
double dist = std::min( 1.0, std::abs( nx - 0.5 ) / 0.5 );
|
||||
base_z = shape_height * 0.5 * ( 1.0 + std::cos( dist * std::numbers::pi ) );
|
||||
break;
|
||||
}
|
||||
case ShapeType::Slope: {
|
||||
base_z = shape_height * nx;
|
||||
break;
|
||||
}
|
||||
case ShapeType::Volcano: {
|
||||
double mountain = shape_height * 0.5 * ( 1.0 + std::cos( center_dist * std::numbers::pi ) );
|
||||
double crater_dist = std::min( 1.0, center_dist / 0.35 );
|
||||
double crater = ( shape_height * 0.7 ) * 0.5 * ( 1.0 + std::cos( crater_dist * std::numbers::pi ) );
|
||||
base_z = mountain - crater;
|
||||
break;
|
||||
}
|
||||
case ShapeType::Valley: {
|
||||
double dist = std::min( 1.0, std::abs( nx - 0.5 ) / 0.5 );
|
||||
base_z = shape_height * 0.5 * ( 1.0 - std::cos( dist * std::numbers::pi ) );
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
double noise_z = 0.0;
|
||||
if ( variance > 0.0 ) {
|
||||
if ( noise_type == NoiseType::Random ) {
|
||||
noise_z = ( random_double() * ( variance * 2.0 ) ) - variance;
|
||||
} else {
|
||||
noise_z = sample_noise( noise_type,
|
||||
( x + seed_x ) * frequency,
|
||||
( y + seed_y ) * frequency ) * variance;
|
||||
}
|
||||
}
|
||||
|
||||
double final_z = target.max_z + base_z + noise_z;
|
||||
if ( shape_type != ShapeType::Flat && terrace_step > 0.0 )
|
||||
final_z = std::floor( final_z / terrace_step ) * terrace_step;
|
||||
|
||||
height_map[{ round2( x ), round2( y ) }] = std::round( final_z );
|
||||
}
|
||||
}
|
||||
|
||||
return height_map;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tunnel heightmaps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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 ){
|
||||
TunnelMaps result;
|
||||
|
||||
double seed_floor_x = random_double() * 10000.0;
|
||||
double seed_floor_y = random_double() * 10000.0;
|
||||
double seed_ceil_x = random_double() * 10000.0;
|
||||
double seed_ceil_y = random_double() * 10000.0;
|
||||
double seed_wall_l = random_double() * 10000.0;
|
||||
double seed_wall_r = random_double() * 10000.0;
|
||||
|
||||
double center_x = ( target.min_x + target.max_x ) / 2.0;
|
||||
double half_width = target.width_x / 2.0;
|
||||
|
||||
// Floor and ceiling
|
||||
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 t = half_width > 0 ? std::min( 1.0, std::abs( x - center_x ) / half_width ) : 0.0;
|
||||
double blend = 1.0 - std::sqrt( std::max( 0.0, 1.0 - t * t ) );
|
||||
|
||||
double floor_noise = 0.0, ceil_noise = 0.0;
|
||||
if ( variance > 0.0 ) {
|
||||
if ( noise_type == NoiseType::Random ) {
|
||||
floor_noise = random_double() * variance;
|
||||
ceil_noise = random_double() * variance;
|
||||
} else {
|
||||
floor_noise = std::abs( sample_noise( noise_type,
|
||||
( x + seed_floor_x ) * frequency, ( y + seed_floor_y ) * frequency ) ) * variance;
|
||||
ceil_noise = std::abs( sample_noise( noise_type,
|
||||
( x + seed_ceil_x ) * frequency, ( y + seed_ceil_y ) * frequency ) ) * variance;
|
||||
}
|
||||
}
|
||||
|
||||
double ny = target.length_y > 0 ? ( y - target.min_y ) / target.length_y : 0.0;
|
||||
double base_z = target.max_z + slope_height * ny;
|
||||
double floor_z = base_z + blend * ( cave_height * 0.25 ) + floor_noise;
|
||||
double ceil_z = base_z + cave_height - blend * ( cave_height * 0.25 ) - ceil_noise;
|
||||
|
||||
if ( floor_z > ceil_z ) {
|
||||
double mid = ( floor_z + ceil_z ) / 2.0;
|
||||
floor_z = mid;
|
||||
ceil_z = mid;
|
||||
}
|
||||
|
||||
if ( terrace_step > 0.0 ) {
|
||||
floor_z = std::floor( floor_z / terrace_step ) * terrace_step;
|
||||
ceil_z = std::ceil( ceil_z / terrace_step ) * terrace_step;
|
||||
}
|
||||
|
||||
result.floor_map[ { round2( x ), round2( y ) }] = std::round( floor_z );
|
||||
result.ceiling_map[ { round2( x ), round2( y ) }] = std::round( ceil_z );
|
||||
}
|
||||
}
|
||||
|
||||
// Wall step in Z — walls must cover the full slope range regardless of direction.
|
||||
// slope_height may be negative (downward slope), so use abs for the span.
|
||||
double total_wall_height = cave_height + std::abs( slope_height );
|
||||
int num_z_steps = std::max( 1, (int)std::round( total_wall_height / step_x ) );
|
||||
double step_z = total_wall_height / num_z_steps;
|
||||
double wall_min_z = target.max_z + std::min( 0.0, slope_height );
|
||||
double wall_max_z = wall_min_z + total_wall_height;
|
||||
result.step_z = step_z;
|
||||
|
||||
// Left and right walls — grid over (Y, Z)
|
||||
for ( double y = target.min_y; y <= target.max_y + 0.01; y += step_y ) {
|
||||
for ( double z = wall_min_z; z <= wall_max_z + 0.01; z += step_z ) {
|
||||
double ry = round2( y );
|
||||
double rz = round2( z );
|
||||
|
||||
double wall_noise = 0.0;
|
||||
if ( variance > 0.0 ) {
|
||||
if ( noise_type == NoiseType::Random ) {
|
||||
wall_noise = random_double() * variance;
|
||||
} else {
|
||||
wall_noise = std::abs( sample_noise( noise_type,
|
||||
( y + seed_wall_l ) * frequency, ( z + seed_wall_l ) * frequency ) ) * variance;
|
||||
}
|
||||
}
|
||||
result.left_wall_map[{ ry, rz }] = std::round( target.min_x + wall_noise );
|
||||
|
||||
wall_noise = 0.0;
|
||||
if ( variance > 0.0 ) {
|
||||
if ( noise_type == NoiseType::Random ) {
|
||||
wall_noise = random_double() * variance;
|
||||
} else {
|
||||
wall_noise = std::abs( sample_noise( noise_type,
|
||||
( y + seed_wall_r ) * frequency, ( z + seed_wall_r ) * frequency ) ) * variance;
|
||||
}
|
||||
}
|
||||
result.right_wall_map[{ ry, rz }] = std::round( target.max_x - wall_noise );
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "terrain_math.h"
|
||||
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <tuple>
|
||||
|
||||
using HeightMap = std::map<std::pair<double, double>, double>;
|
||||
using WallMap = std::map<std::pair<double, double>, double>;
|
||||
|
||||
struct TunnelMaps
|
||||
{
|
||||
HeightMap floor_map;
|
||||
HeightMap ceiling_map;
|
||||
WallMap left_wall_map;
|
||||
WallMap right_wall_map;
|
||||
double step_z;
|
||||
};
|
||||
|
||||
enum class ShapeType {
|
||||
Flat = 0,
|
||||
Hill = 1,
|
||||
Crater = 2,
|
||||
Ridge = 3,
|
||||
Slope = 4,
|
||||
Volcano = 5,
|
||||
Valley = 6,
|
||||
Tunnel = 7,
|
||||
SlopeTunnel = 8
|
||||
};
|
||||
|
||||
enum class NoiseType {
|
||||
Perlin = 0,
|
||||
Simplex = 1,
|
||||
Random = 2
|
||||
};
|
||||
|
||||
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,
|
||||
ShapeType shape_type, double shape_height,
|
||||
double variance, double frequency,
|
||||
NoiseType noise_type, double terrace_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 );
|
||||
@@ -0,0 +1,565 @@
|
||||
#include "terrain_generator.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include "debugging/debugging.h"
|
||||
#include "iplugin.h"
|
||||
#include "string/string.h"
|
||||
#include "modulesystem/singletonmodule.h"
|
||||
#include "typesystem.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QDialog>
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QCheckBox>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include "gtkutil/spinbox.h"
|
||||
#include "gtkutil/combobox.h"
|
||||
|
||||
#include "scenelib.h"
|
||||
|
||||
#include "terrain_math.h"
|
||||
#include "noise.h"
|
||||
#include "terrain_engine.h"
|
||||
#include "brush_builder.h"
|
||||
|
||||
#include "ibrush.h"
|
||||
#include "ientity.h"
|
||||
#include "ieclass.h"
|
||||
#include "iscenegraph.h"
|
||||
#include "iundo.h"
|
||||
#include "iselection.h"
|
||||
#include "qerplugin.h"
|
||||
#include "modulesystem/moduleregistry.h"
|
||||
|
||||
namespace terrain_generator
|
||||
{
|
||||
|
||||
QWidget* main_window;
|
||||
|
||||
const char* init( void* hApp, void* pMainWidget ){
|
||||
main_window = static_cast<QWidget*>( pMainWidget );
|
||||
return "";
|
||||
}
|
||||
|
||||
const char* getName(){
|
||||
return "TerrainGenerator";
|
||||
}
|
||||
|
||||
const char* getCommandList(){
|
||||
return "About;Generate Terrain";
|
||||
}
|
||||
|
||||
const char* getCommandTitleList(){
|
||||
return "";
|
||||
}
|
||||
|
||||
void dispatch( const char* command, float* vMin, float* vMax, bool bSingleBrush ){
|
||||
if ( string_equal( command, "About" ) ) {
|
||||
GlobalRadiant().m_pfnMessageBox( main_window,
|
||||
"Terrain Generator\n\n"
|
||||
"Procedural CSG generation tool for id Tech 3 engines.\n\n"
|
||||
"Developed by vallz and vld",
|
||||
"About Terrain Generator",
|
||||
EMessageBoxType::Info, 0 );
|
||||
return;
|
||||
}
|
||||
if ( string_equal( command, "Generate Terrain" ) ) {
|
||||
// Query the live selection bounds from the selection system.
|
||||
// Called at dialog-open time, on mode switch, and on OK — so the
|
||||
// user can make a selection while the dialog is open and retry.
|
||||
struct SelBounds { double x0, x1, y0, y1, z0, z1; bool valid; };
|
||||
auto query_sel = [&]( double min_size = 64.0 ) -> SelBounds {
|
||||
const AABB& b = GlobalSelectionSystem().getBoundsSelected();
|
||||
SelBounds s;
|
||||
s.x0 = b.origin[0] - b.extents[0];
|
||||
s.x1 = b.origin[0] + b.extents[0];
|
||||
s.y0 = b.origin[1] - b.extents[1];
|
||||
s.y1 = b.origin[1] + b.extents[1];
|
||||
s.z0 = b.origin[2] - b.extents[2];
|
||||
s.z1 = b.origin[2] + b.extents[2];
|
||||
s.valid = ( s.x1 - s.x0 >= min_size && s.y1 - s.y0 >= min_size );
|
||||
return s;
|
||||
};
|
||||
const SelBounds init_sel = query_sel();
|
||||
|
||||
// --- Build dialog ---
|
||||
QDialog dialog( main_window, Qt::Dialog | Qt::WindowCloseButtonHint );
|
||||
dialog.setWindowTitle( "Terrain Generator" );
|
||||
|
||||
dialog.setMinimumWidth( 420 );
|
||||
auto *form = new QFormLayout( &dialog );
|
||||
|
||||
// Target mode
|
||||
auto *target_combo = new ComboBox;
|
||||
target_combo->addItem( "Use Selection", 0 );
|
||||
target_combo->addItem( "Manual Size", 1 );
|
||||
target_combo->setCurrentIndex( init_sel.valid ? 0 : 1 );
|
||||
form->addRow( "Target:", target_combo );
|
||||
|
||||
// Selection size — live labels (left) + frozen reference labels (right).
|
||||
// Reference is captured on the first Generate click and stays fixed.
|
||||
// Helper: build one inline row widget with [current | ref: value]
|
||||
auto make_sel_row = []( QLabel*& cur_lbl, QLabel*& ref_lbl ) -> QWidget* {
|
||||
auto *w = new QWidget;
|
||||
auto *layout = new QHBoxLayout( w );
|
||||
layout->setContentsMargins( 0, 0, 0, 0 );
|
||||
layout->setSpacing( 6 );
|
||||
cur_lbl = new QLabel;
|
||||
ref_lbl = new QLabel;
|
||||
ref_lbl->setStyleSheet( "color: gray;" );
|
||||
ref_lbl->hide();
|
||||
layout->addWidget( cur_lbl );
|
||||
layout->addWidget( ref_lbl );
|
||||
layout->addStretch();
|
||||
return w;
|
||||
};
|
||||
|
||||
QLabel *sel_w_label, *ref_w_label;
|
||||
QLabel *sel_l_label, *ref_l_label;
|
||||
QLabel *sel_h_label, *ref_h_label;
|
||||
auto *sel_w_row = make_sel_row( sel_w_label, ref_w_label );
|
||||
auto *sel_l_row = make_sel_row( sel_l_label, ref_l_label );
|
||||
auto *sel_h_row = make_sel_row( sel_h_label, ref_h_label );
|
||||
|
||||
auto refresh_sel_labels = [&](){
|
||||
const SelBounds s = query_sel();
|
||||
sel_w_label->setText( s.valid ? QString::number( (int)( s.x1 - s.x0 ) ) : "—" );
|
||||
sel_l_label->setText( s.valid ? QString::number( (int)( s.y1 - s.y0 ) ) : "—" );
|
||||
sel_h_label->setText( s.valid ? QString::number( (int)std::max( s.z1 - s.z0, 64.0 ) ) : "—" );
|
||||
};
|
||||
refresh_sel_labels();
|
||||
|
||||
// Reference state — captured once on first Generate, frozen after that.
|
||||
SelBounds ref_bounds{};
|
||||
bool ref_captured = false;
|
||||
|
||||
// "Use reference" checkbox — hidden until first Generate populates it.
|
||||
auto *use_ref_cb = new QCheckBox( "Use reference for regeneration" );
|
||||
use_ref_cb->setChecked( true );
|
||||
use_ref_cb->hide();
|
||||
|
||||
form->addRow( "Width (X):", sel_w_row );
|
||||
form->addRow( "Length (Y):", sel_l_row );
|
||||
form->addRow( "Height (Z):", sel_h_row );
|
||||
form->addRow( "", use_ref_cb );
|
||||
|
||||
// Manual size inputs (shown in Manual Size mode)
|
||||
auto *manual_w_spin = new SpinBox( 64, 131072, 1024, 0, 64 );
|
||||
auto *manual_l_spin = new SpinBox( 64, 131072, 1024, 0, 64 );
|
||||
auto *manual_h_spin = new SpinBox( 64, 131072, 64, 0, 64 );
|
||||
form->addRow( "Width (X):", manual_w_spin );
|
||||
form->addRow( "Length (Y):", manual_l_spin );
|
||||
form->addRow( "Height (Z):", manual_h_spin );
|
||||
|
||||
// Sub-square size: preset combo + Advanced checkbox in one row
|
||||
auto *sq_widget = new QWidget;
|
||||
auto *sq_hbox = new QHBoxLayout( sq_widget );
|
||||
sq_hbox->setContentsMargins( 0, 0, 0, 0 );
|
||||
auto *sq_combo = new ComboBox;
|
||||
for ( int v : { 8, 16, 32, 64, 128, 256, 512, 1024 } )
|
||||
sq_combo->addItem( QString::number( v ), v );
|
||||
sq_combo->setCurrentIndex( 3 ); // 64
|
||||
auto *sq_advanced = new QCheckBox( "Advanced" );
|
||||
sq_hbox->addWidget( sq_combo );
|
||||
sq_hbox->addWidget( sq_advanced );
|
||||
form->addRow( "Sub-square Size:", sq_widget );
|
||||
|
||||
// Advanced step X/Y (hidden until Advanced is checked)
|
||||
auto *step_x_spin = new SpinBox( 8, 512, 64, 0, 8 );
|
||||
auto *step_y_spin = new SpinBox( 8, 512, 64, 0, 8 );
|
||||
form->addRow( "Step X:", step_x_spin );
|
||||
form->addRow( "Step Y:", step_y_spin );
|
||||
|
||||
// Base shape
|
||||
auto *shape_combo = new ComboBox;
|
||||
shape_combo->addItem( "Flat (None)", (int)ShapeType::Flat );
|
||||
shape_combo->addItem( "Hill", (int)ShapeType::Hill );
|
||||
shape_combo->addItem( "Crater", (int)ShapeType::Crater );
|
||||
shape_combo->addItem( "Ridge", (int)ShapeType::Ridge );
|
||||
shape_combo->addItem( "Slope", (int)ShapeType::Slope );
|
||||
shape_combo->addItem( "Volcano", (int)ShapeType::Volcano );
|
||||
shape_combo->addItem( "Valley", (int)ShapeType::Valley );
|
||||
shape_combo->addItem( "Tunnel", (int)ShapeType::Tunnel );
|
||||
shape_combo->addItem( "Slope Tunnel", (int)ShapeType::SlopeTunnel );
|
||||
shape_combo->setCurrentIndex( 0 ); // Flat
|
||||
form->addRow( "Base Shape:", shape_combo );
|
||||
|
||||
// Shape height — editable spinbox (manual mode or non-slope shapes).
|
||||
// For Slope / Slope Tunnel in Use Selection mode, replaced by a
|
||||
// read-only label derived from the selection brush's Z extent.
|
||||
auto *shape_height_spin = new DoubleSpinBox( 0, 4096, 256, 2, 8 );
|
||||
form->addRow( "Peak Height:", shape_height_spin );
|
||||
|
||||
// Tunnel height — only visible for Slope Tunnel
|
||||
auto *tunnel_height_spin = new DoubleSpinBox( 0, 4096, 256, 2, 8 );
|
||||
form->addRow( "Tunnel Height:", tunnel_height_spin );
|
||||
|
||||
// Terrace step — hidden for Flat
|
||||
auto *terrace_spin = new DoubleSpinBox( 0, 512, 0, 2, 8 );
|
||||
form->addRow( "Terrace Step:", terrace_spin );
|
||||
|
||||
// Noise type
|
||||
auto *noise_combo = new ComboBox;
|
||||
noise_combo->addItem( "Perlin Noise", (int)NoiseType::Perlin );
|
||||
noise_combo->addItem( "Simplex Noise", (int)NoiseType::Simplex );
|
||||
noise_combo->addItem( "Regular (Random)", (int)NoiseType::Random );
|
||||
form->addRow( "Noise Type:", noise_combo );
|
||||
|
||||
// Variance / Frequency
|
||||
auto *variance_spin = new DoubleSpinBox( 0, 1024, 32, 2, 1 );
|
||||
form->addRow( "Variance:", variance_spin );
|
||||
auto *frequency_spin = new DoubleSpinBox( 0.0001, 1.0, 0.005, 4, 0.001 );
|
||||
form->addRow( "Frequency:", frequency_spin );
|
||||
|
||||
// Texture — line edit + Pick button to grab from texture browser
|
||||
auto *tex_widget = new QWidget;
|
||||
auto *tex_hbox = new QHBoxLayout( tex_widget );
|
||||
tex_hbox->setContentsMargins( 0, 0, 0, 0 );
|
||||
auto *texture_edit = new QLineEdit( GlobalRadiant().TextureBrowser_getSelectedShader() );
|
||||
auto *tex_pick = new QPushButton( "Pick" );
|
||||
tex_pick->setFixedWidth( 48 );
|
||||
tex_hbox->addWidget( texture_edit );
|
||||
tex_hbox->addWidget( tex_pick );
|
||||
|
||||
// Poll the selection bounds every 250ms so the W/L/H labels stay
|
||||
// current if the user changes their selection while the dialog is open.
|
||||
auto *sel_timer = new QTimer( &dialog );
|
||||
sel_timer->setInterval( 250 );
|
||||
QObject::connect( sel_timer, &QTimer::timeout, [&](){
|
||||
if ( target_combo->currentIndex() == 0 )
|
||||
refresh_sel_labels();
|
||||
} );
|
||||
sel_timer->start();
|
||||
|
||||
// Poll the texture browser every 100ms; auto-update the field and
|
||||
// (if the browser was closed before Pick opened it) auto-close it.
|
||||
QString last_polled_shader = texture_edit->text();
|
||||
bool close_browser_on_pick = false;
|
||||
|
||||
auto *pick_timer = new QTimer( &dialog );
|
||||
pick_timer->setInterval( 100 );
|
||||
QObject::connect( pick_timer, &QTimer::timeout, [&](){
|
||||
const QString current = GlobalRadiant().TextureBrowser_getSelectedShader();
|
||||
if ( current != last_polled_shader ) {
|
||||
last_polled_shader = current;
|
||||
texture_edit->setText( current );
|
||||
if ( close_browser_on_pick ) {
|
||||
close_browser_on_pick = false;
|
||||
GlobalRadiant().TextureBrowser_close();
|
||||
}
|
||||
}
|
||||
} );
|
||||
pick_timer->start();
|
||||
|
||||
QObject::connect( tex_pick, &QPushButton::clicked, [&](){
|
||||
close_browser_on_pick = !GlobalRadiant().TextureBrowser_isShown();
|
||||
GlobalRadiant().TextureBrowser_show();
|
||||
} );
|
||||
form->addRow( "Texture:", tex_widget );
|
||||
|
||||
// Generate (generate without closing) + Close
|
||||
// Buttons: Generate (left) — Close (right), explicit layout so the
|
||||
// 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
|
||||
auto *generate_btn = new QPushButton( "Generate" );
|
||||
auto *close_btn = new QPushButton( "Close" );
|
||||
btn_layout->addStretch();
|
||||
btn_layout->addWidget( generate_btn );
|
||||
btn_layout->addSpacing( 8 );
|
||||
btn_layout->addWidget( close_btn );
|
||||
btn_layout->addStretch();
|
||||
form->addRow( btn_widget );
|
||||
|
||||
// Helper: show/hide a form row (widget + its label)
|
||||
auto set_row_visible = [&]( QWidget *w, bool visible ){
|
||||
w->setVisible( visible );
|
||||
if ( auto *lbl = form->labelForField( w ) )
|
||||
lbl->setVisible( visible );
|
||||
};
|
||||
|
||||
// Shared validation + generation — returns true on success.
|
||||
// Called by both Generate and OK.
|
||||
auto do_generate = [&]() -> bool {
|
||||
if ( target_combo->currentIndex() == 0 && !query_sel().valid ) {
|
||||
GlobalRadiant().m_pfnMessageBox( main_window,
|
||||
"No valid brush is selected.\n\n"
|
||||
"Please select a brush in the viewport first,\n"
|
||||
"or switch the Target to \"Manual Size\".",
|
||||
"Terrain Generator — No Selection",
|
||||
EMessageBoxType::Error, 0 );
|
||||
return false;
|
||||
}
|
||||
if ( texture_edit->text().trimmed().isEmpty() ) {
|
||||
GlobalRadiant().m_pfnMessageBox( main_window,
|
||||
"No texture specified.\n\n"
|
||||
"Please enter a texture path or use the Pick button\n"
|
||||
"to select one from the texture browser.",
|
||||
"Terrain Generator — No Texture",
|
||||
EMessageBoxType::Error, 0 );
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Read parameters ---
|
||||
const bool use_manual = ( target_combo->currentIndex() == 1 );
|
||||
const bool advanced = sq_advanced->isChecked();
|
||||
const double step_x = advanced ? step_x_spin->value() : sq_combo->currentData().toInt();
|
||||
const double step_y = advanced ? step_y_spin->value() : sq_combo->currentData().toInt();
|
||||
const ShapeType shape = (ShapeType)shape_combo->currentData().toInt();
|
||||
const NoiseType noise = (NoiseType)noise_combo->currentData().toInt();
|
||||
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 std::string texture_str = texture_edit->text().toStdString();
|
||||
const char* texture = texture_str.c_str();
|
||||
|
||||
// --- Determine bounds (must happen before deleting the selection brush) ---
|
||||
BrushData target;
|
||||
|
||||
if ( use_manual ) {
|
||||
target = make_manual_brush_data( manual_w_spin->value(), manual_l_spin->value(), manual_h_spin->value() );
|
||||
}
|
||||
else {
|
||||
// Use the frozen reference bounds if captured and checkbox is on,
|
||||
// otherwise sample the live selection.
|
||||
const bool use_ref = ref_captured && use_ref_cb->isChecked();
|
||||
const SelBounds s = use_ref ? ref_bounds : query_sel( step_x < step_y ? step_x : step_y );
|
||||
if ( s.valid ) {
|
||||
target.min_x = s.x0;
|
||||
target.max_x = s.x1;
|
||||
target.min_y = s.y0;
|
||||
target.max_y = s.y1;
|
||||
target.min_z = s.z0;
|
||||
target.max_z = s.z0 + std::max( s.z1 - s.z0, 64.0 );
|
||||
target.width_x = target.max_x - target.min_x;
|
||||
target.length_y = target.max_y - target.min_y;
|
||||
target.height_z = target.max_z - target.min_z;
|
||||
|
||||
// Capture reference on first generation (live bounds only, not ref replay).
|
||||
if ( !ref_captured && !use_ref ) {
|
||||
ref_bounds = s;
|
||||
ref_captured = true;
|
||||
ref_w_label->setText( " Ref (X): " + QString::number( (int)( s.x1 - s.x0 ) ) );
|
||||
ref_l_label->setText( " Ref (Y): " + QString::number( (int)( s.y1 - s.y0 ) ) );
|
||||
ref_h_label->setText( " Ref (Z): " + QString::number( (int)std::max( s.z1 - s.z0, 64.0 ) ) );
|
||||
ref_w_label->show();
|
||||
ref_l_label->show();
|
||||
ref_h_label->show();
|
||||
use_ref_cb->show();
|
||||
if ( target_combo->currentIndex() == 0 )
|
||||
set_row_visible( use_ref_cb, true );
|
||||
}
|
||||
}
|
||||
else {
|
||||
target = make_manual_brush_data( manual_w_spin->value(), manual_l_spin->value(), manual_h_spin->value() );
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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();
|
||||
|
||||
// --- Delete the selection brush now that its bounds are captured ---
|
||||
if ( !use_manual ) {
|
||||
const int sel_count = (int)GlobalSelectionSystem().countSelected();
|
||||
for ( int i = 0; i < sel_count && GlobalSelectionSystem().countSelected() > 0; ++i )
|
||||
Path_deleteTop( GlobalSelectionSystem().ultimateSelected().path() );
|
||||
SceneChangeNotify();
|
||||
}
|
||||
|
||||
adjust_bounds_to_fit_grid( target, step_x, step_y );
|
||||
|
||||
// --- Generate ---
|
||||
const bool is_tunnel = ( shape == ShapeType::Tunnel || shape == ShapeType::SlopeTunnel );
|
||||
|
||||
globalOutputStream() << "TerrainGenerator: generating "
|
||||
<< ( is_tunnel ? "tunnel" : "terrain" )
|
||||
<< " — bounds ("
|
||||
<< target.width_x << " x " << target.length_y << " x " << target.height_z
|
||||
<< "), step (" << step_x << " x " << step_y << ")"
|
||||
<< ", texture: " << texture << "\n";
|
||||
|
||||
if ( is_tunnel ) {
|
||||
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 );
|
||||
build_tunnel_brushes( target, step_x, step_y, maps, texture, cave_height, slope_height );
|
||||
}
|
||||
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 );
|
||||
build_terrain_brushes( target, step_x, step_y, height_map, texture, split_diagonally );
|
||||
}
|
||||
|
||||
globalOutputStream() << "TerrainGenerator: generation complete\n";
|
||||
return true;
|
||||
};
|
||||
|
||||
QObject::connect( generate_btn, &QPushButton::clicked, [&](){ do_generate(); } );
|
||||
QObject::connect( close_btn, &QPushButton::clicked, &dialog, &QDialog::reject );
|
||||
|
||||
// Shape height label per shape type (index matches ShapeType enum value)
|
||||
static const char* shape_height_label[] = {
|
||||
nullptr, // Flat — row hidden
|
||||
"Peak Height:", // Hill
|
||||
"Crater Depth:", // Crater
|
||||
"Ridge Height:", // Ridge
|
||||
"Slope Height:", // Slope
|
||||
"Volcano Height:", // Volcano
|
||||
"Valley Depth:", // Valley
|
||||
"Tunnel Height:", // Tunnel
|
||||
"Slope Height:", // SlopeTunnel
|
||||
};
|
||||
|
||||
// Returns true when slope height should be derived from the selection
|
||||
// (Use Selection mode + Slope or Slope Tunnel shape).
|
||||
auto slope_derived = [&]() -> bool {
|
||||
const ShapeType st = (ShapeType)shape_combo->currentData().toInt();
|
||||
return target_combo->currentIndex() == 0
|
||||
&& ( st == ShapeType::Slope || st == ShapeType::SlopeTunnel );
|
||||
};
|
||||
|
||||
// Target mode toggle: read-only selection labels vs editable spinboxes.
|
||||
// Refresh labels each time the user switches to "Use Selection" so they
|
||||
// reflect whatever is selected at that moment.
|
||||
auto update_target_mode = [&]( int idx ){
|
||||
const bool use_sel = ( idx == 0 );
|
||||
if ( use_sel ) {
|
||||
refresh_sel_labels();
|
||||
if ( slope_derived() ) {
|
||||
const SelBounds s = query_sel();
|
||||
if ( s.valid )
|
||||
shape_height_spin->setValue( s.z1 - s.z0 );
|
||||
}
|
||||
}
|
||||
set_row_visible( sel_w_row, use_sel );
|
||||
set_row_visible( sel_l_row, use_sel );
|
||||
set_row_visible( sel_h_row, use_sel );
|
||||
set_row_visible( manual_w_spin, !use_sel );
|
||||
set_row_visible( manual_l_spin, !use_sel );
|
||||
set_row_visible( manual_h_spin, !use_sel );
|
||||
// Only toggle the checkbox row once a reference has been captured
|
||||
// (it must never appear in Manual Size mode).
|
||||
if ( ref_captured )
|
||||
set_row_visible( use_ref_cb, use_sel );
|
||||
};
|
||||
|
||||
// Advanced sub-square toggle
|
||||
auto update_advanced = [&]( bool advanced ){
|
||||
sq_combo->setVisible( !advanced );
|
||||
set_row_visible( step_x_spin, advanced );
|
||||
set_row_visible( step_y_spin, advanced );
|
||||
};
|
||||
|
||||
// Shape type toggle: labels + visibility
|
||||
auto update_shape = [&]( int idx ){
|
||||
const ShapeType st = (ShapeType)shape_combo->itemData( idx ).toInt();
|
||||
const bool is_flat = ( st == ShapeType::Flat );
|
||||
const bool is_slope_tunnel = ( st == ShapeType::SlopeTunnel );
|
||||
|
||||
set_row_visible( shape_height_spin, !is_flat );
|
||||
if ( !is_flat ) {
|
||||
if ( auto *lbl = qobject_cast<QLabel*>( form->labelForField( shape_height_spin ) ) )
|
||||
lbl->setText( shape_height_label[idx] );
|
||||
// Auto-fill slope height from selection when applicable
|
||||
if ( slope_derived() ) {
|
||||
const SelBounds s = query_sel();
|
||||
if ( s.valid )
|
||||
shape_height_spin->setValue( s.z1 - s.z0 );
|
||||
}
|
||||
}
|
||||
set_row_visible( tunnel_height_spin, is_slope_tunnel );
|
||||
// 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 );
|
||||
};
|
||||
|
||||
// Wire signals
|
||||
QObject::connect( target_combo, QOverload<int>::of( &QComboBox::currentIndexChanged ), update_target_mode );
|
||||
QObject::connect( sq_advanced, &QCheckBox::toggled, update_advanced );
|
||||
QObject::connect( shape_combo, QOverload<int>::of( &QComboBox::currentIndexChanged ), update_shape );
|
||||
|
||||
// Set initial visibility
|
||||
update_target_mode( target_combo->currentIndex() );
|
||||
update_advanced( false );
|
||||
update_shape( shape_combo->currentIndex() );
|
||||
|
||||
// show() instead of exec() so the dialog is non-modal — the texture
|
||||
// browser panel (and all other Radiant windows) remain interactive
|
||||
dialog.show();
|
||||
dialog.raise();
|
||||
{
|
||||
QEventLoop loop;
|
||||
QObject::connect( &dialog, &QDialog::finished, &loop, &QEventLoop::quit );
|
||||
loop.exec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace terrain_generator
|
||||
|
||||
class TerrainGeneratorDependencies :
|
||||
public GlobalRadiantModuleRef,
|
||||
public GlobalUndoModuleRef,
|
||||
public GlobalSceneGraphModuleRef,
|
||||
public GlobalSelectionModuleRef,
|
||||
public GlobalEntityModuleRef,
|
||||
public GlobalEntityClassManagerModuleRef,
|
||||
public GlobalBrushModuleRef
|
||||
{
|
||||
public:
|
||||
TerrainGeneratorDependencies() :
|
||||
GlobalEntityModuleRef( GlobalRadiant().getRequiredGameDescriptionKeyValue( "entities" ) ),
|
||||
GlobalEntityClassManagerModuleRef( GlobalRadiant().getRequiredGameDescriptionKeyValue( "entityclass" ) ),
|
||||
GlobalBrushModuleRef( GlobalRadiant().getRequiredGameDescriptionKeyValue( "brushtypes" ) ){
|
||||
}
|
||||
};
|
||||
|
||||
class TerrainGeneratorModule : public TypeSystemRef
|
||||
{
|
||||
_QERPluginTable m_plugin;
|
||||
public:
|
||||
typedef _QERPluginTable Type;
|
||||
STRING_CONSTANT( Name, "TerrainGenerator" );
|
||||
|
||||
TerrainGeneratorModule(){
|
||||
m_plugin.m_pfnQERPlug_Init = &terrain_generator::init;
|
||||
m_plugin.m_pfnQERPlug_GetName = &terrain_generator::getName;
|
||||
m_plugin.m_pfnQERPlug_GetCommandList = &terrain_generator::getCommandList;
|
||||
m_plugin.m_pfnQERPlug_GetCommandTitleList = &terrain_generator::getCommandTitleList;
|
||||
m_plugin.m_pfnQERPlug_Dispatch = &terrain_generator::dispatch;
|
||||
}
|
||||
|
||||
_QERPluginTable* getTable(){
|
||||
return &m_plugin;
|
||||
}
|
||||
};
|
||||
|
||||
typedef SingletonModule<TerrainGeneratorModule, TerrainGeneratorDependencies> SingletonTerrainGeneratorModule;
|
||||
SingletonTerrainGeneratorModule g_TerrainGeneratorModule;
|
||||
|
||||
extern "C" void RADIANT_DLLEXPORT Radiant_RegisterModules( ModuleServer& server ){
|
||||
initialiseModule( server );
|
||||
g_TerrainGeneratorModule.selfRegister();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
; terrain_generator.def : Declares the module parameters for the DLL.
|
||||
|
||||
LIBRARY "TERRAIN_GENERATOR"
|
||||
|
||||
EXPORTS
|
||||
Radiant_RegisterModules @1
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "modulesystem.h"
|
||||
|
||||
void terrain_generator_register_modules( ModuleServer& server );
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
// Named Vec3d (double precision) to avoid conflict with Radiant's Vec3d (float)
|
||||
struct Vec3d
|
||||
{
|
||||
double x, y, z;
|
||||
|
||||
Vec3d() : x( 0 ), y( 0 ), z( 0 ) {}
|
||||
Vec3d( double x, double y, double z ) : x( x ), y( y ), z( z ) {}
|
||||
|
||||
Vec3d operator+( const Vec3d& b ) const { return { x + b.x, y + b.y, z + b.z }; }
|
||||
Vec3d operator-( const Vec3d& b ) const { return { x - b.x, y - b.y, z - b.z }; }
|
||||
Vec3d operator*( double d ) const { return { x * d, y * d, z * d }; }
|
||||
Vec3d operator/( double d ) const { return { x / d, y / d, z / d }; }
|
||||
|
||||
static double dot( const Vec3d& a, const Vec3d& b ){
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z;
|
||||
}
|
||||
static Vec3d cross( const Vec3d& a, const Vec3d& b ){
|
||||
return {
|
||||
a.y * b.z - a.z * b.y,
|
||||
a.z * b.x - a.x * b.z,
|
||||
a.x * b.y - a.y * b.x
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
struct BrushData
|
||||
{
|
||||
double min_x, max_x;
|
||||
double min_y, max_y;
|
||||
double min_z, max_z;
|
||||
double width_x, length_y, height_z;
|
||||
};
|
||||
|
||||
class Plane
|
||||
{
|
||||
public:
|
||||
Vec3d normal;
|
||||
double d;
|
||||
|
||||
Plane( const Vec3d& p1, const Vec3d& p2, const Vec3d& p3 ){
|
||||
Vec3d v1 = p1 - p2;
|
||||
Vec3d v2 = p3 - p2;
|
||||
normal = Vec3d::cross( v1, v2 );
|
||||
double len = std::sqrt( Vec3d::dot( normal, normal ) );
|
||||
if ( len > 1e-10 ) normal = normal / len;
|
||||
d = Vec3d::dot( normal, p1 );
|
||||
}
|
||||
|
||||
double distance_to_point( const Vec3d& p ) const {
|
||||
return Vec3d::dot( normal, p ) - d;
|
||||
}
|
||||
|
||||
static bool try_get_intersection( const Plane& p1, const Plane& p2, const Plane& p3, Vec3d& out ){
|
||||
double det = Vec3d::dot( p1.normal, Vec3d::cross( p2.normal, p3.normal ) );
|
||||
if ( std::abs( det ) < 0.0001 ) return false;
|
||||
Vec3d v1 = Vec3d::cross( p2.normal, p3.normal ) * p1.d;
|
||||
Vec3d v2 = Vec3d::cross( p3.normal, p1.normal ) * p2.d;
|
||||
Vec3d v3 = Vec3d::cross( p1.normal, p2.normal ) * p3.d;
|
||||
out = ( v1 + v2 + v3 ) / det;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -150,6 +150,11 @@ struct _QERFuncTable_1
|
||||
PFN_QERAPP_DIRDIALOG m_pfnDirDialog;
|
||||
PFN_QERAPP_COLORDIALOG m_pfnColorDialog;
|
||||
PFN_QERAPP_NEWICON m_pfnNewIcon;
|
||||
|
||||
// Plugin extensions — always append new members here to preserve ABI
|
||||
void ( *TextureBrowser_show )( );
|
||||
bool ( *TextureBrowser_isShown )( );
|
||||
void ( *TextureBrowser_close )( );
|
||||
};
|
||||
|
||||
#include "modulesystem.h"
|
||||
|
||||
@@ -39,5 +39,8 @@ inline void RawStringExport( const char* string, const StringImportCallback& imp
|
||||
typedef ConstPointerCaller<char, void(const StringImportCallback&), RawStringExport> RawStringExportCaller;
|
||||
QWidget* GroupDialog_addPage( const char* tabLabel, QWidget* widget, const StringExportCallback& title );
|
||||
|
||||
bool GroupDialog_isShown();
|
||||
void GroupDialog_setShown( bool shown );
|
||||
QWidget* GroupDialog_getPage();
|
||||
void GroupDialog_showPage( QWidget* page );
|
||||
void GroupDialog_updatePageTitle( QWidget* page );
|
||||
|
||||
@@ -151,6 +151,9 @@ public:
|
||||
m_radiantcore.Camera_getOrigin = Camera_getOrigin;
|
||||
|
||||
m_radiantcore.TextureBrowser_getSelectedShader = TextureBrowser_GetSelectedShader;
|
||||
m_radiantcore.TextureBrowser_show = TextureBrowser_show;
|
||||
m_radiantcore.TextureBrowser_isShown = TextureBrowser_isShown;
|
||||
m_radiantcore.TextureBrowser_close = TextureBrowser_close;
|
||||
|
||||
m_radiantcore.m_pfnMessageBox = &qt_MessageBox;
|
||||
m_radiantcore.m_pfnFileDialog = &file_dialog;
|
||||
|
||||
@@ -624,6 +624,23 @@ void TextureBrowser_toggleShow(){
|
||||
GroupDialog_showPage( g_page_textures );
|
||||
}
|
||||
|
||||
void TextureBrowser_show(){
|
||||
if ( !g_page_textures )
|
||||
return;
|
||||
if ( GroupDialog_isShown() && GroupDialog_getPage() == g_page_textures )
|
||||
GroupDialog_show(); // already on textures tab — just raise/focus
|
||||
else
|
||||
GroupDialog_showPage( g_page_textures ); // switch to textures tab and show
|
||||
}
|
||||
|
||||
|
||||
bool TextureBrowser_isShown(){
|
||||
return g_page_textures && GroupDialog_isShown() && GroupDialog_getPage() == g_page_textures;
|
||||
}
|
||||
|
||||
void TextureBrowser_close(){
|
||||
GroupDialog_setShown( false );
|
||||
}
|
||||
|
||||
void TextureBrowser_updateTitle(){
|
||||
GroupDialog_updatePageTitle( g_page_textures );
|
||||
|
||||
@@ -31,6 +31,9 @@ void TextureBrowser_destroyWindow();
|
||||
|
||||
|
||||
const char* TextureBrowser_GetSelectedShader();
|
||||
void TextureBrowser_show();
|
||||
bool TextureBrowser_isShown();
|
||||
void TextureBrowser_close();
|
||||
|
||||
void TextureBrowser_Construct();
|
||||
void TextureBrowser_Destroy();
|
||||
|
||||
Reference in New Issue
Block a user