diff --git a/src/Sourcefile b/src/Sourcefile
index de90bb60910286242ae4b62b41af5d9ad57706ad..fb08c21713128aec5886d39c736cfa9be4dfb7ba 100644
--- a/src/Sourcefile
+++ b/src/Sourcefile
@@ -64,6 +64,7 @@ r_skins.c
 r_sky.c
 r_splats.c
 r_things.c
+r_bbox.c
 r_textures.c
 r_patch.c
 r_patchrotation.c
diff --git a/src/d_netcmd.c b/src/d_netcmd.c
index af44e53d63540ddfcde7cdb51e927522533db243..b5fa6f066c635b91cc9931e3ae4319e5044842ef 100644
--- a/src/d_netcmd.c
+++ b/src/d_netcmd.c
@@ -873,6 +873,9 @@ void D_RegisterClientCommands(void)
 	// screen.c
 	CV_RegisterVar(&cv_fullscreen);
 	CV_RegisterVar(&cv_renderview);
+	CV_RegisterVar(&cv_renderhitboxinterpolation);
+	CV_RegisterVar(&cv_renderhitboxgldepth);
+	CV_RegisterVar(&cv_renderhitbox);
 	CV_RegisterVar(&cv_renderer);
 	CV_RegisterVar(&cv_scr_depth);
 	CV_RegisterVar(&cv_scr_width);
diff --git a/src/hardware/hw_defs.h b/src/hardware/hw_defs.h
index b0859f478bd37d2315d742d0f7c80a580a5300bc..227fdf92b3941176ac6f2c0aae6681c0fe329644 100644
--- a/src/hardware/hw_defs.h
+++ b/src/hardware/hw_defs.h
@@ -136,6 +136,7 @@ typedef struct
 // Predefined shader types
 enum
 {
+	SHADER_NONE = -1,
 	SHADER_DEFAULT = 0,
 
 	SHADER_FLOOR,
@@ -237,7 +238,8 @@ enum EPolyFlags
 	PF_RemoveYWrap      = 0x00010000,   // Forces clamp texture on Y
 	PF_ForceWrapX       = 0x00020000,   // Forces repeat texture on X
 	PF_ForceWrapY       = 0x00040000,   // Forces repeat texture on Y
-	PF_Ripple           = 0x00100000    // Water ripple effect. The current backend doesn't use it for anything.
+	PF_Ripple           = 0x00100000,   // Water ripple effect. The current backend doesn't use it for anything.
+	PF_WireFrame        = 0x00200000,   // Draws vertices as lines instead of triangles
 };
 
 
diff --git a/src/hardware/hw_glob.h b/src/hardware/hw_glob.h
index b5daba82261ce46cbe0393ca765146b4118fcfff..d391c415670846fad27d4a22e1e33f46ccb367b3 100644
--- a/src/hardware/hw_glob.h
+++ b/src/hardware/hw_glob.h
@@ -81,6 +81,7 @@ typedef struct gl_vissprite_s
 
 	boolean flip, vflip;
 	boolean precip; // Tails 08-25-2002
+	boolean bbox;
 	boolean rotated;
 	UINT8 translucency;       //alpha level 0-255
 
diff --git a/src/hardware/hw_main.c b/src/hardware/hw_main.c
index c2390519ef78fbb75407dc77fa1010968f5d664f..4e1b7975e4aaeeb002dd730fe8d0d1ab017018af 100644
--- a/src/hardware/hw_main.c
+++ b/src/hardware/hw_main.c
@@ -66,6 +66,7 @@ static void HWR_ProjectSprite(mobj_t *thing);
 #ifdef HWPRECIP
 static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing);
 #endif
+static void HWR_ProjectBoundingBox(mobj_t *thing);
 
 void HWR_AddTransparentFloor(levelflat_t *levelflat, extrasubsector_t *xsub, boolean isceiling, fixed_t fixedheight, INT32 lightlevel, INT32 alpha, sector_t *FOFSector, FBITFIELD blend, boolean fogplane, extracolormap_t *planecolormap);
 void HWR_AddTransparentPolyobjectFloor(levelflat_t *levelflat, polyobj_t *polysector, boolean isceiling, fixed_t fixedheight,
@@ -4038,6 +4039,54 @@ static void HWR_SplitSprite(gl_vissprite_t *spr)
 		HWR_LinkDrawHackAdd(wallVerts, spr);
 }
 
