diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index f66338afc..4a35ec314 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -40,6 +40,7 @@ list(APPEND CLDLL_SOURCES "postfx_parameters.cpp" "client_weapon_layer_impl.cpp" "weapon_predicting_context.cpp" + "visualizer/debug_visualizer.cpp" ) # shared source files diff --git a/client/cdll_int.cpp b/client/cdll_int.cpp index c95334e81..dc71696e0 100644 --- a/client/cdll_int.cpp +++ b/client/cdll_int.cpp @@ -34,6 +34,7 @@ #include "pm_shared.h" #include "filesystem_utils.h" #include "build_info.h" +#include "visualizer/debug_visualizer.h" #include int developer_level; @@ -331,6 +332,7 @@ extern "C" void DLLEXPORT HUD_Frame( double time ) { // run anti (_-=ZhekA=-_) system for Xash3D engine gEngfuncs.VGui_ViewportPaintBackground( VGUI_GetRect( )); + CDebugVisualizer::GetInstance().RunFrame(); } extern "C" int DLLEXPORT HUD_Key_Event( int down, int keynum, const char *pszCurrentBinding ) diff --git a/client/render/gl_backend.cpp b/client/render/gl_backend.cpp index 5380af4e9..540045f7d 100644 --- a/client/render/gl_backend.cpp +++ b/client/render/gl_backend.cpp @@ -33,6 +33,7 @@ GNU General Public License for more details. #include "gl_shader.h" #include "gl_cvars.h" #include "gl_debug.h" +#include "gl_debug_visualizer_backend.h" #include "imgui_manager.h" #include "screenfade.h" #include "shake.h" @@ -463,6 +464,9 @@ void GL_BackendEndFrame( ref_viewpass_t *rvp, RefParams params ) DBG_DrawLightFrustum(); // 3D + if( RP_NORMALPASS( )) + CDebugVisualizerBackend::GetInstance().DrawFrame(); + R_PushRefState(); RI->params = params; RI->view.fov_x = rvp->fov_x; diff --git a/client/render/gl_debug.cpp b/client/render/gl_debug.cpp index b310105e9..a91243292 100644 --- a/client/render/gl_debug.cpp +++ b/client/render/gl_debug.cpp @@ -22,6 +22,8 @@ #include "vertex_fmt.h" #include "gl_cvars.h" #include "mathlib.h" +#include "visualizer/debug_visualizer.h" +#include "gl_debug_overlay_2d.h" void GL_GpuMemUsage_f( void ) { @@ -101,67 +103,60 @@ void DBG_PrintVertexVBOSizes( void ) ALERT( at_console, "sizeof( svert_v8_gl30_t ) == %d bytes\n", sizeof( svert_v8_gl30_t )); } -// some simple helpers to draw a cube in the special way the ambient visualization wants -static float *CubeSide( const vec3_t pos, float size, int vert ) +// Build the 8 corners of an axis-aligned cube centered at `pos` with the given +// half-size `rad`. Uses the engine's HL corner convention: bit 0 set = -X, bit 1 = -Y, bit 2 = -Z. +static void CubeCorners( const Vector &pos, float rad, std::array &out ) { - static vec3_t side; - - VectorCopy( pos, side ); - side[0] += (vert & 1) ? -size : size; - side[1] += (vert & 2) ? -size : size; - side[2] += (vert & 4) ? -size : size; - - return side; + for (int i = 0; i < 8; i++) { + out[i].x = pos.x + ((i & 1) ? -rad : rad); + out[i].y = pos.y + ((i & 2) ? -rad : rad); + out[i].z = pos.z + ((i & 4) ? -rad : rad); + } } -static void CubeFace( const vec3_t org, int v0, int v1, int v2, int v3, float size, const byte *color ) -{ - vec3_t col; - float scale = tr.lightstyle[0] / 264.0f; - float gamma = 1.0f / tr.light_gamma; - - col[0] = powf( color[0] / 255.0f, gamma ) * scale; - col[1] = powf( color[1] / 255.0f, gamma ) * scale; - col[2] = powf( color[2] / 255.0f, gamma ) * scale; - - pglColor3fv( col ); - pglVertex3fv( CubeSide( org, size, v0 )); - pglVertex3fv( CubeSide( org, size, v1 )); - pglVertex3fv( CubeSide( org, size, v2 )); - pglVertex3fv( CubeSide( org, size, v3 )); -} - void R_RenderLightProbe( mlightprobe_t *probe ) { - float rad = 4.0f; - - pglBegin( GL_QUADS ); - - CubeFace( probe->origin, 4, 6, 2, 0, rad, probe->cube.color[0] ); - CubeFace( probe->origin, 7, 5, 1, 3, rad, probe->cube.color[1] ); - CubeFace( probe->origin, 0, 1, 5, 4, rad, probe->cube.color[2] ); - CubeFace( probe->origin, 3, 2, 6, 7, rad, probe->cube.color[3] ); - CubeFace( probe->origin, 2, 3, 1, 0, rad, probe->cube.color[4] ); - CubeFace( probe->origin, 4, 5, 7, 6, rad, probe->cube.color[5] ); - - pglEnd (); + const float rad = 4.0f; + const float scale = tr.lightstyle[0] / 264.0f; + const float gamma = 1.0f / tr.light_gamma; + + auto toLinear = [&]( const byte *c ) { + return Vector( + powf( c[0] / 255.0f, gamma ) * scale, + powf( c[1] / 255.0f, gamma ) * scale, + powf( c[2] / 255.0f, gamma ) * scale ); + }; + + // probe->cube.color is indexed +X, -X, +Y, -Y, +Z, -Z; + // g_boxpnt (backend face order) is +X, +Y, +Z, -X, -Y, -Z. + std::array faceColors = { + toLinear( probe->cube.color[0] ), // +X + toLinear( probe->cube.color[2] ), // +Y + toLinear( probe->cube.color[4] ), // +Z + toLinear( probe->cube.color[1] ), // -X + toLinear( probe->cube.color[3] ), // -Y + toLinear( probe->cube.color[5] ), // -Z + }; + + std::array corners; + CubeCorners( probe->origin, rad, corners ); + + CDebugVisualizer::GetInstance().DrawFilledBox( corners, faceColors, 1.0f, std::nullopt, true ); } void R_RenderCubemap( mcubemap_t *cube ) { - float rad = (float)cube->size * 0.1f; - byte color[3] = { 127, 127, 127 }; - - pglBegin( GL_QUADS ); + const float rad = (float)cube->size * 0.1f; + const float scale = tr.lightstyle[0] / 264.0f; + const float gamma = 1.0f / tr.light_gamma; + const float gray = powf( 127.0f / 255.0f, gamma ) * scale; + const Vector color( gray, gray, gray ); + const std::array faceColors = { color, color, color, color, color, color }; - CubeFace( cube->origin, 4, 6, 2, 0, rad, color ); - CubeFace( cube->origin, 7, 5, 1, 3, rad, color ); - CubeFace( cube->origin, 0, 1, 5, 4, rad, color ); - CubeFace( cube->origin, 3, 2, 6, 7, rad, color ); - CubeFace( cube->origin, 2, 3, 1, 0, rad, color ); - CubeFace( cube->origin, 4, 5, 7, 6, rad, color ); + std::array corners; + CubeCorners( cube->origin, rad, corners ); - pglEnd (); + CDebugVisualizer::GetInstance().DrawFilledBox( corners, faceColors, 1.0f, std::nullopt, true ); } void R_RenderLightProbeInternal( const Vector &origin, const Vector lightCube[] ) @@ -187,56 +182,15 @@ R_DrawAABB */ static void R_DrawAABB( const Vector &absmin, const Vector &absmax, int contents ) { - vec3_t bbox[8]; - int i; - - // compute a full bounding box - for( i = 0; i < 8; i++ ) - { - bbox[i][0] = ( i & 1 ) ? absmin[0] : absmax[0]; - bbox[i][1] = ( i & 2 ) ? absmin[1] : absmax[1]; - bbox[i][2] = ( i & 4 ) ? absmin[2] : absmax[2]; - } - - GL_Bind( GL_TEXTURE0, tr.whiteTexture ); - pglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); - pglDisable( GL_DEPTH_TEST ); - + Vector color; switch( contents ) { - case CONTENTS_EMPTY: - pglColor4f( 0.5f, 1.0f, 0.0f, 1.0f ); // green for empty - break; - case CONTENTS_SOLID: - pglColor4f( 1.0f, 0.0f, 0.0f, 1.0f ); // red for solid - break; - case CONTENTS_WATER: - pglColor4f( 0.0f, 0.5f, 1.0f, 1.0f ); // blue for water - break; - default: - pglColor4f( 0.5f, 0.5f, 0.5f, 1.0f ); // gray as default - break; + case CONTENTS_EMPTY: color = Vector( 0.5f, 1.0f, 0.0f ); break; // green for empty + case CONTENTS_SOLID: color = Vector( 1.0f, 0.0f, 0.0f ); break; // red for solid + case CONTENTS_WATER: color = Vector( 0.0f, 0.5f, 1.0f ); break; // blue for water + default: color = Vector( 0.5f, 0.5f, 0.5f ); break; // gray otherwise } - pglBegin( GL_LINES ); - - for( i = 0; i < 2; i += 1 ) - { - pglVertex3fv( bbox[i+0] ); - pglVertex3fv( bbox[i+2] ); - pglVertex3fv( bbox[i+4] ); - pglVertex3fv( bbox[i+6] ); - pglVertex3fv( bbox[i+0] ); - pglVertex3fv( bbox[i+4] ); - pglVertex3fv( bbox[i+2] ); - pglVertex3fv( bbox[i+6] ); - pglVertex3fv( bbox[i*2+0] ); - pglVertex3fv( bbox[i*2+1] ); - pglVertex3fv( bbox[i*2+4] ); - pglVertex3fv( bbox[i*2+5] ); - } - - pglEnd(); - pglEnable( GL_DEPTH_TEST ); + CDebugVisualizer::GetInstance().DrawAABB( absmin, absmax, color, std::nullopt, false ); } /* @@ -345,137 +299,45 @@ void DrawCubeMaps( void ) pglColor3f( 1.0f, 1.0f, 1.0f ); } -void DBG_DrawBBox( const Vector &mins, const Vector &maxs ) +void DBG_DrawLightFrustum( void ) { - Vector bbox[8]; - int i; - - for( i = 0; i < 8; i++ ) - { - bbox[i][0] = ( i & 1 ) ? mins[0] : maxs[0]; - bbox[i][1] = ( i & 2 ) ? mins[1] : maxs[1]; - bbox[i][2] = ( i & 4 ) ? mins[2] : maxs[2]; - } - - pglColor4f( 1.0f, 0.0f, 1.0f, 1.0f ); // yellow bboxes for frustum - GL_Bind( GL_TEXTURE0, tr.whiteTexture ); - pglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); - pglBegin( GL_LINES ); + if( !CVAR_TO_BOOL( r_scissor_light_debug ) || !RP_NORMALPASS( )) + return; - for( i = 0; i < 2; i += 1 ) - { - pglVertex3fv( bbox[i+0] ); - pglVertex3fv( bbox[i+2] ); - pglVertex3fv( bbox[i+4] ); - pglVertex3fv( bbox[i+6] ); - pglVertex3fv( bbox[i+0] ); - pglVertex3fv( bbox[i+4] ); - pglVertex3fv( bbox[i+2] ); - pglVertex3fv( bbox[i+6] ); - pglVertex3fv( bbox[i*2+0] ); - pglVertex3fv( bbox[i*2+1] ); - pglVertex3fv( bbox[i*2+4] ); - pglVertex3fv( bbox[i*2+5] ); - } - pglEnd(); -} + auto &visualizer = CDebugVisualizer::GetInstance(); + const Vector frustumColor(1.0f, 1.0f, 0.0f); // yellow + const Vector bboxColor(1.0f, 0.0f, 1.0f); // magenta -void DBG_DrawLightFrustum( void ) -{ - if( CVAR_TO_BOOL( r_scissor_light_debug ) && RP_NORMALPASS( )) + for( int i = 0; i < MAX_DLIGHTS; i++ ) { - GL_DEBUG_SCOPE(); - pglTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); + CDynLight *pl = &tr.dlights[i]; + if( !pl->Active( )) continue; - for( int i = 0; i < MAX_DLIGHTS; i++ ) + if( r_scissor_light_debug->value == 1.0f ) { - CDynLight *pl = &tr.dlights[i]; - - if( !pl->Active( )) continue; - - if( r_scissor_light_debug->value == 1.0f ) + R_DrawScissorRectangle( pl->x, pl->y, pl->w, pl->h ); + } + else if( r_scissor_light_debug->value == 2.0f ) + { + if( pl->type == LIGHT_DIRECTIONAL ) { - R_DrawScissorRectangle( pl->x, pl->y, pl->w, pl->h ); + for (int j = 0; j < NUM_SHADOW_SPLITS + 1; j++) + visualizer.DrawFrustum(pl->splitFrustum[j], frustumColor, std::nullopt, true); } - else if( r_scissor_light_debug->value == 2.0f ) + else if( pl->type == LIGHT_OMNI ) { - if( pl->type == LIGHT_DIRECTIONAL ) - { - for (int j = 0; j < NUM_SHADOW_SPLITS + 1; j++) - DBG_DrawFrustum( pl->splitFrustum[j] ); - } - else DBG_DrawFrustum( pl->frustum ); + visualizer.DrawSphere(pl->origin, pl->radius, frustumColor, std::nullopt, true); } else - { - DBG_DrawBBox( pl->absmin, pl->absmax ); + { + visualizer.DrawFrustum(pl->frustum, frustumColor, std::nullopt, true); } } - } -} - -void DBG_DrawFrustum(const CFrustum &frustum) -{ - Vector bbox[8]; - frustum.ComputeFrustumCorners( bbox ); - - // g-cont. frustum must be yellow :-) - pglColor4f( 1.0f, 1.0f, 0.0f, 1.0f ); - GL_Bind( GL_TEXTURE0, tr.whiteTexture ); - pglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); - pglShadeModel( GL_SMOOTH ); - pglBegin( GL_LINES ); - - for( int i = 0; i < 2; i += 1 ) - { - pglVertex3fv( bbox[i+0] ); - pglVertex3fv( bbox[i+2] ); - pglVertex3fv( bbox[i+4] ); - pglVertex3fv( bbox[i+6] ); - pglVertex3fv( bbox[i+0] ); - pglVertex3fv( bbox[i+4] ); - pglVertex3fv( bbox[i+2] ); - pglVertex3fv( bbox[i+6] ); - pglVertex3fv( bbox[i*2+0] ); - pglVertex3fv( bbox[i*2+1] ); - pglVertex3fv( bbox[i*2+4] ); - pglVertex3fv( bbox[i*2+5] ); - } - - // visualize plane normals - for (int i = 0; i < FRUSTUM_PLANES; i++) - { - Vector plane_midpoint; - switch (i) + else { - case FRUSTUM_LEFT: - plane_midpoint = (bbox[0] + bbox[2] + bbox[4] + bbox[6]) * 0.25f; - break; - case FRUSTUM_RIGHT: - plane_midpoint = (bbox[1] + bbox[3] + bbox[5] + bbox[7]) * 0.25f; - break; - case FRUSTUM_BOTTOM: - plane_midpoint = (bbox[2] + bbox[3] + bbox[6] + bbox[7]) * 0.25f; - break; - case FRUSTUM_TOP: - plane_midpoint = (bbox[0] + bbox[1] + bbox[4] + bbox[5]) * 0.25f; - break; - case FRUSTUM_FAR: - plane_midpoint = (bbox[0] + bbox[1] + bbox[2] + bbox[3]) * 0.25f; - break; - case FRUSTUM_NEAR: - plane_midpoint = (bbox[4] + bbox[5] + bbox[6] + bbox[7]) * 0.25f; - break; + visualizer.DrawAABB(pl->absmin, pl->absmax, bboxColor, std::nullopt, true); } - - pglColor4f(0.0f, 1.0f, 0.0f, 1.0f); - pglVertex3fv(plane_midpoint); - pglColor4f(1.0f, 0.0f, 0.0f, 1.0f); - pglVertex3fv(plane_midpoint + frustum.GetPlane(i)->normal * 32.0); } - - pglEnd(); - pglColor4f( 1.0f, 1.0f, 1.0f, 1.0f ); } void DBG_DrawGlassScissors( void ) @@ -503,18 +365,15 @@ Draws vertex tangent spaces for debugging */ void DrawTangentSpaces( void ) { - float temp[3]; - float vecLen = 4.0f; - if( !CVAR_TO_BOOL( cv_show_tbn )) return; GL_DEBUG_SCOPE(); - GL_Bind( GL_TEXTURE0, tr.whiteTexture ); - pglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); - pglDisable( GL_DEPTH_TEST ); - GL_Blend( GL_FALSE ); - pglBegin( GL_LINES ); + const float vecLen = 4.0f; + const Vector tangentColor( 1.0f, 0.0f, 0.0f ); + const Vector binormalColor( 0.0f, 1.0f, 0.0f ); + const Vector normalColor( 0.0f, 0.0f, 1.0f ); + auto &visualizer = CDebugVisualizer::GetInstance(); for( int i = 0; i < worldmodel->nummodelsurfaces; i++ ) { @@ -528,151 +387,79 @@ void DrawTangentSpaces( void ) for( int j = 0; j < esrf->numverts; j++, mv++ ) { - pglColor3f( 1.0f, 0.0f, 0.0f ); - pglVertex3fv( mv->vertex ); - VectorMA( mv->vertex, vecLen, Vector( mv->tangent ), temp ); - pglVertex3fv( temp ); - - pglColor3f( 0.0f, 1.0f, 0.0f ); - pglVertex3fv( mv->vertex ); - VectorMA( mv->vertex, vecLen, Vector( mv->binormal ), temp ); - pglVertex3fv( temp ); - - pglColor3f( 0.0f, 0.0f, 1.0f ); - pglVertex3fv( mv->vertex ); - VectorMA( mv->vertex, vecLen, Vector( mv->normal ), temp ); - pglVertex3fv( temp ); + visualizer.DrawVector( mv->vertex, Vector( mv->tangent ) * vecLen, tangentColor, std::nullopt, false ); + visualizer.DrawVector( mv->vertex, Vector( mv->binormal ) * vecLen, binormalColor, std::nullopt, false ); + visualizer.DrawVector( mv->vertex, Vector( mv->normal ) * vecLen, normalColor, std::nullopt, false ); } } - - pglEnd(); - pglEnable( GL_DEPTH_TEST ); } void DrawWireFrame( void ) { - int i; - if( !CVAR_TO_BOOL( r_wireframe )) return; GL_DEBUG_SCOPE(); - pglEnable( GL_BLEND ); - pglBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); - pglPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); - pglColor4f( 1.0f, 1.0f, 1.0f, 0.99f ); + auto &visualizer = CDebugVisualizer::GetInstance(); + const Vector solidColor( 1.0f, 1.0f, 1.0f ); // white: solid surfaces + const Vector transColor( 0.0f, 1.0f, 0.0f ); // green: translucent surfaces - pglDisable( GL_DEPTH_TEST ); - pglEnable( GL_LINE_SMOOTH ); - GL_Bind( GL_TEXTURE0, tr.whiteTexture ); - pglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); - pglEnable( GL_POLYGON_SMOOTH ); - pglHint( GL_LINE_SMOOTH_HINT, GL_NICEST ); - pglHint( GL_POLYGON_SMOOTH_HINT, GL_NICEST ); - - for( i = 0; i < RI->frame.solid_faces.Count(); i++ ) - { - if( RI->frame.solid_faces[i].m_bDrawType != DRAWTYPE_SURFACE ) - continue; - - msurface_t *surf = RI->frame.solid_faces[i].m_pSurf; + auto submitSurfaceEdges = [&]( msurface_t *surf, const Vector &color ) { mextrasurf_t *es = surf->info; - - pglBegin( GL_POLYGON ); for( int j = 0; j < es->numverts; j++ ) { - bvert_t *v = &world->vertexes[es->firstvertex + j]; - pglVertex3fv( v->vertex + Vector( v->normal ) * 0.1f ); + bvert_t *va = &world->vertexes[es->firstvertex + j]; + bvert_t *vb = &world->vertexes[es->firstvertex + ((j + 1) % es->numverts)]; + const Vector a = Vector(va->vertex) + Vector(va->normal) * 0.1f; + const Vector b = Vector(vb->vertex) + Vector(vb->normal) * 0.1f; + visualizer.DrawVector( a, b - a, color, std::nullopt, false ); } - pglEnd(); + }; + + for( int i = 0; i < RI->frame.solid_faces.Count(); i++ ) + { + if( RI->frame.solid_faces[i].m_bDrawType != DRAWTYPE_SURFACE ) + continue; + submitSurfaceEdges( RI->frame.solid_faces[i].m_pSurf, solidColor ); } - pglColor4f( 0.0f, 1.0f, 0.0f, 0.99f ); - for( i = 0; i < RI->frame.trans_list.Count(); i++ ) + for( int i = 0; i < RI->frame.trans_list.Count(); i++ ) { if( RI->frame.trans_list[i].m_bDrawType != DRAWTYPE_SURFACE ) continue; - - msurface_t *surf = RI->frame.trans_list[i].m_pSurf; - mextrasurf_t *es = surf->info; - - pglBegin( GL_POLYGON ); - for( int j = 0; j < es->numverts; j++ ) - { - bvert_t *v = &world->vertexes[es->firstvertex + j]; - pglVertex3fv( v->vertex + Vector( v->normal ) * 0.1f ); - } - pglEnd(); + submitSurfaceEdges( RI->frame.trans_list[i].m_pSurf, transColor ); } - - pglPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); - pglDisable( GL_POLYGON_SMOOTH ); - pglDisable( GL_LINE_SMOOTH ); - pglEnable( GL_DEPTH_TEST ); - pglDisable( GL_BLEND ); } void DrawWirePoly( msurface_t *surf ) { - if( !surf ) + if( !surf ) return; GL_DEBUG_SCOPE(); - pglEnable( GL_BLEND ); - pglBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); - pglPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); - pglColor4f( 0.5f, 1.0f, 0.36f, 0.99f ); - pglLineWidth( 4.0f ); - - pglDisable( GL_DEPTH_TEST ); - pglEnable( GL_LINE_SMOOTH ); - pglEnable( GL_POLYGON_SMOOTH ); - pglHint( GL_LINE_SMOOTH_HINT, GL_NICEST ); - pglHint( GL_POLYGON_SMOOTH_HINT, GL_NICEST ); - + auto &visualizer = CDebugVisualizer::GetInstance(); + const Vector color( 0.5f, 1.0f, 0.36f ); mextrasurf_t *es = surf->info; - - pglBegin( GL_POLYGON ); for( int j = 0; j < es->numverts; j++ ) { - bvert_t *v = &world->vertexes[es->firstvertex + j]; - pglVertex3fv( v->vertex + Vector( v->normal ) * 0.1f ); + bvert_t *va = &world->vertexes[es->firstvertex + j]; + bvert_t *vb = &world->vertexes[es->firstvertex + ((j + 1) % es->numverts)]; + const Vector a = Vector(va->vertex) + Vector(va->normal) * 0.1f; + const Vector b = Vector(vb->vertex) + Vector(vb->normal) * 0.1f; + visualizer.DrawVector( a, b - a, color, std::nullopt, false, 4.0f ); } - pglEnd(); - - pglPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); - pglDisable( GL_POLYGON_SMOOTH ); - pglDisable( GL_LINE_SMOOTH ); - pglEnable( GL_DEPTH_TEST ); - pglDisable( GL_BLEND ); - pglLineWidth( 1.0f ); } void R_ShowLightMaps( void ) { - int index = 0; - if( !CVAR_TO_BOOL( r_showlightmaps )) return; - index = r_showlightmaps->value - 1.0f; + int index = r_showlightmaps->value - 1.0f; if( tr.lightmaps[index].state == LM_FREE ) return; - GL_Bind( GL_TEXTURE0, tr.lightmaps[index].lightmap ); - pglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE ); - GL_Setup2D(); - - pglBegin( GL_QUADS ); - pglTexCoord2f( 0.0f, 0.0f ); - pglVertex2f( 0.0f, 0.0f ); - pglTexCoord2f( 1.0f, 0.0f ); - pglVertex2f( glState.width, 0.0f ); - pglTexCoord2f( 1.0f, 1.0f ); - pglVertex2f( glState.width, glState.height ); - pglTexCoord2f( 0.0f, 1.0f ); - pglVertex2f( 0.0f, glState.height ); - pglEnd(); - - GL_Setup3D(); + CDebugOverlay2D::GetInstance().DrawTexturedRect( + 0.0f, 0.0f, (float)glState.width, (float)glState.height, + tr.lightmaps[index].lightmap); } diff --git a/client/render/gl_debug.h b/client/render/gl_debug.h index 7cafdd72a..796a0bc54 100644 --- a/client/render/gl_debug.h +++ b/client/render/gl_debug.h @@ -12,12 +12,10 @@ void GL_CheckForErrorsInternal(const char *filename, int line); void DBG_PrintVertexVBOSizes(); void DBG_DrawLightFrustum(); -void DBG_DrawFrustum(const CFrustum &frustum); void DBG_DrawGlassScissors(); void DrawLightProbes(); void R_ShowLightMaps(); void R_RenderLightProbeInternal(const Vector &origin, const Vector lightCube[]); -void DBG_DrawBBox(const Vector &mins, const Vector &maxs); void DrawWirePoly(msurface_t *surf); void DrawTangentSpaces(); void DrawWireFrame(); diff --git a/client/render/gl_debug_overlay_2d.cpp b/client/render/gl_debug_overlay_2d.cpp new file mode 100644 index 000000000..964b27373 --- /dev/null +++ b/client/render/gl_debug_overlay_2d.cpp @@ -0,0 +1,174 @@ +/* +gl_debug_overlay_2d.cpp - 2D screen-space debug overlay rendering +Copyright (C) 2025 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "gl_debug_overlay_2d.h" +#include "gl_local.h" + +// Constants missing from gl_export.h but standard since GL 3.0. +#define GL_DEBUG_FRAMEBUFFER 0x8CA9 +#define GL_DEBUG_FRAMEBUFFER_BINDING 0x8CA6 + +namespace +{ + // Save and restore the draw-framebuffer around a scope. Debug overlays + // must land on the final screen buffer; intermediate HDR / post-process + // FBOs are often bound when our helpers are invoked. + struct ScopedScreenFBO + { + GLint saved = 0; + ScopedScreenFBO() { + pglGetIntegerv(GL_DEBUG_FRAMEBUFFER_BINDING, &saved); + pglBindFramebuffer(GL_DEBUG_FRAMEBUFFER, 0); + } + ~ScopedScreenFBO() { + pglBindFramebuffer(GL_DEBUG_FRAMEBUFFER, saved); + } + }; +} + +CDebugOverlay2D& CDebugOverlay2D::GetInstance() +{ + static CDebugOverlay2D instance; + return instance; +} + +void CDebugOverlay2D::Initialize() +{ + if (m_bInitialized) + return; + + word shaderHandle = GL_FindShader("common/debug_overlay2d", "common/debug_overlay2d", "common/debug_overlay2d"); + m_Shader.SetShader(shaderHandle); + pglGenVertexArrays(1, &m_iVAO); + pglGenBuffersARB(1, &m_iVBO); + + pglBindBufferARB(GL_ARRAY_BUFFER_ARB, m_iVBO); + // 8 vec3 positions max — enough for a wire-rect (4 lines × 2 verts) or + // a filled rect (2 tris × 3 verts). + pglBufferDataARB(GL_ARRAY_BUFFER_ARB, 8 * sizeof(Vector), nullptr, GL_DYNAMIC_DRAW_ARB); + + pglBindVertexArray(m_iVAO); + pglEnableVertexAttribArrayARB(ATTR_INDEX_POSITION); + pglVertexAttribPointerARB(ATTR_INDEX_POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(Vector), (void *)0); + pglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + pglBindVertexArray(0); + + m_bInitialized = true; +} + +void CDebugOverlay2D::Shutdown() +{ + if (!m_bInitialized) + return; + if (m_iVBO) { + pglDeleteBuffersARB(1, &m_iVBO); + m_iVBO = 0; + } + if (m_iVAO) { + pglDeleteVertexArrays(1, &m_iVAO); + m_iVAO = 0; + } + m_Shader.Invalidate(); + m_bInitialized = false; +} + +void CDebugOverlay2D::UploadMatrixAndSize(struct glsl_prog_s *shader) +{ + const GLint sizeLoc = pglGetUniformLocation(shader->handle, "u_ScreenSize"); + if (sizeLoc >= 0) { + const GLfloat screen[2] = { (float)glState.width, (float)glState.height }; + pglUniform2fvARB(sizeLoc, 1, screen); + } +} + +void CDebugOverlay2D::DrawWireRect(float x, float y, float w, float h, Vector color) +{ + if (!m_bInitialized) + return; + + glsl_program_t *shader = m_Shader.GetShader(); + if (!shader) + return; + + Vector verts[8] = { + Vector(x, y, 0), Vector(x+w, y, 0), + Vector(x, y, 0), Vector(x, y+h, 0), + Vector(x+w, y, 0), Vector(x+w, y+h, 0), + Vector(x, y+h, 0), Vector(x+w, y+h, 0), + }; + + GL_BindShader(shader); + UploadMatrixAndSize(shader); + const GLint colorLoc = pglGetUniformLocation(shader->handle, "u_Color"); + const GLint useTexLoc = pglGetUniformLocation(shader->handle, "u_UseTexture"); + if (colorLoc >= 0) { + const GLfloat rgba[4] = { color.x, color.y, color.z, 1.0f }; + pglUniform4fvARB(colorLoc, 1, rgba); + } + if (useTexLoc >= 0) pglUniform1fARB(useTexLoc, 0.0f); + + // Draw into the currently-bound FBO (HDR intermediate during the debug-draw + // phase). Forcing the default screen framebuffer here would land the output + // on the pre-composite buffer that R_RenderScreenQuad later overwrites. + pglBindVertexArray(m_iVAO); + pglBindBufferARB(GL_ARRAY_BUFFER_ARB, m_iVBO); + pglBufferSubDataARB(GL_ARRAY_BUFFER_ARB, 0, sizeof(verts), verts); + pglDisable(GL_DEPTH_TEST); + pglDisable(GL_BLEND); + pglLineWidth(4.0f); + pglDrawArrays(GL_LINES, 0, 8); + pglLineWidth(1.0f); +} + +void CDebugOverlay2D::DrawTexturedRect(float x, float y, float w, float h, TextureHandle texture) +{ + if (!m_bInitialized) + return; + + glsl_program_t *shader = m_Shader.GetShader(); + if (!shader) + return; + + Vector verts[6] = { + Vector(x, y, 0), + Vector(x+w, y, 0), + Vector(x+w, y+h, 0), + Vector(x, y, 0), + Vector(x+w, y+h, 0), + Vector(x, y+h, 0), + }; + + GL_BindShader(shader); + UploadMatrixAndSize(shader); + const GLint colorLoc = pglGetUniformLocation(shader->handle, "u_Color"); + if (colorLoc >= 0) { + const GLfloat rgba[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; + pglUniform4fvARB(colorLoc, 1, rgba); + } + const GLint useTexLoc = pglGetUniformLocation(shader->handle, "u_UseTexture"); + if (useTexLoc >= 0) pglUniform1fARB(useTexLoc, 1.0f); + const GLint samplerLoc = pglGetUniformLocation(shader->handle, "u_ColorMap"); + if (samplerLoc >= 0) pglUniform1iARB(samplerLoc, 0); + + GL_Bind(GL_TEXTURE0, texture); + + ScopedScreenFBO fbo; + pglBindVertexArray(m_iVAO); + pglBindBufferARB(GL_ARRAY_BUFFER_ARB, m_iVBO); + pglBufferSubDataARB(GL_ARRAY_BUFFER_ARB, 0, sizeof(verts), verts); + pglDisable(GL_DEPTH_TEST); + pglDisable(GL_BLEND); + pglDrawArrays(GL_TRIANGLES, 0, 6); +} diff --git a/client/render/gl_debug_overlay_2d.h b/client/render/gl_debug_overlay_2d.h new file mode 100644 index 000000000..ee9e66fe7 --- /dev/null +++ b/client/render/gl_debug_overlay_2d.h @@ -0,0 +1,51 @@ +/* +gl_debug_overlay_2d.h - 2D screen-space debug overlay rendering +Copyright (C) 2025 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#pragma once +#include "gl_export.h" +#include "shader.h" +#include "texture_handle.h" +#include "vector.h" + +// Small modern-GL helper for 2D debug overlays (screen-space wireframe rectangles +// and textured quads). Used by debug features that previously emitted +// immediate-mode pglBegin calls (scissor-rectangle debug, lightmap inspector). +class CDebugOverlay2D +{ +public: + static CDebugOverlay2D& GetInstance(); + + void Initialize(); + void Shutdown(); + + // Wireframe rectangle outlined with 4 lines. Coordinates in pixels (top-left origin). + void DrawWireRect(float x, float y, float w, float h, Vector color); + + // Textured quad. Coordinates in pixels, UVs default to fill [0..1]. + void DrawTexturedRect(float x, float y, float w, float h, TextureHandle texture); + +private: + CDebugOverlay2D() = default; + ~CDebugOverlay2D() = default; + CDebugOverlay2D(const CDebugOverlay2D&) = delete; + CDebugOverlay2D& operator=(const CDebugOverlay2D&) = delete; + + void UploadMatrixAndSize(struct glsl_prog_s *shader); + + shader_t m_Shader; + GLuint m_iVAO = 0; + GLuint m_iVBO = 0; + bool m_bInitialized = false; +}; diff --git a/client/render/gl_debug_visualizer_backend.cpp b/client/render/gl_debug_visualizer_backend.cpp new file mode 100644 index 000000000..a8a6e2ad2 --- /dev/null +++ b/client/render/gl_debug_visualizer_backend.cpp @@ -0,0 +1,298 @@ +/* +gl_debug_visualizer_backend.cpp - rendering backend for debug visualizer +Copyright (C) 2025 SNMetamorph + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "gl_debug_visualizer_backend.h" +#include "gl_local.h" +#include "utils.h" +#include "mathlib.h" +#include +#include +#include + +#define MAX_DEBUG_VBO_SIZE (64 * 1024) + +CDebugVisualizerBackend& CDebugVisualizerBackend::GetInstance() +{ + static CDebugVisualizerBackend instance; + return instance; +} + +void CDebugVisualizerBackend::Initialize() +{ + if (m_bInitialized) + return; + + word shaderHandle = GL_FindShader("common/debug_draw", "common/debug_draw", "common/debug_draw"); + m_DebugShader.SetShader(shaderHandle); + + pglGenVertexArrays(1, &m_iVAO); + pglGenBuffersARB(1, &m_iVBO); + + pglBindBufferARB(GL_ARRAY_BUFFER_ARB, m_iVBO); + pglBufferDataARB(GL_ARRAY_BUFFER_ARB, MAX_DEBUG_VBO_SIZE * sizeof(SVertex), nullptr, GL_DYNAMIC_DRAW_ARB); + + pglBindVertexArray(m_iVAO); + + pglEnableVertexAttribArrayARB(ATTR_INDEX_POSITION); + pglVertexAttribPointerARB(ATTR_INDEX_POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(SVertex), (void *)0); + + pglEnableVertexAttribArrayARB(ATTR_INDEX_LIGHT_COLOR); + pglVertexAttribPointerARB(ATTR_INDEX_LIGHT_COLOR, 4, GL_FLOAT, GL_FALSE, sizeof(SVertex), (void *)(3 * sizeof(float))); + + pglBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); + pglBindVertexArray(0); + + m_bInitialized = true; +} + +void CDebugVisualizerBackend::Shutdown() +{ + if (!m_bInitialized) + return; + + if (m_iVBO) { + pglDeleteBuffersARB(1, &m_iVBO); + m_iVBO = 0; + } + if (m_iVAO) { + pglDeleteVertexArrays(1, &m_iVAO); + m_iVAO = 0; + } + m_DebugShader.Invalidate(); + m_bInitialized = false; +} + +void CDebugVisualizerBackend::DrawFrame() +{ + if (!m_bInitialized) + return; + + for (auto &kv : m_lineBuckets) kv.second.clear(); + for (auto &kv : m_triBuckets) kv.second.clear(); + + const auto &primitives = CDebugVisualizer::GetInstance().GetPrimitives(); + for (const CDebugVisualizer::Primitive &primitive : primitives) + { + std::visit([&](const auto &payload) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + TriBucketKey key { primitive.depthTest, payload.blendMode }; + TessellateFilledBox(payload, m_triBuckets[key]); + } + else { + LineBucketKey key { primitive.depthTest, primitive.lineWidth }; + std::vector &bucket = m_lineBuckets[key]; + if constexpr (std::is_same_v) + TessellateAABB(payload, primitive.color, bucket); + else if constexpr (std::is_same_v) + TessellateSphere(payload, primitive.color, bucket); + else if constexpr (std::is_same_v) + TessellateVector(payload, primitive.color, bucket); + else if constexpr (std::is_same_v) + TessellateFrustum(payload, primitive.color, bucket); + } + }, primitive.data); + } + + glsl_program_t *shader = m_DebugShader.GetShader(); + if (!shader) + return; + + GL_BindShader(shader); + + GLint modelLoc = pglGetUniformLocation(shader->handle, "u_ModelMatrix"); + GLint projLoc = pglGetUniformLocation(shader->handle, "u_ProjectionMatrix"); + if (modelLoc >= 0) { + static const GLfloat identity[16] = { + 1.f, 0.f, 0.f, 0.f, + 0.f, 1.f, 0.f, 0.f, + 0.f, 0.f, 1.f, 0.f, + 0.f, 0.f, 0.f, 1.f, + }; + pglUniformMatrix4fvARB(modelLoc, 1, GL_FALSE, identity); + } + if (projLoc >= 0) { + pglUniformMatrix4fvARB(projLoc, 1, GL_FALSE, RI->glstate.modelviewProjectionMatrix); + } + + const GLboolean depthWasEnabled = pglIsEnabled(GL_DEPTH_TEST); + const GLboolean blendWasEnabled = pglIsEnabled(GL_BLEND); + // GL_BLEND_SRC_RGB (0x80C9) and GL_BLEND_DST_RGB (0x80C8) aren't declared in + // this project's gl_export.h, but the constants are standard since GL 1.4. + GLint srcBlendRGB = GL_ONE, dstBlendRGB = GL_ZERO; + GLint srcBlendA = GL_ONE, dstBlendA = GL_ZERO; + pglGetIntegerv(0x80C9, &srcBlendRGB); + pglGetIntegerv(0x80C8, &dstBlendRGB); + pglGetIntegerv(0x80CB, &srcBlendA); + pglGetIntegerv(0x80CA, &dstBlendA); + + pglDisable(GL_BLEND); + pglBindVertexArray(m_iVAO); + pglBindBufferARB(GL_ARRAY_BUFFER_ARB, m_iVBO); + + // Opaque line passes first, bucketed by depthTest and lineWidth. + for (auto &kv : m_lineBuckets) { + if (kv.second.empty()) continue; + if (kv.first.depthTest) pglEnable(GL_DEPTH_TEST); + else pglDisable(GL_DEPTH_TEST); + pglLineWidth(kv.first.lineWidth); + RenderPrimitives(kv.second, GL_LINES); + } + pglLineWidth(1.0f); + + // Filled translucent passes last, bucketed by depthTest and blendMode. + for (auto &kv : m_triBuckets) { + if (kv.second.empty()) continue; + if (kv.first.depthTest) pglEnable(GL_DEPTH_TEST); + else pglDisable(GL_DEPTH_TEST); + pglEnable(GL_BLEND); + if (kv.first.blendMode == CDebugVisualizer::BlendMode::Additive) + pglBlendFunc(GL_SRC_ALPHA, GL_ONE); + else + pglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + RenderPrimitives(kv.second, GL_TRIANGLES); + } + + if (blendWasEnabled) + pglEnable(GL_BLEND); + else + pglDisable(GL_BLEND); + pglBlendFuncSeparate(srcBlendRGB, dstBlendRGB, srcBlendA, dstBlendA); + if (depthWasEnabled) + pglEnable(GL_DEPTH_TEST); + else + pglDisable(GL_DEPTH_TEST); +} + +void CDebugVisualizerBackend::RenderPrimitives(const std::vector &verts, GLenum topology) +{ + if (verts.empty()) + return; + + size_t offset = 0; + while (offset < verts.size()) + { + const size_t kBatchLimit = MAX_DEBUG_VBO_SIZE; + size_t drawCount = std::min(kBatchLimit, verts.size() - offset); + + pglBufferSubDataARB(GL_ARRAY_BUFFER_ARB, 0, drawCount * sizeof(SVertex), verts.data() + offset); + pglDrawArrays(topology, 0, drawCount); + + offset += drawCount; + } +} + +void CDebugVisualizerBackend::TessellateAABB(const CDebugVisualizer::AABBData &data, const Vector &color, std::vector &out) +{ + Vector from = data.mins; + Vector to = data.maxs; + + Vector halfExtents = (to - from) * 0.5f; + Vector center = (to + from) * 0.5f; + + Vector edgecoord(1.f, 1.f, 1.f), pa, pb; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 3; j++) + { + pa = Vector(edgecoord[0] * halfExtents[0], edgecoord[1] * halfExtents[1], + edgecoord[2] * halfExtents[2]); + pa += center; + + int othercoord = j % 3; + edgecoord[othercoord] *= -1.f; + pb = Vector(edgecoord[0] * halfExtents[0], edgecoord[1] * halfExtents[1], + edgecoord[2] * halfExtents[2]); + pb += center; + + out.push_back({ pa, color, 1.0f }); + out.push_back({ pb, color, 1.0f }); + } + + edgecoord = Vector(-1.f, -1.f, -1.f); + if (i < 3) + edgecoord[i] *= -1.f; + } +} + +void CDebugVisualizerBackend::TessellateSphere(const CDebugVisualizer::SphereData &data, const Vector &color, std::vector &out) +{ + const unsigned int latitudeDivisions = 8; + const unsigned int longitudeDivisions = 8; + + auto pointAt = [&](unsigned int lat, unsigned int lon) { + float theta = (float)lat * M_PI / (float)latitudeDivisions; + float phi = (float)lon * M_PI2 / (float)longitudeDivisions; + return Vector( + data.center.x + data.radius * sinf(theta) * cosf(phi), + data.center.y + data.radius * sinf(theta) * sinf(phi), + data.center.z + data.radius * cosf(theta)); + }; + + for (unsigned int lat = 0; lat < latitudeDivisions; ++lat) { + for (unsigned int lon = 0; lon < longitudeDivisions; ++lon) { + out.push_back({ pointAt(lat, lon), color, 1.0f }); + out.push_back({ pointAt(lat, lon + 1), color, 1.0f }); + out.push_back({ pointAt(lat, lon), color, 1.0f }); + out.push_back({ pointAt(lat + 1, lon), color, 1.0f }); + } + } +} + +void CDebugVisualizerBackend::TessellateVector(const CDebugVisualizer::VectorData &data, const Vector &color, std::vector &out) +{ + out.push_back({ data.position, color, 1.0f }); + out.push_back({ data.position + data.direction, color, 1.0f }); +} + +void CDebugVisualizerBackend::TessellateFrustum(const CDebugVisualizer::FrustumData &data, const Vector &color, std::vector &out) +{ + Vector corners[8]; + data.frustum.ComputeFrustumCorners(corners); + + // corner layout from CFrustum::ComputeFrustumCorners: + // 0..3 = far (TL, TR, BL, BR), 4..7 = near (TL, TR, BL, BR) + static const int edges[12][2] = { + { 0, 1 }, { 1, 3 }, { 3, 2 }, { 2, 0 }, // far face + { 4, 5 }, { 5, 7 }, { 7, 6 }, { 6, 4 }, // near face + { 0, 4 }, { 1, 5 }, { 2, 6 }, { 3, 7 }, // connecting + }; + + for (const auto &edge : edges) { + out.push_back({ corners[edge[0]], color, 1.0f }); + out.push_back({ corners[edge[1]], color, 1.0f }); + } +} + +void CDebugVisualizerBackend::TessellateFilledBox(const CDebugVisualizer::FilledBoxData &data, std::vector &out) +{ + // Face ordering matches engine-wide g_boxpnt: +X, +Y, +Z, -X, -Y, -Z. + // Corners use HL convention: bit 0 set = min-X, bit 1 set = min-Y, bit 2 set = min-Z. + for (int f = 0; f < 6; f++) { + const Vector &c = data.faceColors[f]; + const float a = data.alpha; + const Vector &p0 = data.corners[g_boxpnt[f][0]]; + const Vector &p1 = data.corners[g_boxpnt[f][1]]; + const Vector &p2 = data.corners[g_boxpnt[f][2]]; + const Vector &p3 = data.corners[g_boxpnt[f][3]]; + // two triangles per quad + out.push_back({ p0, c, a }); + out.push_back({ p1, c, a }); + out.push_back({ p2, c, a }); + out.push_back({ p0, c, a }); + out.push_back({ p2, c, a }); + out.push_back({ p3, c, a }); + } +} diff --git a/client/render/gl_debug_visualizer_backend.h b/client/render/gl_debug_visualizer_backend.h new file mode 100644 index 000000000..e24f4d78e --- /dev/null +++ b/client/render/gl_debug_visualizer_backend.h @@ -0,0 +1,75 @@ +/* +gl_debug_visualizer_backend.h - rendering backend for debug visualizer +Copyright (C) 2025 SNMetamorph + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#pragma once +#include "gl_export.h" +#include "shader.h" +#include "visualizer/debug_visualizer.h" +#include +#include + +class CDebugVisualizerBackend +{ +public: + static CDebugVisualizerBackend& GetInstance(); + + void Initialize(); + void Shutdown(); + void DrawFrame(); + +private: + struct SVertex + { + Vector position; + Vector color; + float alpha; + }; + + CDebugVisualizerBackend() = default; + ~CDebugVisualizerBackend() = default; + CDebugVisualizerBackend(const CDebugVisualizerBackend&) = delete; + CDebugVisualizerBackend(CDebugVisualizerBackend&&) = delete; + CDebugVisualizerBackend& operator=(const CDebugVisualizerBackend&) = delete; + CDebugVisualizerBackend& operator=(CDebugVisualizerBackend&&) = delete; + + struct LineBucketKey { bool depthTest; float lineWidth; }; + struct LineBucketKeyLess { + bool operator()(const LineBucketKey &a, const LineBucketKey &b) const { + if (a.depthTest != b.depthTest) return a.depthTest < b.depthTest; + return a.lineWidth < b.lineWidth; + } + }; + struct TriBucketKey { bool depthTest; CDebugVisualizer::BlendMode blendMode; }; + struct TriBucketKeyLess { + bool operator()(const TriBucketKey &a, const TriBucketKey &b) const { + if (a.depthTest != b.depthTest) return a.depthTest < b.depthTest; + return static_cast(a.blendMode) < static_cast(b.blendMode); + } + }; + + void TessellateAABB(const CDebugVisualizer::AABBData &data, const Vector &color, std::vector &out); + void TessellateSphere(const CDebugVisualizer::SphereData &data, const Vector &color, std::vector &out); + void TessellateVector(const CDebugVisualizer::VectorData &data, const Vector &color, std::vector &out); + void TessellateFrustum(const CDebugVisualizer::FrustumData &data, const Vector &color, std::vector &out); + void TessellateFilledBox(const CDebugVisualizer::FilledBoxData &data, std::vector &out); + void RenderPrimitives(const std::vector &verts, GLenum topology); + + std::map, LineBucketKeyLess> m_lineBuckets; + std::map, TriBucketKeyLess> m_triBuckets; + shader_t m_DebugShader; + GLuint m_iVAO = 0; + GLuint m_iVBO = 0; + bool m_bInitialized = false; +}; diff --git a/client/render/gl_rmisc.cpp b/client/render/gl_rmisc.cpp index 60dcd3696..c5d415221 100644 --- a/client/render/gl_rmisc.cpp +++ b/client/render/gl_rmisc.cpp @@ -27,6 +27,8 @@ GNU General Public License for more details. #include "gl_cvars.h" #include "gl_debug.h" #include "gl_unit_cube.h" +#include "gl_debug_visualizer_backend.h" +#include "gl_debug_overlay_2d.h" #include "r_weather.h" #define DEFAULT_SMOOTHNESS 0.0f @@ -917,6 +919,8 @@ void R_VidInit( void ) glState.height = RENDER_GET_PARM( PARM_SCREEN_HEIGHT, 0 ); COpenGLUnitCube::GetInstance().Initialize(); + CDebugVisualizerBackend::GetInstance().Initialize(); + CDebugOverlay2D::GetInstance().Initialize(); R_InitCommonTextures(); R_InitCubemapShaders(); diff --git a/client/render/gl_studio_draw.cpp b/client/render/gl_studio_draw.cpp index 62a83a867..4a22d67b3 100644 --- a/client/render/gl_studio_draw.cpp +++ b/client/render/gl_studio_draw.cpp @@ -30,6 +30,7 @@ GNU General Public License for more details. #include "gl_shader.h" #include "gl_world.h" #include "gl_cvars.h" +#include "visualizer/debug_visualizer.h" #define LIGHT_INTERP_UPDATE 0.1f #define LIGHT_INTERP_FACTOR (1.0f / LIGHT_INTERP_UPDATE) @@ -1861,55 +1862,34 @@ StudioDrawBones */ void CStudioModelRenderer :: StudioDrawBones( void ) { - mstudiobone_t *pbones = (mstudiobone_t *) ((byte *)m_pStudioHeader + m_pStudioHeader->boneindex); - Vector point; + mstudiobone_t *pbones = (mstudiobone_t *) ((byte *)m_pStudioHeader + m_pStudioHeader->boneindex); + auto &visualizer = CDebugVisualizer::GetInstance(); - GL_Bind( GL_TEXTURE0, tr.whiteTexture ); - pglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); + const Vector limbColor( 1.0f, 0.7f, 0.0f ); // orange: bone -> parent segment + const Vector jointColor( 0.0f, 0.0f, 0.8f ); // blue: interior joint markers + const Vector rootColor( 0.8f, 0.0f, 0.0f ); // red: root-bone marker + const float jointRadius = 1.5f; + const float rootRadius = 2.5f; + Vector origin, parentOrigin; for( int i = 0; i < m_pStudioHeader->numbones; i++ ) { + m_pModelInstance->m_pbones[i].GetOrigin( origin ); + if( pbones[i].parent >= 0 ) { - pglPointSize( 3.0f ); - pglColor3f( 1, 0.7f, 0 ); - pglBegin( GL_LINES ); - - m_pModelInstance->m_pbones[pbones[i].parent].GetOrigin( point ); - pglVertex3fv( point ); - - m_pModelInstance->m_pbones[i].GetOrigin( point ); - pglVertex3fv( point ); - - pglEnd(); - - pglColor3f( 0, 0, 0.8f ); - pglBegin( GL_POINTS ); + m_pModelInstance->m_pbones[pbones[i].parent].GetOrigin( parentOrigin ); + visualizer.DrawVector( parentOrigin, origin - parentOrigin, limbColor, std::nullopt, true ); if( pbones[pbones[i].parent].parent != -1 ) - { - m_pModelInstance->m_pbones[pbones[i].parent].GetOrigin( point ); - pglVertex3fv( point ); - } - - m_pModelInstance->m_pbones[i].GetOrigin( point ); - pglVertex3fv( point ); - pglEnd(); + visualizer.DrawSphere( parentOrigin, jointRadius, jointColor, std::nullopt, true ); + visualizer.DrawSphere( origin, jointRadius, jointColor, std::nullopt, true ); } else { - // draw parent bone node - pglPointSize( 5.0f ); - pglColor3f( 0.8f, 0, 0 ); - pglBegin( GL_POINTS ); - - m_pModelInstance->m_pbones[i].GetOrigin( point ); - pglVertex3fv( point ); - pglEnd(); + visualizer.DrawSphere( origin, rootRadius, rootColor, std::nullopt, true ); } } - - pglPointSize( 1.0f ); } /* @@ -1920,54 +1900,44 @@ StudioDrawHulls */ void CStudioModelRenderer :: StudioDrawHulls( int iHitbox ) { - float alpha, lv; - int i, j; - - if( r_drawentities->value == 4 ) - alpha = 0.5f; - else alpha = 1.0f; + const float alpha = (r_drawentities->value == 4) ? 0.5f : 1.0f; // setup bone lighting - for( i = 0; i < m_pStudioHeader->numbones; i++ ) + for( int i = 0; i < m_pStudioHeader->numbones; i++ ) m_bonelightvecs[i] = m_pModelInstance->m_pbones[i].VectorIRotate( -m_pModelInstance->light.normal ); - GL_Bind( GL_TEXTURE0, tr.whiteTexture ); - pglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); + auto &visualizer = CDebugVisualizer::GetInstance(); - for( i = 0; i < m_pStudioHeader->numhitboxes; i++ ) + for( int i = 0; i < m_pStudioHeader->numhitboxes; i++ ) { - mstudiobbox_t *pbbox = (mstudiobbox_t *)((byte *)m_pStudioHeader + m_pStudioHeader->hitboxindex); - vec3_t tmp, p[8]; - + mstudiobbox_t *pbbox = (mstudiobbox_t *)((byte *)m_pStudioHeader + m_pStudioHeader->hitboxindex); if( iHitbox >= 0 && iHitbox != i ) continue; - for( j = 0; j < 8; j++ ) + std::array corners; + for( int k = 0; k < 8; k++ ) { - tmp[0] = (j & 1) ? pbbox[i].bbmin[0] : pbbox[i].bbmax[0]; - tmp[1] = (j & 2) ? pbbox[i].bbmin[1] : pbbox[i].bbmax[1]; - tmp[2] = (j & 4) ? pbbox[i].bbmin[2] : pbbox[i].bbmax[2]; - p[j] = m_pModelInstance->m_pbones[pbbox[i].bone].VectorTransform( tmp ); + vec3_t tmp; + tmp[0] = (k & 1) ? pbbox[i].bbmin[0] : pbbox[i].bbmax[0]; + tmp[1] = (k & 2) ? pbbox[i].bbmin[1] : pbbox[i].bbmax[1]; + tmp[2] = (k & 4) ? pbbox[i].bbmin[2] : pbbox[i].bbmax[2]; + corners[k] = m_pModelInstance->m_pbones[pbbox[i].bone].VectorTransform( tmp ); } - j = (pbbox[i].group % ARRAYSIZE( g_hullcolor )); - - gEngfuncs.pTriAPI->Begin( TRI_QUADS ); - gEngfuncs.pTriAPI->Color4f( g_hullcolor[j][0], g_hullcolor[j][1], g_hullcolor[j][2], alpha ); + const int colorIdx = pbbox[i].group % ARRAYSIZE( g_hullcolor ); + const Vector baseColor( g_hullcolor[colorIdx][0], g_hullcolor[colorIdx][1], g_hullcolor[colorIdx][2] ); - for( j = 0; j < 6; j++ ) + std::array faceColors; + for( int j = 0; j < 6; j++ ) { - tmp = g_vecZero; - tmp[j % 3] = (j < 3) ? 1.0f : -1.0f; - StudioLighting( &lv, pbbox[i].bone, 0, tmp ); - - gEngfuncs.pTriAPI->Brightness( Q_max( lv, tr.ambientFactor )); - gEngfuncs.pTriAPI->Vertex3fv( p[g_boxpnt[j][0]] ); - gEngfuncs.pTriAPI->Vertex3fv( p[g_boxpnt[j][1]] ); - gEngfuncs.pTriAPI->Vertex3fv( p[g_boxpnt[j][2]] ); - gEngfuncs.pTriAPI->Vertex3fv( p[g_boxpnt[j][3]] ); + vec3_t normal = { 0.f, 0.f, 0.f }; + normal[j % 3] = (j < 3) ? 1.0f : -1.0f; + float lv; + StudioLighting( &lv, pbbox[i].bone, 0, normal ); + faceColors[j] = baseColor * Q_max( lv, tr.ambientFactor ); } - gEngfuncs.pTriAPI->End(); + + visualizer.DrawFilledBox( corners, faceColors, alpha, std::nullopt, true ); } } @@ -1979,57 +1949,44 @@ StudioDrawAbsBBox */ void CStudioModelRenderer :: StudioDrawAbsBBox( void ) { - Vector p[8], tmp; - float lv; - int i; - // looks ugly, skip if( RI->currententity == GET_VIEWMODEL( )) return; - // compute a full bounding box - for( i = 0; i < 8; i++ ) + std::array corners; + for( int i = 0; i < 8; i++ ) { - p[i][0] = ( i & 1 ) ? m_pModelInstance->absmin[0] : m_pModelInstance->absmax[0]; - p[i][1] = ( i & 2 ) ? m_pModelInstance->absmin[1] : m_pModelInstance->absmax[1]; - p[i][2] = ( i & 4 ) ? m_pModelInstance->absmin[2] : m_pModelInstance->absmax[2]; + corners[i].x = (i & 1) ? m_pModelInstance->absmin[0] : m_pModelInstance->absmax[0]; + corners[i].y = (i & 2) ? m_pModelInstance->absmin[1] : m_pModelInstance->absmax[1]; + corners[i].z = (i & 4) ? m_pModelInstance->absmin[2] : m_pModelInstance->absmax[2]; } - GL_Bind( GL_TEXTURE0, tr.whiteTexture ); - gEngfuncs.pTriAPI->Color4f( 0.5f, 0.5f, 1.0f, 0.5f ); - gEngfuncs.pTriAPI->RenderMode( kRenderTransAdd ); - - gEngfuncs.pTriAPI->Begin( TRI_QUADS ); - - for( i = 0; i < 6; i++ ) + const Vector baseColor( 0.5f, 0.5f, 1.0f ); + std::array faceColors; + for( int j = 0; j < 6; j++ ) { - tmp = g_vecZero; - tmp[i % 3] = (i < 3) ? 1.0f : -1.0f; - StudioLighting( &lv, -1, 0, tmp ); - - gEngfuncs.pTriAPI->Brightness( Q_max( lv, tr.ambientFactor )); - gEngfuncs.pTriAPI->Vertex3fv( p[g_boxpnt[i][0]] ); - gEngfuncs.pTriAPI->Vertex3fv( p[g_boxpnt[i][1]] ); - gEngfuncs.pTriAPI->Vertex3fv( p[g_boxpnt[i][2]] ); - gEngfuncs.pTriAPI->Vertex3fv( p[g_boxpnt[i][3]] ); + vec3_t normal = { 0.f, 0.f, 0.f }; + normal[j % 3] = (j < 3) ? 1.0f : -1.0f; + float lv; + StudioLighting( &lv, -1, 0, normal ); + faceColors[j] = baseColor * Q_max( lv, tr.ambientFactor ); } - gEngfuncs.pTriAPI->End(); + CDebugVisualizer::GetInstance().DrawFilledBox( corners, faceColors, 0.5f, std::nullopt, true, CDebugVisualizer::BlendMode::Additive ); } void CStudioModelRenderer :: StudioDrawAttachments( bool bCustomFov ) { - GL_Bind( GL_TEXTURE0, tr.whiteTexture ); - pglTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); - pglDisable( GL_DEPTH_TEST ); - + auto &visualizer = CDebugVisualizer::GetInstance(); + const Vector axisColor( 1.0f, 0.0f, 0.0f ); // red: attachment axes + const Vector originColor( 0.0f, 1.0f, 0.0f ); // green: attachment origin marker + const float originRadius = 2.5f; + for( int i = 0; i < m_pStudioHeader->numattachments; i++ ) { - mstudioattachment_t *pattachments; + mstudioattachment_t *pattachments = (mstudioattachment_t *) ((byte *)m_pStudioHeader + m_pStudioHeader->attachmentindex); Vector v[4]; - pattachments = (mstudioattachment_t *) ((byte *)m_pStudioHeader + m_pStudioHeader->attachmentindex); - v[0] = m_pModelInstance->m_pbones[pattachments[i].bone].VectorTransform( pattachments[i].org ); v[1] = m_pModelInstance->m_pbones[pattachments[i].bone].VectorTransform( g_vecZero ); v[2] = m_pModelInstance->m_pbones[pattachments[i].bone].VectorTransform( g_vecZero ); @@ -2038,30 +1995,11 @@ void CStudioModelRenderer :: StudioDrawAttachments( bool bCustomFov ) for( int j = 0; j < 4 && bCustomFov; j++ ) StudioFormatAttachment( v[j] ); - pglBegin( GL_LINES ); - pglColor3f( 1, 0, 0 ); - pglVertex3fv( v[0] ); - pglColor3f( 1, 1, 1 ); - pglVertex3fv( v[1] ); - pglColor3f( 1, 0, 0 ); - pglVertex3fv( v[0] ); - pglColor3f( 1, 1, 1 ); - pglVertex3fv (v[2] ); - pglColor3f( 1, 0, 0 ); - pglVertex3fv( v[0] ); - pglColor3f( 1, 1, 1 ); - pglVertex3fv( v[3] ); - pglEnd(); - - pglPointSize( 5.0f ); - pglColor3f( 0, 1, 0 ); - pglBegin( GL_POINTS ); - pglVertex3fv( v[0] ); - pglEnd(); - pglPointSize( 1.0f ); + visualizer.DrawVector( v[0], v[1] - v[0], axisColor, std::nullopt, false ); + visualizer.DrawVector( v[0], v[2] - v[0], axisColor, std::nullopt, false ); + visualizer.DrawVector( v[0], v[3] - v[0], axisColor, std::nullopt, false ); + visualizer.DrawSphere( v[0], originRadius, originColor, std::nullopt, false ); } - - pglEnable( GL_DEPTH_TEST ); } void CStudioModelRenderer::StudioDrawBodyPartsBBox() @@ -2096,36 +2034,11 @@ void CStudioModelRenderer::StudioDrawBodyPartsBBox() for (int j = 0; j < pSubModel->meshes.size(); j++) { - Vector p[8]; vbomesh_t *mesh = &pSubModel->meshes[j]; - - // compute a full bounding box CBoundingBox meshBounds = StudioGetMeshBounds(m_pModelInstance, mesh); - for (int k = 0; k < 8; k++) - { - p[k].x = (k & 1) ? meshBounds.GetMins().x : meshBounds.GetMaxs().x; - p[k].y = (k & 2) ? meshBounds.GetMins().y : meshBounds.GetMaxs().y; - p[k].z = (k & 4) ? meshBounds.GetMins().z : meshBounds.GetMaxs().z; - } - - GL_Bind(GL_TEXTURE0, tr.whiteTexture); - gEngfuncs.pTriAPI->Color4f(0.2f, 1.0f, 0.2f, 1.0f); - gEngfuncs.pTriAPI->RenderMode(kRenderTransColor); - gEngfuncs.pTriAPI->Begin(TRI_LINES); - - for (int k = 0; k < 6; k++) - { - gEngfuncs.pTriAPI->Vertex3fv(p[g_boxpnt[k][0]]); - gEngfuncs.pTriAPI->Vertex3fv(p[g_boxpnt[k][1]]); - gEngfuncs.pTriAPI->Vertex3fv(p[g_boxpnt[k][1]]); - gEngfuncs.pTriAPI->Vertex3fv(p[g_boxpnt[k][2]]); - gEngfuncs.pTriAPI->Vertex3fv(p[g_boxpnt[k][2]]); - gEngfuncs.pTriAPI->Vertex3fv(p[g_boxpnt[k][3]]); - gEngfuncs.pTriAPI->Vertex3fv(p[g_boxpnt[k][3]]); - gEngfuncs.pTriAPI->Vertex3fv(p[g_boxpnt[k][0]]); - } - - gEngfuncs.pTriAPI->End(); + CDebugVisualizer::GetInstance().DrawAABB( + meshBounds.GetMins(), meshBounds.GetMaxs(), + Vector(0.2f, 1.0f, 0.2f), std::nullopt, true); } } } diff --git a/client/render/gl_studio_init.cpp b/client/render/gl_studio_init.cpp index a805b139e..c3db095bd 100644 --- a/client/render/gl_studio_init.cpp +++ b/client/render/gl_studio_init.cpp @@ -28,6 +28,7 @@ GNU General Public License for more details. #include "triangleapi.h" #include "entity_types.h" #include "gl_shader.h" +#include "visualizer/debug_visualizer.h" #include "gl_world.h" #include #include @@ -90,15 +91,9 @@ mstudioanim_t *CBaseBoneSetup :: GetAnimSourceData( mstudioseqdesc_t *pseqdesc ) void CBaseBoneSetup :: debugLine( const Vector& origin, const Vector& dest, int r, int g, int b, bool noDepthTest, float duration ) { - if( noDepthTest ) - pglDisable( GL_DEPTH_TEST ); - - pglColor3ub( r, g, b ); - - pglBegin( GL_LINES ); - pglVertex3fv( origin ); - pglVertex3fv( dest ); - pglEnd(); + const Vector color( r / 255.0f, g / 255.0f, b / 255.0f ); + const std::optional lifespan = (duration > 0.0f) ? std::optional( duration ) : std::nullopt; + CDebugVisualizer::GetInstance().DrawVector( origin, dest - origin, color, lifespan, !noDepthTest ); } vbomesh_t::~vbomesh_t() diff --git a/client/utils.cpp b/client/utils.cpp index 148fe5b1d..77edbc51d 100644 --- a/client/utils.cpp +++ b/client/utils.cpp @@ -22,6 +22,7 @@ GNU General Public License for more details. #include "r_efx.h" #include "trace.h" #include "gl_studio.h" +#include "render/gl_debug_overlay_2d.h" #include "meshdesc_factory.h" /* @@ -1020,24 +1021,7 @@ void R_BoundsForAABB( const Vector &absmin, const Vector &absmax, Vector bbox[8] // debug thing void R_DrawScissorRectangle(float x, float y, float w, float h) { - GL_Bind(GL_TEXTURE0, tr.whiteTexture); - pglTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - GL_Setup2D(); - - pglColor3f(1, 0.5, 0); - - pglBegin(GL_LINES); - pglVertex2f(x, y); - pglVertex2f(x + w, y); - pglVertex2f(x, y); - pglVertex2f(x, y + h); - pglVertex2f(x + w, y); - pglVertex2f(x + w, y + h); - pglVertex2f(x, y + h); - pglVertex2f(x + w, y + h); - pglEnd(); - - GL_Setup3D(); + CDebugOverlay2D::GetInstance().DrawWireRect(x, y, w, h, Vector(1.0f, 0.5f, 0.0f)); } bool R_ScissorForAABB( const Vector &absmin, const Vector &absmax, float *x, float *y, float *w, float *h ) diff --git a/client/visualizer/debug_visualizer.cpp b/client/visualizer/debug_visualizer.cpp new file mode 100644 index 000000000..9c94fd6c6 --- /dev/null +++ b/client/visualizer/debug_visualizer.cpp @@ -0,0 +1,81 @@ +/* +debug_visualizer.cpp - utilities for visualizing data for debug purposes +Copyright (C) 2025 SNMetamorph + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "debug_visualizer.h" +#include "hud.h" +#include + +CDebugVisualizer::Primitive::Primitive(Class type, Vector color, std::optional lifespan, bool depthTest, DataVariant data, float lineWidth) : + type(type), + depthTest(depthTest), + rendered(false), + lineWidth(lineWidth), + color(color), + data(std::move(data)) +{ + const float currentTime = gEngfuncs.GetClientTime(); + if (lifespan.has_value()) { + expirationTime = currentTime + lifespan.value(); + } + else { + expirationTime = std::nullopt; + } +} + +CDebugVisualizer& CDebugVisualizer::GetInstance() +{ + static CDebugVisualizer instance; + return instance; +} + +void CDebugVisualizer::RunFrame() +{ + const float currentTime = gEngfuncs.GetClientTime(); + auto predicate = [currentTime](const Primitive& prim) { + if (prim.expirationTime.has_value()) { + return currentTime >= prim.expirationTime.value(); + } + // No lifespan: single-shot, evict on next tick. Deliberately not + // waiting on Primitive::rendered — that would require mutating + // primitives through the const GetPrimitives() view. + return true; + }; + m_primitives.erase(std::remove_if(m_primitives.begin(), m_primitives.end(), predicate), m_primitives.end()); +} + +void CDebugVisualizer::DrawAABB(const Vector &mins, const Vector &maxs, Vector color, std::optional lifespan, bool depthTest, float lineWidth) +{ + m_primitives.emplace_back(Primitive::Class::AABB, color, lifespan, depthTest, AABBData{mins, maxs}, lineWidth); +} + +void CDebugVisualizer::DrawSphere(const Vector ¢er, float radius, Vector color, std::optional lifespan, bool depthTest, float lineWidth) +{ + m_primitives.emplace_back(Primitive::Class::Sphere, color, lifespan, depthTest, SphereData{ center, radius }, lineWidth); +} + +void CDebugVisualizer::DrawVector(const Vector &position, const Vector &direction, Vector color, std::optional lifespan, bool depthTest, float lineWidth) +{ + m_primitives.emplace_back(Primitive::Class::Vector, color, lifespan, depthTest, VectorData{ position, direction }, lineWidth); +} + +void CDebugVisualizer::DrawFrustum(const CFrustum &frustum, Vector color, std::optional lifespan, bool depthTest, float lineWidth) +{ + m_primitives.emplace_back(Primitive::Class::Frustum, color, lifespan, depthTest, FrustumData{ frustum }, lineWidth); +} + +void CDebugVisualizer::DrawFilledBox(const std::array &corners, const std::array &faceColors, float alpha, std::optional lifespan, bool depthTest, BlendMode blendMode) +{ + m_primitives.emplace_back(Primitive::Class::FilledBox, Vector(1.f, 1.f, 1.f), lifespan, depthTest, FilledBoxData{ corners, faceColors, alpha, blendMode }); +} diff --git a/client/visualizer/debug_visualizer.h b/client/visualizer/debug_visualizer.h new file mode 100644 index 000000000..5d75334e2 --- /dev/null +++ b/client/visualizer/debug_visualizer.h @@ -0,0 +1,85 @@ +/* +debug_visualizer.h - utilities for visualizing data for debug purposes +Copyright (C) 2025 SNMetamorph + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#pragma once +#include "vector.h" +#include "frustum.h" +#include +#include +#include +#include + +class CDebugVisualizer +{ +public: + enum class BlendMode { Alpha, Additive }; + + struct AABBData { Vector mins, maxs; }; + struct SphereData { Vector center; float radius; }; + struct VectorData { Vector position, direction; }; + struct FrustumData { CFrustum frustum; }; + struct FilledBoxData { + std::array corners; + std::array faceColors; + float alpha; + BlendMode blendMode; + }; + + class Primitive + { + public: + using DataVariant = std::variant; + + enum class Class + { + AABB, + Sphere, + Vector, + Frustum, + FilledBox, + }; + + Primitive(Class type, Vector color, std::optional lifespan, bool depthTest, DataVariant data, float lineWidth = 1.0f); + + Class type; + bool depthTest; + bool rendered; + float lineWidth; + Vector color; + std::optional expirationTime; + DataVariant data; + }; + + static CDebugVisualizer& GetInstance(); + const std::vector& GetPrimitives() const { return m_primitives; } + void RunFrame(); + + void DrawAABB(const Vector &mins, const Vector &maxs, Vector color, std::optional lifespan, bool depthTest, float lineWidth = 1.0f); + void DrawSphere(const Vector ¢er, float radius, Vector color, std::optional lifespan, bool depthTest, float lineWidth = 1.0f); + void DrawVector(const Vector &position, const Vector& direction, Vector color, std::optional lifespan, bool depthTest, float lineWidth = 1.0f); + void DrawFrustum(const CFrustum &frustum, Vector color, std::optional lifespan, bool depthTest, float lineWidth = 1.0f); + void DrawFilledBox(const std::array &corners, const std::array &faceColors, float alpha, std::optional lifespan, bool depthTest, BlendMode blendMode = BlendMode::Alpha); + // ...and other geometric primitives as needed + +private: + CDebugVisualizer() = default; + ~CDebugVisualizer() = default; + CDebugVisualizer(const CDebugVisualizer&) = delete; + CDebugVisualizer(CDebugVisualizer&&) = delete; + CDebugVisualizer& operator=(const CDebugVisualizer&) = delete; + CDebugVisualizer& operator=(CDebugVisualizer&&) = delete; + + std::vector m_primitives; +}; diff --git a/game_dir/glsl/common/debug_draw_fp.glsl b/game_dir/glsl/common/debug_draw_fp.glsl new file mode 100644 index 000000000..18f0f3cf9 --- /dev/null +++ b/game_dir/glsl/common/debug_draw_fp.glsl @@ -0,0 +1,21 @@ +/* +debug_draw_vp.glsl - debug draw +Copyright (C) 2025 ugo_zapad + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +varying vec4 var_Color; + +void main() +{ + gl_FragColor = var_Color; +} diff --git a/game_dir/glsl/common/debug_draw_vp.glsl b/game_dir/glsl/common/debug_draw_vp.glsl new file mode 100644 index 000000000..d22ddd5e2 --- /dev/null +++ b/game_dir/glsl/common/debug_draw_vp.glsl @@ -0,0 +1,28 @@ +/* +debug_draw_vp.glsl - debug draw +Copyright (C) 2025 ugo_zapad + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +attribute vec3 attr_Position; +attribute vec4 attr_LightColor; + +uniform mat4 u_ModelMatrix; // model-view matrix +uniform mat4 u_ProjectionMatrix; // projection matrix + +varying vec4 var_Color; + +void main() +{ + var_Color = attr_LightColor; + gl_Position = u_ProjectionMatrix * u_ModelMatrix * vec4(attr_Position, 1.0); +} diff --git a/game_dir/glsl/common/debug_overlay2d_fp.glsl b/game_dir/glsl/common/debug_overlay2d_fp.glsl new file mode 100644 index 000000000..4c9d2df69 --- /dev/null +++ b/game_dir/glsl/common/debug_overlay2d_fp.glsl @@ -0,0 +1,23 @@ +/* +debug_overlay2d_fp.glsl - 2D screen-space debug overlay +Copyright (C) 2025 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +*/ + +uniform sampler2D u_ColorMap; +uniform vec4 u_Color; // tint / outright color +uniform float u_UseTexture; // 1.0 = sample texture, 0.0 = u_Color only + +varying vec2 var_TexCoord; + +void main() +{ + if (u_UseTexture > 0.5) + gl_FragColor = texture2D(u_ColorMap, var_TexCoord) * u_Color; + else + gl_FragColor = u_Color; +} diff --git a/game_dir/glsl/common/debug_overlay2d_vp.glsl b/game_dir/glsl/common/debug_overlay2d_vp.glsl new file mode 100644 index 000000000..42d77de99 --- /dev/null +++ b/game_dir/glsl/common/debug_overlay2d_vp.glsl @@ -0,0 +1,29 @@ +/* +debug_overlay2d_vp.glsl - 2D screen-space debug overlay +Copyright (C) 2025 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +*/ + +attribute vec3 attr_Position; // (screen_x, screen_y, unused) + +uniform vec2 u_ScreenSize; // (width, height) in pixels + +varying vec2 var_TexCoord; + +void main() +{ + // Derive texcoord from normalized screen position so we avoid relying + // on extra vertex-attribute streams that have been unreliable for this + // simple overlay use case. + var_TexCoord = vec2(attr_Position.x / u_ScreenSize.x, + attr_Position.y / u_ScreenSize.y); + vec2 clip = vec2( + (2.0 * attr_Position.x / u_ScreenSize.x) - 1.0, + 1.0 - (2.0 * attr_Position.y / u_ScreenSize.y) + ); + gl_Position = vec4(clip, 0.0, 1.0); +}