imgui
imgui copied to clipboard
New Polyline rendering
This is follow up to #2964 Render thick lines with correct thickness and beveled corners PR.
New implementation introduce more features, fixes some long standing issues and aim to be fast.
Code is feature complete, no todo's are left other than bugfixes.
Change is rather large and may look intimidating. Please read description, comment, criticque code and suggest improvements.
New Features
Fully Variable Thickness
New implementation support full range of thickness for Anti-Aliased and Aliased paths.
https://github.com/user-attachments/assets/bf661525-d934-4d13-b240-5eccb707489f
Support for ImDrawListFlags_AllowVtxOffset
With 16-bit indices single command can handle up to 65536 vertices. Huge polylines are now split across multiple draw commands if necessary.
Cap Types
Support for caps on open polylines.
ImDrawFlags_CapNone |
ImDrawFlags_CapButt(new default) |
ImDrawFlags_CapSquare |
ImDrawFlags_CapRound |
|
|---|---|---|---|---|
| AA Mesh |
||||
| AA 1:1 |
||||
| Aliased Mesh |
||||
| Aliased 1:1 |
Important notes:
ImDrawFlags_CapButtis new defaultImDrawFlags_CapNonematch old behavior which leave anti-aliased polyline without a capImDrawFlags_CapButtis new default, this make all edges on checkbox anti-aliasedImDrawFlags_CapRoundis pretty yet expensive in relation to other caps typesImDrawFlags_CapRounduse adaptive rendering viaPathArcTo
Join Types
Support for more join types.
Edit: Rounded joins (and caps) are not more refined, please see https://github.com/ocornut/imgui/pull/7972#issuecomment-2345018733 below.
ImDrawFlags_JoinMiter(default) |
ImDrawFlags_JoinMiterClip |
ImDrawFlags_JoinBevel |
ImDrawFlags_JoinRound |
|
|---|---|---|---|---|
| AA Mesh |
||||
| AA 1:1 |
||||
| Aliased Mesh |
||||
| Aliased 1:1 |
Important notes:
ImDrawFlags_JoinMitercollapse toImDrawFlags_JoinBevelwhen miter limit is exceeded (see below)ImDrawFlags_JoinMiterClipprovide smooth transition between Miter and Bevel, it is bit more expensiveImDrawFlags_JoinBevelis require bit more math for Anti-Aliased polylineImDrawFlags_JoinRoundis pretty yet expensive in relation to other join typesImDrawFlags_JoinRounduse adaptive rendering viaPathArcTo
Miter Limit
Miter distance count from the control point to very tip of the triangle forming a join. Sharpness of the join can be limited by using 'Miter Limit'. Miter does collapse to Bevel, MiterClip does smooth transition to Bevel.
Miter limit match SVG2 stroke-miterlimit behavior.
(borrowed from w3.org)
AddPolyline gained new parameter miter_limit:
IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness, float miter_limit = -1.0f);
Default Miter Limit is set 4.0 in ImDrawListSharedData (internal API) and not exposed to the user.
Fallback
Previous implementation of polyline is still available under different name:
IMGUI_API void AddPolylineLegacy(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness);
New implementation does change behavior. ImDrawListFlags_LegacyPolyline flag is a gateway to opt-in to old rendering.
New fancy tools are new and fancy, but also not mature. Setting ImDrawListFlags_LegacyPolyline on ImDrawList will cause PathStroke() to route all rendering to old AddPolyline(). This in turn for all practicall purposes will route rendering of all primitives to old code path.
Reasons to set ImDrawListFlags_LegacyPolyline flag:
- When you see noticable dips in performance. New implementation does not use texture based rendering, which make
AddPolylineLegacyfaster and generate less geometry. - When new behavior cause issues in rendering.
AddPolylineLegacydoes not support caps and can generate wrong geometry on acute angles.
Related issues
https://github.com/ocornut/imgui/issues/2183 Bug in drawing thick antialiased polylines
Status: Fixed ✅
| Old | New |
|---|---|
https://github.com/ocornut/imgui/issues/3366 Polyline with sharp angles causes segments to nearly disappear
Status: Fixed ✅
| Old | New (default) | New (large Miter Limit) |
|---|---|---|
https://github.com/ocornut/imgui/pull/4091 Optimize IM_NORMALIZE2F_OVER_ZERO
This PR does add IM_NORMALIZE2F_OVER_ZERO_PRECISE() that is built on top of SSE2 _mm_rsqrt intrinsics. SSE2 states that this function is an approximation of 1.0f / sqrt(x) and does have relatively large error. New macro does use new ImRsqrtPrecise() instead of ImRsqrt.
ImRsqrtPrecise() improve precission by converging on answer by performing single step Newton-Raphson method. Exackly like in famous rsqrt found in Quake source code. This single step is enough to keep geometry consistent and to not produce cosines larger than 1.
On debug (and non SSE2) builds ImRsqrtPrecise fallback to 1.0f / sqrt(x), because this is faster than to perform NR step in unoptimized code. On release SSE2 builds this does use method described above.
Performance
Early measurements compare old AddPolyline() with and without texture polylines against new implementation.
I was unable to quickly test #2964, because it crashed on some tests.
Observations:
- consecutive calls (hot path) does have differen performance characteristics than first call (cold path)
- new implementation place itself in between old
AddPolyline()and #2964 - observation is based on test made while in development
- ~~Test performance against #2964~~ as adviced, comparison with previous PR is not necessary
Please draw your own conclusions, or beter do your own benchmark to see change in real world.
| Debug | Release |
|---|---|
Note:
- Old
AddPolyline()does sometimes produce broken meshes in some tests due to lack ofImDrawListFlags_AllowVtxOffsetsupport
Implementation notes
General:
- Implementation see rather heavey use of macros, all are names like
IM_POLYLINE_xxx - They should improve readability and see code flow
IM_POLYLINE_TRIANGLE_xxxare overly verbose to experiment with different methods of filling index buffer- Will be simplified to drop unused arguments after we settle on one particular implementation
- Uses as little local variables as possible
- accessing them on Debug builds does generate extra
movinstructions - computations are done as expressions with intent to end with single assignment to final location in the buffer
- all
_PolylineXXXare basically for loops with a bit of math at the beginning and 'switch' routing to selected join geometry generation code - core responsible for generating geometry is commented with 'ascii art' explaining where magic values in indices came from
- code paths are annotated with
IM_LIKELYandIM_UNLIKELYwhich are resolved to[[likely]]and[[unlikely]]when available - this does guide optimizer, but does not affect Debug builds
New implementation does split problem to three cases:
_PolylineThinAntiAliased- for thickness <= 1, technically less than AA fringe- skips generating solid core geometry
- skips MiterClip logic all together (fringe is 1 pixel thick so jump from Miter to Bevel will not be noticable in most cases)
_PolylineThickAntiAliased- for thickness > 1- presence of AA fringe bump complexity of generated geometry, in most extreme cases can emit 17 vertices per join, it is rare
_PolylineAliased- used for any thickness, does not have AA fringe
AddPolyline():
- act as dispatcher calling one of the methods above
- is responsible of computing polyline normals / segment lengths used by all subroutines
- With SSE2 computes 4 normals at once, then 2 normals at once and finally fall back to scalar implementation
- is responsible for sanatization of input values,
_PolylineXXXdoes not need to validate input
Rounted caps and joins:
_PolylineEmitArcand_PolylineEmitArcsWithFringepost-processing step responsible for generating adaptive arcsImAtan2andAcosare use for every join to feedPathArcToImAtan2is used for capsPathArcTo- Implementation definitely can be improved in the future
- All queued arcs are processed at the end of
_PolylineXXXcall - Arcs are queued in
ImDrawList::_Path, they're appended at the end - [x] Change format so place for encoded arcs can be pre-allocated to avoid memory corruption