+static void HWR_DrawBoundingBox(gl_vissprite_t *vis)
+{
+	FOutVector v[24];
+	FSurfaceInfo Surf = {0};
+
+	//
+	// create a cube (side view)
+	//
+	//  5--4  3
+	//        |
+	//        |
+	//  0--1  2
+	//
+	// repeat this 4 times (overhead)
+	//
+	//
+	// 17    20  21    11
+	//    16 15  14 10
+	// 27 22  *--*  07 12
+	//        |  |
+	// 26 23  *--*  06 13
+	//    24 00  01 02
+	// 25    05  04    03
+	//
+
+	v[000].x = v[005].x = v[015].x = v[016].x = v[017].x = v[020].x =
+		v[022].x = v[023].x = v[024].x = v[025].x = v[026].x = v[027].x = vis->x1; // west
+
+	v[001].x = v[002].x = v[003].x = v[004].x = v[006].x = v[007].x =
+		v[010].x = v[011].x = v[012].x = v[013].x = v[014].x = v[021].x = vis->x2; // east
+
+	v[000].z = v[001].z = v[002].z = v[003].z = v[004].z = v[005].z =
+		v[006].z = v[013].z = v[023].z = v[024].z = v[025].z = v[026].z = vis->z1; // south
+
+	v[007].z = v[010].z = v[011].z = v[012].z = v[014].z = v[015].z =
+		v[016].z = v[017].z = v[020].z = v[021].z = v[022].z = v[027].z = vis->z2; // north
+
+	v[000].y = v[001].y = v[002].y = v[006].y = v[007].y = v[010].y =
+		v[014].y = v[015].y = v[016].y = v[022].y = v[023].y = v[024].y = vis->gz; // bottom
+
+	v[003].y = v[004].y = v[005].y = v[011].y = v[012].y = v[013].y =
+		v[017].y = v[020].y = v[021].y = v[025].y = v[026].y = v[027].y = vis->gzt; // top
+
+	Surf.PolyColor = V_GetColor(R_GetBoundingBoxColor(vis->mobj));
+	
+	HWR_ProcessPolygon(&Surf, v, 24, (cv_renderhitboxgldepth.value ? 0 : PF_NoDepthTest)|PF_Modulated|PF_NoTexture|PF_WireFrame, SHADER_NONE, false);
+}
+
 // -----------------+
 // HWR_DrawSprite   : Draw flat sprites
 //                  : (monsters, bonuses, weapons, lights, ...)
@@ -4482,9 +4531,16 @@ static int CompareVisSprites(const void *p1, const void *p2)
 	int transparency1;
 	int transparency2;
 
+	int linkdraw1;
+	int linkdraw2;
+
+	// draw bbox after everything else
+	if (spr1->bbox || spr2->bbox)
+		return (spr1->bbox - spr2->bbox);
+
 	// check for precip first, because then sprX->mobj is actually a precipmobj_t and does not have flags2 or tracer
-	int linkdraw1 = !spr1->precip && (spr1->mobj->flags2 & MF2_LINKDRAW) && spr1->mobj->tracer;
-	int linkdraw2 = !spr2->precip && (spr2->mobj->flags2 & MF2_LINKDRAW) && spr2->mobj->tracer;
+	linkdraw1 = !spr1->precip && (spr1->mobj->flags2 & MF2_LINKDRAW) && spr1->mobj->tracer;
+	linkdraw2 = !spr2->precip && (spr2->mobj->flags2 & MF2_LINKDRAW) && spr2->mobj->tracer;
 
 	// ^ is the XOR operation
 	// if comparing a linkdraw and non-linkdraw sprite or 2 linkdraw sprites with different tracers, then use
