Skip to content
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

Fix CanvasItem.draw_rect function with filled = false #41239

Merged
merged 1 commit into from
Jan 19, 2023

Conversation

dalexeev
Copy link
Member

@dalexeev dalexeev commented Aug 14, 2020

Closes #35384.


Special thanks to @clayjohn and @kleonc for the explanations.

@aaronfranke
Copy link
Member

@dalexeev Is this still desired? If so, it needs to be rebased and tested on the latest master branch. While there are no conflicts, rebasing is important so that reviewers can easily test the PR.

@dalexeev
Copy link
Member Author

REBASED AND READY FOR REVIEW

Copy link
Member

@aaronfranke aaronfranke left a comment

Choose a reason for hiding this comment

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

Using the code from #35384 on this PR, if I have the base window size set to be small and let Godot stretch the Window to make things more visible, it looks correct:

1

However, if I scale up the Node2D instead of stretching the window, it doesn't look quite right:

2

I'm not super familiar with this function so I'm not sure if it's supposed to draw "one pixel thick" on the screen or relative to the Node2D, but this output is definitely not either.

Also, in addition to the blue rectangle not looking correct, the green one is drawn using filled set to false, but a size of one, but it looks like it's filled. If I change it to a size of not 1 it looks similarly broken to how the blue one is broken.

scene/main/canvas_item.cpp Outdated Show resolved Hide resolved
scene/main/canvas_item.cpp Outdated Show resolved Hide resolved
scene/main/canvas_item.cpp Outdated Show resolved Hide resolved
scene/main/canvas_item.cpp Outdated Show resolved Hide resolved
@dalexeev
Copy link
Member Author

dalexeev commented Aug 22, 2021

Thanks for the review. It looks like the bug in the canvas_item_add_line function, it does not take into account scaling for 1 pixel lines, but does take into account other transformations (for example rotation).

Green and yellow lines are 1 pixel thick, blue and pink lines are 2 pixel thick. Node2D is scaled.

If I set the border thickness to 2 pixels, then the rectangle is drawn correctly:

I'll take a look at the canvas_item_add_line function, but I'm not sure if I can fix this error.

scene/main/canvas_item.cpp Outdated Show resolved Hide resolved
@dalexeev
Copy link
Member Author

Also I would like to know which option is better.

@dalexeev
Copy link
Member Author

dalexeev commented Oct 4, 2022

I fixed the bug, but the solution is quite hacky. The main drawback is p_width rounding.

Also, there is a hack to check the width is 1 due to #51978:

if (p_width > 1.001) {
begin_left = p_from + t;
begin_right = p_from - t;
end_left = p_to + t;
end_right = p_to - t;
line->points[0] = begin_left;
line->points[1] = begin_right;
line->points[2] = end_right;
line->points[3] = end_left;
line->point_count = 4;
} else {
begin_left = p_from;
begin_right = p_from;
end_left = p_to;
end_right = p_to;
line->points[0] = p_from;
line->points[1] = p_to;
line->point_count = 2;
}

In general, with integer scaling factors, everything is displayed correctly. For non-integer scales, lines can be drawn with different widths due to inaccurate mapping to the pixel grid. But I'm not sure if this problem can be solved at all.

With the current behavior, errors occur even at integer scales and linewidths, especially if the color is translucent.

@MewPurPur
Copy link
Contributor

Does this fix #58775?

@akien-mga
Copy link
Member

Or maybe not and it needs to be rebased on top of #69851?

@akien-mga akien-mga reopened this Jan 16, 2023
@dalexeev
Copy link
Member Author

There are a couple of things in this PR that are not in #69851. I will rebase it soon.

@dalexeev
Copy link
Member Author

Essentially this PR is now cosmetic, the main bug was fixed in #69851. For negative widths, #58775 remains, but there is now a note about it in the docs. @kleonc, is there anything else we can do to reduce the chance of missing corners?

@dalexeev dalexeev requested review from kleonc and removed request for reduz, clayjohn and lawnjelly January 16, 2023 13:27
@kleonc
Copy link
Member

kleonc commented Jan 16, 2023

Essentially this PR is now cosmetic, the main bug was fixed in #69851. For negative widths, #58775 remains, but there is now a note about it in the docs. @kleonc, is there anything else we can do to reduce the chance of missing corners?

I think #58775 is not really solvable in a general case. It's not just a matter of obtaining proper final positions of the line endpoints in the coordinate space of the viewport/framebuffer/render target (or whatever you call it) which would be needed to be able to apply a proper offset in that space (small final offset; regardless of the zoom etc.). What we'd also need to know to determine what such offset should be is the exact algorithm used by the Vulkan/OpenGL/etc. for rasterizing the line primitives. And that's problematic, because these can differ between implementations since specifications aren't super strict:

