Python: Geometry: create GeometrySet wrapper for Python
In Geometry Nodes a geometry is represented by a `GeometrySet`. This is a container that can contain one geometry of each of the supported types (mesh, curves, volume, grease pencil, pointcloud, instances). It's possible for a `GeometrySet` to contain e.g. a mesh and a point cloud. This patch creates a Python wrapper for the built-in `GeometrySet`. For now, it's main purpose is to consume the complete evaluated geometry of an object without having to go through complex hoops via `depsgraph.object_instances`. It also also allows retrieving instances that have been created with legacy instancing systems such as dupli-verts or particles. In the future, the `GeometrySet` API could also be used for more kinds of geometry processing from Python, similar to how we use `GeometrySet` internally as generic geometry storage. Since we can't really have constness guarantees in Python currently, it's enforced that the `GeometrySet` wrapper always has its own copy of each geometry type (so e.g. it does not share a `Mesh` data-block pointer with any other place in Blender). Without the copy, changes to the mesh in the geometry set would also affect the evaluated geometry that Blender sees. The copy has a small cost, but typically the overhead should be low, because attributes and other run-time data can still be shared. This should be entirely thread-safe, assuming that no code modifies implicitly shared data, which is forbidden. For historic reasons there are still cases like #132423 where this assumption does not hold in all cases. Those cases should be fixed. To my knowledge, this patch does not introduce any new such issues or makes existing issues worse. Pull Request: https://projects.blender.org/blender/blender/pulls/135318
This commit is contained in:
parent
2ad17b956a
commit
29fddf4710
53
doc/python_api/examples/bpy.types.GeometrySet.py
Normal file
53
doc/python_api/examples/bpy.types.GeometrySet.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""
|
||||
Accessing Evaluated Geometry
|
||||
++++++++++++++++++++++++++++
|
||||
"""
|
||||
import bpy
|
||||
|
||||
# The GeometrySet can only be retrieved from an evaluated object. So one always
|
||||
# needs a depsgraph that has evaluated the object.
|
||||
depsgraph = bpy.context.view_layer.depsgraph
|
||||
ob = bpy.context.active_object
|
||||
ob_eval = depsgraph.id_eval_get(ob)
|
||||
|
||||
# Get the final evaluated geometry of an object.
|
||||
geometry = ob_eval.evaluated_geometry()
|
||||
|
||||
# Print basic information like the number of elements.
|
||||
print(geometry)
|
||||
|
||||
# A geometry set may have a name. It can be set with the Set Geometry Name node.
|
||||
print(geometry.name)
|
||||
|
||||
# Access "realized" geometry components.
|
||||
print(geometry.mesh)
|
||||
print(geometry.pointcloud)
|
||||
print(geometry.curves)
|
||||
print(geometry.volume)
|
||||
print(geometry.grease_pencil)
|
||||
|
||||
# Access the mesh without final subdivision applied.
|
||||
print(geometry.mesh_base)
|
||||
|
||||
# Accessing instances is a bit more tricky, because there is no specific
|
||||
# mechanism to expose instances. Instead, two accessors are provided which
|
||||
# are easy to keep working in the future even if we get a proper Instances type.
|
||||
|
||||
# This is a pointcloud that provides access to all the instance attributes.
|
||||
# There is a point per instances. May return None if there is no instances data.
|
||||
instances_pointcloud = geometry.instances_pointcloud()
|
||||
|
||||
if instances_pointcloud is not None:
|
||||
# This is a list containing the data that is instanced. The list may contain
|
||||
# None, objects, collections or other GeometrySets. If the geometry does not
|
||||
# have instances, the list is empty.
|
||||
references = geometry.instance_references()
|
||||
|
||||
# Besides normal generic attributes, there are also two important
|
||||
# instance-specific attributes. "instance_transform" is a 4x4 matrix attribute
|
||||
# containing the transforms of each instance.
|
||||
instance_transforms = instances_pointcloud.attributes["instance_transform"]
|
||||
|
||||
# ".reference_index" contains indices into the `references` list above and
|
||||
# determines what geometry each instance uses.
|
||||
reference_indices = instances_pointcloud.attributes[".reference_index"]
|
@ -1123,49 +1123,52 @@ def pymodule2sphinx(basepath, module_name, module, title, module_all_extra):
|
||||
if heading:
|
||||
fw(title_string(heading, heading_char))
|
||||
|
||||
# May need to be its own function.
|
||||
if value.__doc__:
|
||||
if value.__doc__.startswith(".. class::"):
|
||||
fw(value.__doc__)
|
||||
else:
|
||||
fw(".. class:: {:s}\n\n".format(type_name))
|
||||
write_indented_lines(" ", fw, value.__doc__, True)
|
||||
else:
|
||||
fw(".. class:: {:s}\n\n".format(type_name))
|
||||
fw("\n")
|
||||
|
||||
write_example_ref(" ", fw, module_name + "." + type_name)
|
||||
|
||||
descr_items = [(key, descr) for key, descr in sorted(value.__dict__.items()) if not key.startswith("_")]
|
||||
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == ClassMethodDescriptorType:
|
||||
py_descr2sphinx(" ", fw, descr, module_name, type_name, key)
|
||||
|
||||
# Needed for pure Python classes.
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == FunctionType:
|
||||
pyfunc2sphinx(" ", fw, module_name, type_name, key, descr, is_class=True)
|
||||
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == MethodDescriptorType:
|
||||
py_descr2sphinx(" ", fw, descr, module_name, type_name, key)
|
||||
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == GetSetDescriptorType:
|
||||
py_descr2sphinx(" ", fw, descr, module_name, type_name, key)
|
||||
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == StaticMethodType:
|
||||
descr = getattr(value, key)
|
||||
write_indented_lines(" ", fw, descr.__doc__ or "Undocumented", False)
|
||||
fw("\n")
|
||||
|
||||
fw("\n\n")
|
||||
pyclass2sphinx(fw, module_name, type_name, value)
|
||||
|
||||
file.close()
|
||||
|
||||
|
||||
def pyclass2sphinx(fw, module_name, type_name, value):
|
||||
if value.__doc__:
|
||||
if value.__doc__.startswith(".. class::"):
|
||||
fw(value.__doc__)
|
||||
else:
|
||||
fw(".. class:: {:s}.{:s}\n\n".format(module_name, type_name))
|
||||
write_indented_lines(" ", fw, value.__doc__, True)
|
||||
else:
|
||||
fw(".. class:: {:s}.{:s}\n\n".format(module_name, type_name))
|
||||
fw("\n")
|
||||
|
||||
write_example_ref(" ", fw, module_name + "." + type_name)
|
||||
|
||||
descr_items = [(key, descr) for key, descr in sorted(value.__dict__.items()) if not key.startswith("_")]
|
||||
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == ClassMethodDescriptorType:
|
||||
py_descr2sphinx(" ", fw, descr, module_name, type_name, key)
|
||||
|
||||
# Needed for pure Python classes.
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == FunctionType:
|
||||
pyfunc2sphinx(" ", fw, module_name, type_name, key, descr, is_class=True)
|
||||
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == MethodDescriptorType:
|
||||
py_descr2sphinx(" ", fw, descr, module_name, type_name, key)
|
||||
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == GetSetDescriptorType:
|
||||
py_descr2sphinx(" ", fw, descr, module_name, type_name, key)
|
||||
|
||||
for key, descr in descr_items:
|
||||
if type(descr) == StaticMethodType:
|
||||
descr = getattr(value, key)
|
||||
write_indented_lines(" ", fw, descr.__doc__ or "Undocumented", False)
|
||||
fw("\n")
|
||||
|
||||
fw("\n\n")
|
||||
|
||||
|
||||
# Changes In Blender will force errors here.
|
||||
context_type_map = {
|
||||
# Support multiple types for each item, where each list item is a possible type:
|
||||
@ -2101,6 +2104,23 @@ def write_rst_ops_index(basepath):
|
||||
file.close()
|
||||
|
||||
|
||||
def write_rst_geometry_set(basepath):
|
||||
"""
|
||||
Write the RST file for ``bpy.types.GeometrySet``.
|
||||
"""
|
||||
if 'bpy.types.GeometrySet' in EXCLUDE_MODULES:
|
||||
return
|
||||
|
||||
# Write the index.
|
||||
filepath = os.path.join(basepath, "bpy.types.GeometrySet.rst")
|
||||
file = open(filepath, "w", encoding="utf-8")
|
||||
fw = file.write
|
||||
fw(title_string("GeometrySet", "="))
|
||||
pyclass2sphinx(fw, "bpy.types", "GeometrySet", bpy.types.GeometrySet)
|
||||
|
||||
EXAMPLE_SET_USED.add("bpy.types.GeometrySet")
|
||||
|
||||
|
||||
def write_rst_msgbus(basepath):
|
||||
"""
|
||||
Write the RST files of ``bpy.msgbus`` module
|
||||
@ -2410,6 +2430,7 @@ def rna2sphinx(basepath):
|
||||
write_rst_types_index(basepath) # `bpy.types`.
|
||||
write_rst_ops_index(basepath) # `bpy.ops`.
|
||||
write_rst_msgbus(basepath) # `bpy.msgbus`.
|
||||
write_rst_geometry_set(basepath) # `bpy.types.GeometrySet`.
|
||||
pyrna2sphinx(basepath) # `bpy.types.*` & `bpy.ops.*`.
|
||||
write_rst_data(basepath) # `bpy.data`.
|
||||
write_rst_importable_modules(basepath)
|
||||
|
@ -254,6 +254,17 @@ class Object(_types.ID):
|
||||
return tuple(scene for scene in bpy.data.scenes
|
||||
if self in scene.objects[:])
|
||||
|
||||
def evaluated_geometry(self):
|
||||
"""
|
||||
Get the evaluated geometry set of this evaluated object. This only works for
|
||||
objects that contain geometry data like meshes and curves but not e.g. cameras.
|
||||
|
||||
:return: The evaluated geometry.
|
||||
:rtype: :class:`bpy.types.GeometrySet`
|
||||
"""
|
||||
from bpy.types import GeometrySet
|
||||
return GeometrySet.from_evaluated_object(self)
|
||||
|
||||
|
||||
class WindowManager(_types.ID):
|
||||
__slots__ = ()
|
||||
|
@ -8,6 +8,9 @@
|
||||
* \ingroup bke
|
||||
*/
|
||||
|
||||
#include "BKE_geometry_set.hh"
|
||||
#include "BKE_instances.hh"
|
||||
|
||||
struct Depsgraph;
|
||||
struct ID;
|
||||
struct ListBase;
|
||||
@ -16,9 +19,6 @@ struct ParticleSystem;
|
||||
struct Scene;
|
||||
struct ViewLayer;
|
||||
struct ViewerPath;
|
||||
namespace blender::bke {
|
||||
struct GeometrySet;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------- */
|
||||
/* Dupli-Geometry */
|
||||
@ -36,6 +36,22 @@ ListBase *object_duplilist_preview(Depsgraph *depsgraph,
|
||||
const ViewerPath *viewer_path);
|
||||
void free_object_duplilist(ListBase *lb);
|
||||
|
||||
/**
|
||||
* Get the legacy instances of this object. That includes instances coming from these sources:
|
||||
* - Particles
|
||||
* - Dupli Verts
|
||||
* - Dupli Faces
|
||||
* - "Objects as Font"
|
||||
*
|
||||
* This does not include collection instances which are not considered legacy and should be treated
|
||||
* properly at a higher level.
|
||||
*
|
||||
* Also see #get_dupli_generator for the different existing dupli generators.
|
||||
*/
|
||||
blender::bke::Instances object_duplilist_legacy_instances(Depsgraph &depsgraph,
|
||||
Scene &scene,
|
||||
Object &ob);
|
||||
|
||||
constexpr int MAX_DUPLI_RECUR = 8;
|
||||
|
||||
struct DupliObject {
|
||||
@ -49,6 +65,8 @@ struct DupliObject {
|
||||
|
||||
short type; /* From #Object::transflag. */
|
||||
char no_draw;
|
||||
/** Depth in the instance hierarchy. */
|
||||
int8_t level;
|
||||
/* If this dupli object is belongs to a preview, this is non-null. */
|
||||
const blender::bke::GeometrySet *preview_base_geometry;
|
||||
/* Index of the top-level instance this dupli is part of or -1 when unused. */
|
||||
|
@ -271,6 +271,7 @@ static DupliObject *make_dupli(const DupliContext *ctx,
|
||||
dob->type = ctx->gen == nullptr ? 0 : ctx->dupli_gen_type_stack->last();
|
||||
dob->preview_base_geometry = ctx->preview_base_geometry;
|
||||
dob->preview_instance_index = ctx->preview_instance_index;
|
||||
dob->level = ctx->level;
|
||||
|
||||
/* Set persistent id, which is an array with a persistent index for each level
|
||||
* (particle number, vertex number, ..). by comparing this we can find the same
|
||||
@ -1836,6 +1837,80 @@ ListBase *object_duplilist_preview(Depsgraph *depsgraph,
|
||||
return duplilist;
|
||||
}
|
||||
|
||||
blender::bke::Instances object_duplilist_legacy_instances(Depsgraph &depsgraph,
|
||||
Scene &scene,
|
||||
Object &ob)
|
||||
{
|
||||
using namespace blender;
|
||||
|
||||
ListBase *duplilist = MEM_callocN<ListBase>("duplilist");
|
||||
DupliContext ctx;
|
||||
Vector<Object *> instance_stack({&ob});
|
||||
Vector<short> dupli_gen_type_stack({0});
|
||||
|
||||
init_context(&ctx, &depsgraph, &scene, &ob, nullptr, instance_stack, dupli_gen_type_stack);
|
||||
if (ctx.gen == &gen_dupli_geometry_set) {
|
||||
/* These are not legacy instances. */
|
||||
return {};
|
||||
}
|
||||
if (ctx.gen) {
|
||||
ctx.duplilist = duplilist;
|
||||
ctx.gen->make_duplis(&ctx);
|
||||
}
|
||||
const bool is_particle_duplis = ctx.gen == &gen_dupli_particles;
|
||||
/* Particle instances are on the second level, because the first level is the particle system
|
||||
* itself. */
|
||||
const int level_to_use = is_particle_duplis ? 1 : 0;
|
||||
|
||||
Vector<DupliObject *> top_level_duplis;
|
||||
LISTBASE_FOREACH (DupliObject *, dob, duplilist) {
|
||||
BLI_assert(dob->ob != &ob);
|
||||
/* We only need the top level instances in the end, because when #Instances references an
|
||||
* object, it implicitly also references all instances of that object. */
|
||||
if (dob->level == level_to_use) {
|
||||
top_level_duplis.append(dob);
|
||||
}
|
||||
}
|
||||
|
||||
bke::Instances top_level_instances;
|
||||
const float4x4 &world_to_object = ob.world_to_object();
|
||||
|
||||
VectorSet<Object *> referenced_objects;
|
||||
const int instances_num = top_level_duplis.size();
|
||||
top_level_instances.resize(instances_num);
|
||||
MutableSpan<float4x4> instances_transforms = top_level_instances.transforms_for_write();
|
||||
MutableSpan<int> instances_reference_handles = top_level_instances.reference_handles_for_write();
|
||||
bke::SpanAttributeWriter<int> instances_ids =
|
||||
top_level_instances.attributes_for_write().lookup_or_add_for_write_only_span<int>(
|
||||
"id", bke::AttrDomain::Instance);
|
||||
for (const int i : IndexRange(instances_num)) {
|
||||
DupliObject &dob = *top_level_duplis[i];
|
||||
Object &instanced_object = *dob.ob;
|
||||
if (referenced_objects.add(&instanced_object)) {
|
||||
top_level_instances.add_new_reference(instanced_object);
|
||||
}
|
||||
const int handle = referenced_objects.index_of(&instanced_object);
|
||||
instances_transforms[i] = world_to_object * float4x4(dob.mat);
|
||||
instances_reference_handles[i] = handle;
|
||||
|
||||
int id = dob.persistent_id[0];
|
||||
if (is_particle_duplis) {
|
||||
const int particle_system_i = dob.persistent_id[0];
|
||||
const int particle_i = dob.persistent_id[1];
|
||||
/* Attempt to build a unique ID for each particle. This allows for unique ids as long as
|
||||
* there are not more than <= 2^26 = 67.108.864 particles per particle system and there are
|
||||
* <= 2^6 = 64 particle systems. Otherwise there will be duplicate IDs but this is quite
|
||||
* unlikely in the legacy particle system. */
|
||||
id = (particle_system_i << 26) + particle_i;
|
||||
}
|
||||
instances_ids.span[i] = id;
|
||||
}
|
||||
instances_ids.finish();
|
||||
|
||||
free_object_duplilist(duplilist);
|
||||
return top_level_instances;
|
||||
}
|
||||
|
||||
void free_object_duplilist(ListBase *lb)
|
||||
{
|
||||
BLI_freelistN(lb);
|
||||
|
@ -36,6 +36,7 @@ set(SRC
|
||||
bpy_capi_utils.cc
|
||||
bpy_cli_command.cc
|
||||
bpy_driver.cc
|
||||
bpy_geometry_set.cc
|
||||
bpy_gizmo_wrap.cc
|
||||
bpy_interface.cc
|
||||
bpy_interface_atexit.cc
|
||||
@ -84,6 +85,7 @@ set(SRC
|
||||
bpy_capi_utils.hh
|
||||
bpy_cli_command.hh
|
||||
bpy_driver.hh
|
||||
bpy_geometry_set.hh
|
||||
bpy_gizmo_wrap.hh
|
||||
bpy_intern_string.hh
|
||||
bpy_library.hh
|
||||
|
@ -36,6 +36,7 @@
|
||||
#include "bpy_app.hh"
|
||||
#include "bpy_cli_command.hh"
|
||||
#include "bpy_driver.hh"
|
||||
#include "bpy_geometry_set.hh"
|
||||
#include "bpy_library.hh"
|
||||
#include "bpy_operator.hh"
|
||||
#include "bpy_props.hh"
|
||||
@ -744,6 +745,7 @@ void BPy_init_modules(bContext *C)
|
||||
|
||||
/* needs to be first so bpy_types can run */
|
||||
PyObject *bpy_types = BPY_rna_types();
|
||||
PyModule_AddObject(bpy_types, "GeometrySet", BPyInit_geometry_set_type());
|
||||
PyModule_AddObject(mod, "types", bpy_types);
|
||||
|
||||
/* needs to be first so bpy_types can run */
|
||||
|
457
source/blender/python/intern/bpy_geometry_set.cc
Normal file
457
source/blender/python/intern/bpy_geometry_set.cc
Normal file
@ -0,0 +1,457 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "BKE_duplilist.hh"
|
||||
#include "BKE_geometry_set.hh"
|
||||
#include "BKE_geometry_set_instances.hh"
|
||||
#include "BKE_idtype.hh"
|
||||
#include "BKE_instances.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_mesh_wrapper.hh"
|
||||
#include "BKE_pointcloud.hh"
|
||||
|
||||
#include "DEG_depsgraph_query.hh"
|
||||
|
||||
#include "DNA_ID.h"
|
||||
#include "DNA_collection_types.h"
|
||||
#include "DNA_mesh_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
#include "DNA_pointcloud_types.h"
|
||||
|
||||
#include "RNA_enum_types.hh"
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
#include "bpy_geometry_set.hh"
|
||||
#include "bpy_rna.hh"
|
||||
|
||||
using blender::bke::GeometrySet;
|
||||
|
||||
extern PyTypeObject bpy_geometry_set_Type;
|
||||
|
||||
struct BPy_GeometrySet {
|
||||
PyObject_HEAD
|
||||
GeometrySet geometry;
|
||||
PointCloud *instances_pointcloud;
|
||||
};
|
||||
|
||||
static BPy_GeometrySet *python_object_from_geometry_set(GeometrySet geometry = {})
|
||||
{
|
||||
BPy_GeometrySet *self = reinterpret_cast<BPy_GeometrySet *>(
|
||||
bpy_geometry_set_Type.tp_alloc(&bpy_geometry_set_Type, 0));
|
||||
if (self == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
new (&self->geometry) GeometrySet(std::move(geometry));
|
||||
self->instances_pointcloud = nullptr;
|
||||
/* We can't safely give access to shared geometries via the Python API currently, because
|
||||
* constness can't be enforced. Therefore, ensure that this Python object has its own copy of
|
||||
* each data-block. Note that attributes may still be shared with other data in Blender. */
|
||||
self->geometry.ensure_no_shared_components();
|
||||
return self;
|
||||
}
|
||||
|
||||
static BPy_GeometrySet *BPy_GeometrySet_new(PyTypeObject * /*type*/,
|
||||
PyObject * /*args*/,
|
||||
PyObject * /*kwds*/)
|
||||
{
|
||||
return python_object_from_geometry_set();
|
||||
}
|
||||
|
||||
static void BPy_GeometrySet_dealloc(BPy_GeometrySet *self)
|
||||
{
|
||||
std::destroy_at(&self->geometry);
|
||||
if (self->instances_pointcloud) {
|
||||
BKE_id_free(nullptr, self->instances_pointcloud);
|
||||
}
|
||||
Py_TYPE(self)->tp_free(reinterpret_cast<PyObject *>(self));
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_from_evaluated_object_doc,
|
||||
".. staticmethod:: from_evaluated_object(evaluated_object)\n"
|
||||
"\n"
|
||||
" Create a geometry set from the evaluated geometry of an evaluated object.\n"
|
||||
" Typically, it's more convenient to use :func:`bpy.types.Object.evaluated_geometry`.\n"
|
||||
"\n"
|
||||
" :arg evaluated_object: The evaluated object to create a geometry set from.\n"
|
||||
" :type evaluated_object: bpy.types.Object\n");
|
||||
static BPy_GeometrySet *BPy_GeometrySet_static_from_evaluated_object(PyObject * /*self*/,
|
||||
PyObject *args,
|
||||
PyObject *kwds)
|
||||
{
|
||||
using namespace blender;
|
||||
static const char *kwlist[] = {"evaluated_object", nullptr};
|
||||
PyObject *py_evaluated_object;
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwds, "O", const_cast<char **>(kwlist), &py_evaluated_object))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
ID *evaluated_object_id = nullptr;
|
||||
if (!pyrna_id_FromPyObject(py_evaluated_object, &evaluated_object_id)) {
|
||||
PyErr_Format(
|
||||
PyExc_TypeError, "Expected an Object, not %.200s", Py_TYPE(py_evaluated_object)->tp_name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (GS(evaluated_object_id->name) != ID_OB) {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"Expected an Object, not %.200s",
|
||||
BKE_idtype_idcode_to_name(GS(evaluated_object_id->name)));
|
||||
return nullptr;
|
||||
}
|
||||
Object *evaluated_object = reinterpret_cast<Object *>(evaluated_object_id);
|
||||
if (!DEG_is_evaluated_object(evaluated_object)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Expected an evaluated object");
|
||||
return nullptr;
|
||||
}
|
||||
const bool is_instance_collection = evaluated_object->type == OB_EMPTY &&
|
||||
evaluated_object->instance_collection;
|
||||
const bool valid_object_type = OB_TYPE_IS_GEOMETRY(evaluated_object->type) ||
|
||||
is_instance_collection;
|
||||
if (!valid_object_type) {
|
||||
const char *ob_type_name = "<unknown>";
|
||||
RNA_enum_name_from_value(rna_enum_object_type_items, evaluated_object->type, &ob_type_name);
|
||||
PyErr_Format(PyExc_TypeError, "Expected a geometry object, not %.200s", ob_type_name);
|
||||
return nullptr;
|
||||
}
|
||||
if (!DEG_object_geometry_is_evaluated(*evaluated_object)) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"Object geometry is not yet evaluated, is the depsgraph evaluated?");
|
||||
return nullptr;
|
||||
}
|
||||
Depsgraph *depsgraph = DEG_get_depsgraph_by_id(*evaluated_object_id);
|
||||
if (!depsgraph) {
|
||||
PyErr_SetString(PyExc_TypeError, "Object is not owned by a depsgraph");
|
||||
return nullptr;
|
||||
}
|
||||
Scene *scene = DEG_get_input_scene(depsgraph);
|
||||
|
||||
GeometrySet geometry;
|
||||
if (is_instance_collection) {
|
||||
bke::Instances *instances = new bke::Instances();
|
||||
instances->add_new_reference(bke::InstanceReference{*evaluated_object->instance_collection});
|
||||
instances->add_instance(0, float4x4::identity());
|
||||
geometry.replace_instances(instances);
|
||||
}
|
||||
else {
|
||||
bke::Instances instances = object_duplilist_legacy_instances(
|
||||
*depsgraph, *scene, *evaluated_object);
|
||||
geometry = bke::object_get_evaluated_geometry_set(*evaluated_object, false);
|
||||
if (instances.instances_num() > 0) {
|
||||
geometry.replace_instances(new bke::Instances(std::move(instances)));
|
||||
}
|
||||
}
|
||||
BPy_GeometrySet *self = python_object_from_geometry_set(std::move(geometry));
|
||||
return self;
|
||||
}
|
||||
|
||||
static PyObject *BPy_GeometrySet_repr(BPy_GeometrySet *self)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << self->geometry;
|
||||
std::string str = ss.str();
|
||||
return PyUnicode_FromString(str.c_str());
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_get_instances_pointcloud_doc,
|
||||
".. method:: instances_pointcloud()\n"
|
||||
"\n"
|
||||
" Get a pointcloud that encodes information about the instances of the geometry.\n"
|
||||
" The returned pointcloud should not be modified.\n"
|
||||
" There is a point per instance and per-instance data is stored in point attributes.\n"
|
||||
" The local transforms are stored in the ``instance_transform`` attribute.\n"
|
||||
" The data instanced by each point is referenced by the ``.reference_index`` attribute,\n"
|
||||
" indexing into the list returned by :func:`bpy.types.GeometrySet.instance_references`.\n"
|
||||
"\n"
|
||||
" :rtype: bpy.types.PointCloud\n");
|
||||
static PyObject *BPy_GeometrySet_get_instances_pointcloud(BPy_GeometrySet *self)
|
||||
{
|
||||
using namespace blender;
|
||||
const bke::Instances *instances = self->geometry.get_instances();
|
||||
if (!instances) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
if (self->instances_pointcloud == nullptr) {
|
||||
const int instances_num = instances->instances_num();
|
||||
PointCloud *pointcloud = BKE_pointcloud_new_nomain(instances_num);
|
||||
bke::gather_attributes(instances->attributes(),
|
||||
bke::AttrDomain::Instance,
|
||||
bke::AttrDomain::Point,
|
||||
{},
|
||||
IndexMask(instances_num),
|
||||
pointcloud->attributes_for_write());
|
||||
self->instances_pointcloud = pointcloud;
|
||||
}
|
||||
return pyrna_id_CreatePyObject(&self->instances_pointcloud->id);
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_get_instance_references_doc,
|
||||
".. method:: instance_references()\n"
|
||||
"\n"
|
||||
" This returns a list of geometries that is indexed by the ``.reference_index``\n"
|
||||
" attribute of the pointcloud returned by \n"
|
||||
" :func:`bpy.types.GeometrySet.instances_pointcloud`.\n"
|
||||
" It may contain other geometry sets, objects, collections and None values.\n"
|
||||
"\n"
|
||||
" :rtype: list[None | bpy.types.Object | bpy.types.Collection | bpy.types.GeometrySet]\n");
|
||||
static PyObject *BPy_GeometrySet_get_instance_references(BPy_GeometrySet *self)
|
||||
{
|
||||
using namespace blender;
|
||||
const bke::Instances *instances = self->geometry.get_instances();
|
||||
if (!instances) {
|
||||
return PyList_New(0);
|
||||
}
|
||||
const Span<bke::InstanceReference> references = instances->references();
|
||||
PyObject *py_references = PyList_New(references.size());
|
||||
for (const int i : references.index_range()) {
|
||||
const bke::InstanceReference &reference = references[i];
|
||||
switch (reference.type()) {
|
||||
case bke::InstanceReference::Type::None: {
|
||||
PyList_SET_ITEM(py_references, i, Py_NewRef(Py_None));
|
||||
break;
|
||||
}
|
||||
case bke::InstanceReference::Type::Object: {
|
||||
Object &object = reference.object();
|
||||
PyList_SET_ITEM(py_references, i, pyrna_id_CreatePyObject(&object.id));
|
||||
break;
|
||||
}
|
||||
case bke::InstanceReference::Type::Collection: {
|
||||
Collection &collection = reference.collection();
|
||||
PyList_SET_ITEM(py_references, i, pyrna_id_CreatePyObject(&collection.id));
|
||||
break;
|
||||
}
|
||||
case bke::InstanceReference::Type::GeometrySet: {
|
||||
const bke::GeometrySet &geometry_set = reference.geometry_set();
|
||||
PyList_SET_ITEM(py_references, i, python_object_from_geometry_set(geometry_set));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return py_references;
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_name_doc,
|
||||
"The name of the geometry set.\n"
|
||||
"\n"
|
||||
":type: str\n");
|
||||
static PyObject *BPy_GeometrySet_get_name(BPy_GeometrySet *self, void * /*closure*/)
|
||||
{
|
||||
return PyUnicode_FromString(self->geometry.name.c_str());
|
||||
}
|
||||
|
||||
static int BPy_GeometrySet_set_name(BPy_GeometrySet *self, PyObject *value, void * /*closure*/)
|
||||
{
|
||||
if (!PyUnicode_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "expected a string");
|
||||
return -1;
|
||||
}
|
||||
const char *name = PyUnicode_AsUTF8(value);
|
||||
self->geometry.name = name ? name : "";
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_mesh_doc,
|
||||
"The mesh data-block in the geometry set.\n"
|
||||
"\n"
|
||||
":type: :class:`bpy.types.Mesh`\n");
|
||||
static PyObject *BPy_GeometrySet_get_mesh(BPy_GeometrySet *self, void * /*closure*/)
|
||||
{
|
||||
Mesh *base_mesh = self->geometry.get_mesh_for_write();
|
||||
Mesh *mesh = BKE_mesh_wrapper_ensure_subdivision(base_mesh);
|
||||
return pyrna_id_CreatePyObject(reinterpret_cast<ID *>(mesh));
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_mesh_base_doc,
|
||||
"The mesh data-block in the geometry set without final subdivision.\n"
|
||||
"\n"
|
||||
":type: :class:`bpy.types.Mesh`\n");
|
||||
static PyObject *BPy_GeometrySet_get_mesh_base(BPy_GeometrySet *self, void * /*closure*/)
|
||||
{
|
||||
Mesh *base_mesh = self->geometry.get_mesh_for_write();
|
||||
return pyrna_id_CreatePyObject(reinterpret_cast<ID *>(base_mesh));
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_pointcloud_doc,
|
||||
"The point cloud data-block in the geometry set.\n"
|
||||
"\n"
|
||||
":type: :class:`bpy.types.PointCloud`\n");
|
||||
static PyObject *BPy_GeometrySet_get_pointcloud(BPy_GeometrySet *self, void * /*closure*/)
|
||||
{
|
||||
return pyrna_id_CreatePyObject(
|
||||
reinterpret_cast<ID *>(self->geometry.get_pointcloud_for_write()));
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_curves_doc,
|
||||
"The curves data-block in the geometry set.\n"
|
||||
"\n"
|
||||
":type: :class:`bpy.types.Curves`\n");
|
||||
static PyObject *BPy_GeometrySet_get_curves(BPy_GeometrySet *self, void * /*closure*/)
|
||||
{
|
||||
return pyrna_id_CreatePyObject(reinterpret_cast<ID *>(self->geometry.get_curves_for_write()));
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_volume_doc,
|
||||
"The volume data-block in the geometry set.\n"
|
||||
"\n"
|
||||
":type: :class:`bpy.types.Volume`\n");
|
||||
static PyObject *BPy_GeometrySet_get_volume(BPy_GeometrySet *self, void * /*closure*/)
|
||||
{
|
||||
return pyrna_id_CreatePyObject(reinterpret_cast<ID *>(self->geometry.get_volume_for_write()));
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_grease_pencil_doc,
|
||||
"The Grease Pencil data-block in the geometry set.\n"
|
||||
"\n"
|
||||
":type: :class:`bpy.types.GreasePencilv3`\n");
|
||||
static PyObject *BPy_GeometrySet_get_grease_pencil(BPy_GeometrySet *self, void * /*closure*/)
|
||||
{
|
||||
return pyrna_id_CreatePyObject(
|
||||
reinterpret_cast<ID *>(self->geometry.get_grease_pencil_for_write()));
|
||||
}
|
||||
|
||||
static PyGetSetDef BPy_GeometrySet_getseters[] = {
|
||||
{
|
||||
"name",
|
||||
reinterpret_cast<getter>(BPy_GeometrySet_get_name),
|
||||
reinterpret_cast<setter>(BPy_GeometrySet_set_name),
|
||||
bpy_geometry_set_name_doc,
|
||||
nullptr,
|
||||
},
|
||||
{
|
||||
"mesh",
|
||||
reinterpret_cast<getter>(BPy_GeometrySet_get_mesh),
|
||||
nullptr,
|
||||
bpy_geometry_set_mesh_doc,
|
||||
nullptr,
|
||||
},
|
||||
{
|
||||
"mesh_base",
|
||||
reinterpret_cast<getter>(BPy_GeometrySet_get_mesh_base),
|
||||
nullptr,
|
||||
bpy_geometry_set_mesh_base_doc,
|
||||
nullptr,
|
||||
},
|
||||
{
|
||||
"pointcloud",
|
||||
reinterpret_cast<getter>(BPy_GeometrySet_get_pointcloud),
|
||||
nullptr,
|
||||
bpy_geometry_set_pointcloud_doc,
|
||||
nullptr,
|
||||
},
|
||||
{
|
||||
"curves",
|
||||
reinterpret_cast<getter>(BPy_GeometrySet_get_curves),
|
||||
nullptr,
|
||||
bpy_geometry_set_curves_doc,
|
||||
nullptr,
|
||||
},
|
||||
{
|
||||
"volume",
|
||||
reinterpret_cast<getter>(BPy_GeometrySet_get_volume),
|
||||
nullptr,
|
||||
bpy_geometry_set_volume_doc,
|
||||
nullptr,
|
||||
},
|
||||
{
|
||||
"grease_pencil",
|
||||
reinterpret_cast<getter>(BPy_GeometrySet_get_grease_pencil),
|
||||
nullptr,
|
||||
bpy_geometry_set_grease_pencil_doc,
|
||||
nullptr,
|
||||
},
|
||||
{nullptr},
|
||||
|
||||
};
|
||||
|
||||
static PyMethodDef BPy_GeometrySet_methods[] = {
|
||||
{"from_evaluated_object",
|
||||
reinterpret_cast<PyCFunction>(BPy_GeometrySet_static_from_evaluated_object),
|
||||
METH_VARARGS | METH_KEYWORDS | METH_STATIC,
|
||||
bpy_geometry_set_from_evaluated_object_doc},
|
||||
{"instances_pointcloud",
|
||||
reinterpret_cast<PyCFunction>(BPy_GeometrySet_get_instances_pointcloud),
|
||||
METH_NOARGS,
|
||||
bpy_geometry_set_get_instances_pointcloud_doc},
|
||||
{"instance_references",
|
||||
reinterpret_cast<PyCFunction>(BPy_GeometrySet_get_instance_references),
|
||||
METH_NOARGS,
|
||||
bpy_geometry_set_get_instance_references_doc},
|
||||
{nullptr, nullptr, 0, nullptr},
|
||||
};
|
||||
|
||||
PyDoc_STRVAR(
|
||||
/* Wrap. */
|
||||
bpy_geometry_set_doc,
|
||||
"Stores potentially multiple geometry components of different types.\n"
|
||||
"For example, it might contain a mesh, curves and nested instances.\n");
|
||||
PyTypeObject bpy_geometry_set_Type = {
|
||||
/*ob_base*/ PyVarObject_HEAD_INIT(nullptr, 0)
|
||||
/*tp_name*/ "GeometrySet",
|
||||
/*tp_basicsize*/ sizeof(BPy_GeometrySet),
|
||||
/*tp_itemsize*/ 0,
|
||||
/*tp_dealloc*/ reinterpret_cast<destructor>(BPy_GeometrySet_dealloc),
|
||||
/*tp_vectorcall_offset*/ 0,
|
||||
/*tp_getattr*/ nullptr,
|
||||
/*tp_setattr*/ nullptr,
|
||||
/*tp_as_async*/ nullptr,
|
||||
/*tp_repr*/ reinterpret_cast<reprfunc>(BPy_GeometrySet_repr),
|
||||
/*tp_as_number*/ nullptr,
|
||||
/*tp_as_sequence*/ nullptr,
|
||||
/*tp_as_mapping*/ nullptr,
|
||||
/*tp_hash*/ nullptr,
|
||||
/*tp_call*/ nullptr,
|
||||
/*tp_str*/ nullptr,
|
||||
/*tp_getattro*/ nullptr,
|
||||
/*tp_setattro*/ nullptr,
|
||||
/*tp_as_buffer*/ nullptr,
|
||||
/*tp_flags*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
/*tp_doc*/ bpy_geometry_set_doc,
|
||||
/*tp_traverse*/ nullptr,
|
||||
/*tp_clear*/ nullptr,
|
||||
/*tp_richcompare*/ nullptr,
|
||||
/*tp_weaklistoffset*/ 0,
|
||||
/*tp_iter*/ nullptr,
|
||||
/*tp_iternext*/ nullptr,
|
||||
/*tp_methods*/ BPy_GeometrySet_methods,
|
||||
/*tp_members*/ nullptr,
|
||||
/*tp_getset*/ BPy_GeometrySet_getseters,
|
||||
/*tp_base*/ nullptr,
|
||||
/*tp_dict*/ nullptr,
|
||||
/*tp_descr_get*/ nullptr,
|
||||
/*tp_descr_set*/ nullptr,
|
||||
/*tp_dictoffset*/ 0,
|
||||
/*tp_init*/ nullptr,
|
||||
/*tp_alloc*/ nullptr,
|
||||
/*tp_new*/ reinterpret_cast<newfunc>(BPy_GeometrySet_new),
|
||||
};
|
||||
|
||||
PyObject *BPyInit_geometry_set_type()
|
||||
{
|
||||
if (PyType_Ready(&bpy_geometry_set_Type) < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<PyObject *>(&bpy_geometry_set_Type);
|
||||
}
|
9
source/blender/python/intern/bpy_geometry_set.hh
Normal file
9
source/blender/python/intern/bpy_geometry_set.hh
Normal file
@ -0,0 +1,9 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
PyObject *BPyInit_geometry_set_type();
|
Loading…
x
Reference in New Issue
Block a user