@@ -4854,6 +4910,9 @@ static void HWR_DrawSprites(void)
 	for (i = 0; i < gl_visspritecount; i++)
 	{
 		gl_vissprite_t *spr = gl_vsprorder[i];
+		if (spr->bbox)
+			HWR_DrawBoundingBox(spr);
+		else
 #ifdef HWPRECIP
 		if (spr->precip)
 			HWR_DrawPrecipitationSprite(spr);
@@ -4953,8 +5012,15 @@ static void HWR_AddSprites(sector_t *sec)
 	hoop_limit_dist = (fixed_t)(cv_drawdist_nights.value) << FRACBITS;
 	for (thing = sec->thinglist; thing; thing = thing->snext)
 	{
-		if (R_ThingVisibleWithinDist(thing, limit_dist, hoop_limit_dist))
-			HWR_ProjectSprite(thing);
+		if (R_ThingWithinDist(thing, limit_dist, hoop_limit_dist))
+		{
+			if (R_ThingVisible(thing))
+			{
+				HWR_ProjectSprite(thing);
+			}
+
+			HWR_ProjectBoundingBox(thing);
+		}
 	}
 
 #ifdef HWPRECIP
@@ -5472,6 +5538,7 @@ static void HWR_ProjectSprite(mobj_t *thing)
 	vis->vflip = vflip;
 
 	vis->precip = false;
+	vis->bbox = false;
 
 	vis->angle = interp.angle;
 }
@@ -5594,6 +5661,7 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 	vis->gz = vis->gzt - FIXED_TO_FLOAT(spritecachedinfo[lumpoff].height);
 
 	vis->precip = true;
+	vis->bbox = false;
 
 	// okay... this is a hack, but weather isn't networked, so it should be ok
 	if (!(thing->precipflags & PCF_THUNK))
@@ -5607,6 +5675,61 @@ static void HWR_ProjectPrecipitationSprite(precipmobj_t *thing)
 }
 #endif
 
+static void HWR_ProjectBoundingBox(mobj_t *thing)
+{
+	gl_vissprite_t *vis;
+	float tr_x, tr_y;
+	float tz;
+	float rad;
+
+	if (!thing)
+		return;
+
+	if (!R_ThingBoundingBoxVisible(thing))
+		return;
+
+	// uncapped/interpolation
+	boolean interpolate = cv_renderhitboxinterpolation.value;
+	interpmobjstate_t interp = {0};
+
+	if (R_UsingFrameInterpolation() && !paused && interpolate)
+	{
+		R_InterpolateMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(thing, FRACUNIT, &interp);
+	}
+
+	// transform the origin point
+	tr_x = FIXED_TO_FLOAT(interp.x) - gl_viewx;
+	tr_y = FIXED_TO_FLOAT(interp.y) - gl_viewy;
+
+	// rotation around vertical axis
+	tz = (tr_x * gl_viewcos) + (tr_y * gl_viewsin);
+
+	// thing is behind view plane?
+	if (tz < ZCLIP_PLANE)
+		return;
+
+	tr_x += gl_viewx;
+	tr_y += gl_viewy;
+
+	rad = FIXED_TO_FLOAT(thing->radius);
+
+	vis = HWR_NewVisSprite();
+	vis->x1 = tr_x - rad;
+	vis->x2 = tr_x + rad;
+	vis->z1 = tr_y - rad;
+	vis->z2 = tr_y + rad;
+	vis->gz = FIXED_TO_FLOAT(interp.z);
+	vis->gzt = vis->gz + FIXED_TO_FLOAT(thing->height);
+	vis->mobj = thing;
+
+	vis->precip = false;
+	vis->bbox = true;
+}
+
 // ==========================================================================
 // Sky dome rendering, ported from PrBoom+
 // ==========================================================================
