diff --git a/include/qbsp/brush.hh b/include/qbsp/brush.hh index 31ab0930..98818b40 100644 --- a/include/qbsp/brush.hh +++ b/include/qbsp/brush.hh @@ -41,7 +41,11 @@ int Brush_ListCountWithCFlags(const brush_t *brush, int cflags); int Brush_ListCount(const brush_t *brush); int Brush_NumFaces(const brush_t *brush); -brush_t *LoadBrush(const mapentity_t *src, const mapbrush_t *mapbrush, int contents, const vec3_t rotate_offset, const int hullnum); +enum class rotation_t { + none, hipnotic, origin_brush +}; + +brush_t *LoadBrush(const mapentity_t *src, const mapbrush_t *mapbrush, int contents, const vec3_t rotate_offset, const rotation_t rottype, const int hullnum); void FreeBrushes(mapentity_t *ent); int FindPlane(const vec3_t normal, const vec_t dist, int *side); diff --git a/man/qbsp.1 b/man/qbsp.1 index f20d52f5..25589271 100644 --- a/man/qbsp.1 +++ b/man/qbsp.1 @@ -133,6 +133,30 @@ brush which you don't want to generate bsp splits or portals. All surfaces of a hint brush must use either the \fIhint\fP or \fIhintskip\fP texture name. +.SS "ORIGIN" +.PP +An origin brush (all faces textured with "origin") can be added to a brush entity +(but not detail or compiler-internal entities like func_group). Doing so causes all of +the brushes in the brush entitiy to be translated so the center of the origin brush +lines up with 0 0 0. The entity key "origin" is then automatically set on the +brush entity to the original cooridnates of the center of the +"origin" brush before it was translated to 0 0 0. +.PP +In Hexen 2, origin brushes are the native way of marking the center point of the +rotation axis for rotating entities. +.PP +In Quake, origin brushes can be used to make some map hacks easier to set up +that would otherwise require placing brushes at the world origin and +entering an "origin" value by hand. +.PP +Note than, unlike the Hipnotic rotation support in QBSP, using origin brushes +does not cause the model bounds to be expanded. (With Hipnotic rotation this was +to ensure that the model is not vis culled, regardless of its rotated angle.) +Origin brushes are useful for more than just rotation, and doing this bounds +expansion would break some use cases, so if you're going to rotate a model +with an origin brush you might need to expand the bounds of it a bit using +clip brushes so it doesn't get vis culled. + .SH "EXTERNAL MAP PREFAB SUPPORT" .PP This qbsp has a prefab system using a point entity named "misc_external_map". diff --git a/qbsp/brush.cc b/qbsp/brush.cc index 6b07c94c..b0d2e595 100644 --- a/qbsp/brush.cc +++ b/qbsp/brush.cc @@ -363,8 +363,8 @@ CreateBrushFaces ================= */ static face_t * -CreateBrushFaces(const mapentity_t *src, hullbrush_t *hullbrush, const vec3_t rotate_offset, - const int hullnum) +CreateBrushFaces(const mapentity_t *src, hullbrush_t *hullbrush, + const vec3_t rotate_offset, const rotation_t rottype, const int hullnum) { int i, j, k; vec_t r; @@ -475,11 +475,20 @@ CreateBrushFaces(const mapentity_t *src, hullbrush_t *hullbrush, const vec3_t ro // if -wrbrushes is in use, don't do this for the clipping hulls because it depends on having // the actual non-hacked bbox (it doesn't write axial planes). - const bool noExpand = static_cast( - atoi(ValueForKey(src, "_no_bbox_rotation_expansion")) - ) || (hullnum < 0); + + // Hexen2 also doesn't want the bbox expansion, it's handled in engine (see: SV_LinkEdict) - if ((rotate_offset[0] || rotate_offset[1] || rotate_offset[2]) && !noExpand) { + // Only do this for hipnotic rotation. For origin brushes in Quake, it breaks some of their + // uses (e.g. func_train). This means it's up to the mapper to expand the model bounds with + // clip brushes if they're going to rotate a model in vanilla Quake and not use hipnotic rotation. + // The idea behind the bounds expansion was to avoid incorrect vis culling (AFAIK). + const bool shouldExpand = + (rotate_offset[0] != 0.0 || rotate_offset[1] != 0.0 || rotate_offset[2] != 0.0) + && rottype == rotation_t::hipnotic + && (hullnum >= 0) // hullnum < 0 corresponds to -wrbrushes clipping hulls + && !options.hexen2; // never do this in Hexen 2 + + if (shouldExpand) { vec_t delta; delta = fabs(max); @@ -865,7 +874,7 @@ LoadBrush Converts a mapbrush to a bsp brush =============== */ -brush_t *LoadBrush(const mapentity_t *src, const mapbrush_t *mapbrush, int contents, const vec3_t rotate_offset, const int hullnum) +brush_t *LoadBrush(const mapentity_t *src, const mapbrush_t *mapbrush, int contents, const vec3_t rotate_offset, const rotation_t rottype, const int hullnum) { hullbrush_t hullbrush; brush_t *brush; @@ -886,11 +895,11 @@ brush_t *LoadBrush(const mapentity_t *src, const mapbrush_t *mapbrush, int conte if (hullnum <= 0) { // for hull 0 or BSPX -wrbrushes collision, apply the rotation offset now - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } else { // for Quake-style clipping hulls, don't apply rotation offset yet.. // it will be applied below - facelist = CreateBrushFaces(src, &hullbrush, vec3_origin, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, vec3_origin, rottype, hullnum); } if (!facelist) { @@ -905,19 +914,19 @@ brush_t *LoadBrush(const mapentity_t *src, const mapbrush_t *mapbrush, int conte vec3_t size[2] = { {-16, -16, -36}, {16, 16, 36} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } else if (hullnum == 2) { vec3_t size[2] = { {-32, -32, -32}, {32, 32, 32} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } else if (hullnum == 3) { vec3_t size[2] = { {-16, -16, -18}, {16, 16, 18} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } } else if (options.hexen2) @@ -926,19 +935,19 @@ brush_t *LoadBrush(const mapentity_t *src, const mapbrush_t *mapbrush, int conte vec3_t size[2] = { {-16, -16, -32}, {16, 16, 24} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } else if (hullnum == 2) { vec3_t size[2] = { {-24, -24, -20}, {24, 24, 20} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } else if (hullnum == 3) { vec3_t size[2] = { {-16, -16, -12}, {16, 16, 16} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } else if (hullnum == 4) { #if 0 @@ -946,21 +955,21 @@ brush_t *LoadBrush(const mapentity_t *src, const mapbrush_t *mapbrush, int conte vec3_t size[2] = { {-40, -40, -42}, {40, 40, 42} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } else #endif { /*mission pack*/ vec3_t size[2] = { {-8, -8, -8}, {8, 8, 8} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } } else if (hullnum == 5) { vec3_t size[2] = { {-48, -48, -50}, {48, 48, 50} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } } else @@ -970,13 +979,13 @@ brush_t *LoadBrush(const mapentity_t *src, const mapbrush_t *mapbrush, int conte ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } else if (hullnum == 2) { vec3_t size[2] = { {-32, -32, -64}, {32, 32, 24} }; ExpandBrush(&hullbrush, size, facelist); FreeBrushFaces(facelist); - facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, hullnum); + facelist = CreateBrushFaces(src, &hullbrush, rotate_offset, rottype, hullnum); } } @@ -1108,7 +1117,7 @@ Brush_LoadEntity(mapentity_t *dst, const mapentity_t *src, const int hullnum) classname = ValueForKey(src, "classname"); /* Origin brush support */ - bool usesOriginBrush = false; + rotation_t rottype = rotation_t::none; VectorCopy(vec3_origin, rotate_offset); for (int i = 0; i < src->nummapbrushes; i++) { @@ -1120,7 +1129,7 @@ Brush_LoadEntity(mapentity_t *dst, const mapentity_t *src, const int hullnum) continue; } - brush_t *brush = LoadBrush(src, mapbrush, contents, vec3_origin, 0); + brush_t *brush = LoadBrush(src, mapbrush, contents, vec3_origin, rotation_t::none, 0); if (brush) { vec3_t origin; VectorAdd(brush->mins, brush->maxs, origin); @@ -1131,7 +1140,7 @@ Brush_LoadEntity(mapentity_t *dst, const mapentity_t *src, const int hullnum) SetKeyValue(dst, "origin", value); VectorCopy(origin, rotate_offset); - usesOriginBrush = true; + rottype = rotation_t::origin_brush; FreeBrush(brush); } @@ -1139,10 +1148,11 @@ Brush_LoadEntity(mapentity_t *dst, const mapentity_t *src, const int hullnum) } /* Hipnotic rotation */ - if (!usesOriginBrush) { + if (rottype == rotation_t::none) { if (!strncmp(classname, "rotate_", 7)) { FixRotateOrigin(dst); GetVectorForKey(dst, "origin", rotate_offset); + rottype = rotation_t::hipnotic; } } @@ -1245,7 +1255,7 @@ Brush_LoadEntity(mapentity_t *dst, const mapentity_t *src, const int hullnum) */ if (contents == CONTENTS_CLIP) { if (hullnum == 0) { - brush_t *brush = LoadBrush(src, mapbrush, contents, rotate_offset, hullnum); + brush_t *brush = LoadBrush(src, mapbrush, contents, rotate_offset, rottype, hullnum); if (brush) { AddToBounds(dst, brush->mins); AddToBounds(dst, brush->maxs); @@ -1291,7 +1301,7 @@ Brush_LoadEntity(mapentity_t *dst, const mapentity_t *src, const int hullnum) if (hullnum > 0 && contents == CONTENTS_SKY) contents = CONTENTS_SOLID; - brush_t *brush = LoadBrush(src, mapbrush, contents, rotate_offset, hullnum); + brush_t *brush = LoadBrush(src, mapbrush, contents, rotate_offset, rottype, hullnum); if (!brush) continue; diff --git a/qbsp/map.cc b/qbsp/map.cc index 1401f6ed..82dd305f 100644 --- a/qbsp/map.cc +++ b/qbsp/map.cc @@ -2370,7 +2370,7 @@ TestExpandBrushes(const mapentity_t *src) for (int i = 0; i < src->nummapbrushes; i++) { const mapbrush_t *mapbrush = &src->mapbrush(i); - brush_t *hull1brush = LoadBrush(src, mapbrush, CONTENTS_SOLID, vec3_origin, 1); + brush_t *hull1brush = LoadBrush(src, mapbrush, CONTENTS_SOLID, vec3_origin, rotation_t::none, 1); if (hull1brush != nullptr) hull1brushes.push_back(hull1brush); diff --git a/qbsp/test_qbsp.cc b/qbsp/test_qbsp.cc index e1594b98..e1d62c8a 100644 --- a/qbsp/test_qbsp.cc +++ b/qbsp/test_qbsp.cc @@ -115,7 +115,7 @@ TEST(qbsp, duplicatePlanes) { EXPECT_EQ(0, worldspawn.numbrushes); EXPECT_EQ(6, worldspawn.mapbrush(0).numfaces); - brush_t *brush = LoadBrush(&worldspawn, &worldspawn.mapbrush(0), CONTENTS_SOLID, vec3_origin, 0); + brush_t *brush = LoadBrush(&worldspawn, &worldspawn.mapbrush(0), CONTENTS_SOLID, vec3_origin, rotation_t::none, 0); ASSERT_NE(nullptr, brush); EXPECT_EQ(6, Brush_NumFaces(brush)); FreeBrush(brush); @@ -141,7 +141,7 @@ static brush_t *load128x128x32Brush() mapentity_t worldspawn = LoadMap(map); Q_assert(1 == worldspawn.nummapbrushes); - brush_t *brush = LoadBrush(&worldspawn, &worldspawn.mapbrush(0), CONTENTS_SOLID, vec3_origin, 0); + brush_t *brush = LoadBrush(&worldspawn, &worldspawn.mapbrush(0), CONTENTS_SOLID, vec3_origin, rotation_t::none, 0); Q_assert(nullptr != brush); brush->contents = CONTENTS_SOLID;