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 <ewasylishen@gmail.com>
This commit is contained in:
L-P 2024-12-09 08:06:26 +01:00 committed by GitHub
parent 0f1a7186d6
commit d0788ac01f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 155 additions and 0 deletions

View File

@ -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.

View File

@ -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;

77
testmaps/q1_hulls.map Normal file
View File

@ -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"
}

View File

@ -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();