-
-
Notifications
You must be signed in to change notification settings - Fork 10.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Render thick lines with correct thickness and beveled corners #2964
base: docking
Are you sure you want to change the base?
Conversation
Wow, that's an elegant solution with respect to acute angle specialization, and maybe the smallest code I've seen to solve the problem in general. Very nice! |
@potocpav Drawing libraries usualy provide a way to set maximum length of the miter, atfer that it is just capped. Maybe this is what you're looking for? |
@thedmd That's another way to do it, for sure. I think the results would be similar, and the code would be a little bit more complicated. With my algorithm, bevel vertices are calculated trivially: b1x = p1.x + (dx1 - dy1 * miter_sign) * half_thickness;
// similar for b1y, b2x, b2y Limiting the length of the miter may require one extra line-line intersection and a vector normalization per vertex. Miter length would be customizable, but is is something somebody actually needs? Admittedly, I didn't research SoTA and didn't know about this solution beforehand. Now I am curious to see how they handle very acute angles where "inner miter" is longer than adjacent line segments. My algorithm is quite hacky in that case. Maybe we can even adapt some existing code? I will probably look into it. |
@potocpav Yeah, miter version is more costly. I made a polyline rasterizer some time ago: It supports bevel, round and miter joints between segments and square, round, triangle and none end caps, closed polylines too. As far as I know it covers all styles SVG can do: Question is, does ImGui need full-featured version? |
This looks potentially good but I would appreciate if the code followed on dear imgui coding style, didn't rely on C++11 and didn't make unnecessary/unrelated modifications. Thank you. |
@ocornut This PR is work-in-progress. I wanted to get some feedback before finalizing the algorithm and code. I will clean it up (as you suggested) before merging. @thedmd Thanks for the pointers & link. I plan to implement AA soon, should be quite a simple addition if I sacrifice some sub-pixel accuracy around bevels. The thickness<=1px case may require a special (simpler) algorithm for performance & maintainability. |
@potocpav I'll give you access to the WIP imgui_tests repo in which you can add some performance tests if you want. |
@ocornut Would be great, thanks |
I implemented AA. The code is extremely ugly, but everything seems to work.
There are probably some small-ish bugs. |
This is probably now more-or-less finished. I did some crude benchmarks with 1M pts and -O2, and it is very similar in performance to the old code. Only the number of generated vertices and faces seem to matter to performance, not the algorithm they are generated by. I may still merge the thin-line case with the rest of the code, because it would generate fewer vertices and produce nicer output. @ocornut I removed non-related changes & tried to improve code style, but IDK if I followed all the ImGui conventions. Is there a document somewhere documenting code style? Should I squash commits and/or rebase on master instead of the docking branch? |
Here are some performance measurements. I tested on gcc -O2 only, because I don't have access to a Windows PC. I will be able to do benchmarks on MSVC debug next week, probably. Benchmarks consisted of repeatedly drawing a bunch of given geometry primitives using the imgui_test repo. Several notes:
|
Thanks for your work @potocpav! This is looking very good. Don't worry about squashing, we can do it when we merge but until them there's a possibility the history can come in useful. Coding style looks good as well (there's presently no document unfortunately). You may push your test code branch into imgui_tests/ - in its own branch - so we'll have access to them, can perform further tests and eventually the measurement can also make it into that repository. PS: Will also need look into adding code to use the screen capturing api in imgui_tests to quickly compare all our render paths (espcially since Ben's changes above will add a new set of variations into the mix, being able to output XX exactly aligned .png to compare will be nice). |
@potocpav, thanks for looking into this - the results look very promising! So yeah, as @ocornut mentioned, there's some shared ground between this and the texture-based approach I've implemented, which basically just uses a set of prebuilt textures for known widths to give (approximately) one pixel of antialiasing without needing extra geometry. Unfortunately I don't immediately see a way that we can apply that trick here - the fact that the bevels make the lines variable-width immediately causes a problem as the texturing relies on the width being constant. So it's possible that the most practical "solution" to merging them might be to have both drawing paths and come up with some metric to choose which to use based on the desired speed/quality tradeoff... does that sound like a reasonable analysis or have I missed some clever alternative approach? Also, one small question about the code - might it be simpler to calculate unused_vertices and unused_indices at the end (based on recording _IdxWritePtr and _VtxWritePtr prior to filling the buffer)? It strikes me that might be less prone to errors if anyone alters the code, and could potentially be (very slightly) faster as it avoids updating those in the inner loop... |
@ocornut I pushed my changes to imgui_tests to the branch "line-perf". There are some new perf tests, new cmdline parameters, and standard deviation is calculated for each test. After using it for a bit, I am not convinced that SD is that useful of a value. Every change is in a separate commit and can be cherry-picked. @ShironekoBen Would it be possible to use the textures for segments, and geometry for bevels? This would add 2 triangles per acute angle, but acute angles may not be very frequent in real-world UIs. Or just don't AA bevels at all, I think the artefacts caused by that would be quite small compared to status quo. Can we abuse texture wrapping? Is wrapping mode defined by imgui? @ShironekoBen What do the textures & geometry look like? Is there a PR with the changes? @ShironekoBen And yes, that's definitely a better way to compute unused_indices. |
(moving this to wip/private repo)
Attached is a patch to fix most warnings of the PR (see CI servers above) along with a few stylistic issues, if you could apply it in your branch? 0001-Fix-various-warnings-tweak-comments.zip There are still some "variable may be uninitialized" warnings that I haven't decided how to best fix yet. Our CI actions (above) have "extra warnings" actions which I now realize are misleading because they are each for a given compiler, and sometimes digging in the detailed actions you can find other warning. Will tweaks the name of those actions now. |
Any progress on the optimized line rendering by @ShironekoBen, which this PR depends on? |
Sorry about the slow response! So from my perspective the texture-based line rendering is pretty much complete - I've done various tests to check that rendering behaviour matches the old render path and they all seem fine. On the subject of how we merge them, I personally feel like initially we should probably set it up so that a given line is either all-textured or all-geometry - I'm nervous that we'll have lots of (quite literal!) edge-cases trying to mix the two, so in terms of getting things in and working it seems reasonable to get everything co-existing first and then worry about if/how we can combine them. If we initially just had a drawlist flag to choose which to render with that would let us do comparison tests and figure out a real metric or mechanism for choosing between them. Does that sound sensible? |
I'm going to work on resurrecting this PR, sorry to keep you waiting. |
It's not defined, would not mind defining it if worthy FYI some web stack only support Clamping not repeating.
That's done in the amend mentioned just below. Rebased branchCaught it with this a little today: https://github.com/ocornut/imgui/tree/features/potocpav-newer-lines-1 https://github.com/ocornut/imgui/tree/features/potocpav-newer-lines-2 In the function you can replace bool USE_NEW_CODE = true; by bool USE_NEW_CODE = ImGui::GetIO().KeyShift; To compare both. Bonus idea, you can use ImDrawList itself to visually debug some of it, e.g... ImDrawList* fg_draw_list = ImGui::GetForegroundDrawList();
if (this != fg_draw_list && bevel)
fg_draw_list->AddCircleFilled(_VtxWritePtr[bi].pos, 4, IM_COL32(0, 255, 0, 255)); Some thoughts
Unfortunately at this point I am unable to make any good conclusion or decision. Recaps of code pathsI'm going to now make a recap of the code paths we have, in the hope that writing everything down can help up see things clearer. Not sure where this is going! "N" here stands for EITHER "points_count" or "points_count - 1", ignoring the difference for simplification...
So we have 7 paths to pick from right now. All the paths in master don't yield correct-looking thickness for acute angles (including for thickness = 1.0f, even if it is less noticeable).
I meant to draw some conclusion from that recap but I'm too tired for it today but leaving this is, will resume later! |
Some news on that:
Main reasons blocking this:
For reference, here are the test cases from #4053 with this PR, it's good looking |
I would vote to merge this even with the small missing pixels, as the results are way better in the problematic cases. |
I'm picking this up on the New code is working by doing necessary leg work to replace mitter for acute angles into semi-bevel. I will throw for reference how standard bevel does look like.
Semi-bevel does look like a miter with a limit appied to it. Let's look at the implementation. Simplified version does look like this: void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, ImDrawFlags flags, float thickness)
{
const bool thick_line = (thickness > _FringeScale); // usualy _FringeScale == 1.0f
const bool antialias = (Flags & ImDrawListFlags_AntiAliasedLines) != 0;
if (antialias && !thick_line)
{
// Case 1 - thin anti-aliased lines
}
else
{
// Case 2 - thick anti-aliased and non anti-aliased lines
}
} Let's strart with general code path. Case 2 - thick anti-aliased and non anti-aliased linesCase 2 is a general code path taken for all thick lines. Thick line is anything that does have thickness greater than 1. This code path work by producing semi-bevel in place of the miter:
Code work by replacing majority of polyline miter joins with capped miter, producing bevel. That being said taking closer look at the code one can notice that bevel is applied to almost all joins: const bool bevel = (dx1 * dx2 + dy1 * dy2) > 1e-5f;
I'm not sure how intentional this behavior is. If bevel is intended to always be applied, join code can be significantly simplified. New code does improve in some cases but stil does not solve #3366 issue: Case 1 - thin anti-aliased linesCase 1 is taken for all anti-aliased lines with thickness equal 1 or less. This mean all frame borders drawn by ImGui does take this route. When this code path is in action missing pixels can be spotted as reported above. Taking closer look at the code it does turn out it perform basic segment extrusion and does not compute any jois of the polyline.
This does make corners with thickness equal to 1 produce corners like this:
This empty corner does hit exactly on the edge of the grid which make hardware rasterizer skip a pixel. Remedy to that is to mimic what Case 2 does and produce joins between polylines, like original algorithm did. Review sumaryCode does need work before before it can be considered to be merged:
|
Closer look at the issue of mesh bending from in #3366 with new code: Imgui.Polyline.Mesh.Bending.mp4 |
Polyline rendering is currently problematic. Antialiased and non-antialiased lines are handled separately, and each has its unique set of problems.
Antialiased line artifacts:
Non-antialiased line artifacts:
These problems are discussed in issue #2183, but without arriving at a single solution.
This PR contains code for non-antialiased rendering with correct line width and bevels around points. I will implement anti-aliased rendering as soon as there is consensus regarding the desired polyline appearance.
It results in this render:
I modified imgui_demo.cpp to better showcase the algorithm (see the images above). These changes should probably be reverted before merging.
The new algorithm does polyline triangulation like this:
This requires 3 verts and 3 faces for each segment. Previously, it was 4 verts and 2 faces. I did not do any rigorous benchmarks, but the code doesn't contain anything heavy.
The main question is: how to bevel the lines? With the algorithm as I implemented it, all angles are bevelled. This changes the appearance of rectangles, checkbox ticks, etc., as right angles were previously not bevelled. It may be better to bevel only acute angles, which complicates the algorithm a bit but should not be a big problem.
Should bevelling be performed only for acute angles, most segments may in practice consist of fewer segments and vertices (namely, 2 and 2). Is it possible to emit variable number of vertices to the draw buffers?
Edit: my use-case is graph plotting and currently, the graphs are a bit ugly. I tried rendering with miters (no bevel), but sometimes the miters are just too long.