Python API: Direct access to attribute arrays of meshes, curves, point clouds and GP drawings #122091

Open
Sietse Brouwer wants to merge 5 commits from SietseB/blender:core-rna-python-attribute-array into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
Member

This PR implements a Python API for direct access to attribute arrays of meshes, curves, point clouds and Grease Pencil drawings.

Current access to attribute arrays has its limitations, mainly because they are defined as property collections, which always return RNA property objects, not direct values.

>>> radius = curve.attributes['radius']
>>> radius.data[0]
bpy.data.hair_curves['Curves']...FloatAttributeValue
>>> radius.data[0].value
1.0
>>> radius.data[0].value = 0.5  # Pretty verbose
>>> radius.data[0:5] = ???  # Slice assign: pretty hard to do

This PR implements a more direct approach to lift these limitations.

Direct access to attributes
.attributes[name].data_array

radius = curve.attributes['radius']
radius.data_array[0]  # Returns float 0.5
radius.data_array[0] = 2.0  # Assign float directly
radius.data_array[1:3] = [1.0, 2.0, 1.5]  # Slice assign

Attribute type aware
All attribute type are automatically handled, so retrieving a float3 attribute will return a Vector(x, y, z), a ColorGeometry4f attribute will give a [r, g, b, a], etc.

positions = curve.attributes['position']
positions.data_array[0] = Vector((1.0, 0.0, 2.5))  # Assign mathutils.Vector or...
positions.data_array[0] = [1.0, 0.0, 2.5]  # Assign list
curve.attributes['rotation_quaternion'].data_array[0] = Quaternion((1.0, 0.0, 0.0, 0.0))
curve.attributes['.selection'].data_array[0] = True

Foreach_get and foreach_set
.data_array.foreach_get(array)
.data_array.foreach_set(array)

num_points = len(radius.data_array)
radii = np.ndarray(num_points, dtype=np.float32)
radius.data_array.foreach_get(radii)
radii[:] = 2.0
radius.data_array.foreach_set(radii)

Fill attribute
.data_array.fill(value)
.data_array.fill(value, indices)

radius.data_array.fill(2.0)  # Set the radius of all the control points in the curves
radius.data_array.fill(2.0, [0, 1, 2, 3])  # Use indices: set the radius of the first four control points

# Select points with a radius between 1.0 and 2.0
num_points = len(radius.data_array)
radii = np.ndarray(num_points, dtype=np.float32)
radius.data_array.foreach_get(radii)
indices = np.nonzero((radii > 1.0) & (radii < 2.0))[0]
curve.attributes['.selection'].data_array.fill(True, indices)

Technical notes

  • Changing attribute values won't trigger an object redraw, so that trigger has to be done manually in the Python code.
  • To keep things clean, I placed the BPy_AttributeArray code in its own submodule in python/generic, similar to ID Properties Access.