diff --git a/src/hardware/r_opengl/r_opengl.c b/src/hardware/r_opengl/r_opengl.c
index 569ddfee84032f9c88650216fa5f2127ce54df33..2fc1df8816c937dea8a43158f92c10c664467d7e 100644
--- a/src/hardware/r_opengl/r_opengl.c
+++ b/src/hardware/r_opengl/r_opengl.c
@@ -1030,6 +1030,12 @@ EXPORT void HWRAPI(LoadCustomShader) (int number, char *code, size_t size, boole
 EXPORT void HWRAPI(SetShader) (int type)
 {
 #ifdef GL_SHADERS
+	if (type == SHADER_NONE)
+	{
+		UnSetShader();
+		return;
+	}
+
 	if (gl_allowshaders != HWD_SHADEROPTION_OFF)
 	{
 		gl_shader_t *shader = gl_shaderstate.current;
@@ -2290,7 +2296,7 @@ EXPORT void HWRAPI(DrawPolygon) (FSurfaceInfo *pSurf, FOutVector *pOutVerts, FUI
 
 	pglVertexPointer(3, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].x);
 	pglTexCoordPointer(2, GL_FLOAT, sizeof(FOutVector), &pOutVerts[0].s);
-	pglDrawArrays(GL_TRIANGLE_FAN, 0, iNumPts);
+	pglDrawArrays(PolyFlags & PF_WireFrame ? GL_LINES : GL_TRIANGLE_FAN, 0, iNumPts);
 
 	if (PolyFlags & PF_RemoveYWrap)
 		pglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
diff --git a/src/p_mobj.h b/src/p_mobj.h
index 6717c4add0ab016d32821e3e6b1de7c44c06d205..f573e9020375e450234d200bfd0735f33de1cdcf 100644
--- a/src/p_mobj.h
+++ b/src/p_mobj.h
@@ -122,7 +122,7 @@ typedef enum
 	MF_AMBIENT          = 1<<10,
 	// Slide this object when it hits a wall.
 	MF_SLIDEME          = 1<<11,
-	// Player cheat.
+	// Don't collide with walls or solid objects. Two MF_NOCLIP objects can't touch each other at all!
 	MF_NOCLIP           = 1<<12,
 	// Allow moves to any height, no gravity. For active floaters.
 	MF_FLOAT            = 1<<13,
diff --git a/src/r_bbox.c b/src/r_bbox.c
new file mode 100644
index 0000000000000000000000000000000000000000..59d0893c4bfe32bd0307573b9406bbff2df28924
--- /dev/null
+++ b/src/r_bbox.c
@@ -0,0 +1,318 @@
+// SONIC ROBO BLAST 2
+//-----------------------------------------------------------------------------
+// Copyright (C) 1993-1996 by id Software, Inc.
+// Copyright (C) 1998-2000 by DooM Legacy Team.
+// Copyright (C)      2022 by Kart Krew.
+// Copyright (C) 1999-2022 by Sonic Team Junior.
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  r_bbox.c
+/// \brief Boundary box (cube) renderer
+
+#include "doomdef.h"
+#include "command.h"
+#include "r_local.h"
+#include "screen.h" // cv_renderhitbox
+#include "v_video.h" // V_DrawFill
+
+enum {
+	RENDERHITBOX_OFF,
+	RENDERHITBOX_TANGIBLE,
+	RENDERHITBOX_ALL,
+	RENDERHITBOX_INTANGIBLE,
+	RENDERHITBOX_RINGS,
+};
+
+static CV_PossibleValue_t renderhitbox_cons_t[] = {
+	{RENDERHITBOX_OFF, "Off"},
+	{RENDERHITBOX_TANGIBLE, "Tangible"},
+	{RENDERHITBOX_ALL, "All"},
+	{RENDERHITBOX_INTANGIBLE, "Intangible"},
+	{RENDERHITBOX_RINGS, "Rings"},
+	{0}};
+
+consvar_t cv_renderhitbox = CVAR_INIT ("renderhitbox", "Off", CV_CHEAT, renderhitbox_cons_t, NULL);
+consvar_t cv_renderhitboxinterpolation = CVAR_INIT ("renderhitbox_interpolation", "On", CV_SAVE, CV_OnOff, NULL);
+consvar_t cv_renderhitboxgldepth = CVAR_INIT ("renderhitbox_gldepth", "Off", CV_SAVE, CV_OnOff, NULL);
+
+struct bbox_col {
+	INT32 x;
+	INT32 y;
+	INT32 h;
+};
+
+struct bbox_config {
+	fixed_t height;
+	fixed_t tz;
+	struct bbox_col col[4];
+	UINT8 color;
+};
+
+static inline void
+raster_bbox_seg
+(		INT32 x,
+		fixed_t y,
+		fixed_t h,
+		UINT8 pixel)
+{
+	y /= FRACUNIT;
+
+	if (y < 0)
+		y = 0;
+
+	h = y + (FixedCeil(abs(h)) / FRACUNIT);
+
+	if (h >= viewheight)
+		h = viewheight;
+
+	while (y < h)
+	{
+		topleft[x + y * vid.width] = pixel;
+		y++;
+	}
+}
+
+static void
+draw_bbox_col
+(		struct bbox_config * bb,
+		size_t p,
+		fixed_t tx,
+		fixed_t ty)
+{
+	struct bbox_col *col = &bb->col[p];
+
+	fixed_t xscale, yscale;
+
+	if (ty < FRACUNIT) // projection breaks down here
+		ty = FRACUNIT;
+
+	xscale = FixedDiv(projection, ty);
+	yscale = FixedDiv(projectiony, ty);
+
+	col->x = (centerxfrac + FixedMul(tx, xscale)) / FRACUNIT;
+	col->y = (centeryfrac - FixedMul(bb->tz, yscale));
+	col->h = FixedMul(bb->height, yscale);
+
+	// Using this function is TOO EASY!
+	V_DrawFill(
+			viewwindowx + col->x,
+			viewwindowy + col->y / FRACUNIT, 1,
+			col->h / FRACUNIT, V_NOSCALESTART | bb->color);
+}
+
+static void
+draw_bbox_row
+(		struct bbox_config * bb,
+		size_t p1,
+		size_t p2)
+{
+	struct bbox_col
+		*a = &bb->col[p1],
+		*b = &bb->col[p2];
+
+	INT32 x1, x2; // left, right
+	INT32 dx; // width
+
+	fixed_t y1, y2; // top, bottom
+	fixed_t s1, s2; // top and bottom increment
+
+	if (a->x > b->x)
+	{
+		struct bbox_col *c = a;
+		a = b;
+		b = c;
+	}
+
+	x1 = a->x;
+	x2 = b->x;
+
+	if (x2 >= viewwidth)
+		x2 = viewwidth - 1;
+
+	if (x1 == x2 || x1 >= viewwidth || x2 < 0)
+		return;
+
+	dx = x2 - x1;
+
+	y1 = a->y;
+	y2 = b->y;
+	s1 = (y2 - y1) / dx;
+
+	y2 = y1 + a->h;
+	s2 = ((b->y + b->h) - y2) / dx;
+
+	// FixedCeil needs a minimum!!! :D :D
+
+	if (s1 == 0)
+		s1 = 1;
+
+	if (s2 == 0)
+		s2 = 1;
+
+	if (x1 < 0)
+	{
+		y1 -= x1 * s1;
+		y2 -= x1 * s2;
+		x1 = 0;
+	}
+
+	while (x1 < x2)
+	{
+		raster_bbox_seg(x1, y1, s1, bb->color);
+		raster_bbox_seg(x1, y2, s2, bb->color);
+
+		y1 += s1;
+		y2 += s2;
+
+		x1++;
+	}
+}
+
+UINT8 R_GetBoundingBoxColor(mobj_t *thing)
+{
+	UINT32 flags = thing->flags;
+
+	if (thing->player)
+		return 255; // 0FF
+
+	if (flags & (MF_NOCLIPTHING))
+		return 7; // BFBFBF
+
+	if (flags & (MF_BOSS|MF_ENEMY))
+		return 35; // F00
+
+	if (flags & (MF_MISSILE|MF_PAIN))
+		return 54; // F70
+
+	if (flags & (MF_SPECIAL|MF_MONITOR))
+		return 73; // FF0
+
+	if (flags & MF_PUSHABLE)
+		return 112; // 0F0
+
+	if (flags & (MF_SPRING))
+		return 181; // F0F
+
+	if (flags & (MF_NOCLIP))
+		return 152; // 00F
+
+	return 0; // FFF
+}
+
+void R_DrawThingBoundingBox(vissprite_t *vis)
+{
+	// radius offsets
+	fixed_t rs = vis->scale;
+	fixed_t rc = vis->xscale;
+
+	// translated coordinates
+	fixed_t tx = vis->gx;
+	fixed_t ty = vis->gy;
+
+	struct bbox_config bb = {
+		.height = vis->thingheight,
+		.tz = vis->texturemid,
+		.color = R_GetBoundingBoxColor(vis->mobj),
+	};
+
+	// 1--3
+	// |  |
+	// 0--2
+
+	// left
+
+	draw_bbox_col(&bb, 0, tx, ty); // bottom
+	draw_bbox_col(&bb, 1, tx - rc, ty + rs); // top
+
+	// right
+
+	tx += rs;
+	ty += rc;
+
+	draw_bbox_col(&bb, 2, tx, ty); // bottom
+	draw_bbox_col(&bb, 3, tx - rc, ty + rs); // top
+
+	// connect all four columns
+
+	draw_bbox_row(&bb, 0, 1);
+	draw_bbox_row(&bb, 1, 3);
+	draw_bbox_row(&bb, 3, 2);
+	draw_bbox_row(&bb, 2, 0);
+}
+
+static boolean is_tangible (mobj_t *thing)
+{
+	// These objects can never touch another
+	if (thing->flags & (MF_NOCLIPTHING))
+	{
+		return false;
+	}
+
+	// These objects probably do nothing! :D
+	if ((thing->flags & (MF_SPECIAL|MF_SOLID|MF_SHOOTABLE
+					|MF_PUSHABLE|MF_BOSS|MF_MISSILE|MF_SPRING
+					|MF_BOUNCE|MF_MONITOR|MF_FIRE|MF_ENEMY
+					|MF_PAIN|MF_STICKY
+					|MF_GRENADEBOUNCE)) == 0U)
+	{
+		return false;
+	}
+
+	return true;
+}
+
+boolean R_ThingBoundingBoxVisible(mobj_t *thing)
+{
+	INT32 cvmode = cv_renderhitbox.value;
+
+	// Do not render bbox for these
+	switch (thing->type)
+	{
+		default:
+			// First person / awayviewmobj -- rendering
+			// a bbox too close to the viewpoint causes
+			// anomalies and these are exactly on the
+			// viewpoint!
+			if (thing != r_viewmobj)
+			{
+				break;
+			}
+			// FALLTHRU
+
+		case MT_SKYBOX:
+			// Ditto for skybox viewpoint but because they
+			// are rendered using portals in Software,
+			// r_viewmobj does not point here.
+			return false;
+	}
+
+	switch (cvmode)
+	{
+		case RENDERHITBOX_OFF:
+			return false;
+
+		case RENDERHITBOX_ALL:
+			return true;
+
+		case RENDERHITBOX_INTANGIBLE:
+			return !is_tangible(thing);
+
+		case RENDERHITBOX_TANGIBLE:
+			// Exclude rings from here, lots of them!
+			if (thing->type == MT_RING)
+			{
+				return false;
+			}
+
+			return is_tangible(thing);
+
+		case RENDERHITBOX_RINGS:
+			return (thing->type == MT_RING || thing->type == MT_BLUESPHERE);
+
+		default:
+			return false;
+	}
+}
diff --git a/src/r_things.c b/src/r_things.c
index 89b9fe07ef89ddd0307317ca0933608b7cbdf7ff..19b8ee83d01081271d456db7478c00210421c941 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -1426,6 +1426,105 @@ static void R_ProjectDropShadow(mobj_t *thing, vissprite_t *vis, fixed_t scale,
 	objectsdrawn++;
 }
 
+static void R_ProjectBoundingBox(mobj_t *thing, vissprite_t *vis)
+{
+	fixed_t gx, gy;
+	fixed_t tx, tz;
+
+	vissprite_t *box;
+
+	if (!R_ThingBoundingBoxVisible(thing))
+	{
+		return;
+	}
+
+	// uncapped/interpolation
+	boolean interpolate = cv_renderhitboxinterpolation.value;
+	interpmobjstate_t interp = {0};
+
+	// do interpolation
+	if (R_UsingFrameInterpolation() && !paused && interpolate)
+	{
+		R_InterpolateMobjState(thing, rendertimefrac, &interp);
+	}
+	else
+	{
+		R_InterpolateMobjState(thing, FRACUNIT, &interp);
+	}
+
+	// 1--3
+	// |  |
+	// 0--2
+
+	// start in the (0) corner
+	gx = interp.x - thing->radius - viewx;
+	gy = interp.y - thing->radius - viewy;
+
+	tz = FixedMul(gx, viewcos) + FixedMul(gy, viewsin);
+
+	// thing is behind view plane?
+	// if parent vis is visible, ignore this
+	if (!vis && (tz < FixedMul(MINZ, interp.scale)))
+	{
+		return;
+	}
+
+	tx = FixedMul(gx, viewsin) - FixedMul(gy, viewcos);
+
+	// too far off the side?
+	if (!vis && abs(tx) > FixedMul(tz, fovtan)<<2)
+	{
+		return;
+	}
+
+	box = R_NewVisSprite();
+	box->mobj = thing;
+	box->mobjflags = thing->flags;
+	box->thingheight = thing->height;
+	box->cut = SC_BBOX;
+
+	box->gx = tx;
+	box->gy = tz;
+
+	box->scale = 2 * FixedMul(thing->radius, viewsin);
+	box->xscale = 2 * FixedMul(thing->radius, viewcos);
+
+	box->pz = interp.z;
+	box->pzt = box->pz + box->thingheight;
+
+	box->gzt = box->pzt;
+	box->gz = box->pz;
+	box->texturemid = box->gzt - viewz;
+
+	if (vis)
+	{
+		box->x1 = vis->x1;
+		box->x2 = vis->x2;
+		box->szt = vis->szt;
+		box->sz = vis->sz;
+
+		box->sortscale = vis->sortscale; // link sorting to sprite
+		box->dispoffset = vis->dispoffset + 5;
+
+		box->cut |= SC_LINKDRAW;
+	}
+	else
+	{
+		fixed_t xscale = FixedDiv(projection, tz);
+		fixed_t yscale = FixedDiv(projectiony, tz);
+		fixed_t top = (centeryfrac - FixedMul(box->texturemid, yscale));
+
+		box->x1 = (centerxfrac + FixedMul(box->gx, xscale)) / FRACUNIT;
+		box->x2 = box->x1;
+
+		box->szt = top / FRACUNIT;
+		box->sz = (top + FixedMul(box->thingheight, yscale)) / FRACUNIT;
+
+		box->sortscale = yscale;
+		box->dispoffset = 0;
+	}
+}
+
 //
 // R_ProjectSprite
 // Generates a vissprite for a thing
@@ -2187,6 +2286,8 @@ static void R_ProjectSprite(mobj_t *thing)
 	if (oldthing->shadowscale && cv_shadow.value)
 		R_ProjectDropShadow(oldthing, vis, oldthing->shadowscale, basetx, basetz);
 
+	R_ProjectBoundingBox(oldthing, vis);
+
 	// Debug
 	++objectsdrawn;
 }
@@ -2412,8 +2513,26 @@ void R_AddSprites(sector_t *sec, INT32 lightlevel)
 	hoop_limit_dist = (fixed_t)(cv_drawdist_nights.value) << FRACBITS;
 	for (thing = sec->thinglist; thing; thing = thing->snext)
 	{
-		if (R_ThingVisibleWithinDist(thing, limit_dist, hoop_limit_dist))
-			R_ProjectSprite(thing);
+		if (R_ThingWithinDist(thing, limit_dist, hoop_limit_dist))
+		{
+			const INT32 oldobjectsdrawn = objectsdrawn;
+
+			if (R_ThingVisible(thing))
+			{
+				R_ProjectSprite(thing);
+			}
+
+			// I'm so smart :^)
+			if (objectsdrawn == oldobjectsdrawn)
+			{
+				/*
+				Object is invisible OR is off screen but
+				render its bbox even if the latter because
+				radius could be bigger than sprite.
+				*/
+				R_ProjectBoundingBox(thing, NULL);
+			}
+		}
 	}
 
 	// no, no infinite draw distance for precipitation. this option at zero is supposed to turn it off
@@ -2501,6 +2620,10 @@ static void R_SortVisSprites(vissprite_t* vsprsortedhead, UINT32 start, UINT32 e
 			if (dsfirst->cut & SC_SHADOW)
 				continue;
 
+			// don't connect to your bounding box!
+			if (dsfirst->cut & SC_BBOX)
+				continue;
+
 			// don't connect if it's not the tracer
 			if (dsfirst->mobj != ds->mobj)
 				continue;
@@ -2941,7 +3064,9 @@ static void R_DrawSprite(vissprite_t *spr)
 	mfloorclip = spr->clipbot;
 	mceilingclip = spr->cliptop;
 
-	if (spr->cut & SC_SPLAT)
+	if (spr->cut & SC_BBOX)
+		R_DrawThingBoundingBox(spr);
+	else if (spr->cut & SC_SPLAT)
 		R_DrawFloorSplat(spr);
 	else
 		R_DrawVisSprite(spr);
@@ -3180,9 +3305,13 @@ void R_ClipSprites(drawseg_t* dsstart, portal_t* portal)
 	for (; clippedvissprites < visspritecount; clippedvissprites++)
 	{
 		vissprite_t *spr = R_GetVisSprite(clippedvissprites);
-		INT32 x1 = (spr->cut & SC_SPLAT) ? 0 : spr->x1;
-		INT32 x2 = (spr->cut & SC_SPLAT) ? viewwidth : spr->x2;
-		R_ClipVisSprite(spr, x1, x2, dsstart, portal);
+
+		if (!(spr->cut & SC_BBOX)) // Do not clip bounding boxes
+		{
+			INT32 x1 = (spr->cut & SC_SPLAT) ? 0 : spr->x1;
+			INT32 x2 = (spr->cut & SC_SPLAT) ? viewwidth : spr->x2;
+			R_ClipVisSprite(spr, x1, x2, dsstart, portal);
+		}
 	}
 }
 
@@ -3196,16 +3325,11 @@ boolean R_ThingVisible (mobj_t *thing)
 	));
 }
 
