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:
23
doc/python_api/examples/bpy.types.InlineShaderNodes.py
Normal file
23
doc/python_api/examples/bpy.types.InlineShaderNodes.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
269
source/blender/python/intern/bpy_inline_shader_nodes.cc
Normal file
269
source/blender/python/intern/bpy_inline_shader_nodes.cc
Normal 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);
|
||||
}
|
||||
13
source/blender/python/intern/bpy_inline_shader_nodes.hh
Normal file
13
source/blender/python/intern/bpy_inline_shader_nodes.hh
Normal 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();
|
||||
Reference in New Issue
Block a user