# PyNodes, the bare basics

I know it's a very new thing (post 2.4 era anyways) but there seems to be very little documentation and examples for PyNodes in general, and the little there is doesn't seem to work in latest SVN (currently r57165).

My idea is to make a Cycles material node that has a vector output socket for each of the object(s) UV Maps, so that I don't need to look up what the name of the UV Map is in the Object Data Properties and copy-paste that into the Attribute node each time. This seems like a reasonably easy node to code, failing that even just a node like the Attribute one that can fetch an objects Custom Property to be used in the material.

Either way, I'd like some help in setting up a simple PyNode like that. Is it possible? Can someone provide example code of simple working PyNodes?

PyNodes do not make it possible to extend Blender Internal, Cycles or the compositor with new nodes.

They are designed to make user interfaces for new node systems. For example developers of external render engines can use them to make their own shader node systems, or an addon could make a new geometry node system.

It would of course be very useful if they did, but that would be a much bigger project, as it requires each node system to expose an API with access to its inner workings. For Cycles the closest you can get is Open Shading Language nodes.

sorry to necro

I have no idea how i stumbled across this one, but just for completeness sake: while brecht is correct to the point itself, your goal can still be achieved! In other words: "think goal - not implementation details".

Instead of using your new node as input, use it as dummy and auto-add the correct uv-map whenever you draw a connection. This node has been successfully tested in 2.78, the complete example is at the bottom.

## step by step description

The update-function is a magic function, called on every update of the node itself (creation of the node, linking/unlinking/adding and removing sockets). It checks if we linked a uv_map and if so, replaces itself with the uv_map-node requested:

# this is called on linking/unlinking/adding/removing sockets and on startup before init
def update(self):# search for links and replace node with fitting uv_map node
for idx, skt in enumerate(self.outputs):
if not skt.is_linked: continue
ntree   = self.id_data
lskt    = link.to_socket# input-socket you linked to
uv_node = ntree.nodes.new('ShaderNodeUVMap')# add new uv-map node
uv_node.location = self.location# copy location
uv_node.uv_map = skt.name# copy uv-map-id
# optionally: copy selection state
#   (activate or deselect the uv_map-node)
if self==ntree.nodes.active:
ntree.nodes.active = uv_node
elif not self.select:
uv_node.select = False
# optionally: remove this dummy-node
ntree.nodes.remove(self)
break # we allready found our link
else:# only if we found no link, we rebuild our sockets
self.rebuild()


If there's no link made, the update-function calls self.rebuild. This function tries to fetch all uv_maps from the selected object and builds sockets, naming them accordingly. We also call it when updating the requested name for the referred object:

# build new sockets to link from
def rebuild(self):
if not self.is_updating:
uv_maps = self.uv_textures()
if uv_maps!=sorted(self.outputs.keys()):
self.is_updating = True # locking to prevent infinite updates
self.outputs.clear()# kick out old sockets
for uv_map in uv_maps:# add new sockets
self.outputs.new('NodeSocketVector', uv_map)
self.is_updating = False # locking to prevent infinite updates


This is a helper-function to fetch the uv_maps and sort them by name. If we can't find the requested object, we'll try the active object instead, otherwise we try the first object in bpy.data. If we find no uv_maps we print the error and return a blank list, so self.rebuild will clear out all sockets:

def uv_textures(self):
obj = bpy.data.objects.get(self.object) # first try name
if obj==None:                           # then try active
obj = bpy.context.scene.objects.active
if obj==None:                           # finally try first object
obj = bpy.data.objects[0]
try:
k = sorted(obj.data.uv_textures.keys())
except Exception as e:
print(self.name,": ", e.args[0])
k = []
return k


Of course, one could just as easily write the node to contain any uv_map on any object what-so-ever, the question is how you would produce the correct inputnode. Hopefully we one day may be able to extend the build-in nodetrees functionality, but I wouldn't hold my breath (appearantly 2.8x provides partial support for that, though I couldn't stress test it yet).

## The complete example:

import bpy

# Derived from the Node base type.
class MyCustomNode(bpy.types.Node):
# === Basics ===
'''A dummy-node for linking uv_maps to nodes''' # Description string
# Optional identifier string. If not explicitly defined,
bl_idname = 'CustomNodeType'# the python class name is used.
bl_label = 'UV-Map helper'  # Label for nice name display
bl_icon = 'SOUND'           # Icon identifier

# === Custom Properties === # we store the object-name and the update-state here
is_updating = bpy.props.BoolProperty(default=False)# update-state safety lock
object = bpy.props.StringProperty(update=lambda self,context:self.update())

# this is called on linking/unlinking/adding/removing sockets and on startup before init
def update(self):# search for links and replace node with fitting uv_map node
for idx, skt in enumerate(self.outputs):
if not skt.is_linked: continue
ntree   = self.id_data
lskt    = link.to_socket# input-socket you linked to
uv_node = ntree.nodes.new('ShaderNodeUVMap')# add new uv-map node
uv_node.location = self.location# copy location
uv_node.uv_map = skt.name# copy uv-map-id
# optionally: copy selection state
#   (activate or deselect the uv_map-node)
if self==ntree.nodes.active:
ntree.nodes.active = uv_node
elif not self.select:
uv_node.select = False
# optionally: remove this dummy-node
ntree.nodes.remove(self)
break # we allready found our link
else:# only if we found no link, we rebuild our sockets
self.rebuild()

# build new sockets to link from
def rebuild(self):
if not self.is_updating:
uv_maps = self.uv_textures()
if uv_maps!=sorted(self.outputs.keys()):
self.is_updating = True # locking to prevent infinite updates
self.outputs.clear()# kick out old sockets
for uv_map in uv_maps:# add new sockets
self.outputs.new('NodeSocketVector', uv_map)
self.is_updating = False # locking to prevent infinite updates

def uv_textures(self):
obj = bpy.data.objects.get(self.object) # first try name
if obj==None:                           # then try active
obj = bpy.context.scene.objects.active
if obj==None:                           # finally try first object
obj = bpy.data.objects[0]
try:
k = sorted(obj.data.uv_textures.keys())
except Exception as e:
print(self.name,": ", e.args[0])
k = []
return k# return the sorted uv_map-ids (else print the error and return blank list)

def draw_buttons(self, context, layout):
layout.prop_search(self, "object", bpy.data, "objects")

### Node Categories ###
import nodeitems_utils
from nodeitems_utils import NodeCategory, NodeItem

# we want this node only to show up in shader node trees
class MyNodeCategory(NodeCategory):
@classmethod
def poll(cls, context):
return context.space_data.tree_type == 'ShaderNodeTree'

# all categories in a list
node_categories = [
# identifier, label, items list
MyNodeCategory("SOMENODES", "Dummy-Nodes", items=[
# type, label, settings (as dict of repr-strings)
NodeItem("CustomNodeType", label="UV-Map helper", settings={
}),
]),
]

def register():
bpy.utils.register_class(MyCustomNode)
nodeitems_utils.register_node_categories("CUSTOM_NODES", node_categories)

def unregister():
nodeitems_utils.unregister_node_categories("CUSTOM_NODES")
bpy.utils.unregister_class(MyCustomNode)

if __name__ == "__main__":
try:
unregister()
except:
pass

register()