diff --git a/Makefile b/Makefile index 06716320..54ccbb6e 100644 --- a/Makefile +++ b/Makefile @@ -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): \ diff --git a/contrib/terrain_generator/brush_builder.cpp b/contrib/terrain_generator/brush_builder.cpp new file mode 100644 index 00000000..84006e95 --- /dev/null +++ b/contrib/terrain_generator/brush_builder.cpp @@ -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(); +} diff --git a/contrib/terrain_generator/brush_builder.h b/contrib/terrain_generator/brush_builder.h new file mode 100644 index 00000000..03c880e6 --- /dev/null +++ b/contrib/terrain_generator/brush_builder.h @@ -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 ); diff --git a/contrib/terrain_generator/noise.cpp b/contrib/terrain_generator/noise.cpp new file mode 100644 index 00000000..ef207218 --- /dev/null +++ b/contrib/terrain_generator/noise.cpp @@ -0,0 +1,157 @@ +#include "noise.h" + +#include + +// --------------------------------------------------------------------------- +// 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 ); +} diff --git a/contrib/terrain_generator/noise.h b/contrib/terrain_generator/noise.h new file mode 100644 index 00000000..6ac9d93e --- /dev/null +++ b/contrib/terrain_generator/noise.h @@ -0,0 +1,11 @@ +#pragma once + +namespace Perlin +{ + double noise( double x, double y ); +} + +namespace Simplex +{ + double noise( double x, double y ); +} diff --git a/contrib/terrain_generator/terrain_engine.cpp b/contrib/terrain_generator/terrain_engine.cpp new file mode 100644 index 00000000..ab261343 --- /dev/null +++ b/contrib/terrain_generator/terrain_engine.cpp @@ -0,0 +1,241 @@ +#include "terrain_engine.h" +#include "noise.h" + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 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; +} diff --git a/contrib/terrain_generator/terrain_engine.h b/contrib/terrain_generator/terrain_engine.h new file mode 100644 index 00000000..856f9d67 --- /dev/null +++ b/contrib/terrain_generator/terrain_engine.h @@ -0,0 +1,51 @@ +#pragma once + +#include "terrain_math.h" + +#include +#include +#include + +using HeightMap = std::map, double>; +using WallMap = std::map, 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 ); diff --git a/contrib/terrain_generator/terrain_generator.cpp b/contrib/terrain_generator/terrain_generator.cpp new file mode 100644 index 00000000..2612040b --- /dev/null +++ b/contrib/terrain_generator/terrain_generator.cpp @@ -0,0 +1,565 @@ +#include "terrain_generator.h" + +#include +#include "debugging/debugging.h" +#include "iplugin.h" +#include "string/string.h" +#include "modulesystem/singletonmodule.h" +#include "typesystem.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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( 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( 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::of( &QComboBox::currentIndexChanged ), update_target_mode ); + QObject::connect( sq_advanced, &QCheckBox::toggled, update_advanced ); + QObject::connect( shape_combo, QOverload::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 SingletonTerrainGeneratorModule; +SingletonTerrainGeneratorModule g_TerrainGeneratorModule; + +extern "C" void RADIANT_DLLEXPORT Radiant_RegisterModules( ModuleServer& server ){ + initialiseModule( server ); + g_TerrainGeneratorModule.selfRegister(); +} diff --git a/contrib/terrain_generator/terrain_generator.def b/contrib/terrain_generator/terrain_generator.def new file mode 100644 index 00000000..10ae7b52 --- /dev/null +++ b/contrib/terrain_generator/terrain_generator.def @@ -0,0 +1,6 @@ +; terrain_generator.def : Declares the module parameters for the DLL. + +LIBRARY "TERRAIN_GENERATOR" + +EXPORTS + Radiant_RegisterModules @1 diff --git a/contrib/terrain_generator/terrain_generator.h b/contrib/terrain_generator/terrain_generator.h new file mode 100644 index 00000000..ec3bea5c --- /dev/null +++ b/contrib/terrain_generator/terrain_generator.h @@ -0,0 +1,5 @@ +#pragma once + +#include "modulesystem.h" + +void terrain_generator_register_modules( ModuleServer& server ); diff --git a/contrib/terrain_generator/terrain_math.h b/contrib/terrain_generator/terrain_math.h new file mode 100644 index 00000000..ba982ce2 --- /dev/null +++ b/contrib/terrain_generator/terrain_math.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +// 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; + } +}; diff --git a/include/qerplugin.h b/include/qerplugin.h index f6312ef2..e1ee7fc5 100644 --- a/include/qerplugin.h +++ b/include/qerplugin.h @@ -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" diff --git a/radiant/groupdialog.h b/radiant/groupdialog.h index e2d0267a..286db86f 100644 --- a/radiant/groupdialog.h +++ b/radiant/groupdialog.h @@ -39,5 +39,8 @@ inline void RawStringExport( const char* string, const StringImportCallback& imp typedef ConstPointerCaller 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 ); diff --git a/radiant/plugin.cpp b/radiant/plugin.cpp index fa875e94..43343b30 100644 --- a/radiant/plugin.cpp +++ b/radiant/plugin.cpp @@ -151,6 +151,9 @@ class RadiantCoreAPI 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; diff --git a/radiant/texwindow.cpp b/radiant/texwindow.cpp index 48b1d565..cac4b0cf 100644 --- a/radiant/texwindow.cpp +++ b/radiant/texwindow.cpp @@ -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 ); diff --git a/radiant/texwindow.h b/radiant/texwindow.h index 1f992df0..51a27809 100644 --- a/radiant/texwindow.h +++ b/radiant/texwindow.h @@ -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();