Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Implement 2D vector graphics draw command #75278

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

SlugFiller
Copy link
Contributor

@SlugFiller SlugFiller commented Mar 24, 2023

Closes godotengine/godot-proposals#2924

Adds a draw_filled_curve method to CanvasItem, as well as a few methods used in its implementation. Supports cubic beziers, elliptical arcs, and, optionally, order-5 beziers (for easy C2 continuity).

Maintains smoothness at any level of zoom or curvature.

@SlugFiller SlugFiller requested review from a team as code owners March 24, 2023 00:51
@SlugFiller SlugFiller force-pushed the curve-2d-draw-fill branch 2 times, most recently from fe797db to 436a948 Compare March 24, 2023 01:41
@Calinou Calinou added this to the 4.x milestone Mar 24, 2023
@SlugFiller SlugFiller force-pushed the curve-2d-draw-fill branch 4 times, most recently from 3038558 to 1786430 Compare March 26, 2023 22:44
@fire
Copy link
Member

fire commented Mar 31, 2023

I support this as someone who assisted with adding thorvg vector graphics to godot engine.

Don't see anything obviously wrong, but didn't review.

@SlugFiller
Copy link
Contributor Author

The Bentley-Ottmann implementation here is still imperfect. Various mathematical invariants the algorithm depends on simply do not apply in floating point. I'm testing various ways to mitigate issues stemming from rounding errors. If I don't get a satisfactory result, I might test an implementation based on fixed point instead.

@fire
Copy link
Member

fire commented Mar 31, 2023

Remember godot has a 64.64 fixed point in the thirdparty misc folder

@SlugFiller SlugFiller force-pushed the curve-2d-draw-fill branch 2 times, most recently from f532c53 to 59c87fd Compare April 1, 2023 05:06
@SlugFiller
Copy link
Contributor Author

Okay, I achieved all that can be done using floating points. This implementation is guaranteed to execute in finite time with no assertion failures 100% of the time, and give a correct result 99.999% of the time. Short of using fixed point, and a whole bunch of tricks to handle every tiny edge case, this is the best that can be achieved.

@SlugFiller
Copy link
Contributor Author

Okay, I have a slight dilemma. After a lot of effort, I was able to make an implementation based on fixed point that's 100% fool-proof. But I'm hesitating to push it because it's noticeably slower. By an order of magnitude. There are a few points that could maybe be made faster. But it's not certain that the gap can be reasonably closed.

The question becomes: Is it better to have a solution that works 99% of the time and is fast, or should I make the effort to optimize the solution that works 100% of the time, but may not run as fast?

@clayjohn
Copy link
Member

The question becomes: Is it better to have a solution that works 99% of the time and is fast, or should I make the effort to optimize the solution that works 100% of the time, but may not run as fast?

It depends on the performance metrics. Anything merged into the engine needs to be fast enough to be used in games (ideally on all target platforms as well). It would be helpful to see a comparison (visual and performance) between this and the previous approaches to drawing curves to get a feel for the tradeoffs being made

@SlugFiller
Copy link
Contributor Author

@clayjohn
Here's the visual issue in the old version. It stems from failing to find an intersection with an edge of the form (x, y+a)-(x+b, y) where a,b>0 and b/a is too small to be represented as float:
vector_visualbug.webm

Missing the intersection causes it to get the shape wrong. A "wrong" shape will still use the same original points, but have different triangles filled. There may be other set-ups that cause similar issues due to floating points being rounded in some direction, but this is the one that I could create consistently.

Here's the same shape in the new version. The same visual bug can't be created:
vector_no_visualbug.webm

Now for a performance comparison. Performance heavily depends on the complexity of the objects, so I used a few scenes for testing.

Here's the old version:
vector_performance_old.webm

And here's the new one after the first round of optimizations:
vector_performance_new.webm

There's still more room to optimize, but the new version will never be as fast or faster than the old one. At best, it will reach the point of a small slowdown.

Of course, my desktop PC might not be the best representation for performance. And I also didn't jump through the hoops of trying to build other exports to see how much performance is harmed on web or mobile (in which the low memory footprint of vector should give the most advantage over raster).

For the time being, I uploaded the fixed point version to a new fork, so anyone can test both versions:
https://github.com/SlugFiller/godot/tree/curve-2d-draw-fill-fixed-point

