Curves: Add extrude operator #116354

Merged
Hans Goudey merged 26 commits from laurynas/blender:curves-extrude-op into main 2024-01-10 17:27:04 +01:00
Contributor

Adds extrude operator to new curves. Press key E in edit mode to invoke.

Demo shows extrusion on endpoint NURBS curve.

extrude.blend

It works correctly on Bezier curves also, but result is weird as there is no way to control Bezier handles interactively.

Currently operator works same way as in old curves, but effect is a bit inconsistent in cyclic curves.

cyclic.blend
Maybe it is reasonable to unify end point extrusion with inner point extrusion in cyclics?

PS
Algorithms idea is same as in https://archive.blender.org/developer/D15524.

Adds extrude operator to new curves. Press key E in edit mode to invoke. Demo shows extrusion on endpoint NURBS curve. <video src="/attachments/e2fb2c95-277d-4ecc-9149-b85359f16879" title="extrude.mov" controls></video> [extrude.blend](/attachments/143c0530-062b-4ee2-9358-4722acd555ff) It works correctly on Bezier curves also, but result is weird as there is no way to control Bezier handles interactively. Currently operator works same way as in old curves, but effect is a bit inconsistent in cyclic curves. <video src="/attachments/ff39e763-47b9-4d73-b9fb-34422c4a543a" title="cyclic.mov" controls></video> [cyclic.blend](/attachments/7dd9638b-8aa4-49cd-98b6-84287a48fd81) Maybe it is reasonable to unify end point extrusion with inner point extrusion in cyclics? PS Algorithms idea is same as in https://archive.blender.org/developer/D15524.
Laurynas Duburas added 1 commit 2023-12-19 16:43:33 +01:00
Laurynas Duburas requested review from Hans Goudey 2023-12-19 16:43:56 +01:00
Hans Goudey reviewed 2023-12-19 19:10:24 +01:00
Hans Goudey left a comment
Member

Making quick progress, nice! I left some big-picture comments for now, didn't test it yet.

Making quick progress, nice! I left some big-picture comments for now, didn't test it yet.
@ -0,0 +12,4 @@
namespace blender::ed::curves {
struct CurveCopy {
Member

I'd suggest doing a "array of structs" to "struct of arrays" transformation and split this struct into separate arrays for each member. That would have a few benefits here:

  • Possibly easier use of existing abstractions like selection.foreach_range, OffsetIndices, array_utils::gather, etc.
  • Better performance since hot loops only read the data they'll actually use
  • Easier separation of concerns in code, i.e. only calculate one thing at a time
I'd suggest doing a "array of structs" to "struct of arrays" transformation and split this struct into separate arrays for each member. That would have a few benefits here: - Possibly easier use of existing abstractions like `selection.foreach_range`, `OffsetIndices`, `array_utils::gather`, etc. - Better performance since hot loops only read the data they'll actually use - Easier separation of concerns in code, i.e. only calculate one thing at a time
Author
Contributor

Well CurveCopy is refactored, but I'll have to think deeper regarding abstractions. Somehow I didn't notice selection.foreach_range exists, only it might change a lot.
By the way I was thinking about using OffsetIndices, but comment says "References an array of ascending indices."
Here I have "non decreasing indices". Will it work?

Well `CurveCopy` is refactored, but I'll have to think deeper regarding abstractions. Somehow I didn't notice `selection.foreach_range` exists, only it might change a lot. By the way I was thinking about using `OffsetIndices`, but comment says "References an array of ascending indices." Here I have "non decreasing indices". Will it work?
Member

Not totally sure what the requirements are here, didn't look that in depth yet. But an array like 5, 5, 6, 8, 21, 21 will work; repeating offsets are okay, it just means that group is empty.

Not totally sure what the requirements are here, didn't look that in depth yet. But an array like `5, 5, 6, 8, 21, 21` will work; repeating offsets are okay, it just means that group is empty.
Author
Contributor
  1. I need to express ranges like [0, 2][2, 2][2, 4]. Ranges always share end points with a neighbor.
    Copied to destination indexes would result in [0, 1, 2, 2, 2, 3, 4].
    This would be extrusion of point 2 in 5 point curve [0, .., 4].
    Because of sharing neighbor I currently express this as [0, 2, 2, 4] and iterate with (i, i + 1).
    So for destination I see use of OffsetIndices, but for source indexes not sure.

