From c3f49cd24e9658b699c86768b5ef1c072291c506 Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Thu, 11 Sep 2025 06:08:30 +0200 Subject: [PATCH] Shader Nodes: add Python API for inlined shader nodes This makes the shader node inlining from #141936 available to external renderers which use the Python API. Existing external renderer add-ons need to be updated to get the inlined node tree from a material like below instead of using the original node tree of the material directly. The main contribution are these three methods: `Material.inline_shader_nodes()`, `Light.inline_shader_nodes()` and `World.inline_shader_nodes()`. In theory, there could be an inlining API for node trees more generally, but some aspects of the inlining are specific to shader nodes currently. For example the detection of output nodes and implicit input handling. Furthermore, having the method on e.g. `Material` instead of on the node tree might be more future proof for the case when we want to store input properties of the material on the `Material` which are then passed into the shader node tree. Example from API docs: ```python 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) ``` Pull Request: https://projects.blender.org/blender/blender/pulls/145811 --- .../examples/bpy.types.InlineShaderNodes.py | 23 ++ doc/python_api/sphinx_doc_gen.py | 19 ++ scripts/modules/_bpy_types.py | 45 +++ scripts/modules/rna_info.py | 1 + .../nodes/intern/shader_nodes_inline.cc | 3 - source/blender/python/intern/CMakeLists.txt | 3 + source/blender/python/intern/bpy.cc | 2 + .../python/intern/bpy_inline_shader_nodes.cc | 269 ++++++++++++++++++ .../python/intern/bpy_inline_shader_nodes.hh | 13 + 9 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 doc/python_api/examples/bpy.types.InlineShaderNodes.py create mode 100644 source/blender/python/intern/bpy_inline_shader_nodes.cc create mode 100644 source/blender/python/intern/bpy_inline_shader_nodes.hh 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();