The easiest way to test this PR without needing to mess with code is using my SVG import plugin:
https://github.com/SlugFiller/godot-vector2d

@SlugFiller
Copy link
Contributor Author

Also, I'm noticing there's been a trivial conflict introduced since I pushed this PR. Should I rebase? Now that I've put the new version in a separate branch, I can rebase the old version more easily.

@Giwayume
Copy link
Contributor

Giwayume commented Apr 23, 2023

I'd lean towards the one that performs better but has 99% accuracy, from a practical standpoint. You could add code to draw_filled_curve that detects when the numerical error scenario occurs, and offsets the numbers slightly before passing to the shader to avoid it. The performance drop looks really bad with the new version.

I have a question about p_types, how are you allowing it to define holes? Like using Z or M to separate subpaths in SVG. Or is that not coded at the moment?

@SlugFiller
Copy link
Contributor Author

SlugFiller commented Apr 23, 2023

I created a project to benchmark what I consider "typical usage" of vector graphics. It doesn't have tweening/skeleton, but the performance of those would be independent of the PR anyway. The important part is that it's game-like, and has a game-like object count and update rate:
CloverFever.zip

I've also made an improved version using fixed-point. It still has some areas that can be improved, but the optimization surface is dwindling. I clocked the various options on my (relatively powerful) machine, and got the following:

Bunny count Floating point Fixed point original Fixed point optimized
0 60 60 60
10 60 60 60
20 60 53 59
30 60 36 40
40 60 27 30
50 60 22 24
60 56 18 20
70 48 16 17
80 40 14 15
90 36 12 13
100 33 11 12
110 30 10 11
120 27 9 10
130 25 8 9
140 24 8 8
150 21 7 8
160 20 7 7
170 20 6 7
180 18 6 6
190 17 6 6
200 15 6 6
210 15 5 6
220 15 5 6
230 15 5 5
240 14 5 5
250 13 4 5
260 12 4 5
270 12 4 4
280 12 4 4
290 11 4 4
300 10 3 4

I'll try to do a similar benchmark in HTML5, to see how a "professional implementation" compares. Setting aside how much slower the fixed point solution is, the fact that none of the options are able to handle more than a few dozen bunnies before facing a frame drop, even on a desktop computer, is somewhat alarming.

@Giwayume p_types is just a bitfield for choosing between beziers and arcs. It's used for making perfect arcs, e.g. for drawing a circle, ellipse, or rectangle with rounded corners.

To make a multi-path (Z or M) simply put multiple vector arrays in p_points. It's an array of point arrays. Each point array is a single closed shape. So you make one point array for the contour, one for the hole, and then put both of them in a single array of point arrays.

Note that since this draws a filled curve, each shape is assumed to be closed, i.e. there's no notion of an "open shape". Every M is also a Z.

@Giwayume
Copy link
Contributor

the fact that none of the options are able to handle more than a few dozen bunnies before facing a frame drop, even on a desktop computer, is somewhat alarming

Hmm, I still think this effort is useful for things like path morphing, specifically because it can be mixed with other techniques that perform better for static assets.

I did have another question, any way you can think this could be combined with custom shaders, as opposed to just being able to render a raster texture? For example, COLOR would have the shape alpha already stored in its vec4 so you could use a vector path like a mask over drawing any effect you like. Without this, certain things like drawing radial gradients with sharp stops could look bad with a low res raster texture, whereas it could be implemented perfectly in shader code.

@SlugFiller
Copy link
Contributor Author

@Giwayume There's no issue putting a shader on a canvas item that uses draw_filled_curve. The actual drawing is done like any other 2D mesh. A fragment shader can be used to apply color effects to any pixel inside the shape. And anti-aliasing (e.g. MSAA) can be applied via global/viewport settings as well.

If you want to use it as a mask, setting "clip children" on the containing canvas item would do the trick.

You can look at the SVG import plugin I linked above, and import an SVG with gradients, to see how linear and radial gradients are implemented via a shader.

@SlugFiller SlugFiller force-pushed the curve-2d-draw-fill branch from 6344550 to 657c3ae Compare May 13, 2023 22:40
@SlugFiller
Copy link
Contributor Author

I finally managed to create a fixed point (well, big rational, technically) solution that outperforms the floating point one. It cleverly uses the added accuracy to reduce the edge cases, and cleanly resolve the remaining ones.