This PR implements a Python API for direct access to attribute arrays of meshes, curves, point clouds and Grease Pencil drawings. Current access to attribute arrays has its limitations, mainly because they are defined as property collections, which always return RNA property objects, not direct values. ```python >>> radius = curve.attributes['radius'] >>> radius.data[0] bpy.data.hair_curves['Curves']...FloatAttributeValue >>> radius.data[0].value 1.0 >>> radius.data[0].value = 0.5 # Pretty verbose >>> radius.data[0:5] = ??? # Slice assign: pretty hard to do ``` This PR implements a more direct approach to lift these limitations. **Direct access to attributes** _`.attributes[name].data_array`_ ```python radius = curve.attributes['radius'] radius.data_array[0] # Returns float 0.5 radius.data_array[0] = 2.0 # Assign float directly radius.data_array[1:3] = [1.0, 2.0, 1.5] # Slice assign ``` **Attribute type aware** All attribute type are automatically handled, so retrieving a `float3` attribute will return a `Vector(x, y, z)`, a `ColorGeometry4f` attribute will give a `[r, g, b, a]`, etc. ```python positions = curve.attributes['position'] positions.data_array[0] = Vector((1.0, 0.0, 2.5)) # Assign mathutils.Vector or... positions.data_array[0] = [1.0, 0.0, 2.5] # Assign list curve.attributes['rotation_quaternion'].data_array[0] = Quaternion((1.0, 0.0, 0.0, 0.0)) curve.attributes['.selection'].data_array[0] = True ``` **Foreach_get and foreach_set** _`.data_array.foreach_get(array)`_ _`.data_array.foreach_set(array)`_ ```python num_points = len(radius.data_array) radii = np.ndarray(num_points, dtype=np.float32) radius.data_array.foreach_get(radii) radii[:] = 2.0 radius.data_array.foreach_set(radii) ``` **Fill attribute** _`.data_array.fill(value)`_ _`.data_array.fill(value, indices)`_ ```python radius.data_array.fill(2.0) # Set the radius of all the control points in the curves radius.data_array.fill(2.0, [0, 1, 2, 3]) # Use indices: set the radius of the first four control points # Select points with a radius between 1.0 and 2.0 num_points = len(radius.data_array) radii = np.ndarray(num_points, dtype=np.float32) radius.data_array.foreach_get(radii) indices = np.nonzero((radii > 1.0) & (radii < 2.0))[0] curve.attributes['.selection'].data_array.fill(True, indices) ``` --- **Technical notes** - Changing attribute values won't trigger an object redraw, so that trigger has to be done manually in the Python code. - To keep things clean, I placed the `BPy_AttributeArray` code in its own submodule in `python/generic`, similar to ID Properties Access.
Sietse Brouwer added 4 commits 2024-05-22 11:34:49 +02:00
Sietse Brouwer added this to the Module: Python API project 2024-05-22 11:35:10 +02:00
Sietse Brouwer requested review from Falk David 2024-05-22 12:12:32 +02:00
Hans Goudey requested changes 2024-05-22 15:26:00 +02:00
Hans Goudey left a comment
Member

I'm not convinced it's worth it for us to maintain such an API outside of RNA. Some of these improvements would be more generally beneficial applied to RNA itself rather than just one custom type. The need for RNA_struct_is_attribute_array shows how this doesn't fit that well into existing design IMO. At least this should be discussed with core module members before moving forward further.

There are a few more important aspects with RNA/Python access to attribute data. However we improve attribute access, these issues should be improved too.

  • Implicit Sharing: Currently accessing attribute data in Python un-shares the array, whether the access is for writing or only reading. This is a significant performance problem because it negates the memory usage benefits of implicit sharing as soon as Python/RNA touches the data.
  • Caches: Currently we have various caches that depend on some attributes. They need to be tagged dirty when the data is changed.
  • Validation: Some builtin attributes have constraints on what values can be set. These should be respected too.
  • Separation from CustomData: We are looking to replace CustomData at some point with a more flexible/modern storage system. We should avoid building abstractions directly on top of CustomData directly in the meantime.

The last three issues can be resolved by building on top of the C++ attribute API in BKE_attribute.hh rather than the CustomData system.

I'm not convinced it's worth it for us to maintain such an API outside of RNA. Some of these improvements would be more generally beneficial applied to RNA itself rather than just one custom type. The need for `RNA_struct_is_attribute_array` shows how this doesn't fit that well into existing design IMO. At least this should be discussed with core module members before moving forward further. There are a few more important aspects with RNA/Python access to attribute data. However we improve attribute access, these issues should be improved too. - **Implicit Sharing:** Currently accessing attribute data in Python un-shares the array, whether the access is for writing or only reading. This is a significant performance problem because it negates the memory usage benefits of implicit sharing as soon as Python/RNA touches the data. - **Caches:** Currently we have various caches that depend on some attributes. They need to be tagged dirty when the data is changed. - **Validation:** Some builtin attributes have constraints on what values can be set. These should be respected too. - **Separation from CustomData:** We are looking to replace `CustomData` at some point with a more flexible/modern storage system. We should avoid building abstractions directly on top of CustomData directly in the meantime. The last three issues can be resolved by building on top of the C++ attribute API in `BKE_attribute.hh` rather than the CustomData system.
@ -43,2 +43,4 @@
/** Shape key-block unique id reference. */
int uid;
/** Only for use in RNA and #bpy_rna: number of items in the layer data. */
int length;
Member

