diff --git a/bspinfo/bspinfo.cc b/bspinfo/bspinfo.cc index fece2c99..51bccd97 100644 --- a/bspinfo/bspinfo.cc +++ b/bspinfo/bspinfo.cc @@ -318,6 +318,10 @@ static void serialize_bsp(const bspdata_t &bspdata, const mbsp_t &bsp, const fs: node.push_back({"maxs", src_node.maxs}); node.push_back({"firstface", src_node.firstface}); node.push_back({"numfaces", src_node.numfaces}); + + // human-readable plane + auto& plane = bsp.dplanes.at(src_node.planenum); + node.push_back({"plane", json::array({plane.normal[0], plane.normal[1], plane.normal[2], plane.dist})}); } } diff --git a/include/common/bspfile.hh b/include/common/bspfile.hh index 9a2cb3d9..33c475da 100644 --- a/include/common/bspfile.hh +++ b/include/common/bspfile.hh @@ -770,6 +770,13 @@ struct bsp2_dclipnode_t auto stream_data() { return std::tie(planenum, children); } }; +/* +* Clipnodes need to be stored as a 16-bit offset. Originally, this was a +* signed value and only the positive values up to 32767 were available. Since +* the negative range was unused apart from a few values reserved for flags, +* this has been extended to allow up to 65520 (0xfff0) clipnodes (with a +* suitably modified engine). +*/ struct bsp29_dclipnode_t { int32_t planenum; diff --git a/include/qbsp/qbsp.hh b/include/qbsp/qbsp.hh index 1fb6c77f..aabccb6d 100644 --- a/include/qbsp/qbsp.hh +++ b/include/qbsp/qbsp.hh @@ -150,8 +150,6 @@ extern setting_group debugging_group; class qbsp_settings : public common_settings { public: - inline qbsp_settings() { } - 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"}; @@ -250,15 +248,6 @@ public: extern settings::qbsp_settings options; -/* - * Clipnodes need to be stored as a 16-bit offset. Originally, this was a - * signed value and only the positive values up to 32767 were available. Since - * the negative range was unused apart from a few values reserved for flags, - * this has been extended to allow up to 65520 (0xfff0) clipnodes (with a - * suitably modified engine). - */ -#define MAX_BSP_CLIPNODES 0xfff0 - // 0-2 are axial planes // 3-5 are non-axial planes snapped to the nearest #define PLANE_X 0 @@ -268,7 +257,7 @@ extern settings::qbsp_settings options; #define PLANE_ANYY 4 #define PLANE_ANYZ 5 -// planenum for a leaf (?) +// planenum for a leaf constexpr int32_t PLANENUM_LEAF = -1; /* diff --git a/include/vis/leafbits.hh b/include/vis/leafbits.hh index 9d33ec25..0f1fe6a1 100644 --- a/include/vis/leafbits.hh +++ b/include/vis/leafbits.hh @@ -119,5 +119,5 @@ public: } }; - constexpr reference operator[](const size_t &index) { return {bits, index >> shift, 1u << (index & mask)}; } + constexpr reference operator[](const size_t &index) { return {bits, index >> shift, static_cast(1) << (index & mask)}; } }; diff --git a/light/CMakeLists.txt b/light/CMakeLists.txt index d025f139..691ee455 100644 --- a/light/CMakeLists.txt +++ b/light/CMakeLists.txt @@ -64,6 +64,15 @@ if (embree_FOUND) message(STATUS "Found embree license: ${EMBREE_LICENSE}") endif() + # HACK: Windows embree .dll's from https://github.com/embree/embree/releases ship with a tbb12.dll + # and we need to copy it from the embree/bin directory to our light.exe/testlight.exe dir in order for them to run + find_file(EMBREE_TBB_DLL tbb12.dll + "${EMBREE_ROOT_DIR}/bin" + NO_DEFAULT_PATH) + if (NOT EMBREE_TBB_DLL STREQUAL EMBREE_TBB_DLL-NOTFOUND) + message(STATUS "Found embree EMBREE_TBB_DLL: ${EMBREE_TBB_DLL}") + endif() + add_custom_command(TARGET light POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$" COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$") @@ -72,6 +81,10 @@ if (embree_FOUND) add_custom_command(TARGET light POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${EMBREE_LICENSE}" "$/LICENSE-embree.txt") endif() + if (NOT EMBREE_TBB_DLL STREQUAL EMBREE_TBB_DLL-NOTFOUND) + add_custom_command(TARGET light POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${EMBREE_TBB_DLL}" "$") + endif() # so the executable will search for dylib's in the same directory as the executable if(APPLE) @@ -123,11 +136,9 @@ if (embree_FOUND) COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$" COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "$") - # HACK: copy embree's tbb12.dll - # FIXME: this is only desired with the .zip release of embree, not e.g. vcpkg - if (WIN32) + if (NOT EMBREE_TBB_DLL STREQUAL EMBREE_TBB_DLL-NOTFOUND) add_custom_command(TARGET testlight POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$/tbb12.dll" "$") + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${EMBREE_TBB_DLL}" "$") endif() add_definitions(-DHAVE_EMBREE) diff --git a/qbsp/map.cc b/qbsp/map.cc index 6be9e042..46c73aee 100644 --- a/qbsp/map.cc +++ b/qbsp/map.cc @@ -2095,7 +2095,7 @@ void ConvertMapFile(void) } fs::path filename = options.bsp_path; - filename.replace_filename(options.bsp_path.stem().string() + append); + filename.replace_filename(options.bsp_path.stem().string() + append).replace_extension(".map"); std::ofstream f(filename); diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index 50b63c12..c4fa108b 100644 --- a/qbsp/qbsp.cc +++ b/qbsp/qbsp.cc @@ -109,7 +109,8 @@ void qbsp_settings::postinitialize(int argc, const char **argv) set_target_version(&bspver_hl); } - if (q2bsp.value() || q2rtx.value()) { + if (q2bsp.value() || + (q2rtx.value() && !q2bsp.isChanged() && !qbism.isChanged())) { set_target_version(&bspver_q2); } @@ -167,10 +168,6 @@ void qbsp_settings::postinitialize(int argc, const char **argv) if (!includeskip.isChanged()) { includeskip.setValueLocked(true); } - - if (!notriggermodels.isChanged()) { - notriggermodels.setValueLocked(true); - } } common_settings::postinitialize(argc, argv); @@ -596,6 +593,23 @@ winding_t BaseWindingForPlane(const qplane3d &p) return winding_t::from_plane(p, options.worldextent.value()); } +static bool IsTrigger(const mapentity_t *entity) +{ + auto &tex = entity->mapbrush(0).face(0).texname; + + if (tex.length() < 6) { + return false; + } + + size_t trigger_pos = tex.rfind("trigger"); + + if (trigger_pos == std::string::npos) { + return false; + } + + return trigger_pos == (tex.size() - strlen("trigger")); +} + /* =============== ProcessEntity @@ -619,9 +633,9 @@ static void ProcessEntity(mapentity_t *entity, const int hullnum) return; // for notriggermodels: if we have at least one trigger-like texture, do special trigger stuff - bool discarded_trigger = (entity != map.world_entity() && + bool discarded_trigger = entity != map.world_entity() && options.notriggermodels.value() && - entity->mapbrush(0).face(0).texname.find_last_of("trigger") == entity->mapbrush(0).face(0).texname.size() - strlen("trigger")); + IsTrigger(entity); // Export a blank model struct, and reserve the index (only do this once, for all hulls) if (!discarded_trigger) { diff --git a/qbsp/surfaces.cc b/qbsp/surfaces.cc index cce1906c..b45ed083 100644 --- a/qbsp/surfaces.cc +++ b/qbsp/surfaces.cc @@ -30,6 +30,21 @@ #include #include +static bool ShouldOmitFace(face_t *f) +{ + if (!options.includeskip.value() && map.mtexinfos.at(f->texinfo).flags.is_skip) + return true; + if (map.mtexinfos.at(f->texinfo).flags.is_hint) + return true; + + // HACK: to save a few faces, don't output the interior faces of sky brushes + if (f->contents[0].is_sky(options.target_game)) { + return true; + } + + return false; +} + /* =============== SubdivideFace @@ -312,9 +327,7 @@ FindFaceEdges */ static void FindFaceEdges(mapentity_t *entity, face_t *face) { - if (!options.includeskip.value() && map.mtexinfos.at(face->texinfo).flags.is_skip) - return; - if (map.mtexinfos.at(face->texinfo).flags.is_hint) + if (ShouldOmitFace(face)) return; FindFaceFragmentEdges(entity, face, face); @@ -387,9 +400,7 @@ EmitFace */ static void EmitFace(mapentity_t *entity, face_t *face) { - if (!options.includeskip.value() && map.mtexinfos.at(face->texinfo).flags.is_skip) - return; - if (map.mtexinfos.at(face->texinfo).flags.is_hint) + if (ShouldOmitFace(face)) return; EmitFaceFragment(entity, face, face); @@ -426,9 +437,7 @@ static void GrowNodeRegion(mapentity_t *entity, node_t *node) static void CountFace(mapentity_t *entity, face_t *f, size_t &facesCount, size_t &vertexesCount) { - if (!options.includeskip.value() && map.mtexinfos.at(f->texinfo).flags.is_skip) - return; - if (map.mtexinfos.at(f->texinfo).flags.is_hint) + if (ShouldOmitFace(f)) return; if (f->lmshift[1] != 4) diff --git a/qbsp/test_qbsp.cc b/qbsp/test_qbsp.cc index 4b8d4874..5e4b50db 100644 --- a/qbsp/test_qbsp.cc +++ b/qbsp/test_qbsp.cc @@ -16,6 +16,8 @@ #include #include +using namespace testing; + // FIXME: Clear global data (planes, etc) between each test static const mapface_t *Mapbrush_FirstFaceWithTextureName(const mapbrush_t *brush, const std::string &texname) @@ -492,15 +494,26 @@ TEST(testmaps_q1, simple_worldspawn_sky) EXPECT_EQ(5, textureToFace.at("orangestuff8").size()); // leaf/node counts - EXPECT_EQ(6, bsp.dnodes.size()); + // - we'd get 7 nodes if it's cut like a cube (solid outside), with 1 additional cut inside to divide sky / empty + // - we'd get 11 if it's cut as the sky plane (1), then two open cubes (5 nodes each) + // - can get in between values if it does some vertical cuts, then the sky plane, then other vertical cuts + // + // the 7 solution is better but the BSP heuristics won't help reach that one in this trivial test map + EXPECT_THAT(bsp.dnodes.size(), AllOf(Ge(7), Le(11))); EXPECT_EQ(3, bsp.dleafs.size()); // shared solid leaf + empty + sky // check contents const qvec3d player_pos{-88, -64, 120}; + const double inside_sky_z = 232; EXPECT_EQ(CONTENTS_EMPTY, BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos)->contents); - EXPECT_EQ(CONTENTS_SKY, BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d(0,0,500))->contents); + // way above map is solid - sky should not fill outwards + // (otherwise, if you had sky with a floor further up above it, it's not clear where the leafs would be divided, or + // if the floor contents would turn to sky, etc.) + EXPECT_EQ(CONTENTS_SOLID, BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d(0,0,500))->contents); + + EXPECT_EQ(CONTENTS_SKY, BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], qvec3d(player_pos[0], player_pos[1], inside_sky_z))->contents); EXPECT_EQ(CONTENTS_SOLID, BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d( 500, 0, 0))->contents); EXPECT_EQ(CONTENTS_SOLID, BSP_FindLeafAtPoint(&bsp, &bsp.dmodels[0], player_pos + qvec3d(-500, 0, 0))->contents); @@ -752,30 +765,31 @@ TEST(testmaps_q2, detail) { // stats EXPECT_EQ(1, bsp.dmodels.size()); + // Q2 reserves leaf 0 as an invalid leaf + // leafs: - // 6 solid leafs outside the room + // 6 solid leafs outside the room (* can be more depending on when the "divider" is cut) // 1 empty leaf filling the room above the divider // 2 empty leafs + 1 solid leaf for divider // 1 detail leaf for button // 4 empty leafs around + 1 on top of button - // total: 16 - // Q2 reserves leaf 0 as an invalid leaf, so dleafs size is 17 - EXPECT_EQ(17, bsp.dleafs.size()); std::map counts_by_contents; for (size_t i = 1; i < bsp.dleafs.size(); ++i) { ++counts_by_contents[bsp.dleafs[i].contents]; } + EXPECT_EQ(3, counts_by_contents.size()); // number of types - EXPECT_EQ((std::map{{Q2_CONTENTS_SOLID, 7}, {Q2_CONTENTS_SOLID | Q2_CONTENTS_DETAIL, 1}, {0, 8}}), - counts_by_contents); + + EXPECT_EQ(1, counts_by_contents.at(Q2_CONTENTS_SOLID | Q2_CONTENTS_DETAIL)); + EXPECT_EQ(8, counts_by_contents.at(0)); // empty leafs + EXPECT_THAT(counts_by_contents.at(Q2_CONTENTS_SOLID), AllOf(Ge(7), Le(9))); // clusters: - // 6 solid leafs outside the room - // 1 empty leaf filling the room above the divider - // 2 empty leafs + 1 solid leaf for divider + // 1 empty cluster filling the room above the divider + // 2 empty clusters created by divider // 1 cluster for the part of the room with the button - // total: 10 + std::set clusters; // first add the empty leafs for (size_t i = 1; i < bsp.dleafs.size(); ++i) {