To simply the math and prevent overflow, the fixed point numbers used are 20 bits. The floating points are normalized to the largest exponent, in the canvas item's local space. So, while rounding errors may occur if the vector is far from the origin of the canvas item (the canvas item can be as far from the origin of the scene/camera as desired, however), from largest detail to smallest, it gives roughly the accuracy of a sprite that's about one million by one million pixels. Put differently, a full-screen sprite at 4k can be magnified 25,000% without being even a single pixel off.

I've also tested the performance of equivalent drawing using HTML5 canvas on Brave. While the canvas version is not perfectly minimal, I doubt sub-optimal JS is any more of a bottleneck than GDScript. The Brave version also ran at slightly lower, but roughly similar resolution, although it's impossible to tell if Brave subdivides to the same accuracy. Nevertheless, my version managed to outperform the browser, meaning it's as fast as would be standard for this sort of graphics.

Here are the final benchmark results:

Bunny count Floating point Brave Final
0 60 60 60
10 60 60 60
20 60 60 60
30 60 60 60
40 60 58 60
50 60 45 60
60 56 40 60
70 48 35 54
80 40 30 46
90 36 28 42
100 33 25 38
110 30 23 33
120 27 21 31
130 25 19 29
140 24 17 26
150 21 16 24
160 20 15 23
170 20 14 22
180 18 14 20
190 17 13 19
200 15 13 18
210 15 11 17
220 15 11 16
230 15 11 16
240 14 10 15
250 13 10 15
260 12 9 13
270 12 9 13
280 12 9 13
290 11 8 12
300 10 8 12

Since the biggest bottleneck is CPU, and the algorithm is running on a single core, performance can be increased by using a worker pool to divide the work to multiple cores. However, this would potentially take away resources from other services (physics, navigation), so I'm leaving this as a possible future improvement.

At any rate, this PR is now merge-ready. It has reached perfect correctness without sacrificing performance.

@SlugFiller
Copy link
Contributor Author

Update to anyone following:
It was discussed in a meeting that since the only part of core this PR touches is a hook to a step of the rendering process, it may be better to just have a generic hook that could be used by addons/extensions. However, a proper design and proposal for the API of such a hook is necessary.

Input on this would be welcome.


static void tessellate_cubic_bezier_in_rect(Vector<Vector2> &r_out, Vector2 p_start, Vector2 p_control1, Vector2 p_control2, Vector2 p_end, Vector2 p_start_transformed, Vector2 p_control1_transformed, Vector2 p_control2_transformed, Vector2 p_end_transformed, const Rect2 &p_limit) {
while (true) {
// Stop condition - Completely out of bounds
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Stop condition - Completely out of bounds
// Stop condition - Completely out of bounds.

As per the comment style, same elsewhere (including code you've just copied)

@SlugFiller SlugFiller force-pushed the curve-2d-draw-fill branch from fe64877 to 2a7d7bd Compare July 28, 2023 03:20
@SlugFiller
Copy link
Contributor Author

Fixed comment formatting

@tobiasBora
Copy link

tobiasBora commented Jun 14, 2024

Any update on this? Given the great, hard work that has been put on this, it would be a shame not to integrate it. And it seems to be a necessary step to solve godotengine/godot-proposals#9846

@SlugFiller
Copy link
Contributor Author

It's been rejected in a rendering meeting a few months ago. Here are a few reasons:

  • It's CPU-bound, which matches current browser capabilities, but doesn't match modern GPU-based techniques. Notably, it's only capable of rendering a handful of paths before performance drops. Compare with Pathfinder and Vello which use shader-based sorting and sum-of-prefixes, or Rive which uses stencil/PLS for winding count.
  • Godot does not currently support vector graphics first-party. Adding such support would create performance and long-term maintenance expectations among users who would expect it to work on-par with a dedicated vector graphics game engine.
  • It could be possible to implement this as a plugin if hooks are added to the 2D renderer, and/or if drawable textures are used for allowing generic shader math, and/or if stencil operations or PLS are made available for 2D meshes. A combination of the above could make for a much more performant solution.

So, there are currently no plans to merge this, and alternative features are mostly in long-term planning status.

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement real-time vector graphics rendering (e.g. SVG, Rive)
8 participants