So even assuming that rasterization is done according to the diamond exit rule (which is not guaranteed; but maybe it's actually the most common case?:thinking: no idea about that) we would still need to find a way to somehow obtain the final positions of the line endpoints within the fragments' coordinate space (I don't see how we could currently do it). And then we would still need to find a way of how to offset the vertices (probably branching into many different cases) to always produce satisfying fragments. E.g. take a look at the image from this post and imagine all possible configurations of the vertices (colored dots); and yeah, the rect can end up being transformed in there (so take into consideration rotations or... skewing; yeah, good luck with that 😄).

TL;DR: I don't see us solving it. Would love to be surprised by someone but I don't think it will happen.

For a non-general solution / hacky solution what maybe could be done is adding a method (are a bool param to the current draw_rect) which would be supposed to work properly only in a very specific conditions: when the given CanvasItem is not transformed so the offset we would apply to the vertices would actually be equivalent to the offset in the fragments' space. Still it wouldn't be guaranteed to always work because of the loose specifications mentioned previously. Anyway, I'm not convinced that trying to handle such edge case is a good idea.


Regarding #35384 I guess it's solved by combination of #69851 + this PR + godotengine/godot-docs#6455?

Copy link
Member

@kleonc kleonc left a comment

Choose a reason for hiding this comment

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

There's a question whether we should handle rects with negative size properly? Currently (AFAICT, haven't tested) only filled ones would be drawn correctly (I mean that rect with negative size would be drawn the same as rect.abs()). Shouldn't be hard to handle them too.

@dalexeev
Copy link
Member Author

There's a question whether we should handle rects with negative size properly?

Fixed. Rect2 rect = p_rect.abs(); is OK?

@kleonc
Copy link
Member

kleonc commented Jan 17, 2023

Fixed. Rect2 rect = p_rect.abs(); is OK?

I guess it should be fine, that's a quite cheap call. Potentially could be done just for non-filled rects though.


Also does changing how we draw the border for width >= 0.0 fixes some specific case (after #69851)?:thinking:

(left=before; right=this PR)
a

I wonder if there is any specific reason to prefer one over the other.

@dalexeev
Copy link
Member Author

I wonder if there is any specific reason to prefer one over the other.

I'm not sure since this PR started quite a while ago and these changes have carried over through all the rebases. I will remove them if they are unnecessary.

@kleonc
Copy link
Member

kleonc commented Jan 17, 2023

I'm not sure since this PR started quite a while ago and these changes have carried over through all the rebases. I will remove them if they are unnecessary.

To me they do seem like not changing anything (so leaving the changes should be fine too).

Copy link
Member

@kleonc kleonc left a comment

Choose a reason for hiding this comment

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

LGTM. Would welcome @clayjohn's opinion on #41239 (comment) before someone will merge this.

@kleonc kleonc requested a review from clayjohn January 17, 2023 16:24
Comment on lines +575 to 602
// Top line.
RenderingServer::get_singleton()->canvas_item_add_line(
canvas_item,
p_rect.position + Size2(-offset, 0),
p_rect.position + Size2(p_rect.size.width + offset, 0),
rect.position + Size2(-offset, 0),
rect.position + Size2(-offset + rect.size.width, 0),
p_color,
p_width);
// Right line.
RenderingServer::get_singleton()->canvas_item_add_line(
canvas_item,
p_rect.position + Size2(p_rect.size.width, offset),
p_rect.position + Size2(p_rect.size.width, p_rect.size.height - offset),
rect.position + Size2(rect.size.width, -offset),
rect.position + Size2(rect.size.width, -offset + rect.size.height),
p_color,
p_width);
// Bottom line.
RenderingServer::get_singleton()->canvas_item_add_line(
canvas_item,
p_rect.position + Size2(p_rect.size.width + offset, p_rect.size.height),
p_rect.position + Size2(-offset, p_rect.size.height),
rect.position + Size2(offset + rect.size.width, rect.size.height),
rect.position + Size2(offset, rect.size.height),
p_color,
p_width);
// Left line.
RenderingServer::get_singleton()->canvas_item_add_line(
canvas_item,
p_rect.position + Size2(0, p_rect.size.height - offset),
p_rect.position + Size2(0, offset),
rect.position + Size2(0, offset + rect.size.height),
rect.position + Size2(0, offset),
p_color,
p_width);
Copy link
Member Author

@dalexeev dalexeev Jan 19, 2023

Choose a reason for hiding this comment

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

Should draw_lines be replaced with canvas_item_add_polyline after #71679 is merged? Like here:

Vector<Vector2> stroke_points;
stroke_points.resize(5);
stroke_points.write[0] = -size * 0.5;
stroke_points.write[1] = Vector2(size.x, -size.y) * 0.5;
stroke_points.write[2] = size * 0.5;
stroke_points.write[3] = Vector2(-size.x, size.y) * 0.5;
stroke_points.write[4] = -size * 0.5;
Vector<Color> stroke_colors;
stroke_colors.resize(5);
for (int i = 0; i < 5; i++) {
stroke_colors.write[i] = (p_color);
}
RenderingServer::get_singleton()->canvas_item_add_polyline(p_to_rid, stroke_points, stroke_colors);

(draw_rect doesn't have antialiased parameter anyway.)

Copy link
Member

@clayjohn clayjohn Jan 19, 2023

Choose a reason for hiding this comment

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

I guess so. A polyline should be slightly faster than having multiple regular lines. But that should be done in a later PR and needs be profiled before changing

@clayjohn
Copy link
Member

LGTM. Would welcome @clayjohn's opinion on #41239 (comment) before someone will merge this.

I don't think there is a good reason to prefer one behaviour over the other.

@akien-mga akien-mga merged commit 360b610 into godotengine:master Jan 19, 2023
@akien-mga
Copy link
Member

Thanks!

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

Successfully merging this pull request may close these issues.

draw_rect is drawn at wrong position. Also size and shape is wrong if filled parapeter is false.
8 participants