/* Copyright (C) 1996-1997 Id Software, Inc. Copyright (C) 1997 Greg Lewis This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA See file, 'COPYING', for details. */ // qbsp.h #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#include #include #include #include #include #include #include #include #include enum texcoord_style_t { TX_QUAKED = 0, TX_QUARK_TYPE1 = 1, TX_QUARK_TYPE2 = 2, TX_VALVE_220 = 3, TX_BRUSHPRIM = 4 }; enum class conversion_t { none, quake, quake2, valve, bp }; // data representation of only extended flags // used by Q2 format; used by various systems. struct extended_texinfo_t { contentflags_t contents = {0}; surfflags_t flags = {0}; int value = 0; std::string animation; }; namespace settings { struct wadpath { fs::path path; bool external; // wads from this path are not to be embedded into the bsp, but will instead require the engine // to load them from elsewhere. strongly recommended for eg halflife.wad inline bool operator<(const wadpath &other) const { return path < other.path; } }; struct setting_wadpathset : public setting_base { private: std::set _paths; public: inline setting_wadpathset(setting_container *dictionary, const nameset &names, const setting_group *group = nullptr, const char *description = "") : setting_base(dictionary, names, group, description) { } inline void addPath(const wadpath &path) { _paths.insert(path); } constexpr const std::set &pathsValue() const { return _paths; } inline bool copyFrom(const setting_base& other) override { if (auto *casted = dynamic_cast(&other)) { _paths = casted->_paths; _source = casted->_source; return true; } return false; } inline void reset() override { _paths = {}; _source = source::DEFAULT; } bool parse(const std::string &settingName, parser_base_t &parser, source source) override { if (!parser.parse_token()) { return false; } if (changeSource(source)) { _paths.insert(wadpath{fs::path(parser.token), settingName[0] == 'x'}); } return true; } std::string stringValue() const override { std::string paths; for (auto &path : _paths) { if (!paths.empty()) { paths += " ; "; } paths += path.path.string(); if (path.external) { paths += " (external)"; } } return paths; } std::string format() const override { return "path/to/wads"; } }; extern setting_group game_target_group; extern setting_group map_development_group; extern setting_group common_format_group; extern setting_group debugging_group; enum class filltype_t { AUTO, OUTSIDE, INSIDE }; enum class tjunclevel_t { NONE, // don't attempt to adjust faces at all - pass them through unchanged ROTATE, // allow faces' vertices to be rotated to prevent zero-area triangles RETOPOLOGIZE, // if a face still has zero-area triangles, allow it to be re-topologized // by splitting it into multiple fans DELAUNAY // attempt a delaunay triangulation first, only falling back to the prior two steps if it fails. }; struct setting_tjunc : public setting_enum { public: using setting_enum::setting_enum; bool parse(const std::string &settingName, parser_base_t &parser, source source) override { if (settingName == "notjunc") { this->setValue(tjunclevel_t::NONE, source); return true; } return this->setting_enum::parse(settingName, parser, source); } }; class qbsp_settings : public common_settings { public: setting_bool hexen2{this, "hexen2", false, &game_target_group, "target Hexen II's BSP format"}; setting_bool hlbsp{this, "hlbsp", false, &game_target_group, "target Half Life's BSP format"}; setting_bool q2bsp{this, "q2bsp", false, &game_target_group, "target Quake II's BSP format"}; setting_bool qbism{this, "qbism", false, &game_target_group, "target Qbism's extended Quake II BSP format"}; setting_bool bsp2{this, "bsp2", false, &game_target_group, "target Quake's extended BSP2 format"}; setting_bool bsp2rmq{ this, "2psb", false, &game_target_group, "target Quake's extended 2PSB format (RMQ compatible)"}; setting_int32 subdivide{this, "subdivide", 240, &common_format_group, "change the subdivide threshold, in luxels. 0 will disable subdivision entirely"}; setting_bool nofill{this, "nofill", false, &debugging_group, "don't perform outside filling"}; setting_bool nomerge{this, "nomerge", false, &debugging_group, "don't perform face merging"}; setting_bool noclip{this, "noclip", false, &common_format_group, "don't write clip nodes (Q1-like BSP formats)"}; setting_bool noskip{this, "noskip", false, &debugging_group, "don't remove faces with the 'skip' texture"}; setting_bool nodetail{this, "nodetail", false, &debugging_group, "treat all detail brushes to structural"}; setting_bool onlyents{this, "onlyents", false, &map_development_group, "only updates .MAP entities"}; setting_bool splitsky{this, "splitsky", false, &debugging_group, "doesn't combine sky faces into one large face"}; setting_bool splitturb{this, {"litwater", "splitturb"}, true, &common_format_group, "doesn't combine water faces into one large face"}; setting_redirect splitspecial{this, "splitspecial", {&splitsky, &splitturb}, &debugging_group, "doesn't combine sky and water faces into one large face (splitturb + splitsky)"}; setting_invertible_bool transwater{ this, "transwater", true, &common_format_group, "compute portal information for transparent water"}; setting_bool transsky{ this, "transsky", false, &map_development_group, "compute portal information for transparent sky"}; setting_bool notextures{this, "notex", false, &common_format_group, "write only placeholder textures to depend upon replacements, keep file sizes down, or to skirt copyrights"}; setting_enum convertmapformat{this, "convert", conversion_t::none, {{"quake", conversion_t::quake}, {"quake2", conversion_t::quake2}, {"valve", conversion_t::valve}, {"bp", conversion_t::bp}}, &common_format_group, "convert a .MAP to a different .MAP format"}; setting_invertible_bool oldaxis{this, "oldaxis", true, &debugging_group, "uses alternate texture alignment which was default in tyrutils-ericw v0.15.1 and older"}; setting_bool forcegoodtree{ this, "forcegoodtree", false, &debugging_group, "force use of expensive processing for SolidBSP stage"}; setting_scalar midsplitsurffraction{this, "midsplitsurffraction", 0.f, 0.f, 1.f, &debugging_group, "if 0 (default), use `maxnodesize` for deciding when to switch to midsplit bsp heuristic.\nif 0 < midsplitSurfFraction <= 1, switch to midsplit if the node contains more than this fraction of the model's\ntotal surfaces. Try 0.15 to 0.5. Works better than maxNodeSize for maps with a 3D skybox (e.g. +-128K unit maps)"}; setting_int32 maxnodesize{this, "maxnodesize", 1024, &debugging_group, "triggers simpler BSP Splitting when node exceeds size (default 1024, 0 to disable)"}; setting_bool oldrottex{ this, "oldrottex", false, &debugging_group, "use old rotate_ brush texturing aligned at (0 0 0)"}; setting_scalar epsilon{ this, "epsilon", 0.0001, 0.0, 1.0, &debugging_group, "customize epsilon value for point-on-plane checks"}; setting_scalar microvolume{ this, "microvolume", 1.0, 0.0, 1000.0, &debugging_group, "microbrush volume"}; setting_bool contenthack{this, "contenthack", false, &debugging_group, "hack to fix leaks through solids. causes missing faces in some cases so disabled by default"}; setting_bool leaktest{this, "leaktest", false, &map_development_group, "make compilation fail if the map leaks"}; setting_bool outsidedebug{this, "outsidedebug", false, &debugging_group, "write a .map after outside filling showing non-visible brush sides"}; setting_bool debugchop{this, "debugchop", false, &debugging_group, "write a .map after ChopBrushes"}; setting_bool keepprt{this, "keepprt", false, &debugging_group, "avoid deleting the .prt file on leaking maps"}; setting_bool includeskip{this, "includeskip", false, &common_format_group, "don't cull skip faces from the list of renderable surfaces (Q2RTX)"}; setting_scalar worldextent{ this, "worldextent", 0.0, &debugging_group, "explicitly provide world extents; 0 will auto-detect"}; setting_int32 leakdist{this, "leakdist", 2, &debugging_group, "space between leakfile points"}; setting_bool forceprt1{ this, "forceprt1", false, &debugging_group, "force a PRT1 output file even if PRT2 is required for vis"}; setting_tjunc tjunc{this, { "tjunc", "notjunc" }, tjunclevel_t::DELAUNAY, { { "none", tjunclevel_t::NONE }, { "rotate", tjunclevel_t::ROTATE }, { "retopologize", tjunclevel_t::RETOPOLOGIZE }, { "delaunay", tjunclevel_t::DELAUNAY } }, &debugging_group, "T-junction fix level"}; setting_bool objexport{ this, "objexport", false, &debugging_group, "export the map file as .OBJ models during various CSG phases"}; setting_bool wrbrushes{this, {"wrbrushes", "bspx"}, false, &common_format_group, "includes a list of brushes for brush-based collision"}; setting_redirect wrbrushesonly{this, {"wrbrushesonly", "bspxonly"}, {&wrbrushes, &noclip}, &common_format_group, "includes BSPX brushes and does not output clipping hulls (wrbrushes + noclip)"}; setting_bool omitdetail{ this, "omitdetail", false, &map_development_group, "omit *all* detail brushes from the compile"}; setting_bool omitdetailwall{ this, "omitdetailwall", false, &map_development_group, "func_detail_wall brushes are omitted from the compile"}; setting_bool omitdetailillusionary{this, "omitdetailillusionary", false, &map_development_group, "func_detail_illusionary brushes are omitted from the compile"}; setting_bool omitdetailfence{this, "omitdetailfence", false, &map_development_group, "func_detail_fence brushes are omitted from the compile"}; setting_bool expand{ this, "expand", false, &common_format_group, "write hull 1 expanded brushes to expanded.map for debugging"}; setting_wadpathset wadpaths{this, {"wadpath", "xwadpath"}, &map_development_group, "add a path to the wad search paths; wads found in xwadpath's will not be embedded, otherwise they will be embedded (if not -notex)"}; setting_bool notriggermodels{this, "notriggermodels", false, &common_format_group, "for supported game code only: triggers will not write a model\nout, and will instead just write out their mins/maxs."}; setting_set aliasdefs{this, "aliasdef", "\"path/to/file.def\" ", &map_development_group, "path to an alias definition file, which can transform entities in the .map into other entities."}; setting_set texturedefs{this, "texturedefs", "\"path/to/file.def\" ", &map_development_group, "path to a texture definition file, which can transform textures in the .map into other textures."}; setting_numeric lmscale{this, "lmscale", 1.0, &common_format_group, "change global lmscale (force _lmscale key on all entities). outputs the LMSCALE BSPX lump." }; setting_enum filltype{this, "filltype", filltype_t::AUTO, { { "auto", filltype_t::AUTO }, { "inside", filltype_t::INSIDE }, { "outside", filltype_t::OUTSIDE } }, &common_format_group, "whether to fill the map from the outside in (lenient), from the inside out (aggressive), or to automatically decide based on the hull being used."}; setting_invertible_bool allow_upgrade{this, "allowupgrade", true, &common_format_group, "allow formats to \"upgrade\" to compatible extended formats when a limit is exceeded (ie Quake BSP to BSP2)"}; setting_validator maxedges{ [](setting_int32 &setting) { return setting.value() == 0 || setting.value() >= 3; }, this, "maxedges", 64, &map_development_group, "the max number of edges/vertices on a single face before it is split into another face"}; void setParameters(int argc, const char **argv) override { common_settings::setParameters(argc, argv); programDescription = "qbsp performs geometric level processing of Quake .MAP files to create\nQuake .BSP files.\n\n"; remainderName = "sourcefile.map [destfile.bsp]"; } void initialize(int argc, const char **argv) override; void postinitialize(int argc, const char **argv) override; bool fVerbose = true; bool fAllverbose = false; bool fNoverbose = false; const bspversion_t *target_version = nullptr; const gamedef_t *target_game = nullptr; fs::path map_path; fs::path bsp_path; std::unordered_map>> loaded_texture_defs; std::unordered_map loaded_entity_defs; private: void load_texture_def(const std::string &pathname); void load_entity_def(const std::string &pathname); }; }; // namespace settings extern settings::qbsp_settings qbsp_options; // planenum for a leaf constexpr int32_t PLANENUM_LEAF = -1; /* * The quality of the bsp output is highly sensitive to these epsilon values. * Notes: * - some calculations are sensitive to errors and need the various * epsilons to be such that EQUAL_EPSILON < CONTINUOUS_EPSILON. * ( TODO: re-check if CONTINUOUS_EPSILON is still directly related ) */ constexpr vec_t ANGLEEPSILON = 0.000001; constexpr vec_t ZERO_EPSILON = 0.0001; constexpr vec_t EQUAL_EPSILON = 0.0001; constexpr vec_t CONTINUOUS_EPSILON = 0.0005; // the exact bounding box of the brushes is expanded some for the headnode // volume. this is done to avoid a zero-bounded node/leaf, the particular // value doesn't matter but it shows up in the .bsp output. constexpr double SIDESPACE = 24.0; #include #include struct maptexinfo_t { texvecf vecs; /* [s/t][xyz offset] */ int32_t miptex = 0; surfflags_t flags = {}; int32_t value = 0; // Q2-specific int32_t next = -1; // Q2-specific std::optional outputnum = std::nullopt; // nullopt until added to bsp constexpr auto as_tuple() const { return std::tie(vecs, miptex, flags, value, next); } constexpr bool operator<(const maptexinfo_t &other) const { return as_tuple() < other.as_tuple(); } constexpr bool operator>(const maptexinfo_t &other) const { return as_tuple() > other.as_tuple(); } }; class mapentity_t; struct face_fragment_t { std::vector output_vertices; // filled in by TJunc std::vector edges; // only filled in MakeFaceEdges std::optional outputnumber; // only valid for original faces after // write surfaces }; struct portal_t; struct face_t { int planenum; planeside_t planeside; // which side is the front of the face int texinfo; contentflags_t contents; // contents on the front of the face int16_t lmshift; winding_t w; std::vector original_vertices; // the vertices of this face before fragmentation; filled in by EmitVertices std::vector fragments; // the vertices of this face post-fragmentation; filled in by TJunc qvec3d origin; vec_t radius; portal_t *portal; }; // there is a node_t structure for every node and leaf in the bsp tree struct bspbrush_t; struct side_t; struct node_t { // both leafs and nodes aabb3d bounds; // bounding volume, not just points inside node_t *parent; // this is also a bounding volume like `bounds` std::unique_ptr volume; // one for each leaf/node // information for decision nodes int planenum; // -1 = leaf node int firstface; // decision node only int numfaces; // decision node only twosided> children; // children[0] = front side, children[1] = back side of plane. only valid for decision nodes std::list> facelist; // decision nodes only, list for both sides side_t *side; // the side that created the node // information for leafs contentflags_t contents; // leaf nodes (0 for decision nodes) std::vector original_brushes; std::vector markfaces; // leaf nodes only, point to node faces portal_t *portals; int visleafnum; // -1 = solid int viscluster; // detail cluster for faster vis int outside_distance; // -1 = can't reach outside, 0 = first void node, >0 = distance from void, in number of // portals used to write leak lines that take the shortest path to the void int occupied; // 0=can't reach entity, 1 = has entity, >1 = distance from leaf with entity mapentity_t *occupant; // example occupant, for leak hunting bool detail_separator; // for vis portal generation. true if ALL faces on node, and on all descendant nodes/leafs, // are detail. uint32_t firstleafbrush; // Q2 uint32_t numleafbrushes; int32_t area; }; void InitQBSP(int argc, const char **argv); void InitQBSP(const std::vector& args); void ProcessFile(); int qbsp_main(int argc, const char **argv);