GPv3: Cutter tool #113953

Merged
Falk David merged 28 commits from SietseB/blender:gpv3-cutter-tool into main 2024-04-22 14:05:33 +02:00
Member

This PR implements the Cutter Tool for GPv3. The Cutter tool deletes
points in between intersecting strokes. New points are created at the
exact intersection points, so as a result the cutted strokes will fit
perfectly.

For feature parity, the tool follows the GPv2 behavior:

  • The cutter tool works on all editable layers.
  • Intersections are only detected for curves on the same layer,
    so intersection of curves on seperate layers are not handled.

Technical notes
The implementation uses the compute_topology_change function
created for the Hard Eraser. So at intersection points, point
attributes will be interpolated.

To do:

  • Paint mode needs a fix for non-brush tools. At the moment the
    grease_pencil_stroke_cutter_exec isn't called when you select
    the Cutter tool in the toolbar.
This PR implements the Cutter Tool for GPv3. The Cutter tool deletes points in between intersecting strokes. New points are created at the exact intersection points, so as a result the cutted strokes will fit perfectly. For feature parity, the tool follows the GPv2 behavior: - The cutter tool works on all editable layers. - Intersections are only detected for curves on the _same_ layer, so intersection of curves on _seperate_ layers are not handled. Technical notes The implementation uses the `compute_topology_change` function created for the Hard Eraser. So at intersection points, point attributes will be interpolated. To do: - [x] Paint mode needs a fix for non-brush tools. At the moment the `grease_pencil_stroke_cutter_exec` isn't called when you select the Cutter tool in the toolbar.
Sietse Brouwer added 1 commit 2023-10-19 22:36:52 +02:00
Sietse Brouwer added this to the Grease Pencil project 2023-10-19 22:42:24 +02:00
Sietse Brouwer added 2 commits 2023-10-20 16:25:30 +02:00
Sietse Brouwer added 1 commit 2023-10-25 15:13:40 +02:00
Sietse Brouwer added 1 commit 2023-10-25 16:21:35 +02:00
Sietse Brouwer added 4 commits 2024-01-25 22:52:42 +01:00
Sietse Brouwer changed title from WIP: GPv3: Cutter tool to GPv3: Cutter tool 2024-01-25 22:58:39 +01:00
Sietse Brouwer requested review from Falk David 2024-01-25 23:00:16 +01:00
Member

@blender-bot build

@blender-bot build
Falk David requested changes 2024-01-26 15:54:36 +01:00
Dismissed
Falk David left a comment
Member

First pass, this should fix the toolbar issue.

Some higher-level remarks:

  • The cutter implementation should be moved out of EraseOperationExecutor. It's not a painting operator, so I'd like to avoid mixing it in. I would suggest to put it in editors/grease_pencil/ into it's own file (maybe just grease_pencil_cutter.cc).
  • It seems like the only dependency is compute_topology_change (correct me if I'm wrong). So I would refactor that out and also put it in the ed::greasepencil namespace. grease_pencil_utils.cc seems like a good candidate.
First pass, this should fix the toolbar issue. Some higher-level remarks: * The cutter implementation should be moved out of `EraseOperationExecutor`. It's not a painting operator, so I'd like to avoid mixing it in. I would suggest to put it in `editors/grease_pencil/` into it's own file (maybe just `grease_pencil_cutter.cc`). * It seems like the only dependency is `compute_topology_change` (correct me if I'm wrong). So I would refactor that out and also put it in the `ed::greasepencil` namespace. `grease_pencil_utils.cc` seems like a good candidate.
@ -1793,0 +1804,4 @@
label="Cutter",
icon="ops.gpencil.stroke_cutter",
cursor='KNIFE',
widget=None,
Member

widget=None, can be removed

