GPv3: Segment selection support for all relevant selection operators #109221
No reviewers
Labels
No Label
Interest
Alembic
Interest
Animation & Rigging
Interest
Asset Browser
Interest
Asset Browser Project Overview
Interest
Audio
Interest
Automated Testing
Interest
Blender Asset Bundle
Interest
BlendFile
Interest
Collada
Interest
Compatibility
Interest
Compositing
Interest
Core
Interest
Cycles
Interest
Dependency Graph
Interest
Development Management
Interest
EEVEE
Interest
EEVEE & Viewport
Interest
Freestyle
Interest
Geometry Nodes
Interest
Grease Pencil
Interest
ID Management
Interest
Images & Movies
Interest
Import Export
Interest
Line Art
Interest
Masking
Interest
Metal
Interest
Modeling
Interest
Modifiers
Interest
Motion Tracking
Interest
Nodes & Physics
Interest
OpenGL
Interest
Overlay
Interest
Overrides
Interest
Performance
Interest
Physics
Interest
Pipeline, Assets & IO
Interest
Platforms, Builds & Tests
Interest
Python API
Interest
Render & Cycles
Interest
Render Pipeline
Interest
Sculpt, Paint & Texture
Interest
Text Editor
Interest
Translations
Interest
Triaging
Interest
Undo
Interest
USD
Interest
User Interface
Interest
UV Editing
Interest
VFX & Video
Interest
Video Sequencer
Interest
Virtual Reality
Interest
Vulkan
Interest
Wayland
Interest
Workbench
Interest: X11
Legacy
Blender 2.8 Project
Legacy
Milestone 1: Basic, Local Asset Browser
Legacy
OpenGL Error
Meta
Good First Issue
Meta
Papercut
Meta
Retrospective
Meta
Security
Module
Animation & Rigging
Module
Core
Module
Development Management
Module
EEVEE & Viewport
Module
Grease Pencil
Module
Modeling
Module
Nodes & Physics
Module
Pipeline, Assets & IO
Module
Platforms, Builds & Tests
Module
Python API
Module
Render & Cycles
Module
Sculpt, Paint & Texture
Module
Triaging
Module
User Interface
Module
VFX & Video
Platform
FreeBSD
Platform
Linux
Platform
macOS
Platform
Windows
Priority
High
Priority
Low
Priority
Normal
Priority
Unbreak Now!
Status
Archived
Status
Confirmed
Status
Duplicate
Status
Needs Info from Developers
Status
Needs Information from User
Status
Needs Triage
Status
Resolved
Type
Bug
Type
Design
Type
Known Issue
Type
Patch
Type
Report
Type
To Do
No Milestone
No project
No Assignees
4 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: blender/blender#109221
Loading…
Reference in New Issue
No description provided.
Delete Branch "SietseB/blender:gp3-select-segments"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This PR implements the 'Segment' mode for all relevant Grease Pencil
selection operators. Segments are the points in a stroke between
other, intersecting strokes.
Selecting and deselecting segments is supported by the following
operators:
No action was needed for 'Select linked'. It works the same for 'Point'
and 'Segment' mode.
Technical notes
Since segment is not a natively supported selection domain, segments
are handled in the point domain, with the following pre- and post-
processing steps for each selection operator.
Pre-processing:
vector
and stored. This 2D representation is used to detect stroke
intersections.
After this the selection operator alters the point selection.
Post-processing:
selection. This 'diff' tells us which points are selected or deselected.
points backward and forward until an intersection with an other stroke
is detected (or the end of the stroke is reached).
Where possible, parallel execution of the computations is used. The old
GPv2 was lacking simple bounding box checks when searching for stroke
intersections. So with this new implementation, in large scenes a
noticeable performance improvement is to be expected.
First pass, did not look at the segment selection algorithm yet.
@ -189,3 +216,3 @@
ot->exec = select_random_exec;
ot->poll = editable_grease_pencil_poll;
ot->poll = editable_grease_pencil_no_segment_selection_poll;
Not sure if this is necessary. IMO selecting random segments makes sense.
@ -34,2 +39,3 @@
* \param segment_mode: Pointer to bool, indicating 'select segment' mode is active.
*/
eAttrDomain ED_grease_pencil_selection_domain_get(struct bContext *C);
eAttrDomain ED_grease_pencil_selection_domain_get(struct bContext *C, bool *segment_mode);
I prefer if this would be it's own function. E.g.
bool ED_grease_pencil_segment_selection_mode(struct bContext *C);
I had a brainwave and added a Select Random method for segments with relative ease.
GPv3: Segment selection support for all relevant selection operatorsto WIP: GPv3: Segment selection support for all relevant selection operatorsThanks for the contribution!
I left some significant comments inline, mostly about how the algorithm is structured and how data is laid out in memory. But I also think some larger refactoring would be helpful here, since this is starting to muddy the distinction in abstraction levels (the high-level GP operators calling lower level curves functions). That distinction isn't working so well anymore.
In chat I mentioned a concern that this is quadratic. In theory it is, since it does things like "for all curves: for all other curves". But you're right in the patch description that the bounding box checks should help a lot at least. In the future an acceleration structure like a KD tree might help though, if that becomes a problem.
@ -250,0 +326,4 @@
Vector<bool> point_selection_get(const GreasePencilDrawing *drawing)
{
/* Get point selection in the drawing. */
bke::CurvesGeometry curves = drawing->geometry.wrap();
This is making a copy of the curves, like above. Best to use a const reference. Then you can't call
ensure_selection_attribute
, but that's on purpose. A function like this shouldn't be able to modify the curves.We have to ensure then that is selection domain is always correct. That's something for a separate PR, I guess. I made a task for it. @filedescriptor
@ -250,0 +332,4 @@
MutableSpan<bool> selection_typed = selection.span.typed<bool>();
/* Copy selection. */
Vector<bool> point_selection(selection.span.size());
Vector
->Array
, since the size doesn't need to change.@ -0,0 +23,4 @@
{
/* Count total number of editable strokes in grease pencil object. */
int stroke_count = 0;
grease_pencil->foreach_editable_drawing(
I'd recommend running
foreach_editable_drawing
just once, at the start of the algorithm, and keeping aVector
of drawings. Then iteration can be simpler after that, and code can be parallelized over all drawing.But actually, even better might be to handle each drawing separately, so:
That way memory access is more localized (i.e. only dealing with a single drawing at a time means CPU caches aren't being repeatedly clobbered by loading the data from a new drawing), and each piece of code only has to deal with a single
CurvesGeometry
I'll handle the first one.
Unfortunately, the second one isn't going to work. A drawing is just a layer, intersections can come from other layers to. So the 'project to 2d' can't be inside the drawing loop, then we're missing information.
Ah, I didn't realize this was meant to handle intersections with other drawings too
@ -0,0 +43,4 @@
grease_pencil->foreach_editable_drawing(
vc->scene->r.cfra, [&](int drawing_index, GreasePencilDrawing &drawing) {
/* Get deformed geomtry. */
const bke::CurvesGeometry curves = drawing.geometry.wrap();
It's critical not to forget the
&
here when declaring aCurvesGeometry
variable. Currently this copies the struct (though implicit sharing will keep this from copying all the attribute layers).@ -0,0 +50,4 @@
ob_eval, *vc->obedit, drawing_index);
/* Loop all strokes (curves). */
threading::parallel_for(curves.curves_range(), 256, [&](const IndexRange range) {
For a single
CurvesGeometry
, I'd suggest structuring it like this:Not storing a
Vector
for every curve is important, since it means all the positions are in one contiguous array. This makes it faster to access, and avoids the 56 bytes of overhead for each Vector (pointers and inline buffer). Doing the projection in one loop just makes it simpler, more parallel, and easier to optimize (with SIMD, etc.) in the future.Will do, thanks for the insights!
@ -32,2 +36,3 @@
/**
* Get the selection mode for Grease Pencil selection operators: point, stroke, segment.
* Get the selection domain for Grease Pencil selection operators.
* \param C: Context.
Avoid this sort of meaningless comment. "Context" is obvious from the name of the variable's type, so this just takes up space.
@ -1197,0 +1205,4 @@
/* In segment mode, store the pre-change point selection. */
Vector<bool> selection_before;
if (segment_mode) {
selection_before = ed::greasepencil::point_selection_get(&drawing);
Combining these two levels of abstraction is a bit troublesome. One the one hand, currently these grease pencil operators just call the
curves
functions, which are meant to do all of the real work. The GP operator doesn't have anything to do with the actual selection values (i.e.Vector<bool>
). Combining the two is confusing, because it means this code is more "tacked on at the end" than it should be.I wonder if maybe
select_lasso
and friends should return anArray<bool>
orIndexMask
of the points to change. Then those points could be adjusted later, at the same level.Maybe @filedescriptor has a different opinion about this though.
I do think that the better solution is to make the selection operators return not just a bool, but what they changed. I can also see this being useful for Curves in the future.
Yes, that would be great, when the operators return what they changed. Then I don't have to hack into these functions, which I don't like either.
Modifying all the
curves
functions to support that though – it's quite a task, I'm not confident that I can pull that off myself...Just had a chat with Hans about this.
What might make the most sense is refactor the selection code to do the following instead:
IndexMask
of the elements that are affected by the particular selection. E.g.IndexMask mask = elements_inside_lasso(...)
.ed::curves::select_lasso
would callpoints_inside_lasso
then useindex_mask::masked_fill(...)
to filltrue
orfalse
based on the selection operation (deselect/select/invert) using the index mask.This way, we wouldn't use
ed::curves::select_lasso
but theIndexMask
instead to do the selection in grease pencil.And that solves the problem we have with the segment selection now.
I'm afraid I have to reopen this discussion. A problem with the
IndexMask
solution is that it returns affected points, but for an efficient segment expansion you only want to inspect the selection points that really changed. For circle select for example, it won't be performant at all when the segments under the 'brush' are recalculated all the time.So I could convert the
IndexMask
to an array of changed selection points. But then it's getting pretty bloated, IMHO. To be honest, the leanest code I can think of is what it is now in the PR: 1. take a snapshot of the selection, 2. apply the selection function, 3. expand the changes to segments.I think the underlying problem of this 'level trouble' is that there isn't a 'segment' selection domain (and there never will be). That's why we need the post-processing step.
Why does a segment have such complex implicit behavior?
More precisely, if it must have such a complex behavior, why should it create complexity for other behaviors?
First, if want to end up with selected curve segments, then make segments.
I mean, you can define curve intersections before the start operator?
Based on the intersections, construct the vectors of offsets for each segment. And after that, make selection simular both for selection curves, or such segments (only based on offsets and positions), and index mas as result.
Interesting idea, but finding intersections is the heavy part of the operation. So finding all intersections before the select operator starts, would be horribly slow, I'm afraid.
If you already in
blender::ed::greasepencil::
name space, you may not added this in any places.@ -0,0 +95,4 @@
/* Loop all strokes, looking for an intersecting segment. */
std::atomic<bool> intersect = false;
threading::parallel_for(strokes_2d.index_range(), 256, [&](const IndexRange range) {
Take a look at
parallel_reduce
. It's used for a similar purpose, and removes the need for an atomic.has_anything_selected
also returns a boolean, so you can copy the structure with that. It's not really that different in the end, but at least it tells the reader that a reduction is happening (many computations giving one result).@HooglyBoogly Thanks for the review! I learned a lot from it already. I replied to some of your remarks – the others I will just implement.
I know the algorithm is rather poor and scales badly. I did think about using a KD tree. The thing is, finding intersections is not just about proximity of other points. You can have a line of two points across the entire viewport, the two points far away from the vector you're inspecting and still that line will intersect with the vector.
With the GP primitives like line and rectangle with low subdivision in mind, that's not a purely hypothetical problem.
So, to avoid that this task would culminate in a mathematical quest, I took the poor man's option.
@SietseB I'll work on a PR to do the refactor mentioned here: #109221 (comment)
Maybe since you're on holiday anyway, this PR can be on hold in the meantime :)
EDIT: I now did this for one of the selection functions as an example of how that would work. See #109293
Sounds like a good plan to me. And even better: you are already on it! ;-)
WIP: GPv3: Segment selection support for all relevant selection operatorsto GPv3: Segment selection support for all relevant selection operatorsIn my new commit I've:
@ -418,0 +528,4 @@
}
/* Copy selection. */
const VArray<bool> selection = *attributes.lookup(".selection").typed<bool>();
Would be simpler to look up the selection with a default of true like is done elsewhere, and use the virtual array's
materialize
function to fill the array.@filedescriptor @HooglyBoogly
To revive this PR, I'll summarize the discussion:
select_lasso
and friends to return anIndexMask
of affected points and apply the change in selection after obtaining the mask.OB_CURVES
andOB_GREASE_PENCIL
. To avoid code duplication, it makes sense to put the apply-selection in a shared function, which handles all the selection operations (set, add, sub, and, xor etc.).Which brings us to the question: does that solve the main objection? TBH, I don't see that much of a difference. And since rewriting the selection functions to
IndexMask
ones, isn't a task done in 10 minutes, I prefer to be sure in advance that we are happy with the outcome of such rewrite.Checkout
From your project repository, check out a new branch and test the changes.