-boolean R_ThingVisibleWithinDist (mobj_t *thing,
+boolean R_ThingWithinDist (mobj_t *thing,
 		fixed_t      limit_dist,
 		fixed_t hoop_limit_dist)
 {
-	fixed_t approx_dist;
-
-	if (! R_ThingVisible(thing))
-		return false;
-
-	approx_dist = P_AproxDistance(viewx-thing->x, viewy-thing->y);
+	const fixed_t approx_dist = P_AproxDistance(viewx-thing->x, viewy-thing->y);
 
 	if (thing->sprite == SPR_HOOP)
 	{
diff --git a/src/r_things.h b/src/r_things.h
index 22a37a0eaccbd622bcb92ec6c4fa2836a5aa9456..bb8a1e97b024e5b2cb749d71b46ef2eff785460b 100644
--- a/src/r_things.h
+++ b/src/r_things.h
@@ -66,9 +66,12 @@ void R_AddSprites(sector_t *sec, INT32 lightlevel);
 void R_InitSprites(void);
 void R_ClearSprites(void);
 
+UINT8 R_GetBoundingBoxColor(mobj_t *thing);
+boolean R_ThingBoundingBoxVisible(mobj_t *thing);
+
 boolean R_ThingVisible (mobj_t *thing);
 
-boolean R_ThingVisibleWithinDist (mobj_t *thing,
+boolean R_ThingWithinDist (mobj_t *thing,
 		fixed_t        draw_dist,
 		fixed_t nights_draw_dist);
 
@@ -132,6 +135,7 @@ typedef enum
 	SC_SHADOW     = 1<<10,
 	SC_SHEAR      = 1<<11,
 	SC_SPLAT      = 1<<12,
+	SC_BBOX       = 1<<13,
 	// masks
 	SC_CUTMASK    = SC_TOP|SC_BOTTOM,
 	SC_FLAGMASK   = ~SC_CUTMASK
@@ -221,6 +225,9 @@ void R_ClipSprites(drawseg_t* dsstart, portal_t* portal);
 void R_ClipVisSprite(vissprite_t *spr, INT32 x1, INT32 x2, drawseg_t* dsstart, portal_t* portal);
 
 boolean R_SpriteIsFlashing(vissprite_t *vis);
+
+void R_DrawThingBoundingBox(vissprite_t *spr);
+
 UINT8 *R_GetSpriteTranslation(vissprite_t *vis);
 
 // ----------
diff --git a/src/screen.h b/src/screen.h
index 19103b0df59036c80ea85112f47a7673596ded67..9222805fb4eaff2cf9669b20a3d57ea597b2e86c 100644
--- a/src/screen.h
+++ b/src/screen.h
@@ -200,6 +200,7 @@ extern INT32 scr_bpp;
 extern UINT8 *scr_borderpatch; // patch used to fill the view borders
 
 extern consvar_t cv_scr_width, cv_scr_height, cv_scr_depth, cv_renderview, cv_renderer, cv_fullscreen;
+extern consvar_t cv_renderhitbox, cv_renderhitboxinterpolation, cv_renderhitboxgldepth;
 // wait for page flipping to end or not
 extern consvar_t cv_vidwait;
 extern consvar_t cv_timescale;