`widget=None,` can be removed
SietseB marked this conversation as resolved
@ -254,0 +262,4 @@
Vector<MutableDrawingInfo> editable_drawings;
if (!grease_pencil.has_active_layer()) {
return editable_drawings.as_span();
Member

return {};

`return {};`
SietseB marked this conversation as resolved
@ -254,0 +267,4 @@
const Layer &layer = *grease_pencil.get_active_layer();
const std::optional<int> layer_index = grease_pencil.get_layer_index(layer);
if (!layer.is_editable() || !layer_index.has_value()) {
return editable_drawings.as_span();
Member

return {};

`return {};`
SietseB marked this conversation as resolved
@ -305,0 +313,4 @@
ot->invoke = WM_gesture_lasso_invoke;
ot->modal = WM_gesture_lasso_modal;
ot->exec = greasepencil::grease_pencil_stroke_cutter_exec;
ot->poll = ed::greasepencil::editable_grease_pencil_poll;
Member

This needs to be grease_pencil_painting_poll.

This needs to be `grease_pencil_painting_poll`.
SietseB marked this conversation as resolved
Member

@HooglyBoogly I suspect that compute_topology_change could be refactored. It sort of does a gather_attributes, but some values in the destination are interpolated between two sources. Also handles changes to the cyclic attribute and the caps.

@HooglyBoogly I suspect that `compute_topology_change` could be refactored. It sort of does a `gather_attributes`, but some values in the destination are interpolated between two sources. Also handles changes to the `cyclic` attribute and the `caps`.
Sietse Brouwer added 2 commits 2024-01-27 12:24:57 +01:00
Author
Member

@filedescriptor Thanks a lot for fixing that tool-system mystery. It's working now.
I moved the operator into its own file in editors/grease_pencil/.

@filedescriptor Thanks a lot for fixing that tool-system mystery. It's working now. I moved the operator into its own file in `editors/grease_pencil/`.
Falk David requested changes 2024-02-01 13:40:55 +01:00
Dismissed
Falk David left a comment
Member

Another pass

Another pass
@ -0,0 +549,4 @@
/* Lambda function for executing the cutter on each drawing. */
const auto execute_cutter_on_drawing =
[&](const int layer_index, const int frame_number, Drawing &drawing) {
Member

This lambda is getting long! I would consider putting the code into a static function.

This lambda is getting long! I would consider putting the code into a static function.
SietseB marked this conversation as resolved
@ -0,0 +570,4 @@
/* Compute bounding boxes of curves in screen space. The bounding boxes are used to speed
* up the search for intersecting curves. */
Array<rcti> screen_space_bbox(src.curves_num());
Member

This should be using Bounds<float2> or Bounds<int2>.

This should be using `Bounds<float2>` or `Bounds<int2>`.
Author
Member

I surely want to, but it's a bit difficult at the moment. There are no alternatives for BLI_rcti_isect and BLI_rcti_isect_pt_v yet in the Bounds type, as I understood it.

I surely want to, but it's a bit difficult at the moment. There are no alternatives for `BLI_rcti_isect` and `BLI_rcti_isect_pt_v` yet in the `Bounds` type, as I understood it.
Member

Ah dang it, I thought that was merged! Ok no problem then.

Ah dang it, I thought that was merged! Ok no problem then.
SietseB marked this conversation as resolved
@ -0,0 +589,4 @@
/* Apply cutter. */
bke::CurvesGeometry dst;
const bool cutted = stroke_cutter_find_and_remove_segments(
Member

Return an std::optional<bke::CurvesGeometry> cut_strokes.

Return an `std::optional<bke::CurvesGeometry> cut_strokes`.
SietseB marked this conversation as resolved
@ -0,0 +592,4 @@
const bool cutted = stroke_cutter_find_and_remove_segments(
src, dst, mcoords, mcoords_len, screen_space_positions, screen_space_bbox, keep_caps);
if (cutted) {
Member

if (cut_strokes.has_value())

`if (cut_strokes.has_value())`
SietseB marked this conversation as resolved
@ -0,0 +596,4 @@
/* Set the new geometry. */
drawing.geometry.wrap() = std::move(dst);
drawing.tag_topology_changed();
changed = true;
Member

Since we're running this in parallel, this should be using std::atomic<bool> changed

Since we're running this in parallel, this should be using `std::atomic<bool> changed`
SietseB marked this conversation as resolved
@ -327,6 +352,7 @@ void ED_operatortypes_grease_pencil_draw()
using namespace blender::ed::sculpt_paint;
WM_operatortype_append(GREASE_PENCIL_OT_brush_stroke);
WM_operatortype_append(GREASE_PENCIL_OT_draw_mode_toggle);
WM_operatortype_append(GREASE_PENCIL_OT_stroke_cutter);
Member

We don't need to append the operator here anymore. I suggest to put GREASE_PENCIL_OT_stroke_cutter in the same file (grease_pencil_cutter.cc) and declare it in ED_grease_pencil.hh underneath the other ED_operatortypes_* functions. Then you can put this line WM_operatortype_append(GREASE_PENCIL_OT_stroke_cutter); in ED_operatortypes_grease_pencil().

We don't need to append the operator here anymore. I suggest to put `GREASE_PENCIL_OT_stroke_cutter` in the same file (`grease_pencil_cutter.cc`) and declare it in `ED_grease_pencil.hh` underneath the other `ED_operatortypes_*` functions. Then you can put this line `WM_operatortype_append(GREASE_PENCIL_OT_stroke_cutter);` in `ED_operatortypes_grease_pencil()`.
SietseB marked this conversation as resolved
@ -34,6 +36,8 @@
namespace blender::ed::sculpt_paint::greasepencil {
using namespace blender::ed::greasepencil;
Member

I think it's better to just write ed::greasepencil::compute_topology_change(...);. We're already doing this for e.g. ed::greasepencil::retrieve_editable_drawings in this file.

I think it's better to just write `ed::greasepencil::compute_topology_change(...);`. We're already doing this for e.g. `ed::greasepencil::retrieve_editable_drawings` in this file.
SietseB marked this conversation as resolved
Sietse Brouwer added 1 commit 2024-02-01 19:58:22 +01:00
Sietse Brouwer added 1 commit 2024-02-01 20:18:14 +01:00
Sietse Brouwer added 2 commits 2024-03-04 22:31:28 +01:00
Sietse Brouwer added 1 commit 2024-03-07 16:48:55 +01:00
Sietse Brouwer added 2 commits 2024-03-14 21:07:03 +01:00
Sietse Brouwer added 1 commit 2024-03-15 12:58:01 +01:00
Sietse Brouwer added 2 commits 2024-04-02 23:56:52 +02:00
Member

Tested it and it feels quite nice.

I noticed that GPv2 cutter only deletes one segment per stroke, while this implementation removes anything within the lasso (generally, some corner cases below). It seems more logical this way, so no need to change that IMO.

The segments it actually deletes seem to depend a lot on the actual shape of of the lasso. I think this is because it requires the lasso to contain actual points. If the lasso threads the needle between two points the segment does not count as covered by the lasso (see video). This seems to be more common with the more aggressive simplification in GPv3. I'm not sure how hard it would be to detect lasso intersections with line segments, maybe that can be a TODO.

Tested it and it feels quite nice. I noticed that GPv2 cutter only deletes one segment per stroke, while this implementation removes anything within the lasso (generally, some corner cases below). It seems more logical this way, so no need to change that IMO. The segments it actually deletes seem to depend a lot on the actual shape of of the lasso. I think this is because it requires the lasso to contain actual points. If the lasso threads the needle between two points the segment does not count as covered by the lasso (see video). This seems to be more common with the more aggressive simplification in GPv3. I'm not sure how hard it would be to detect lasso intersections with line segments, maybe that can be a TODO.
Member

I'm not sure how hard it would be to detect lasso intersections with line segments, maybe that can be a TODO.

Looks like there is an API for it. Instead of BLI_lasso_is_point_inside, it would be BLI_lasso_is_edge_inside.
But I agree that this is not crictical and could also be done later.

> I'm not sure how hard it would be to detect lasso intersections with line segments, maybe that can be a TODO. Looks like there is an API for it. Instead of `BLI_lasso_is_point_inside`, it would be `BLI_lasso_is_edge_inside`. But I agree that this is not crictical and could also be done later.
Lukas Tönne requested changes 2024-04-08 17:34:15 +02:00
Dismissed
Lukas Tönne left a comment
Member

Quite a complex feature, and it works better than the GPv2 version! Just some minor nitpicks and some possible performance improvements. Not critical, but I'd like to confirm with someone who understands thread behavior better than me.

Quite a complex feature, and it works better than the GPv2 version! Just some minor nitpicks and some possible performance improvements. Not critical, but I'd like to confirm with someone who understands thread behavior better than me.
@ -0,0 +34,4 @@
*/
enum class SegmentRangeIndex : uint8_t { Start, End };
template<typename T> struct SegmentRange {
Member

IndexRange can be used instead of a custom range class. That also comes with a bunch of useful functions, e.g. contains to check the range in point_is_in_segment.

Using SegmentRange for the intersection_distance and is_intersected flag doesn't add much value, these are not really ranges. Just start/end properties seems more appropriate.

`IndexRange` can be used instead of a custom range class. That also comes with a bunch of useful functions, e.g. `contains` to check the range in `point_is_in_segment`. Using `SegmentRange` for the `intersection_distance` and `is_intersected` flag doesn't add much value, these are not really ranges. Just start/end properties seems more appropriate.
SietseB marked this conversation as resolved
@ -0,0 +230,4 @@
BLI_rcti_pad(&bbox_ab, BBOX_PADDING, BBOX_PADDING);
/* Loop all curves, looking for intersecting segments. */
threading::parallel_for(src.curves_range(), 512, [&](const IndexRange curves) {
Member

Second opinion would be welcome, but i think calling parallel_for here could have significant overhead. This function is called for every point, and then starts a threaded loop over all the points, and waits before moving to the next point. If my intuition is correct it would be faster to do the threading in the outer loop (stroke_cutter_find_and_remove_segments) and use a simple for loop here.

Second opinion would be welcome, but i think calling `parallel_for` here could have significant overhead. This function is called for every point, and then starts a threaded loop over all the points, and waits before moving to the next point. If my intuition is correct it would be faster to do the threading in the outer loop (`stroke_cutter_find_and_remove_segments`) and use a simple `for` loop here.
Member

Profiler indicates a lot of time spent in receive_or_steal_task (65%). I guess doing threading in the outer loop is a more difficult because of how CutterSegments gets extended and needs to be immutable while extending from a given point. Finding a way to parallelize this could be challenging. I won't mind leaving it as it is now and make a ToDo task, if this turns out to be too complicated.

Profiler indicates a lot of time spent in `receive_or_steal_task` (65%). I guess doing threading in the outer loop is a more difficult because of how `CutterSegments` gets extended and needs to be immutable while extending from a given point. Finding a way to parallelize this could be challenging. I won't mind leaving it as it is now and make a ToDo task, if this turns out to be too complicated.
Member

I think this can be made more parallelizable by doing the intersection tests up-front: There is essentially one intersection test per point. The expand_cutter_segment loop may get started multiple times for each point, but only do the intersection test the first time, then insert the point into a segment and skip subsequent checks. So the intersections can be detected for all the lasso'd strokes in advance, and this can be neatly parallelized.

After that the building of segments is just linear, and can be parallelized in the stroke domain as well. No complicated data structure required.

I think this can be made more parallelizable by doing the intersection tests up-front: There is essentially one intersection test per point. The `expand_cutter_segment` loop may get started multiple times for each point, but only do the intersection test the first time, then insert the point into a segment and skip subsequent checks. So the intersections can be detected for all the lasso'd strokes in advance, and this can be neatly parallelized. After that the building of segments is just linear, and can be parallelized in the stroke domain as well. No complicated data structure required.
SietseB marked this conversation as resolved
@ -0,0 +245,4 @@
/* Find intersecting curve segments. */
for (const int point_c : points) {
if (!is_cyclic[curve] && point_c == points.last()) {
Member

Can avoid this check inside the loop by modifying the range

IndexRange points = points_by_curve[curve].drop_back(is_cyclic[curve] ? 1 : 0);
Can avoid this check inside the loop by modifying the range ``` IndexRange points = points_by_curve[curve].drop_back(is_cyclic[curve] ? 1 : 0); ```
SietseB marked this conversation as resolved
@ -0,0 +352,4 @@
&distance_min,
&distance_max);
/* Avoid orphant points at the end of a curve. */
Member

orphaned

orphaned
SietseB marked this conversation as resolved
@ -0,0 +389,4 @@
/* When a curve end is reached and the curve is cyclic, we add an extra cutter segment for the
* cyclic second part. */
if (check_cyclic && is_cyclic[curve] &&
Member

It would be easier to understand if this last part for cyclic curves is moved out of the expand_cutter_segment function. Currently it looks at first glance like the function is recursive. Doing both calls for the non-cyclic and the cyclic part outside of the function is easier to understand i think.

It would be easier to understand if this last part for cyclic curves is moved out of the `expand_cutter_segment` function. Currently it looks at first glance like the function is recursive. Doing both calls for the non-cyclic and the cyclic part outside of the function is easier to understand i think.
SietseB marked this conversation as resolved
@ -0,0 +442,4 @@
const IndexRange src_points = src_points_by_curve[src_curve];
for (const int src_point : src_points) {
/* Skip point when it is already part of a cutter segment. */
if (cutter_segments.point_is_in_segment(src_curve, src_point)) {
Member

I think it would be easier and more efficient to just have a per-point bool array to mark points in segments. We're not interested in which segment a point is part of, so just setting the flag when a point gets added to any segment should be sufficient.

The point_is_in_segment linear search is reasonably fast when there are few segments, but with dense drawings might degrade into a N^2 loop when each segment contains just a few points. I did a simple test with ~13k strokes and 100k points, and it spends a significant amount of time in this function.

I think it would be easier and more efficient to just have a per-point bool array to mark points in segments. We're not interested in _which_ segment a point is part of, so just setting the flag when a point gets added to any segment should be sufficient. The `point_is_in_segment` linear search is reasonably fast when there are few segments, but with dense drawings might degrade into a N^2 loop when each segment contains just a few points. I did a simple test with ~13k strokes and 100k points, and it spends a significant amount of time in this function.
SietseB marked this conversation as resolved
@ -0,0 +595,4 @@
});
/* Apply cutter. */
std::optional<bke::CurvesGeometry> cutted_strokes = stroke_cutter_find_and_remove_segments(
Member

nit picking: past tense of "cut" is "cut", so cut_strokes

nit picking: past tense of "cut" is "cut", so `cut_strokes`
SietseB marked this conversation as resolved
Sietse Brouwer added 2 commits 2024-04-11 00:36:37 +02:00
buildbot/vexp-code-patch-lint Build done. Details
buildbot/vexp-code-patch-linux-x86_64 Build done. Details
buildbot/vexp-code-patch-darwin-x86_64 Build done. Details
buildbot/vexp-code-patch-darwin-arm64 Build done. Details
buildbot/vexp-code-patch-windows-amd64 Build done. Details
buildbot/vexp-code-patch-coordinator Build done. Details
164336f8ea
Parallelization moved to lassoed strokes, calculation of intersections done up-front
Author
Member

I skipped the use of BLI_lasso_is_edge_inside for now, because it adds some complexity. Line segments touched by the lasso can be intersected by other curves.
Handling cases like:
Intersected line segments touched by lasso.png
is doable, but not entirely trivial.

Something to save for a later date, I agree with that.

I skipped the use of `BLI_lasso_is_edge_inside` for now, because it adds some complexity. Line segments touched by the lasso can be intersected by other curves. Handling cases like: ![Intersected line segments touched by lasso.png](/attachments/05b3aae0-059b-4b6e-8da1-39ec3d7929fc) is doable, but not entirely trivial. Something to save for a later date, I agree with that.
Lukas Tönne reviewed 2024-04-11 09:33:59 +02:00
@ -0,0 +444,4 @@
}
/* Create new cutter segment. */
mutex.lock();
Member

This is probably fine, but if you want to optimize it a little bit more you can use the EnumerableThreadSpecific data to construct segments without locking a mutex, then combine them after the threaded construction. (example).

This is probably fine, but if you want to optimize it a little bit more you can use the `EnumerableThreadSpecific` data to construct segments without locking a mutex, then combine them after the threaded construction. ([example](https://projects.blender.org/blender/blender/src/commit/88526ab5f40a4011d03cb09b5c30edc371917205/source/blender/blenlib/intern/index_mask_expression.cc#L1205-L1227)).
SietseB marked this conversation as resolved
Lukas Tönne approved these changes 2024-04-11 09:34:14 +02:00
Member

@blender-bot build

@blender-bot build
Sietse Brouwer added 2 commits 2024-04-11 12:47:58 +02:00
Falk David requested changes 2024-04-11 13:07:59 +02:00
Dismissed
Falk David left a comment
Member

Have a few cleanup comments, but in general it looks very close!

Have a few cleanup comments, but in general it looks very close!
@ -0,0 +156,4 @@
const float c2 = a2 * co_c[0] + b2 * co_c[1];
const float det = float(a1 * b2 - a2 * b1);
BLI_assert(det != 0.0f);
Member

Are we sure that this can never happen? Would be better if this could return something if det is 0.0f, otherwise we just crash.

Are we sure that this can never happen? Would be better if this could return something if `det` is `0.0f`, otherwise we just crash.
Author
Member

We are sure (because the segments intersect), but I changed it anyway. You never know, in a leap year with full moon... 😉

We are sure (because the segments intersect), but I changed it anyway. You never know, in a leap year with full moon... 😉
filedescriptor marked this conversation as resolved
@ -0,0 +158,4 @@
const float det = float(a1 * b2 - a2 * b1);
BLI_assert(det != 0.0f);
float2 isect;
Member

Would prefer const float2 isect((b2 * c1 - b1 * c2) / det, (a1 * c2 - a2 * c1) / det);

Would prefer `const float2 isect((b2 * c1 - b1 * c2) / det, (a1 * c2 - a2 * c1) / det);`
SietseB marked this conversation as resolved
@ -0,0 +291,4 @@
const int8_t directions[2] = {-1, 1};
/* Walk along the curve in both directions. */
for (const int8_t direction : directions) {
Member

Would be a bit nicer imo if the body of this loop was a function expand_cutter_segment_direction and you would just call it two times with -1 and 1.

Would be a bit nicer imo if the body of this loop was a function `expand_cutter_segment_direction` and you would just call it two times with `-1` and `1`.
SietseB marked this conversation as resolved
@ -0,0 +555,4 @@
}
/* Create the new curves geometry. */
bke::CurvesGeometry dst;
Member

Hm this makes me think that compute_topology_change should probably return a new CurvesGeometry. Could be done in a separate cleanup later.

Hm this makes me think that `compute_topology_change` should probably return a new `CurvesGeometry`. Could be done in a separate cleanup later.
@ -0,0 +693,4 @@
WM_event_add_notifier(C, NC_GEOM | ND_DATA, &grease_pencil);
}
return (changed ? OPERATOR_FINISHED : OPERATOR_CANCELLED);
Member

This should just return OPERATOR_FINISHED (even if no drawing was changed).

This should just return `OPERATOR_FINISHED` (even if no drawing was changed).
SietseB marked this conversation as resolved
@ -0,0 +704,4 @@
return OPERATOR_PASS_THROUGH;
}
const int result = stroke_cutter_execute(op, C, mcoords);
Member

Just return stroke_cutter_execute(op, C, mcoords); is fine.

Just `return stroke_cutter_execute(op, C, mcoords);` is fine.
SietseB marked this conversation as resolved
Sietse Brouwer added 1 commit 2024-04-11 16:11:21 +02:00
Sietse Brouwer added 1 commit 2024-04-21 13:43:40 +02:00
Falk David added 1 commit 2024-04-22 13:14:28 +02:00
buildbot/vexp-code-patch-lint Build done. Details
buildbot/vexp-code-patch-linux-x86_64 Build done. Details
buildbot/vexp-code-patch-darwin-x86_64 Build done. Details
buildbot/vexp-code-patch-darwin-arm64 Build done. Details
buildbot/vexp-code-patch-windows-amd64 Build done. Details
buildbot/vexp-code-patch-coordinator Build done. Details
321a79540c
Merge branch 'main' into gpv3-cutter-tool
Member

@blender-bot build

@blender-bot build
Falk David approved these changes 2024-04-22 14:04:15 +02:00
Falk David merged commit 4fbef3dc6b into main 2024-04-22 14:05:33 +02:00
Falk David referenced this issue from a commit 2024-04-22 14:05:34 +02:00
Sietse Brouwer deleted branch gpv3-cutter-tool 2024-04-22 22:48:56 +02:00
Sign in to join this conversation.
No reviewers
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
3 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: blender/blender#113953
No description provided.