From e30d8e78880f615303b3123e01fe3a24d9b81c49 Mon Sep 17 00:00:00 2001 From: Mikko Mononen Date: Tue, 26 May 2026 15:52:59 +0300 Subject: [PATCH] DrawList: fixed visual pop in stroke rendering when thickness is between 1 and 2 pixels. - added animated thickness to demo to make it easier to evaluate thickness visual progression - added super sampled textures in thickness range [1..3] to avoid visual pop --- imgui.h | 7 ++- imgui_demo.cpp | 20 ++++-- imgui_draw.cpp | 165 ++++++++++++++++++++++++++++++++++++------------- 3 files changed, 142 insertions(+), 50 deletions(-) diff --git a/imgui.h b/imgui.h index b4372bef1..0079dfcff 100644 --- a/imgui.h +++ b/imgui.h @@ -3323,6 +3323,10 @@ struct ImGuiSelectionExternalStorage #define IM_DRAWLIST_TEX_LINES_WIDTH_MAX (32) #endif +#ifndef IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX +#define IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX (2) // How many samples for lines between [1..2) thickness. +#endif + #ifndef IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX #define IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX (16) #endif @@ -3653,6 +3657,7 @@ struct ImDrawList IMGUI_API void _AddRectBaked(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float r, float t, ImVec4 tex_uvs, ImDrawFlags flags); IMGUI_API void _AddPolyline(const ImVec2* points, ImVec2* normals, float* sqr_lengths, const int points_count, ImU32 col, float thickness, ImDrawFlags flags, ImVec4 tex_uvs, float fringe); 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& tex_uvs, float& fringe); }; // All draw data to render a Dear ImGui frame @@ -4014,7 +4019,7 @@ struct ImFontAtlas ImVec2 TexUvWhitePixel; // Texture coordinates to a white pixel. May change as new texture gets created. ImVector Fonts; // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font. ImVector Sources; // Source/configuration data - ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1]; // UVs for baked anti-aliased lines + ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1 + IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX + 1]; // UVs for baked anti-aliased lines ImVec4 TexUvCorners[IM_DRAWLIST_TEX_CORNERS_ROUNDING_MAX * IM_DRAWLIST_TEX_CORNERS_THICKNESS_MAX]; // UVs for baked anti-aliased corners (0= fill, 1> stroke thickness) int TexNextUniqueID; // Next value to be stored in TexData->UniqueID int FontNextUniqueID; // Next value to be stored in ImFont->FontID diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 892825eef..b41012bca 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -10411,17 +10411,25 @@ static void ShowExampleAppCustomRendering(bool* p_open) // Draw a bunch of primitives ImGui::Text("All primitives"); static float sz = 42.0f; - static float thickness = 3.0f; - static int ngon_sides = 6; + static bool animate_thickness = false; + static float base_thickness = 3.0f; + static int ngon_segments = 6; static bool circle_segments_override = false; static int circle_segments_override_v = 12; static bool curve_segments_override = false; static int curve_segments_override_v = 8; static ImVec4 colf_stroke = ImVec4(1.00f, 1.00f, 0.40f, 1.0f); static ImVec4 colf_fill = ImVec4(0.70f, 0.18f, 0.62f, 1.0f); + const float thickness_anim_period = 5.f; + float thickness_wave = 1.0f - fabs(fmod((float)ImGui::GetTime(), thickness_anim_period) * (2.0f / thickness_anim_period) - 1.0f); + float thickness = animate_thickness ? thickness_wave * base_thickness : base_thickness; + ImGui::DragFloat("Size", &sz, 0.2f, 0.2f, 100.0f, "%.0f"); - ImGui::DragFloat("Thickness", &thickness, 0.1f, 0.0f, 30.0f, "%.02f"); - ImGui::SliderInt("N-gon sides", &ngon_sides, 3, 12); + ImGui::DragFloat("Thickness", &base_thickness, 0.1f, 0.0f, 30.0f, "%.02f"); + ImGui::Checkbox("Animate Thickness", &animate_thickness); + ImGui::SameLine(); + ImGui::Text("%.2f", thickness); + ImGui::SliderInt("N-gon sides", &ngon_segments, 3, 12); ImGui::Checkbox("##circlesegmentoverride", &circle_segments_override); ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); circle_segments_override |= ImGui::SliderInt("Circle segments override", &circle_segments_override_v, 3, 40); @@ -10473,7 +10481,7 @@ static void ShowExampleAppCustomRendering(bool* p_open) { // FILLED SHAPES const ImU32 col = ImColor(colf_fill); - draw_list->AddNgonFilled({ x + half_sz, y + half_sz }, half_sz, col, ngon_sides); x += sz + spacing; // N-gon + draw_list->AddNgonFilled({ x + half_sz, y + half_sz }, half_sz, col, ngon_segments); x += sz + spacing; // N-gon draw_list->AddCircleFilled({ x + half_sz, y + half_sz }, half_sz, col, circle_segments); x += sz + spacing; // Circle draw_list->AddEllipseFilled({ x + half_sz, y + half_sz }, { sz * 0.5f, sz * 0.3f }, col, -0.3f, circle_segments); x += sz + spacing;// Ellipse draw_list->AddRectFilled({ x, y }, { x + sz, y + sz }, col); x += sz + spacing; // Square @@ -10523,7 +10531,7 @@ static void ShowExampleAppCustomRendering(bool* p_open) // First row uses a thickness of 1.0f, second row uses the configurable thickness const ImU32 col = ImColor(colf_stroke); float th = (row == 0) ? 1.0f : thickness; - draw_list->AddNgon({ x + half_sz, y + half_sz }, half_sz, col, ngon_sides, th, flags); x += sz + spacing; // N-gon + draw_list->AddNgon({ x + half_sz, y + half_sz }, half_sz, col, ngon_segments, th, flags); x += sz + spacing; // N-gon draw_list->AddCircle({ x + half_sz, y + half_sz }, half_sz, col, circle_segments, th, flags); x += sz + spacing; // Circle draw_list->AddEllipse({ x + half_sz, y + half_sz }, { sz * 0.5f, sz * 0.3f }, col, -0.3f, circle_segments, th, flags); x += sz + spacing; // Ellipse draw_list->AddRect({ x, y }, { x + sz, y + sz }, col, 0.0f, th, flags); x += sz + spacing; // Square diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 0fcd14987..d32027d02 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -1187,6 +1187,27 @@ static ImU32 ImAlphaMultiply(ImU32 col, float alpha_mul) return (col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); } +void ImDrawList::_SelectFringeTexture(float screen_thickness, ImVec4& tex_uvs, float& fringe) +{ + if (screen_thickness <= 2.f) + { + // Handle the thickness between [1..2]. The texture scaling in this range will cause visual pop near 1, so we generate super sampled textures in this range. + // There IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX+1 textures, where 0 maps to 1.0 and IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX maps 2.0. + constexpr float base_width = 1.f; + const int texture_idx = ImClamp((int)((screen_thickness - base_width) * IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX + 0.5f), 0, IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX); + const float tex_width = base_width + (float)texture_idx / IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX; + fringe = _FringeScale * (screen_thickness / tex_width); // Scale the fringe to cover the discrepancy between the texture and requested size. + tex_uvs = _Data->TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1 + texture_idx]; + } + else + { + // Bias the texture selection towards higher resolution to avoid visual pops when the texture change. + const int texture_idx = ImClamp((int)(screen_thickness + 0.95f), 1, IM_DRAWLIST_TEX_LINES_WIDTH_MAX - 1); + fringe = _FringeScale * (screen_thickness / (float)texture_idx); // Scale the fringe to cover the discrepancy between the texture and requested size. + tex_uvs = _Data->TexUvLines[texture_idx]; + } +} + extern bool g_LEGACY_STROKES; void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, float thickness, ImDrawFlags flags) @@ -1244,10 +1265,9 @@ void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 sqr_lengths[points_count - 1] = 0.0f; } - // Bias the texture selection towards higher resolution to avoid visual pops when the texture change. - const int texture_idx = ImClamp((int)(screen_thickness + 0.95f), 1, IM_DRAWLIST_TEX_LINES_WIDTH_MAX - 1); - const float fringe = _FringeScale * (screen_thickness / (float)texture_idx); // Scale the fringe to cover the discrepancy between the texture and requested size. - const ImVec4 tex_uvs = _Data->TexUvLines[texture_idx]; + ImVec4 tex_uvs; + float fringe; + _SelectFringeTexture(screen_thickness, tex_uvs, fringe); _AddPolyline(points, normals, sqr_lengths, points_count, col, thickness, flags, tex_uvs, fringe); } @@ -2352,10 +2372,9 @@ void ImDrawList::_AddRectTinyRounding(const ImVec2& p_min, const ImVec2& p_max, thickness = _FringeScale; } - // Bias the texture selection towards higher resolution to avoid visual pops when the texture change. - const int texture_idx = ImClamp((int)(screen_thickness + 0.95f), 1, IM_DRAWLIST_TEX_LINES_WIDTH_MAX - 1); - const float fringe = _FringeScale * (screen_thickness / (float)texture_idx); // Scale the fringe to cover the discrepancy between the texture and requested size. - const ImVec4 tex_uvs = _Data->TexUvLines[texture_idx]; + ImVec4 tex_uvs; + float fringe; + _SelectFringeTexture(screen_thickness, tex_uvs, fringe); const int arc_step = ImClamp(IM_DRAWLIST_ARCFAST_SAMPLE_MAX / this->_CalcCircleAutoSegmentCount(rounding), 1, IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4); const int arc_point_count = (IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4) / arc_step + 1; @@ -5101,54 +5120,114 @@ static void ImFontAtlasBuildUpdateTexDataLines(ImFontAtlas* atlas) bool add_and_draw = atlas->GetCustomRect(builder->PackIdLinesTexData, &r) == false; if (add_and_draw) { - ImVec2i pack_size = ImVec2i(IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 2, IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1); + ImVec2i pack_size = ImVec2i(IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 2, IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1 + IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX + 1); builder->PackIdLinesTexData = atlas->AddCustomRect(pack_size.x, pack_size.y, &r); IM_ASSERT(builder->PackIdLinesTexData != ImFontAtlasRectId_Invalid); } - // Register texture region for thick lines - // The +2 here is to give space for the end caps, whilst height +1 is to accommodate the fact we have a zero-width row - // This generates a triangular shape in the texture, with the various line widths stacked on top of each other to allow interpolation between them - for (int n = 0; n < IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1; n++) // +1 because of the zero-width row { - // Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle - const int y = n; - const int line_width = n; - const int pad_left = (r.w - line_width) / 2; - const int pad_right = r.w - (pad_left + line_width); - IM_ASSERT(pad_left + line_width + pad_right == r.w && y < r.h); // Make sure we're inside the texture bounds before we start writing pixels - - // Write each slice - if (add_and_draw && tex->Format == ImTextureFormat_Alpha8) + // Register texture region for thick lines + // The +2 here is to give space for the end caps, whilst height +1 is to accommodate the fact we have a zero-width row + // This generates a triangular shape in the texture, with the various line widths stacked on top of each other to allow interpolation between them + for (int n = 0; n < IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1; n++) // +1 because of the zero-width row { - ImU8* write_ptr = (ImU8*)tex->GetPixelsAt(r.x, r.y + y); - for (int i = 0; i < pad_left; i++) - *(write_ptr + i) = 0x00; + // Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle + const int y = n; + const int line_width = n; + const int pad_left = (r.w - line_width) / 2; + const int pad_right = r.w - (pad_left + line_width); + IM_ASSERT(pad_left + line_width + pad_right == r.w && y < r.h); // Make sure we're inside the texture bounds before we start writing pixels - for (int i = 0; i < line_width; i++) - *(write_ptr + pad_left + i) = 0xFF; + // Write each slice + if (add_and_draw && tex->Format == ImTextureFormat_Alpha8) + { + ImU8* write_ptr = (ImU8*)tex->GetPixelsAt(r.x, r.y + y); + for (int i = 0; i < pad_left; i++) + *(write_ptr + i) = 0x00; - for (int i = 0; i < pad_right; i++) - *(write_ptr + pad_left + line_width + i) = 0x00; + for (int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = 0xFF; + + for (int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = 0x00; + } + else if (add_and_draw && tex->Format == ImTextureFormat_RGBA32) + { + ImU32* write_ptr = (ImU32*)(void*)tex->GetPixelsAt(r.x, r.y + y); + for (int i = 0; i < pad_left; i++) + *(write_ptr + i) = IM_COL32(255, 255, 255, 0); + + for (int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = IM_COL32_WHITE; + + for (int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = IM_COL32(255, 255, 255, 0); + } + + // Refresh UV coordinates + ImVec2 uv0 = ImVec2((float)(r.x + pad_left - 1), (float)(r.y + y)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(r.x + pad_left + line_width + 1), (float)(r.y + y + 1)) * atlas->TexUvScale; + float half_v = (uv0.y + uv1.y) * 0.5f; // Calculate a constant V in the middle of the row to avoid sampling artifacts + atlas->TexUvLines[n] = ImVec4(uv0.x, half_v, uv1.x, half_v); } - else if (add_and_draw && tex->Format == ImTextureFormat_RGBA32) + } + + { + + ImU8 ramp[IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX]; + for (int n = 0; n < IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX; n++) + ramp[n] = (ImU8)(((float)n / IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX) * 255.f); + + for (int n = 0; n < IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX + 1; n++) { - ImU32* write_ptr = (ImU32*)(void*)tex->GetPixelsAt(r.x, r.y + y); - for (int i = 0; i < pad_left; i++) - *(write_ptr + i) = IM_COL32(255, 255, 255, 0); + // Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle + const int y = IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1 + n; + const int line_width = IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX + n; + IM_ASSERT(IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX + line_width + IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX <= r.w && y < r.h); // Make sure we're inside the texture bounds before we start writing pixels - for (int i = 0; i < line_width; i++) - *(write_ptr + pad_left + i) = IM_COL32_WHITE; + // Write each slice + if (add_and_draw && tex->Format == ImTextureFormat_Alpha8) + { + ImU8* write_ptr = (ImU8*)tex->GetPixelsAt(r.x, r.y + y); + int idx = 0; - for (int i = 0; i < pad_right; i++) - *(write_ptr + pad_left + line_width + i) = IM_COL32(255, 255, 255, 0); + for (int i = 0; i < IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX; i++) + write_ptr[idx++] = ramp[i]; + + for (int i = 0; i < line_width; i++) + write_ptr[idx++] = 0xFF; + + for (int i = IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX-1; i >= 0; i--) + write_ptr[idx++] = ramp[i]; + + while (idx < r.w) + write_ptr[idx++] = 0x0; + } + else if (add_and_draw && tex->Format == ImTextureFormat_RGBA32) + { + ImU32* write_ptr = (ImU32*)(void*)tex->GetPixelsAt(r.x, r.y + y); + + int idx = 0; + + for (int i = 0; i < IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX; i++) + write_ptr[idx++] = IM_COL32(255, 255, 255, ramp[i]); + + for (int i = 0; i < line_width; i++) + write_ptr[idx++] = IM_COL32(255, 255, 255, 255); + + for (int i = IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX - 1; i >= 0; i--) + write_ptr[idx++] = IM_COL32(255, 255, 255, ramp[i]); + + while (idx < r.w) + write_ptr[idx++] = IM_COL32(255, 255, 255, 0); + } + + // Refresh UV coordinates + ImVec2 uv0 = ImVec2((float)(r.x), (float)(r.y + y)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(r.x + IM_DRAWLIST_TEX_LINES_SUPERSAMPLE_MAX * 2 + line_width), (float)(r.y + y + 1)) * atlas->TexUvScale; + float half_v = (uv0.y + uv1.y) * 0.5f; // Calculate a constant V in the middle of the row to avoid sampling artifacts + atlas->TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1 + n] = ImVec4(uv0.x, half_v, uv1.x, half_v); } - - // Refresh UV coordinates - ImVec2 uv0 = ImVec2((float)(r.x + pad_left - 1), (float)(r.y + y)) * atlas->TexUvScale; - ImVec2 uv1 = ImVec2((float)(r.x + pad_left + line_width + 1), (float)(r.y + y + 1)) * atlas->TexUvScale; - float half_v = (uv0.y + uv1.y) * 0.5f; // Calculate a constant V in the middle of the row to avoid sampling artifacts - atlas->TexUvLines[n] = ImVec4(uv0.x, half_v, uv1.x, half_v); } }