由于网上关于blender自定义节点的文章极少(这也是我特意写本文的原因),本文均为自己摸索和部分参考Blender文档得到,可能会有不严谨和不正确的地方,请观者酌情采纳
目的:借用blender的节点实现自己的可视化节点编程,初步完成基本的输入输出节点
- import bpy
- from bpy.types import NodeTree
- from bpy.types import NodeSocket
- from bpy.types import Node
- from bpy.utils import register_class
- from bpy.utils import unregister_class
在正式进入Blender节点插件编写之前,先弄清楚开发流程和包含关系,如下图
白色和蓝色方框中的内容是重点,黑色方框是本次实例要完成的内容,即创建一个和几何节点面板一样的自定义节点面板,并编写几个简单的节点
不愿意看介绍可以直接看实例
如果你用过Blender,Blender中自带了几何节点和材质节点,他们都有单独的面板使用他们,同样的,我们创建的自定义节点也可以拥有自己的独立面板,我们编写的节点程序除了拥有python提供的能力外,同时可以使用Blender提供的API。
如图所示:一个节点可见的部分有5个部分
如果把节点分为UI和内部方法两个部分,我们的UI工作也就是这些
这个部分来自bpy.props,props全称是property,直译为属性。blender中很多模块都使用到了属性,属性中常用的方法有get()和set(),在需要读取和写入属性值时,会自动调用他们。
这个部分我看了很久,至今也没太明白,主要是创建一个实例后,我怎么通过多种方法访问和修改到它的属性,以及理解他的工作方式。
直译为节点套接字,我觉得这个不好理解,下文都称为节点组件/NodeSocket。
节点组件是节点的基本组成之一,节点组件内一般都会定义一个property,blender也自带了种类丰富的组件。
为了方便开发,需要安装一个python库:使用 pip install fake-bpy-module-2.92 或 进入 https://pypi.org/project/fake-bpy-module-2.92/ 下载,安装在VScode环境中。该库的作用如其名,假的bpy库,只提供代码自动补齐,使用时同样是import bpy。
我推荐用VScode编写,里面有一个Blender Development插件,可以极大的方便插件的编写和调试工作。
所有代码:链接:https://pan.baidu.com/s/1xtUTL4gf46DT5geIUnN9ig
提取码:ldz8
NodeTree部分的工作量不大,只是注册一个面板,填入必要的信息即可
### NOTE:第一步:创建节点树 ###
from bpy.types import NodeTree
# 自定义节点编辑面板的信息
class myCustomTree(NodeTree):
"""一个节点树类型,它将展示在编辑器类型列表中"""
bl_idname = 'CustomTreeType' # 面板的id,唯一
bl_label = "My NodeTree" # 面板的标签,用于展示
bl_icon = 'RNA' # 面板的图标,使用Blender自带的图标
# 定义节点时需要这个类
class MyCustomTreeNode:
@classmethod
def poll(cls, ntree):
return ntree.bl_idname == 'CustomTreeType'
import nodeitems_utils
from nodeitems_utils import NodeCategory, NodeItem
# 创建节点目录时需要这个类
class MyNodeCategory(NodeCategory):
@classmethod
def poll(cls, context):
return context.space_data.tree_type == 'CustomTreeType'
# Created by Jiacong Zhao @CSDN-奇偕
# XXX The last modification at:2021.06.04
# XXX Topic: 自定义单选组件示例
# XXX Discription: 组件是构成节点的基础
# blender本身自带了NodeSocketInt、NodeSocketFloat、NodeSocketString等组件
import bpy
from bpy.types import NodeSocket
class Socket_Enum_List(NodeSocket):
bl_idname = '_enum_list'
bl_label = "enum_list"
# 选项列表
my_items = (
# [(identifier, name, description, icon, number), ...]
('C://', "C://", ""),
('D://', "D://", ""),
('E://', "E://", ""),
('F://', "F://", ""),
)
# 组件基本信息
myenum: bpy.props.EnumProperty(
name="Direction",# 名称
description="一个例子",# 描述
items=my_items,# 可选择的项目
default='C://',# 默认选择
)
def val(self):
return self.myenum
# 用于绘制在节点框里显示的文本【可选】
def draw(self, context, layout, node, text):
if self.is_linked:
# 如果被连接,只显示的文本
# text += bpy.props.
layout.label(text=text)
else:
# 没有被连接时,显示文本和选项
layout.prop(self, "myenum", text=text)
# 组件颜色【那个小点点】
def draw_color(self, context, node):
return (1.0, 0.4, 0.216, 0.5)
'''
in info s
'''
# Created by Jiacong Zhao @CSDN-奇偕
# XXX The last modification at:2021.06.04
# XXX Topic: 打印节点
# XXX Discription: 将得到的任何数据转为字符串并打印,实时刷新
import time
import bpy
from bpy.types import Node
from ..MyCustomTree import *
from ..MyCustomSocket import *
class PrintNode(Node, MyCustomTreeNode):
bl_idname = 'PrintNode'
bl_label = "打印"
bl_icon = 'CONSOLE'
def init(self, context):
self.inputs.new('NodeSocketString', "info")
def parent_socket(self):
return self.inputs[0].links[0].from_socket
def draw_buttons(self, context, layout):
if self.inputs[0].is_linked:
layout.label(text=str(self.parent_socket().default_value))
else:
layout.label(text='')
def draw_buttons_ext(self, context, layout):
...
def draw_label(self):
return "打印"
# 拓扑图更新时调用
def update(self):
print(time.ctime(),end='>>> ')
print(str(self.parent_socket().default_value))
# 复制时调用
def copy(self, node):
print("Copying from node ", node)
# 释放时调用
def free(self):
print("Removing node ", self, ", Goodbye!")
# 插件信息
bl_info = {
"name" : "MyCustomAddon",
"author" : "QiXie",
"description" : "",
"blender" : (2, 92, 0),
"version" : (0, 0, 1),
"location" : "",
"warning" : "",
"category" : "Generic"
}
import bpy
# View3d面板
from .TestHello import *
from .TestMyPanel import *
''' 自定义节点面板 '''
# 节点树--节点组件--节点--节点目录
# from .myNodes import *
from .MyCustomTree import *
from .MyCustomSocket import Socket_classes
from .MyCustomNodes import Node_classes
classes = (
Test_OT_Hello,
Test_PT_HelloPanel,
myCustomTree,
)
### NOTE:创建节点目录 ###
node_categories = [
# identifier, label, items list
# -->标识符、标签(展示的名称)、项目列表
MyNodeCategory('INPUT', "输入节点", items=[
NodeItem("TextNode"),
NodeItem("FileNode"),
]),
MyNodeCategory('OUTPUT', "输出节点", items=[
NodeItem("PrintNode"),
]),
MyNodeCategory('OTHERNODES', "测试节点", items=[
# --> 节点项可以有额外的设置,可以应用于新的节点
# --> 注意:设置值存储为字符串表达式,因此应该使用repr()将其转换为字符串。
# 由一个节点设置不同的值派生出新节点
# 一个节点有四个属性
# nodetype【类型|必填】
# label【名称】
# settings【设置】:settings[0]【名称】settings[1]【值】
# poll
NodeItem("TestNode", label="Node A", settings={
"my_string_prop": repr("文本"),
}),
NodeItem("TestNode", label="Node B", settings={
"my_string_prop": repr("文本"),
}),
]),
]
# 类的注册【方法一】
# register, unregister = bpy.utils.register_classes_factory(classes)
# 类的注册【方法二】
def register():
from bpy.utils import register_class
# 注册
for cls in classes:
register_class(cls)
# 组件注册
for cls in Socket_classes:
register_class(cls)
# 节点注册
for cls in Node_classes:
register_class(cls)
# 节点目录注册
nodeitems_utils.register_node_categories('CUSTOM_NODES', node_categories)
def unregister():
nodeitems_utils.unregister_node_categories('CUSTOM_NODES')
from bpy.utils import unregister_class
# 节点
for cls in Node_classes:
unregister_class(cls)
# 组件
for cls in Socket_classes:
unregister_class(cls)
#
for cls in reversed(classes):
unregister_class(cls)
# if __name__ == "__main__":
# register()
打开自带插件icon Viewer,在文本编辑窗口的侧边栏Dev选项中即可查看全部图标,点击即可复制图标名称。
参考文章
当节点自身拓扑结构发生变化时,Blender会自动调用节点的update方法,因此我们将输入与输出的逻辑关系放在这里
当我们改变某节点的父结点的参数时,此时update方法不会被调用,也就是说变化的传递会即刻终止,这在大多数情况下不符合需求,我的解决办法如下:
编写自定义NodeSocket,在property of NodeSocket的set方法中,调用NodeSocket父节点的update方法,这样自身参数改变时,输出也会跟着刷新
在原有的update方法中运用递归的思想,自身输出刷新后调用子节点的update方法,如下所示
for item in self.outputs:
for cell in item.links:
cell.to_socket.node.update()
# 我必须要说明的是,这是一种简单粗暴的方法,当整个节点树比较复杂时,update方法的调用次数是成倍上升的,好的算法应该让每个节点按照一定顺序逐个执行一遍
# 如果你需要掐断这种传递,那么添加一个不含这类代码的节点即可
遇到困难的时候不妨求助以下路径
目前只是简单做了几个节点,当然,会做几个基本的节点后,后面会快很多。
最终的想法是做一个能处理数据和解决数学问题的节点插件,我认为这种方式去做这些会更为直观