From d0788ac01f7eb73446e34fb12525a375a71162a0 Mon Sep 17 00:00:00 2001 From: L-P Date: Mon, 9 Dec 2024 08:06:26 +0100 Subject: [PATCH] Add _hulls property to allow a bmodel to set for which hulls it should have clipnodes (#442) * Add _hulls bmodel property Whitelist hulls for which to generate clipnodes. * Fix _hulls when hull 0 is omitted Add test --------- Co-authored-by: Eric Wasylishen --- docs/qbsp.rst | 11 +++++++ qbsp/qbsp.cc | 34 +++++++++++++++++++ testmaps/q1_hulls.map | 77 +++++++++++++++++++++++++++++++++++++++++++ tests/test_qbsp.cc | 33 +++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 testmaps/q1_hulls.map diff --git a/docs/qbsp.rst b/docs/qbsp.rst index 1dd76f8c..f9419a57 100644 --- a/docs/qbsp.rst +++ b/docs/qbsp.rst @@ -811,6 +811,17 @@ Model Entity Keys Defaults to 0, brushes with higher values (equivalent to appearing later in the .map file) will clip away lower valued brushes. +.. bmodel-key:: "_hulls" "n" + + Bitmap ("Flags" type in FGD) that selects for which hulls collision data + will be generated. eg. a decimal value of 11 (0b1011) would generate hull 0, hull 1, + and hull 3. + Faces are computed using data from hull 0, not generating this hull will + prevent a bmodel from being rendered, acting as a CLIP brush only active for + the specified hulls. + + Defaults to 0 which will generate clipnodes for all hulls. + .. bmodel-key:: "_chop" "n" Set to 0 to prevent these brushes from being chopped. diff --git a/qbsp/qbsp.cc b/qbsp/qbsp.cc index 2fe2c0ff..db8763f4 100644 --- a/qbsp/qbsp.cc +++ b/qbsp/qbsp.cc @@ -984,6 +984,24 @@ static void GatherLeafVolumes_r(node_t *node, bspbrush_t::container &container) GatherLeafVolumes_r(nodedata->children[1], container); } +/* Returns true if the user requested to generate an entity bmodel clipnodes + * for a given hull. */ +static bool ShouldGenerateClipnodes(mapentity_t &entity, hull_index_t hullnum) +{ + // Default to generating clipnodes for all hulls. + if (!entity.epairs.has("_hulls")) { + return true; + } + + const int hulls = entity.epairs.get_int("_hulls"); + // Ensure 0 means all hulls even in the case we have more than 32 hulls. + if (hulls == 0) { + return true; + } + + return hulls & (1 << hullnum.value_or(0)); +} + /* =============== ProcessEntity @@ -1086,6 +1104,22 @@ static void ProcessEntity(mapentity_t &entity, hull_index_t hullnum) return; } + // _hulls key + if (!ShouldGenerateClipnodes(entity, hullnum)) { + // We still need to emit an empty tree otherwise hull 0 will point past + // the clipnode array (FIXME?). + bspbrush_t::container empty; + tree_t tree; + BrushBSP(tree, entity, empty, tree_split_t::FAST); + if (hullnum.value_or(0)) { + ExportClipNodes(entity, tree.headnode, hullnum.value()); + } else { + MakeTreePortals(tree); // needed to assign leaf bounds + ExportDrawNodes(entity, tree.headnode, map.bsp.dfaces.size()); + } + return; + } + // simpler operation for hulls if (hullnum.value_or(0)) { tree_t tree; diff --git a/testmaps/q1_hulls.map b/testmaps/q1_hulls.map new file mode 100644 index 00000000..e6faff46 --- /dev/null +++ b/testmaps/q1_hulls.map @@ -0,0 +1,77 @@ +// Game: Quake +// Format: Valve +// entity 0 +{ +"mapversion" "220" +"classname" "worldspawn" +"wad" "deprecated/free_wad.wad;deprecated/fence.wad;deprecated/origin.wad;deprecated/hintskip.wad" +"_wateralpha" "0.5" +"_tb_def" "builtin:Quoth2.fgd" +// brush 0 +{ +( -288 -256 96 ) ( -288 -255 96 ) ( -288 -256 97 ) orangestuff8 [ 0 -1 0 0 ] [ 0 0 -1 16 ] 0 1 1 +( -160 -432 96 ) ( -160 -432 97 ) ( -159 -432 96 ) orangestuff8 [ 1 0 0 0 ] [ 0 0 -1 16 ] 0 1 1 +( -160 -256 96 ) ( -159 -256 96 ) ( -160 -255 96 ) orangestuff8 [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( -80 64 112 ) ( -80 65 112 ) ( -79 64 112 ) orangestuff8 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( -80 576 112 ) ( -79 576 112 ) ( -80 576 113 ) orangestuff8 [ -1 0 0 0 ] [ 0 0 -1 16 ] 0 1 1 +( 288 64 112 ) ( 288 64 113 ) ( 288 65 112 ) orangestuff8 [ 0 1 0 0 ] [ 0 0 -1 16 ] 0 1 1 +} +// brush 1 +{ +( -160 0 96 ) ( -160 1 96 ) ( -160 0 97 ) blood3 [ 0 0 -1.0000000000000002 0 ] [ 0 -1.0000000000000002 0 0 ] 0 1 1 +( -160 -32 96 ) ( -160 -32 97 ) ( -159 -32 96 ) blood3 [ 1.0000000000000002 0 0 0 ] [ 0 0 1.0000000000000002 32 ] 0 1 1 +( -160 0 96 ) ( -159 0 96 ) ( -160 1 96 ) blood3 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( -96 80 112 ) ( -96 81 112 ) ( -95 80 112 ) blood3 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( -96 80 112 ) ( -95 80 112 ) ( -96 80 113 ) blood3 [ 1.0000000000000002 0 0 0 ] [ 0 0 -1.0000000000000002 16 ] 0 1 1 +( -144 80 112 ) ( -144 80 113 ) ( -144 81 112 ) blood3 [ 0 0 1.0000000000000002 0 ] [ 0 -1.0000000000000002 0 0 ] 0 1 1 +} +} +// entity 1 +{ +"classname" "info_player_start" +"origin" "-48 -160 136" +"angle" "180" +} +// entity 2 +{ +"classname" "monster_shambler" +"origin" "-240 -160 136" +} +// entity 3 +{ +"classname" "func_wall" +"_hulls" "5" +// brush 0 +{ +( -160 -192 112 ) ( -160 -191 112 ) ( -160 -192 113 ) orangestuff8 [ 0 0 -1.0000000000000002 -48 ] [ 0 -1.0000000000000002 0 0 ] 0 1 1 +( -160 -224 112 ) ( -160 -224 113 ) ( -159 -224 112 ) orangestuff8 [ 1.0000000000000002 0 0 0 ] [ 0 0 1.0000000000000002 16 ] 0 1 1 +( -160 -192 112 ) ( -159 -192 112 ) ( -160 -191 112 ) orangestuff8 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( -96 -112 208 ) ( -96 -111 208 ) ( -95 -112 208 ) orangestuff8 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( -96 -112 128 ) ( -95 -112 128 ) ( -96 -112 129 ) orangestuff8 [ 1.0000000000000002 0 0 0 ] [ 0 0 -1.0000000000000002 32 ] 0 1 1 +( -144 -112 128 ) ( -144 -112 129 ) ( -144 -111 128 ) orangestuff8 [ 0 0 1.0000000000000002 -16 ] [ 0 -1.0000000000000002 0 0 ] 0 1 1 +} +} +// entity 4 +{ +"classname" "info_null" +"origin" "-152 -168 168" +} +// entity 5 +{ +"classname" "func_wall" +"_hulls" "6" +// brush 0 +{ +( -160 0 112 ) ( -160 1 112 ) ( -160 0 113 ) blood3 [ 0 0 -1.0000000000000002 -48 ] [ 0 -1.0000000000000002 0 0 ] 0 1 1 +( -160 -32 112 ) ( -160 -32 113 ) ( -159 -32 112 ) blood3 [ 1.0000000000000002 0 0 0 ] [ 0 0 1.0000000000000002 16 ] 0 1 1 +( -160 0 112 ) ( -159 0 112 ) ( -160 1 112 ) blood3 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( -96 80 208 ) ( -96 81 208 ) ( -95 80 208 ) blood3 [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1 +( -96 80 128 ) ( -95 80 128 ) ( -96 80 129 ) blood3 [ 1.0000000000000002 0 0 0 ] [ 0 0 -1.0000000000000002 32 ] 0 1 1 +( -144 80 128 ) ( -144 80 129 ) ( -144 81 128 ) blood3 [ 0 0 1.0000000000000002 -16 ] [ 0 -1.0000000000000002 0 0 ] 0 1 1 +} +} +// entity 6 +{ +"classname" "info_null" +"origin" "-152 24 168" +} diff --git a/tests/test_qbsp.cc b/tests/test_qbsp.cc index a19b6c09..4ded090a 100644 --- a/tests/test_qbsp.cc +++ b/tests/test_qbsp.cc @@ -1493,6 +1493,39 @@ TEST(testmapsQ1, sealingHull1Onnode) EXPECT_EQ(CONTENTS_SOLID, BSP_FindContentsAtPoint(&bsp, 2, &bsp.dmodels[0], player_start_pos + qvec3d(0, 0, 1000))); } +TEST(testmapsQ1, hullsFlag) +{ + const auto [bsp, bspx, prt] = LoadTestmapQ1("q1_hulls.map"); + + ASSERT_EQ(3, bsp.dmodels.size()); // world and 2 func_wall's + + { + const auto in_bmodel_pos = qvec3d(-152, -168, 168); + + // the func_wall has _hulls is set to 5 = 0b101, so generate hulls 0 and 2 (blocks shambler and line traces but + // player can walk through) + + EXPECT_EQ(CONTENTS_SOLID, BSP_FindContentsAtPoint(&bsp, 0, &bsp.dmodels[1], in_bmodel_pos)); + EXPECT_EQ(CONTENTS_EMPTY, BSP_FindContentsAtPoint(&bsp, 1, &bsp.dmodels[1], in_bmodel_pos)); + EXPECT_EQ(CONTENTS_SOLID, BSP_FindContentsAtPoint(&bsp, 2, &bsp.dmodels[1], in_bmodel_pos)); + + EXPECT_TRUE(BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[1], in_bmodel_pos + qvec3d(8, 0, 0))); + } + + { + // the second one has _hulls 6 = 0b110, so generate hulls 1 and 2 (blocks player + shambler, but no visual + // faces and point-size hull traces can pass through) + + const auto in_bmodel_pos2 = qvec3d(-152, 24, 168); + + EXPECT_EQ(CONTENTS_EMPTY, BSP_FindContentsAtPoint(&bsp, 0, &bsp.dmodels[2], in_bmodel_pos2)); + EXPECT_EQ(CONTENTS_SOLID, BSP_FindContentsAtPoint(&bsp, 1, &bsp.dmodels[2], in_bmodel_pos2)); + EXPECT_EQ(CONTENTS_SOLID, BSP_FindContentsAtPoint(&bsp, 2, &bsp.dmodels[2], in_bmodel_pos2)); + + EXPECT_FALSE(BSP_FindFaceAtPoint(&bsp, &bsp.dmodels[2], in_bmodel_pos2 + qvec3d(8, 0, 0))); + } +} + TEST(testmapsQ1, 0125UnitFaces) { GTEST_SKIP();