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
This commit is contained in:
Jacques Lucke
2025-09-11 06:08:30 +02:00
parent 0b19cb35b6
commit c3f49cd24e
9 changed files with 375 additions and 3 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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",

View File

@@ -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()

View File

@@ -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}

View File

@@ -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. */

View File

@@ -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_InlineShaderNodes *>(
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<char **>(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 *>(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<char **>(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 *>(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<char **>(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 *>(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<PyObject *>(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<ID *>(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<destructor>(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<PyObject *>(&bpy_inline_shader_nodes_Type);
}

View File

@@ -0,0 +1,13 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup pythonintern
*/
#pragma once
#include <Python.h>
[[nodiscard]] PyObject *BPyInit_inline_shader_nodes_type();