I think it's important to avoid adding this. It should be retrieved from the "context" as necessary. That sort of redundant storage adds plenty of problems with syncing and invalid states.

I think it's important to avoid adding this. It should be retrieved from the "context" as necessary. That sort of redundant storage adds plenty of problems with syncing and invalid states.
Author
Member

The 'context' is the problem here. The function that creates the Python object only receives a PointerRNA *ptr, which contains an ID owner_id and void *data. For meshes and curves, we can derive the length of the attribute array from the owner_id, but for Grease Pencil we can't, because the geometry is in a Drawing, which isn't an ID type.
I see the problems you mention, of course, but I don't see a better solution for now. Any ideas, perhaps?

The 'context' is the problem here. The function that creates the Python object only receives a `PointerRNA *ptr`, which contains an `ID owner_id` and `void *data`. For meshes and curves, we can derive the length of the attribute array from the `owner_id`, but for Grease Pencil we can't, because the geometry is in a `Drawing`, which isn't an `ID` type. I see the problems you mention, of course, but I don't see a better solution for now. Any ideas, perhaps?
Member

There was some discussion on improving the state of things regarding ownership of data pointed at by PointerRNA. See #122431

There was some discussion on improving the state of things regarding ownership of data pointed at by `PointerRNA`. See https://projects.blender.org/blender/blender/issues/122431

I think storing the array length in the custom data layer is not that bad, if this was a C++ data type this would be a Vector and so it would have the size too. But if this is done, I think it requires some refactoring in customdata.cc to ensure any array pointer changes go along with size changes. Maybe affects some public API functions as well, where maybe it needs to then pass an Array instead of a pointer.

I think storing the array length in the custom data layer is not that bad, if this was a C++ data type this would be a `Vector` and so it would have the size too. But if this is done, I think it requires some refactoring in customdata.cc to ensure any array pointer changes go along with size changes. Maybe affects some public API functions as well, where maybe it needs to then pass an Array instead of a pointer.
Member

if this was a C++ data type this would be a Vector

Not quite. ImplicitSharingInfo serves as the ownership here. data just serves as quick access, pointing to any contiguous array. At the very least it should be stored in CustomData, not CustomDataLayer.

Anyway, improvements to PointerRNA should make this change unnecessary.

>if this was a C++ data type this would be a Vector Not quite. `ImplicitSharingInfo` serves as the ownership here. `data` just serves as quick access, pointing to any contiguous array. At the very least it should be stored in `CustomData`, not `CustomDataLayer`. Anyway, improvements to `PointerRNA` should make this change unnecessary.

I think you'd still have some way of freeing the data in a destructor, which for some data types requires the length. To me it's strange for a data structure to not know the length of its own array members, and instead passing it into various CustomData_ API functions.

I think you'd still have some way of freeing the data in a destructor, which for some data types requires the length. To me it's strange for a data structure to not know the length of its own array members, and instead passing it into various `CustomData_` API functions.
Member

ImplicitSharingInfo handles the destruction itself. All CustomData should have to do is decrement the user count. But your overall point is fair.

`ImplicitSharingInfo` handles the destruction itself. All `CustomData` should have to do is decrement the user count. But your overall point is fair.
@ -0,0 +425,4 @@
char buffer_format_a;
char buffer_format_b;
std::string description;
std::function<PyObject *(void *, int)> get_attribute;
Member

Use simpler types here: function pointers and StringRef will do the trick and will be faster. That said, I'm not sure about passing everything through a function pointer. The indirection seems unnecessarily complex. Why not just use a switch statement?