  2. Ideally if to handle multiple curves in one data structure. Extrusion of points (2) and (7, 8) in curves [0, .., 4][5, .., 9] could look like:
    a. [0, 2][2, 2][2, 4][5, 7][7, 8][8, 9]. Here sharing endpoint rule is broken.
    b. [0, 2][2, 2][2, 7][7, 8][8, 9]. This would reduce copying count, but because of special cases is very hard to implement. Before I had a working version, I gave up on this after few days of trying.
    Any Thoughts?

1. I need to express ranges like `[0, 2][2, 2][2, 4]`. Ranges always share end points with a neighbor. Copied to destination indexes would result in `[0, 1, 2, 2, 2, 3, 4]`. This would be extrusion of point 2 in 5 point curve `[0, .., 4]`. Because of sharing neighbor I currently express this as `[0, 2, 2, 4]` and iterate with `(i, i + 1)`. So for destination I see use of `OffsetIndices`, but for source indexes not sure. 2. Ideally if to handle multiple curves in one data structure. Extrusion of points (2) and (7, 8) in curves `[0, .., 4][5, .., 9]` could look like: a. `[0, 2][2, 2][2, 4][5, 7][7, 8][8, 9]`. Here sharing endpoint rule is broken. b. `[0, 2][2, 2][2, 7][7, 8][8, 9]`. This would reduce copying count, but because of special cases is very hard to implement. Before I had a working version, I gave up on this after few days of trying. Any Thoughts?
Author
Contributor

Just checked code of OffsetIndices . It uses second index as exclusive, in this situation both inclusive needed.
OffsetIndices calculates IndexRange size by size = end - begin;, I need size = last - first + 1;.
Can't think of any smart way to restructure data to make use of OffsetIndices ...

Just checked code of `OffsetIndices `. It uses second index as exclusive, in this situation both inclusive needed. `OffsetIndices ` calculates `IndexRange` size by `size = end - begin;`, I need `size = last - first + 1;`. Can't think of any smart way to restructure data to make use of `OffsetIndices `...
Member

That's fine, no need to force it! Thanks for taking a look though.

That's fine, no need to force it! Thanks for taking a look though.
laurynas marked this conversation as resolved
@ -0,0 +70,4 @@
}
static int sel_to_copy_ints(const IndexMask selection,
const Span<int> &offsets,
Member

Pass Span by value, otherwise it's like a pointer to a pointer. And IndexMask is passed by const reference :P

Pass `Span` by value, otherwise it's like a pointer to a pointer. And `IndexMask` is passed by const reference :P
Author
Contributor

I don't have a good explanation for how it happened :)

I don't have a good explanation for how it happened :)
laurynas marked this conversation as resolved
@ -0,0 +144,4 @@
Object *obedit = CTX_data_edit_object(C);
Curves *curves_id = static_cast<Curves *>(obedit->data);
const eAttrDomain selection_domain = eAttrDomain(curves_id->selection_domain);
if (selection_domain == ATTR_DOMAIN_POINT) {
Member

Flip the condition and un-indent the rest of the function, same below with the empty selection check

Flip the condition and un-indent the rest of the function, same below with the empty selection check
laurynas marked this conversation as resolved
@ -0,0 +152,4 @@
bke::CurvesGeometry &curves = curves_id->geometry.wrap();
const int max = selection.size() * 2 + curves.curves_num() * 2;
int *const all_copy_intervals = static_cast<int *>(
Member

Use Array rather than manual allocations

Use `Array` rather than manual allocations
laurynas marked this conversation as resolved
@ -0,0 +164,4 @@
bke::CurvesGeometry new_curves(new_points_count, curves.curves_num());
MutableSpan<int> offsets = new_curves.offsets_for_write();
const MutableSpan<int8_t> new_types = new_curves.curve_types_for_write();
Member

Unused types variables here

Unused types variables here
laurynas marked this conversation as resolved
@ -0,0 +167,4 @@
const MutableSpan<int8_t> new_types = new_curves.curve_types_for_write();
const VArray<int8_t> old_types = curves.curve_types();
CustomData_copy(
Member

Let's use the attribute API fully, without CustomData. That might require a bit more boilerplate, but that's easy to clean up in the future, and we have a high level goal of replacing CustomData storage internally

Let's use the attribute API fully, without `CustomData`. That might require a bit more boilerplate, but that's easy to clean up in the future, and we have a high level goal of replacing `CustomData` storage internally
laurynas marked this conversation as resolved
@ -0,0 +179,4 @@
}
bke::MutableAttributeAccessor new_attributes = new_curves.attributes_for_write();
bke::MutableAttributeAccessor old_attributes = curves.attributes_for_write();
Member

I guess old_attributes could be a bke::AttributeAccessor since those values don't have to be changed?

I guess `old_attributes` could be a `bke::AttributeAccessor` since those values don't have to be changed?
Author
Contributor

Couldn't find how to do attribute value copying from bke::AttributeAccessor to MutableAttributeAccessor in chunks.
Like with type.copy_assign_n(src.data(), dst.data(), dst.size());

Couldn't find how to do attribute value copying from `bke::AttributeAccessor` to `MutableAttributeAccessor` in chunks. Like with `type.copy_assign_n(src.data(), dst.data(), dst.size());`
Member

You can use const GVArraySpan src_data = *src.lookup(name); to make sure you have a span of source data

You can use `const GVArraySpan src_data = *src.lookup(name);` to make sure you have a span of source data
laurynas marked this conversation as resolved
@ -0,0 +193,4 @@
const int last = copy.intervals[i + 1];
const int size = last - first + 1;
copy_attributes(old_attributes, new_attributes, first, d, size);
Member

Usually these algorithms are structured to only iterate over all the attributes once. So the first part will build mappings from input data to result data. In this case, that might be a combination of instructions for copying contiguous sections of data, and some data for which indices the copied points are from.

That structure can significantly improve performance, since it works much better with the cache of CPUs, since we access less data at once. It might also help separate concerns too.

Usually these algorithms are structured to only iterate over all the attributes once. So the first part will build mappings from input data to result data. In this case, that might be a combination of instructions for copying contiguous sections of data, and some data for which indices the copied points are from. That structure can significantly improve performance, since it works much better with the cache of CPUs, since we access less data at once. It might also help separate concerns too.
laurynas marked this conversation as resolved
@ -0,0 +217,4 @@
void CURVES_OT_extrude(wmOperatorType *ot)
{
ot->name = "Extrude";
ot->idname = __func__;
Member

I think leaving ot->description blank will give some warning somewhere in the future

I think leaving `ot->description` blank will give some warning somewhere in the future
laurynas marked this conversation as resolved
Laurynas Duburas added 1 commit 2023-12-19 20:50:36 +01:00
Laurynas Duburas added 1 commit 2023-12-19 23:16:57 +01:00
Laurynas Duburas changed title from Curves extrude operator to WIP: Curves extrude operator 2023-12-19 23:26:38 +01:00
Laurynas Duburas added 1 commit 2023-12-20 07:07:36 +01:00
Laurynas Duburas added 1 commit 2023-12-21 14:29:09 +01:00
Laurynas Duburas added 2 commits 2023-12-21 18:58:12 +01:00
Laurynas Duburas changed title from WIP: Curves extrude operator to Curves extrude operator 2023-12-21 18:59:59 +01:00
Hans Goudey reviewed 2023-12-29 21:38:39 +01:00
Hans Goudey left a comment
Member

Functionality wise this is working great. I didn't have the time (or energy, I guess) to go over the logic completely. But I'll leave some opinions here anyway :)

  • To me the CurvesExtrusion class doesn't seem necessary, compared to implementing the same logic with free functions that have the necessary arguments themselves. It's not a big deal, but generally it's easiest to work on this sort of code if the arguments are clear and functions are separated.
  • It would probably be worth considering how the attribute copying could be multithreaded. Currently there's the d variable that's used as a counter in the result points. Maybe that could be precomputed. Again, not a big deal for now, but multithreaded copying like this can often make a noticeable difference.
Functionality wise this is working great. I didn't have the time (or energy, I guess) to go over the logic completely. But I'll leave some opinions here anyway :) - To me the `CurvesExtrusion` class doesn't seem necessary, compared to implementing the same logic with free functions that have the necessary arguments themselves. It's not a big deal, but generally it's easiest to work on this sort of code if the arguments are clear and functions are separated. - It would probably be worth considering how the attribute copying could be multithreaded. Currently there's the `d` variable that's used as a counter in the result points. Maybe that could be precomputed. Again, not a big deal for now, but multithreaded copying like this can often make a noticeable difference.
@ -0,0 +156,4 @@
curve_intervals_[interval_offset_ + ins] = last_elem;
ins++;
}
// check for extrusion from one point
Member

Comment style

Comment style
laurynas marked this conversation as resolved
@ -0,0 +208,4 @@
const Span<int> old_offsets = curves.offsets();
CurvesExtrusion curves_copy(old_offsets, extruded_points);
bke::CurvesGeometry new_curves(curves_copy.curve_offsets().last(), curves.curves_num());
Member

The bke::curves::copy_only_curve_domain utility in BKE_curves_utils.hh takes care of creating new curves and copying over the curve domain attributes for you.

The `bke::curves::copy_only_curve_domain` utility in `BKE_curves_utils.hh` takes care of creating new curves and copying over the curve domain attributes for you.
laurynas marked this conversation as resolved
@ -0,0 +214,4 @@
offsets.copy_from(curves_copy.curve_offsets());
int d = 0;
const int *curve_intervals = curves_copy.curve_intervals();
Member

Using a Span still makes sense here probably, rather than a raw pointer

Using a Span still makes sense here probably, rather than a raw pointer
laurynas marked this conversation as resolved
@ -0,0 +247,4 @@
const GVArraySpan src = (*src_attributes.lookup(id, meta_data.domain));
if (meta_data.domain == ATTR_DOMAIN_POINT) {
d = 0;
Member

This variable could be declared here, and could have a better name besides d that reflected what it was doing

This variable could be declared here, and could have a better name besides `d` that reflected what it was doing
laurynas marked this conversation as resolved
Laurynas Duburas added 3 commits 2023-12-30 22:27:00 +01:00
Author
Contributor

No multithreading yet. I'll think about it next year :)

No multithreading yet. I'll think about it next year :)
Laurynas Duburas added 3 commits 2024-01-01 12:31:31 +01:00
Laurynas Duburas added 1 commit 2024-01-01 13:59:27 +01:00
Author
Contributor

Done.

Done.
Laurynas Duburas added 1 commit 2024-01-03 09:08:45 +01:00
Laurynas Duburas added 2 commits 2024-01-05 00:22:26 +01:00
Hans Goudey requested changes 2024-01-09 18:19:13 +01:00
@ -0,0 +13,4 @@
namespace blender::ed::curves {
/* Stores information need to create new curves by copying data from original ones. */
struct CurvesCopy {
Member

This is getting more subjective now, and I think the PR is getting close. But my one larger remaining comment is that bundling all this data in a single struct (CurvesCopy) doesn't really help the situation. It may be clearer to declare each array as its needed in the caller. That gives a nice order of execution to the reader, and clarifies which parts of the algorithm need which data.

Sometimes that will make it easier to separate different parts of the algorithm into separate loops too. For example, maybe some data can be calculated in parallel while other data can't. Not sure if that applies here.

This is getting more subjective now, and I think the PR is getting close. But my one larger remaining comment is that bundling all this data in a single struct (`CurvesCopy`) doesn't really help the situation. It may be clearer to declare each array as its needed in the caller. That gives a nice order of execution to the reader, and clarifies which parts of the algorithm need which data. Sometimes that will make it easier to separate different parts of the algorithm into separate loops too. For example, maybe some data can be calculated in parallel while other data can't. Not sure if that applies here.
laurynas marked this conversation as resolved
@ -0,0 +121,4 @@
void finish_curve(int &curve_index,
int &interval_offset,
int ins,
Member

Could you expand ins to a name that helps explain the code to the reader? Generally this sort of contraction makes this sort of thing more complex

Could you expand `ins` to a name that helps explain the code to the reader? Generally this sort of contraction makes this sort of thing more complex
laurynas marked this conversation as resolved
@ -0,0 +144,4 @@
curve_index++;
}
void finish_curve_or_shallow_copy(int &curve_index,
Member

Add static to functions that aren't exposed to the header

Add `static` to functions that aren't exposed to the header
laurynas marked this conversation as resolved
@ -0,0 +146,4 @@
void finish_curve_or_shallow_copy(int &curve_index,
int &interval_offset,
int ins,
Member

It's not totally clear what "shallow copy" means here. Usually I'd imagine that copying some data structure but not any data it owns itself.

It's not totally clear what "shallow copy" means here. Usually I'd imagine that copying some data structure but not any data it owns itself.
Author
Contributor

I meant here: copy curve as is or copy curve without modifications. Maybe finish_curve_or_full_copy ?

I meant here: copy curve as is or copy curve without modifications. Maybe finish_curve_or_full_copy ?
Member

Full copy sounds good!

Full copy sounds good!
laurynas marked this conversation as resolved
@ -0,0 +216,4 @@
{
Object *obedit = CTX_data_edit_object(C);
Curves *curves_id = static_cast<Curves *>(obedit->data);
const bke::AttrDomain selection_domain = bke::AttrDomain(curves_id->selection_domain);
Member

Just realized this should handle multi-object edit mode too. There's an example of that in CURVES_OT_delete, or other operators around there.

Just realized this should handle multi-object edit mode too. There's an example of that in `CURVES_OT_delete`, or other operators around there.
laurynas marked this conversation as resolved
@ -0,0 +227,4 @@
return OPERATOR_FINISHED;
}
bke::CurvesGeometry &curves = curves_id->geometry.wrap();
Member

Looks like this can be a const pointer, since the new curves are created separately

Looks like this can be a const pointer, since the new curves are created separately
laurynas marked this conversation as resolved
@ -0,0 +238,4 @@
bke::CurvesGeometry new_curves = bke::curves::copy_only_curve_domain(curves);
new_curves.resize(curve_offsets.last(), curves.curves_num());
MutableSpan<int> offsets = new_curves.offsets_for_write();
Member

If you create the new curves before calc_curves_extrusion, you can copy the new offsets directly into the new curves' offsets array. If you don't know the number of points at that point, you can resize just the point domain after.

If you create the new curves before `calc_curves_extrusion`, you can copy the new offsets directly into the new curves' offsets array. If you don't know the number of points at that point, you can resize just the point domain after.
laurynas marked this conversation as resolved
@ -0,0 +246,4 @@
curves_copy.curve_intervals.size()};
bke::GSpanAttributeWriter selection = ensure_selection_attribute(
new_curves, selection_domain, CD_PROP_BOOL);
Member

We know the domain is bke::AttrDomain::Point at this point.

I noticed that this won't copy the float vs. bool status of the input selection though. Since these curves are completely new, it will always create a boolean selection.

It would be pretty fancy to be able to copy the float values from the input curves as well. But if you don't feel inspired to do that, it's not a big deal either :)

We know the domain is `bke::AttrDomain::Point` at this point. I noticed that this won't copy the float vs. bool status of the input selection though. Since these curves are completely new, it will always create a boolean selection. It would be pretty fancy to be able to copy the float values from the input curves as well. But if you don't feel inspired to do that, it's not a big deal either :)
Author
Contributor

Don't know why selection is made with two possible types, but extrude shouldn't change the type.

Don't know why selection is made with two possible types, but extrude shouldn't change the type.
Member

In sculpt mode there's "soft" selection which uses floats. Any value greater than zero is considered selected in edit mode.

In sculpt mode there's "soft" selection which uses floats. Any value greater than zero is considered selected in edit mode.
Author
Contributor

I was going to ask if other values besides 1.0f are possible, but decided that this can not be :)
Good that you told this.

I was going to ask if other values besides 1.0f are possible, but decided that this can not be :) Good that you told this.
laurynas marked this conversation as resolved
@ -0,0 +249,4 @@
new_curves, selection_domain, CD_PROP_BOOL);
threading::parallel_for(curves.curves_range(), 256, [&](IndexRange curves_range) {
for (const int c : curves_range) {
Member

c -> curve

`c` -> `curve`
laurynas marked this conversation as resolved
@ -0,0 +288,4 @@
});
attribute.dst.finish();
}
new_curves.update_curve_types();
Member

This should be unnecessary, since copy_only_curve_domain copies this info

This should be unnecessary, since `copy_only_curve_domain` copies this info
laurynas marked this conversation as resolved
@ -0,0 +290,4 @@
}
new_curves.update_curve_types();
curves_id->geometry.wrap() = new_curves;
Member

std::move(new_curves) to avoid a copy

`std::move(new_curves)` to avoid a copy
laurynas marked this conversation as resolved
Laurynas Duburas added 6 commits 2024-01-10 01:30:42 +01:00
Laurynas Duburas requested review from Hans Goudey 2024-01-10 01:32:10 +01:00
Hans Goudey approved these changes 2024-01-10 02:09:13 +01:00
Hans Goudey changed title from Curves extrude operator to Curves: Add extrude operator 2024-01-10 16:45:19 +01:00
Hans Goudey added 1 commit 2024-01-10 17:21:13 +01:00
9a8c2c2c11 Various small style changes
- Array & -> MutableSpan
- Remove unnecessary `blender::`
- Remove "obj" from function dealing with just curves
- Use reference instead of pointer
- Explicitly write operator idname
- Comment style
- Expand one cryptic variable name
Hans Goudey added 1 commit 2024-01-10 17:23:38 +01:00
8a25b7a75d Add depsgraph update tag
Somehow modifiers still reevaluate without this, but better to be sure.
In other areas, operators must tag the depsgraph when changing geometry.
Hans Goudey added 1 commit 2024-01-10 17:24:19 +01:00
Hans Goudey merged commit 0d964f91a2 into main 2024-01-10 17:27:04 +01:00
Laurynas Duburas deleted branch curves-extrude-op 2024-01-10 20:45:50 +01:00
Member

Not sure if this can be reused for GPv3 extrude operator.

Not sure if this can be reused for GPv3 extrude operator.
Author
Contributor

Not sure if this can be reused for GPv3 extrude operator.

It's hard to tell. I had to search what GP stands for :)
I'd don't know code basis for GP, but if it's based on CurvesGeometry then should be.
From UI side also I'm not sure what extrude should do on a stroke. I couldn't not find how to select single point.
Maybe there is an architecture document to read?

> Not sure if this can be reused for GPv3 extrude operator. It's hard to tell. I had to search what GP stands for :) I'd don't know code basis for GP, but if it's based on `CurvesGeometry` then should be. From UI side also I'm not sure what extrude should do on a stroke. I couldn't not find how to select single point. Maybe there is an architecture document to read?
Member

I had to search what GP stands fo

GPv3: new grease pencil architecture (3rd generation)
It uses CurvesGeometry but curve type is CURVE_TYPE_POLY

Maybe there is an architecture document to read?

https://developer.blender.org/docs/features/grease_pencil/architecture/ :)

> I had to search what GP stands fo GPv3: new grease pencil architecture (3rd generation) It uses CurvesGeometry but curve type is `CURVE_TYPE_POLY` > Maybe there is an architecture document to read? https://developer.blender.org/docs/features/grease_pencil/architecture/ :)
Author
Contributor

Curve type doesn't matter, extrude treats all curves the same. At least at this stage.

From functional side or user perspective GP extrude in Blender 4.0 does different things
than extrude in old curves. It doesn't extend stroke, but creates new ones.
Extrude in this PR is equivalent to one of old curves.
So if in 4.0 GP extrude does what it should, then this one will not do the work.

Curve type doesn't matter, extrude treats all curves the same. At least at this stage. From functional side or user perspective GP extrude in Blender 4.0 does different things than extrude in old curves. It doesn't extend stroke, but creates new ones. Extrude in this PR is equivalent to one of old curves. So if in 4.0 GP extrude does what it should, then this one will not do the work.
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#116354
No description provided.