From 30f70ac19c361cdd14b10b7b4cb2885e0e5b06ce Mon Sep 17 00:00:00 2001 From: Mikko Mononen Date: Fri, 31 Jul 2026 11:45:41 +0300 Subject: [PATCH] DrawList: fixed rendering issues on small and degenerate shapes. The "does stroke cover the whole shape" used to be overly pessimistic to stroking artifacts. This was due to the fringe, where the geometry would mess up before visually closing the shape. Fixed the issue by adding max inner offest for the AddPolyline(), which limits the inner offset and also adjusts the texture coords. Second part of the fix was to calculate the pass the max inner offset from the common shapes (circle, ngon, ellipse, rect). While this does not fix all the ellipse rendering issues, it limits the issue a lot. e.g. AddRect() w/ size=5, thickness=2 used to trigger AddRectFilled() fallback because threshold was pessimistic to prevent the AddPolyline() issues. --- imgui.h | 5 ++-- imgui_draw.cpp | 76 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/imgui.h b/imgui.h index 16b468ad9..c42102d4e 100644 --- a/imgui.h +++ b/imgui.h @@ -3568,7 +3568,7 @@ struct ImDrawList // General polygon // - Only simple polygons are supported by filling functions (no self-intersections, no holes). // - Concave polygon fill is more expensive than convex one: it has O(N^2) complexity. Provided as a convenience for the user but not used by the main library. - IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, float thickness, ImDrawFlags flags = 0); + inline void AddPolyline(const ImVec2* points, int num_points, ImU32 col, float thickness, ImDrawFlags flags = 0) { _AddPolyline(points, num_points, col, thickness, flags, FLT_MAX); } IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags = 0); IMGUI_API void AddConcavePolyFilled(const ImVec2* points, int num_points, ImU32 col); @@ -3588,7 +3588,7 @@ struct ImDrawList inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } inline void PathFillConvex(ImU32 col, ImDrawFlags flags = 0) { AddConvexPolyFilled(_Path.Data, _Path.Size, col, flags); _Path.Size = 0; } inline void PathFillConcave(ImU32 col) { AddConcavePolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } - inline void PathStroke(ImU32 col, float thickness = 1.0f, ImDrawFlags flags = 0) { AddPolyline(_Path.Data, _Path.Size, col, thickness, flags); _Path.Size = 0; } + inline void PathStroke(ImU32 col, float thickness = 1.0f, ImDrawFlags flags = 0) { _AddPolyline(_Path.Data, _Path.Size, col, thickness, flags, FLT_MAX); _Path.Size = 0; } IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle IMGUI_API void PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments = 0); // Ellipse @@ -3672,6 +3672,7 @@ struct ImDrawList IMGUI_API void _AddRectTinyRounding(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, float thickness, ImDrawFlags flags); IMGUI_API void _SelectFringeTexture(float screen_thickness, ImVec4* out_tex_uvs, float* out_fringe, ImDrawFlags flags); IMGUI_API float _CalculateCenterBiasedOffset(float thickness); + IMGUI_API void _AddPolyline(const ImVec2* points, int num_points, ImU32 col, float thickness, ImDrawFlags flags, float max_inner_thickness); }; // All draw data to render a Dear ImGui frame diff --git a/imgui_draw.cpp b/imgui_draw.cpp index e3fb1df2c..b77d7567a 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -948,7 +948,7 @@ static inline ImDrawFlags _GetStrokePos(ImDrawFlags flags, ImDrawFlags default_s IM_MSVC_RUNTIME_CHECKS_RESTORE -void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, float thickness, ImDrawFlags flags) +void ImDrawList::_AddPolyline(const ImVec2* points, const int points_count, ImU32 col, float thickness, ImDrawFlags flags, float max_inner_offset) { if (points_count < 2 || (col & IM_COL32_A_MASK) == 0) return; @@ -1050,6 +1050,16 @@ void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 uv2.x = uv0.x + (uv1.x - uv0.x) * ratio; uv2.y = tex_uvs.z; + // Clamp inner offset and adjust texture coordinates to match the change. + // This is used by some of the convex shape rendering to avoid rendering artifacts when thickness is about to fill the whole shape. + if (thickness1 > max_inner_offset) + { + thickness1 = max_inner_offset; + const float s = (thickness0 + thickness1) / (thickness + fringe); + const float du = uv1.x - uv0.x; + uv1.x = uv0.x + du * s; + } + const float half_aa = _FringeScale * 0.5f; // Used for end caps, does not use "fringe" since end cap AA is not using the texture. const float half_thickness = thickness * 0.5f; const bool use_aa_ends = (flags & (ImDrawFlags_AALines | ImDrawFlags_AALineEnds)) == (ImDrawFlags_AALines | ImDrawFlags_AALineEnds); @@ -2375,7 +2385,8 @@ void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, fl const float width = outer_max.x - outer_min.x; const float height = outer_max.y - outer_min.y; - if (ImMin(width, height) < (thickness + _FringeScale) * 2.0f) IM_UNLIKELY + const float half_min_dim = ImMin(width, height) * 0.5f; + if (half_min_dim <= thickness) IM_UNLIKELY { // Rectangle has collapsed into filled rectangle. AddRectFilled(outer_min, outer_max, col, outer_rounding, flags); @@ -2448,7 +2459,8 @@ void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, fl if (!has_rounding || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) { const ImVec2 points[4] = { outer_min, ImVec2(outer_max.x, outer_min.y), outer_max, ImVec2(outer_min.x, outer_max.y) }; - AddPolyline(points, 4, col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | ImDrawFlags_StrokeInside | (flags & ~ImDrawFlags_StrokeMask_)); + _AddPolyline(points, 4, col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | ImDrawFlags_StrokeInside, half_min_dim); + _Path.Size = 0; } else { @@ -2468,7 +2480,9 @@ void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, fl PathArcToFast(ImVec2(outer_max.x - rounding_tr, outer_min.y + rounding_tr), rounding_tr, 9, 12); PathArcToFast(ImVec2(outer_max.x - rounding_br, outer_max.y - rounding_br), rounding_br, 0, 3); PathArcToFast(ImVec2(outer_min.x + rounding_bl, outer_max.y - rounding_bl), rounding_bl, 3, 6); - PathStroke(col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | ImDrawFlags_StrokeInside | (flags & ~ImDrawFlags_StrokeMask_)); + + _AddPolyline(_Path.Data, _Path.Size, col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | ImDrawFlags_StrokeInside | (flags & ~ImDrawFlags_StrokeMask_), half_min_dim); + _Path.Size = 0; } } @@ -2634,7 +2648,7 @@ void ImDrawList::AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, c flags = (flags & ~ImDrawFlags_StrokeMask_) | stroke_pos; // As AddPolyline() default is different const ImVec2 points[4] = { p1, p2, p3, p4 }; - AddPolyline(points, 4, col, thickness, flags | ImDrawFlags_Closed); + _AddPolyline(points, 4, col, thickness, flags | ImDrawFlags_Closed, FLT_MAX); } void ImDrawList::AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col) @@ -2656,7 +2670,7 @@ void ImDrawList::AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p flags = (flags & ~ImDrawFlags_StrokeMask_) | stroke_pos; // As AddPolyline() default is different const ImVec2 points[3] = { p1, p2, p3 }; - AddPolyline(points, 3, col, thickness, flags | ImDrawFlags_Closed); + _AddPolyline(points, 3, col, thickness, flags | ImDrawFlags_Closed, FLT_MAX); } void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col) @@ -2668,6 +2682,14 @@ void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImV AddConvexPolyFilled(points, 3, col); } +static float ApproxApothem(int num_segments) +{ + // Calculates approximation (within 0.001%) distance from the center of the circumscribed circle to the edge of the polygon, cos(pi / n) + IM_ASSERT_PARANOID(num_segments >= 3); + const float u = 1.0f / num_segments; + return ((4.058712f * u * u) - 4.934802f) * u * u + 1.0f; +} + void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness, ImDrawFlags flags) { if ((col & IM_COL32_A_MASK) == 0) @@ -2691,7 +2713,8 @@ void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int nu outer_radius = radius; else if (stroke_pos == ImDrawFlags_StrokeOutside) outer_radius = radius + thickness; - if (outer_radius < (thickness + _FringeScale)) IM_UNLIKELY + + if (outer_radius <= thickness) IM_UNLIKELY { // The circle has collapsed into a filled circle. AddCircleFilled(center, outer_radius, col, num_segments); @@ -2715,7 +2738,15 @@ void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int nu PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); } - PathStroke(col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | stroke_pos); + if (_Path.Size < 3) + return; + + // Calculate distance to the edge of the polygon describing the circle. + // This will be used by _AddPolyline() to clamp the inner stroke expansion, to avoid creating rendering artifacts. + const float unit_apothem = ApproxApothem(_Path.Size); + const float edge_dist = radius * unit_apothem; + _AddPolyline(_Path.Data, _Path.Size, col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | stroke_pos, edge_dist); + _Path.Size = 0; } void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) @@ -2766,22 +2797,24 @@ void ImDrawList::AddNgon(const ImVec2& center, float radius, ImU32 col, int num_ } radius = ImMax(0.01f, radius); - // Check overestimate first before testing details (+1.0f is intended, not FringeScale) - if (radius < (thickness * 2.0f + 1.0f)) - { - const float unit_apothem = ImCos(IM_PI / (float)num_segments); - const float miter_thickness = thickness / unit_apothem; + // Calculate distance to the edge of the polygon describing the circle. + // This will be used by _AddPolyline() to clamp the inner stroke expansion, to avoid creating rendering artifacts. + const float unit_apothem = ApproxApothem(num_segments); + const float edge_dist = radius * unit_apothem; + // Check overestimate first before testing details. + if (radius < thickness * 2.0f) + { float outer_radius = radius; if (stroke_pos == ImDrawFlags_StrokeCenter) - outer_radius = radius + miter_thickness * 0.5f; + outer_radius = radius + thickness * 0.5f; else if (stroke_pos == ImDrawFlags_StrokeCenterBiased) - outer_radius = radius + _CalculateCenterBiasedOffset(miter_thickness); + outer_radius = radius + _CalculateCenterBiasedOffset(thickness); else if (stroke_pos == ImDrawFlags_StrokeInside) outer_radius = radius; else if (stroke_pos == ImDrawFlags_StrokeOutside) - outer_radius = radius + miter_thickness; - if (outer_radius < (miter_thickness + _FringeScale)) IM_UNLIKELY + outer_radius = radius + thickness; + if (outer_radius * unit_apothem <= thickness) IM_UNLIKELY { // The polygon has collapsed into a filled polygon. AddNgonFilled(center, outer_radius, col, num_segments); @@ -2792,7 +2825,8 @@ void ImDrawList::AddNgon(const ImVec2& center, float radius, ImU32 col, int num_ // Because we are filling a closed shape we remove 1 from the count of segments/points const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); - PathStroke(col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | stroke_pos); + _AddPolyline(_Path.Data, _Path.Size, col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | stroke_pos, edge_dist); + _Path.Size = 0; } // Guaranteed to honor 'num_segments' @@ -2838,7 +2872,11 @@ void ImDrawList::AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 co // Because we are filling a closed shape we remove 1 from the count of segments/points const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; PathEllipticalArcTo(center, rad, rot, 0.0f, a_max, num_segments - 1); - PathStroke(col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | stroke_pos); + + // Prevent the inside part of the stroke to be expanded too much to prevent some rendering artifacts. + const float max_inner_offset = ImMin(rad.x, rad.y); + _AddPolyline(_Path.Data, _Path.Size, col, thickness, ImDrawFlags_Closed | ImDrawFlags_MiterOnly | stroke_pos, max_inner_offset); + _Path.Size = 0; } void ImDrawList::AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments)