Improve on automatic circle segment count calculation. (#3808)

This commit is contained in:
thedmd 2021-02-17 12:55:25 +01:00 committed by ocornut
parent b47aa46d81
commit f107693d9b
5 changed files with 80 additions and 32 deletions

View File

@ -986,7 +986,7 @@ ImGuiStyle::ImGuiStyle()
AntiAliasedLinesUseTex = true; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering. AntiAliasedLinesUseTex = true; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering.
AntiAliasedFill = true; // Enable anti-aliased filled shapes (rounded rectangles, circles, etc.). AntiAliasedFill = true; // Enable anti-aliased filled shapes (rounded rectangles, circles, etc.).
CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality.
CircleSegmentMaxError = 1.60f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. CircleTessellationMaxError = 0.25f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry.
// Default theme // Default theme
ImGui::StyleColorsDark(this); ImGui::StyleColorsDark(this);
@ -3878,7 +3878,7 @@ void ImGui::NewFrame()
virtual_space.Add(g.Viewports[n]->GetMainRect()); virtual_space.Add(g.Viewports[n]->GetMainRect());
g.DrawListSharedData.ClipRectFullscreen = virtual_space.ToVec4(); g.DrawListSharedData.ClipRectFullscreen = virtual_space.ToVec4();
g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol;
g.DrawListSharedData.SetCircleSegmentMaxError(g.Style.CircleSegmentMaxError); g.DrawListSharedData.SetCircleTessellationMaxError(g.Style.CircleTessellationMaxError);
g.DrawListSharedData.InitialFlags = ImDrawListFlags_None; g.DrawListSharedData.InitialFlags = ImDrawListFlags_None;
if (g.Style.AntiAliasedLines) if (g.Style.AntiAliasedLines)
g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines; g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines;
@ -7056,7 +7056,7 @@ static void ImGui::ErrorCheckNewFrameSanityChecks()
IM_ASSERT(g.IO.Fonts->Fonts.Size > 0 && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()?"); IM_ASSERT(g.IO.Fonts->Fonts.Size > 0 && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()?");
IM_ASSERT(g.IO.Fonts->Fonts[0]->IsLoaded() && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()?"); IM_ASSERT(g.IO.Fonts->Fonts[0]->IsLoaded() && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()?");
IM_ASSERT(g.Style.CurveTessellationTol > 0.0f && "Invalid style setting!"); IM_ASSERT(g.Style.CurveTessellationTol > 0.0f && "Invalid style setting!");
IM_ASSERT(g.Style.CircleSegmentMaxError > 0.0f && "Invalid style setting!"); IM_ASSERT(g.Style.CircleTessellationMaxError > 0.0f && "Invalid style setting!");
IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f && "Invalid style setting!"); // Allows us to avoid a few clamps in color computations IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f && "Invalid style setting!"); // Allows us to avoid a few clamps in color computations
IM_ASSERT(g.Style.WindowMinSize.x >= 1.0f && g.Style.WindowMinSize.y >= 1.0f && "Invalid style setting."); IM_ASSERT(g.Style.WindowMinSize.x >= 1.0f && g.Style.WindowMinSize.y >= 1.0f && "Invalid style setting.");
IM_ASSERT(g.Style.WindowMenuButtonPosition == ImGuiDir_None || g.Style.WindowMenuButtonPosition == ImGuiDir_Left || g.Style.WindowMenuButtonPosition == ImGuiDir_Right); IM_ASSERT(g.Style.WindowMenuButtonPosition == ImGuiDir_None || g.Style.WindowMenuButtonPosition == ImGuiDir_Left || g.Style.WindowMenuButtonPosition == ImGuiDir_Right);

View File

@ -1723,7 +1723,7 @@ struct ImGuiStyle
bool AntiAliasedLinesUseTex; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering. Latched at the beginning of the frame (copied to ImDrawList). bool AntiAliasedLinesUseTex; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering. Latched at the beginning of the frame (copied to ImDrawList).
bool AntiAliasedFill; // Enable anti-aliased edges around filled shapes (rounded rectangles, circles, etc.). Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). bool AntiAliasedFill; // Enable anti-aliased edges around filled shapes (rounded rectangles, circles, etc.). Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList).
float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality.
float CircleSegmentMaxError; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. float CircleTessellationMaxError; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry.
ImVec4 Colors[ImGuiCol_COUNT]; ImVec4 Colors[ImGuiCol_COUNT];
IMGUI_API ImGuiStyle(); IMGUI_API ImGuiStyle();
@ -2463,6 +2463,7 @@ struct ImDrawList
IMGUI_API void _OnChangedClipRect(); IMGUI_API void _OnChangedClipRect();
IMGUI_API void _OnChangedTextureID(); IMGUI_API void _OnChangedTextureID();
IMGUI_API void _OnChangedVtxOffset(); IMGUI_API void _OnChangedVtxOffset();
IMGUI_API int _CalcCircleAutoSegmentCount(float radius) const;
}; };
// All draw data to render a Dear ImGui frame // All draw data to render a Dear ImGui frame

View File

@ -6034,22 +6034,44 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref)
if (style.CurveTessellationTol < 0.10f) style.CurveTessellationTol = 0.10f; if (style.CurveTessellationTol < 0.10f) style.CurveTessellationTol = 0.10f;
// When editing the "Circle Segment Max Error" value, draw a preview of its effect on auto-tessellated circles. // When editing the "Circle Segment Max Error" value, draw a preview of its effect on auto-tessellated circles.
ImGui::DragFloat("Circle Segment Max Error", &style.CircleSegmentMaxError, 0.01f, 0.10f, 10.0f, "%.2f"); ImGui::DragFloat("Circle Tessellation Max Error", &style.CircleTessellationMaxError , 0.005f, 0.10f, 10.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp);
if (ImGui::IsItemActive()) if (ImGui::IsItemActive())
{ {
ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos()); ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos());
ImGui::BeginTooltip(); ImGui::BeginTooltip();
ImVec2 p = ImGui::GetCursorScreenPos(); ImGui::TextUnformatted("N - number of segments");
ImGui::TextUnformatted("R - radius");
ImGui::Spacing();
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList* draw_list = ImGui::GetWindowDrawList();
float RAD_MIN = 10.0f, RAD_MAX = 80.0f; const float min_widget_width = ImGui::CalcTextSize("N: MM\nR: MM.MM").x;
float off_x = 10.0f; float RAD_MIN = 5.0f, RAD_MAX = 80.0f;
for (int n = 0; n < 7; n++) for (int n = 0; n < 9; n++)
{ {
const float rad = RAD_MIN + (RAD_MAX - RAD_MIN) * (float)n / (7.0f - 1.0f); const float rad = RAD_MIN + (RAD_MAX - RAD_MIN) * (float)n / (9.0f - 1.0f);
draw_list->AddCircle(ImVec2(p.x + off_x + rad, p.y + RAD_MAX), rad, ImGui::GetColorU32(ImGuiCol_Text), 0);
off_x += 10.0f + rad * 2.0f; const int segment_count = draw_list->_CalcCircleAutoSegmentCount(rad);
ImGui::BeginGroup();
ImGui::Text("R: %.f", rad);
ImGui::Text("N: %d", segment_count);
const float circle_diameter = rad * 2.0f;
const float canvas_width = IM_MAX(min_widget_width, circle_diameter);
const float offset_x = floorf(canvas_width * 0.5f);
const float offset_y = floorf(RAD_MAX);
const ImVec2 p = ImGui::GetCursorScreenPos();
draw_list->AddCircle(ImVec2(p.x + offset_x, p.y + offset_y), rad, ImGui::GetColorU32(ImGuiCol_Text));
ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2));
ImGui::Text("N: %d", segment_count);
const ImVec2 p2 = ImGui::GetCursorScreenPos();
draw_list->AddCircleFilled(ImVec2(p2.x + offset_x, p2.y + offset_y), rad, ImGui::GetColorU32(ImGuiCol_Text));
ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2));
ImGui::EndGroup();
ImGui::SameLine();
} }
ImGui::Dummy(ImVec2(off_x, RAD_MAX * 2.0f));
ImGui::EndTooltip(); ImGui::EndTooltip();
} }
ImGui::SameLine(); ImGui::SameLine();

View File

@ -376,7 +376,7 @@ ImDrawListSharedData::ImDrawListSharedData()
} }
} }
void ImDrawListSharedData::SetCircleSegmentMaxError(float max_error) void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error)
{ {
if (CircleSegmentMaxError == max_error) if (CircleSegmentMaxError == max_error)
return; return;
@ -384,8 +384,7 @@ void ImDrawListSharedData::SetCircleSegmentMaxError(float max_error)
for (int i = 0; i < IM_ARRAYSIZE(CircleSegmentCounts); i++) for (int i = 0; i < IM_ARRAYSIZE(CircleSegmentCounts); i++)
{ {
const float radius = (float)i; const float radius = (float)i;
const int segment_count = (i > 0) ? IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleSegmentMaxError) : 0; CircleSegmentCounts[i] = (ImU8)((i > 0) ? IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleSegmentMaxError) : 0);
CircleSegmentCounts[i] = (ImU8)ImMin(segment_count, 255);
} }
} }
@ -543,6 +542,21 @@ void ImDrawList::_OnChangedVtxOffset()
curr_cmd->VtxOffset = _CmdHeader.VtxOffset; curr_cmd->VtxOffset = _CmdHeader.VtxOffset;
} }
int ImDrawList::_CalcCircleAutoSegmentCount(float radius) const
{
int num_segments = 0;
const int radius_idx = (int)ImCeil(radius); // Use ceil to never reduce accuracy
// Automatic segment count
if (radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts))
num_segments = _Data->CircleSegmentCounts[radius_idx]; // Use cached value
else
num_segments = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError);
return num_segments;
}
// Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling)
void ImDrawList::PushClipRect(ImVec2 cr_min, ImVec2 cr_max, bool intersect_with_current_clip_rect) void ImDrawList::PushClipRect(ImVec2 cr_min, ImVec2 cr_max, bool intersect_with_current_clip_rect)
{ {
@ -1286,11 +1300,7 @@ void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int nu
if (num_segments <= 0) if (num_segments <= 0)
{ {
// Automatic segment count // Automatic segment count
const int radius_idx = (int)radius; num_segments = _CalcCircleAutoSegmentCount(radius);
if (radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts))
num_segments = _Data->CircleSegmentCounts[radius_idx]; // Use cached value
else
num_segments = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError);
} }
else else
{ {
@ -1316,11 +1326,7 @@ void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col,
if (num_segments <= 0) if (num_segments <= 0)
{ {
// Automatic segment count // Automatic segment count
const int radius_idx = (int)radius; num_segments = _CalcCircleAutoSegmentCount(radius);
if (radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts))
num_segments = _Data->CircleSegmentCounts[radius_idx]; // Use cached value
else
num_segments = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError);
} }
else else
{ {

View File

@ -617,10 +617,29 @@ struct IMGUI_API ImChunkStream
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// ImDrawList: Helper function to calculate a circle's segment count given its radius and a "maximum error" value. // ImDrawList: Helper function to calculate a circle's segment count given its radius and a "maximum error" value.
// FIXME: the minimum number of auto-segment may be undesirably high for very small radiuses (e.g. 1.0f) //
#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN 12 // Estimation of number of circle segment based on error is derived using method described in
// this post (https://stackoverflow.com/a/2244088/15194693).
// Number of segments (N) is calculated using equation:
//
// +- -+
// | pi |
// N = ceil | --------------------- | where r > 0, error <= r
// | acos(1 - error / r) |
// +- -+
//
// Note:
// Equation is significantly simpler that one in the post thanks for choosing segment
// that is perpendicular to X axis. Follow steps in the article from this starting condition
// and you will get this result.
//
// Rendering circles with an odd number of segments, while mathematically correct will produce
// asymmetrical results on the raster grid. Therefore we're rounding N to next even number.
// (7 became 8, 11 became 12, but 8 will still be 8).
//
#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN 4
#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX 512 #define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX 512
#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(_RAD,_MAXERROR) ImClamp((int)((IM_PI * 2.0f) / ImAcos(((_RAD) - (_MAXERROR)) / (_RAD))), IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX) #define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(_RAD,_MAXERROR) ImClamp((((int)ImCeil(IM_PI / ImAcos(1 - ImMin((_MAXERROR), (_RAD)) / (_RAD))) + 1) / 2) * 2, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX)
// ImDrawList: You may set this to higher values (e.g. 2 or 3) to increase tessellation of fast rounded corners path. // ImDrawList: You may set this to higher values (e.g. 2 or 3) to increase tessellation of fast rounded corners path.
#ifndef IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER #ifndef IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER
@ -641,11 +660,11 @@ struct IMGUI_API ImDrawListSharedData
// [Internal] Lookup tables // [Internal] Lookup tables
ImVec2 ArcFastVtx[12 * IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER]; // FIXME: Bake rounded corners fill/borders in atlas ImVec2 ArcFastVtx[12 * IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER]; // FIXME: Bake rounded corners fill/borders in atlas
ImU8 CircleSegmentCounts[64]; // Precomputed segment count for given radius before we calculate it dynamically (to avoid calculation overhead) ImU8 CircleSegmentCounts[64]; // Precomputed segment count for given radius before we calculate it dynamically (to avoid calculation overhead)
const ImVec4* TexUvLines; // UV of anti-aliased lines in the atlas const ImVec4* TexUvLines; // UV of anti-aliased lines in the atlas
ImDrawListSharedData(); ImDrawListSharedData();
void SetCircleSegmentMaxError(float max_error); void SetCircleTessellationMaxError(float max_error);
}; };
struct ImDrawDataBuilder struct ImDrawDataBuilder