Use simpler types here: function pointers and `StringRef` will do the trick and will be faster. That said, I'm not sure about passing everything through a function pointer. The indirection seems unnecessarily complex. Why not just use a switch statement?
SietseB marked this conversation as resolved
@ -0,0 +429,4 @@
std::function<bool(void *, int, PyObject *)> set_attribute;
};
std::unordered_map<int, CustomDataTypeMapping> data_types{
Member

Use Map in Blender code rather than the std library types

Use `Map` in Blender code rather than the std library types
SietseB marked this conversation as resolved
Sietse Brouwer added 1 commit 2024-05-23 00:03:26 +02:00

Repeating my comment from devtalk:

From a planning point of view, I think the first priority should be wrapping the attributes the same as it works for meshes and curves now. Improving the array access for attributes in general would be great but should not be a blocker for GPv3.

There is an issue to solve there regarding efficiently looking up the array length with multiple drawings. Also as a first step there I suggest to implement the inefficient solution (looping through all drawings and their custom data layers), and then work on making it faster.

Repeating my comment from devtalk: > From a planning point of view, I think the first priority should be wrapping the attributes the same as it works for meshes and curves now. Improving the array access for attributes in general would be great but should not be a blocker for GPv3. > > There is an issue to solve there regarding efficiently looking up the array length with multiple drawings. Also as a first step there I suggest to implement the inefficient solution (looping through all drawings and their custom data layers), and then work on making it faster.

I'm not convinced it's worth it for us to maintain such an API outside of RNA. Some of these improvements would be more generally beneficial applied to RNA itself rather than just one custom type. The need for RNA_struct_is_attribute_array shows how this doesn't fit that well into existing design IMO. At least this should be discussed with core module members before moving forward further.

I think this can be implemented as a generic RNA feature for arrays rather than something attribute specific. It could define a multi dimensional array (like number of elements x 3), set it a vector or color subtype, and have the Python RNA wrapping recognize that.

> I'm not convinced it's worth it for us to maintain such an API outside of RNA. Some of these improvements would be more generally beneficial applied to RNA itself rather than just one custom type. The need for `RNA_struct_is_attribute_array` shows how this doesn't fit that well into existing design IMO. At least this should be discussed with core module members before moving forward further. I think this can be implemented as a generic RNA feature for arrays rather than something attribute specific. It could define a multi dimensional array (like number of elements x 3), set it a vector or color subtype, and have the Python RNA wrapping recognize that.
This pull request has changes conflicting with the target branch.
  • source/blender/python/intern/bpy.cc
  • source/blender/python/intern/bpy_interface.cc
  • source/blender/python/intern/bpy_rna.cc

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u core-rna-python-attribute-array:SietseB-core-rna-python-attribute-array
git checkout SietseB-core-rna-python-attribute-array
Sign in to join this conversation.
No reviewers
No Label
Interest
Alembic
Interest
Animation & Rigging
Interest
Asset System
Interest
Audio
Interest
Automated Testing
Interest
Blender Asset Bundle
Interest
BlendFile
Interest
Code Documentation
Interest
Collada
Interest
Compatibility
Interest
Compositing
Interest
Core
Interest
Cycles
Interest
Dependency Graph
Interest
Development Management
Interest
EEVEE
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 & 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
Viewport & EEVEE
Interest
Virtual Reality
Interest
Vulkan
Interest
Wayland
Interest
Workbench
Interest: X11
Legacy
Asset Browser Project
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
Asset System
Module
Core
Module
Development Management
Module
Grease Pencil
Module
Modeling
Module
Nodes & Physics
Module
Pipeline & IO
Module
Platforms, Builds & Tests
Module
Python API
Module
Render & Cycles
Module
Sculpt, Paint & Texture
Module
Triaging
Module
User Interface
Module
VFX & Video
Module
Viewport & EEVEE
Platform
FreeBSD
Platform
Linux
Platform
macOS
Platform
Windows
Severity
High
Severity
Low
Severity
Normal
Severity
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 Assignees
4 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#122091
No description provided.