diff --git a/doc/python_api/examples/bpy.types.InlineShaderNodes.py b/doc/python_api/examples/bpy.types.InlineShaderNodes.py new file mode 100644 index 00000000000..584073b97fc --- /dev/null +++ b/doc/python_api/examples/bpy.types.InlineShaderNodes.py @@ -0,0 +1,23 @@ +""" +Inline Shader Nodes ++++++++++++++++++++ +""" +import bpy + +# The materials should be retrieved from the evaluated object to make sure that +# e.g. edits of Geometry Nodes are applied. +depsgraph = bpy.context.view_layer.depsgraph +ob = bpy.context.active_object +ob_eval = depsgraph.id_eval_get(ob) +material_eval = ob_eval.material_slots[0].material + +# Compute the inlined shader nodes. +# Important: Do not loose the reference to this object while accessing the inlined +# node tree. Otherwise there will be a crash due to a dangling pointer. +inline_shader_nodes = material_eval.inline_shader_nodes() + +# Get the actual inlined `bpy.types.NodeTree`. +tree = inline_shader_nodes.node_tree + +for node in tree.nodes: + print(node.name) diff --git a/doc/python_api/sphinx_doc_gen.py b/doc/python_api/sphinx_doc_gen.py index 0164188fe1f..82f2dfbddd0 100644 --- a/doc/python_api/sphinx_doc_gen.py +++ b/doc/python_api/sphinx_doc_gen.py @@ -2189,6 +2189,24 @@ def write_rst_geometry_set(basepath): EXAMPLE_SET_USED.add("bpy.types.GeometrySet") +def write_rst_inline_shader_nodes(basepath): + """ + Write the RST files for ``bpy.types.InlineShaderNodes``. + """ + if 'bpy.types.InlineShaderNodes' in EXCLUDE_MODULES: + return + + # Write the index. + filepath = os.path.join(basepath, "bpy.types.InlineShaderNodes.rst") + with open(filepath, "w", encoding="utf-8") as fh: + fw = fh.write + fw(title_string("InlineShaderNodes", "=")) + write_example_ref("", fw, "bpy.types.InlineShaderNodes") + pyclass2sphinx(fw, "bpy.types", "InlineShaderNodes", bpy.types.InlineShaderNodes, False) + + EXAMPLE_SET_USED.add("bpy.types.InlineShaderNodes") + + def write_rst_msgbus(basepath): """ Write the RST files of ``bpy.msgbus`` module @@ -2541,6 +2559,7 @@ def rna2sphinx(basepath): write_rst_ops_index(basepath) # `bpy.ops`. write_rst_msgbus(basepath) # `bpy.msgbus`. write_rst_geometry_set(basepath) # `bpy.types.GeometrySet`. + write_rst_inline_shader_nodes(basepath) # `bpy.types.InlineShaderNodes`. pyrna2sphinx(basepath) # `bpy.types.*` & `bpy.ops.*`. write_rst_data(basepath) # `bpy.data`. write_rst_importable_modules(basepath) diff --git a/scripts/modules/_bpy_types.py b/scripts/modules/_bpy_types.py index 17ce0cf0a36..ece8633b27f 100644 --- a/scripts/modules/_bpy_types.py +++ b/scripts/modules/_bpy_types.py @@ -1517,3 +1517,48 @@ class GreasePencilDrawing(_StructRNA): from _bpy_internal.grease_pencil.stroke import GreasePencilStrokeSlice num_strokes = self.attributes.domain_size('CURVE') return GreasePencilStrokeSlice(self, 0, num_strokes) + + +class Material(_types.ID): + __slots__ = () + + def inline_shader_nodes(self): + """ + Get the inlined shader nodes of this material. This preprocesses the node tree + to remove nested groups, repeat zones and more. + + :return: The inlined shader nodes. + :rtype: :class:`bpy.types.InlineShaderNodes` + """ + from bpy.types import InlineShaderNodes + return InlineShaderNodes.from_material(self) + + +class Light(_types.ID): + __slots__ = () + + def inline_shader_nodes(self): + """ + Get the inlined shader nodes of this light. This preprocesses the node tree + to remove nested groups, repeat zones and more. + + :return: The inlined shader nodes. + :rtype: :class:`bpy.types.InlineShaderNodes` + """ + from bpy.types import InlineShaderNodes + return InlineShaderNodes.from_light(self) + + +class World(_types.ID): + __slots__ = () + + def inline_shader_nodes(self): + """ + Get the inlined shader nodes of this world. This preprocesses the node tree + to remove nested groups, repeat zones and more. + + :return: The inlined shader nodes. + :rtype: :class:`bpy.types.InlineShaderNodes` + """ + from bpy.types import InlineShaderNodes + return InlineShaderNodes.from_world(self) diff --git a/scripts/modules/rna_info.py b/scripts/modules/rna_info.py index 615f0dc5c1e..640da6fdb81 100644 --- a/scripts/modules/rna_info.py +++ b/scripts/modules/rna_info.py @@ -704,6 +704,7 @@ def BuildRNAInfo(): # Don't report when these types are ignored. suppress_warning = { "GeometrySet", + "InlineShaderNodes", "bpy_func", "bpy_prop", "bpy_prop_array", diff --git a/source/blender/nodes/intern/shader_nodes_inline.cc b/source/blender/nodes/intern/shader_nodes_inline.cc index 3ce9f5f3b43..6c19229ab2b 100644 --- a/source/blender/nodes/intern/shader_nodes_inline.cc +++ b/source/blender/nodes/intern/shader_nodes_inline.cc @@ -198,9 +198,6 @@ class ShaderNodesInliner { params_(params), data_type_conversions_(bke::get_implicit_type_conversions()) { - if (dst_tree.id.tag & ID_TAG_NO_MAIN) { - BLI_assert(src_tree.id.tag & ID_TAG_NO_MAIN); - } } bool do_inline() diff --git a/source/blender/python/intern/CMakeLists.txt b/source/blender/python/intern/CMakeLists.txt index 58345a64d45..7584aa33fef 100644 --- a/source/blender/python/intern/CMakeLists.txt +++ b/source/blender/python/intern/CMakeLists.txt @@ -37,6 +37,7 @@ set(SRC bpy_driver.cc bpy_geometry_set.cc bpy_gizmo_wrap.cc + bpy_inline_shader_nodes.cc bpy_interface.cc bpy_interface_atexit.cc bpy_interface_run.cc @@ -85,6 +86,7 @@ set(SRC bpy_driver.hh bpy_geometry_set.hh bpy_gizmo_wrap.hh + bpy_inline_shader_nodes.hh bpy_intern_string.hh bpy_library.hh bpy_msgbus.hh @@ -130,6 +132,7 @@ set(LIB PRIVATE bf::animrig bf_python_gpu PRIVATE bf::imbuf::opencolorio + PRIVATE bf::nodes ${PYTHON_LINKFLAGS} ${PYTHON_LIBRARIES} diff --git a/source/blender/python/intern/bpy.cc b/source/blender/python/intern/bpy.cc index db89e74bd04..8f0061c9892 100644 --- a/source/blender/python/intern/bpy.cc +++ b/source/blender/python/intern/bpy.cc @@ -37,6 +37,7 @@ #include "bpy_cli_command.hh" #include "bpy_driver.hh" #include "bpy_geometry_set.hh" +#include "bpy_inline_shader_nodes.hh" #include "bpy_library.hh" #include "bpy_operator.hh" #include "bpy_props.hh" @@ -748,6 +749,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(bpy_types, "InlineShaderNodes", BPyInit_inline_shader_nodes_type()); PyModule_AddObject(mod, "types", bpy_types); /* Needs to be first so `_bpy_types` can run. */ diff --git a/source/blender/python/intern/bpy_inline_shader_nodes.cc b/source/blender/python/intern/bpy_inline_shader_nodes.cc new file mode 100644 index 00000000000..5ced3349236 --- /dev/null +++ b/source/blender/python/intern/bpy_inline_shader_nodes.cc @@ -0,0 +1,269 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup pythonintern + */ + +#include "BKE_idtype.hh" +#include "BKE_lib_id.hh" +#include "BKE_node.hh" + +#include "DNA_light_types.h" +#include "DNA_material_types.h" +#include "DNA_node_types.h" + +#include "DNA_world_types.h" +#include "NOD_shader_nodes_inline.hh" + +#include "bpy_inline_shader_nodes.hh" +#include "bpy_rna.hh" + +#include "../generic/py_capi_utils.hh" + +extern PyTypeObject bpy_inline_shader_nodes_Type; + +struct BPy_InlineShaderNodes { + PyObject_HEAD + bNodeTree *inline_node_tree; +}; + +static BPy_InlineShaderNodes *create_from_shader_node_tree(const bNodeTree &tree) +{ + BPy_InlineShaderNodes *self = reinterpret_cast( + bpy_inline_shader_nodes_Type.tp_alloc(&bpy_inline_shader_nodes_Type, 0)); + if (!self) { + return nullptr; + } + self->inline_node_tree = blender::bke::node_tree_add_tree( + nullptr, (blender::StringRef(tree.id.name) + " Inlined").c_str(), tree.idname); + blender::nodes::InlineShaderNodeTreeParams params; + blender::nodes::inline_shader_node_tree(tree, *self->inline_node_tree, params); + return self; +} + +PyDoc_STRVAR( + /* Wrap. */ + bpy_inline_shader_nodes_from_material_doc, + ".. staticmethod:: from_material(material)\n" + "\n" + " Create an inlined shader node tree from a material.\n" + "\n" + " :arg material: The material to inline the node tree of.\n" + " :type material: bpy.types.Material\n"); +static BPy_InlineShaderNodes *BPy_InlineShaderNodes_static_from_material(PyObject * /*self*/, + PyObject *args, + PyObject *kwds) +{ + static const char *kwlist[] = {"material", nullptr}; + PyObject *py_material; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast(kwlist), &py_material)) { + return nullptr; + } + ID *material_id = nullptr; + if (!pyrna_id_FromPyObject(py_material, &material_id)) { + PyErr_Format( + PyExc_TypeError, "Expected a Material, not %.200s", Py_TYPE(py_material)->tp_name); + return nullptr; + } + if (GS(material_id->name) != ID_MA) { + PyErr_Format(PyExc_TypeError, + "Expected a Material, not %.200s", + BKE_idtype_idcode_to_name(GS(material_id->name))); + return nullptr; + } + Material *material = blender::id_cast(material_id); + if (!material->nodetree) { + PyErr_Format(PyExc_TypeError, "Material '%s' has no node tree", BKE_id_name(*material_id)); + return nullptr; + } + return create_from_shader_node_tree(*material->nodetree); +} + +PyDoc_STRVAR( + /* Wrap. */ + bpy_inline_shader_nodes_from_light_doc, + ".. staticmethod:: from_light(light)\n" + "\n" + " Create an inlined shader node tree from a light.\n" + "\n" + " :arg light: The light to online the node tree of.\n" + " :type light: bpy.types.Light\n"); +static BPy_InlineShaderNodes *BPy_InlineShaderNodes_static_from_light(PyObject * /*self*/, + PyObject *args, + PyObject *kwds) +{ + static const char *kwlist[] = {"light", nullptr}; + PyObject *py_light; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast(kwlist), &py_light)) { + return nullptr; + } + ID *light_id = nullptr; + if (!pyrna_id_FromPyObject(py_light, &light_id)) { + PyErr_Format(PyExc_TypeError, "Expected a Light, not %.200s", Py_TYPE(py_light)->tp_name); + return nullptr; + } + if (GS(light_id->name) != ID_LA) { + PyErr_Format(PyExc_TypeError, + "Expected a Light, not %.200s", + BKE_idtype_idcode_to_name(GS(light_id->name))); + return nullptr; + } + Light *light = blender::id_cast(light_id); + if (!light->nodetree) { + PyErr_Format(PyExc_TypeError, "Light '%s' has no node tree", BKE_id_name(*light_id)); + return nullptr; + } + return create_from_shader_node_tree(*light->nodetree); +} + +PyDoc_STRVAR( + /* Wrap. */ + bpy_inline_shader_nodes_from_world_doc, + ".. staticmethod:: from_world(world)\n" + "\n" + " Create an inlined shader node tree from a world.\n" + "\n" + " :arg world: The world to inline the node tree of.\n" + " :type world: bpy.types.World\n"); +static BPy_InlineShaderNodes *BPy_InlineShaderNodes_static_from_world(PyObject * /*self*/, + PyObject *args, + PyObject *kwds) +{ + static const char *kwlist[] = {"world", nullptr}; + PyObject *py_world; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast(kwlist), &py_world)) { + return nullptr; + } + ID *world_id = nullptr; + if (!pyrna_id_FromPyObject(py_world, &world_id)) { + PyErr_Format(PyExc_TypeError, "Expected a World, not %.200s", Py_TYPE(py_world)->tp_name); + return nullptr; + } + if (GS(world_id->name) != ID_WO) { + PyErr_Format(PyExc_TypeError, + "Expected a World, not %.200s", + BKE_idtype_idcode_to_name(GS(world_id->name))); + return nullptr; + } + World *world = blender::id_cast(world_id); + if (!world->nodetree) { + PyErr_Format(PyExc_TypeError, "World '%s' has no node tree", BKE_id_name(*world_id)); + return nullptr; + } + return create_from_shader_node_tree(*world->nodetree); +} + +static void BPy_InlineShaderNodes_dealloc(BPy_InlineShaderNodes *self) +{ + BKE_id_free(nullptr, self->inline_node_tree); + Py_TYPE(self)->tp_free(reinterpret_cast(self)); +} + +PyDoc_STRVAR( + /* Wrap. */ + bpy_inline_shader_nodes_node_tree_doc, + "The inlined node tree.\n" + "\n" + ":type: :class:`bpy.types.NodeTree`\n"); +static PyObject *BPy_InlineShaderNodes_get_node_tree(BPy_InlineShaderNodes *self) +{ + return pyrna_id_CreatePyObject(blender::id_cast(self->inline_node_tree)); +} + +static PyGetSetDef BPy_InlineShaderNodes_getseters[] = { + {"node_tree", + (getter)BPy_InlineShaderNodes_get_node_tree, + nullptr, + bpy_inline_shader_nodes_node_tree_doc, + nullptr}, + {nullptr}, +}; + +#ifdef __GNUC__ +# ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wcast-function-type" +# else +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wcast-function-type" +# endif +#endif + +static PyMethodDef BPy_InlineShaderNodes_methods[] = { + {"from_material", + (PyCFunction)BPy_InlineShaderNodes_static_from_material, + METH_VARARGS | METH_KEYWORDS | METH_STATIC, + bpy_inline_shader_nodes_from_material_doc}, + {"from_light", + (PyCFunction)BPy_InlineShaderNodes_static_from_light, + METH_VARARGS | METH_KEYWORDS | METH_STATIC, + bpy_inline_shader_nodes_from_light_doc}, + {"from_world", + (PyCFunction)BPy_InlineShaderNodes_static_from_world, + METH_VARARGS | METH_KEYWORDS | METH_STATIC, + bpy_inline_shader_nodes_from_world_doc}, + {nullptr}, +}; + +#ifdef __GNUC__ +# ifdef __clang__ +# pragma clang diagnostic pop +# else +# pragma GCC diagnostic pop +# endif +#endif + +PyDoc_STRVAR( + /* Wrap. */ + bpy_inline_shader_nodes_doc, + "An inlined shader node tree.\n"); +PyTypeObject bpy_inline_shader_nodes_Type = { + /*ob_base*/ PyVarObject_HEAD_INIT(nullptr, 0) + /*tp_name*/ "InlineShaderNodes", + /*tp_basicsize*/ sizeof(BPy_InlineShaderNodes), + /*tp_itemsize*/ 0, + /*tp_dealloc*/ reinterpret_cast(BPy_InlineShaderNodes_dealloc), + /*tp_vectorcall_offset*/ 0, + /*tp_getattr*/ nullptr, + /*tp_setattr*/ nullptr, + /*tp_as_async*/ nullptr, + /*tp_repr*/ nullptr, + /*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_inline_shader_nodes_doc, + /*tp_traverse*/ nullptr, + /*tp_clear*/ nullptr, + /*tp_richcompare*/ nullptr, + /*tp_weaklistoffset*/ 0, + /*tp_iter*/ nullptr, + /*tp_iternext*/ nullptr, + /*tp_methods*/ BPy_InlineShaderNodes_methods, + /*tp_members*/ nullptr, + /*tp_getset*/ BPy_InlineShaderNodes_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*/ nullptr, +}; + +PyObject *BPyInit_inline_shader_nodes_type() +{ + if (PyType_Ready(&bpy_inline_shader_nodes_Type) < 0) { + return nullptr; + } + return reinterpret_cast(&bpy_inline_shader_nodes_Type); +} diff --git a/source/blender/python/intern/bpy_inline_shader_nodes.hh b/source/blender/python/intern/bpy_inline_shader_nodes.hh new file mode 100644 index 00000000000..e1383774f3a --- /dev/null +++ b/source/blender/python/intern/bpy_inline_shader_nodes.hh @@ -0,0 +1,13 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup pythonintern + */ + +#pragma once + +#include + +[[nodiscard]] PyObject *BPyInit_inline_shader_nodes_type();