15
$\begingroup$

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?

$\endgroup$
1
10
$\begingroup$

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.

$\endgroup$
0
3
$\begingroup$

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
        link    = skt.links[0]
        lskt    = link.to_socket# input-socket you linked to
        ntree.links.remove(link)# clear out old link
        uv_node = ntree.nodes.new('ShaderNodeUVMap')# add new uv-map node
        uv_node.location = self.location# copy location
        ntree.links.new(uv_node.outputs[0], lskt)# set up new link
        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
            link    = skt.links[0]
            lskt    = link.to_socket# input-socket you linked to
            ntree.links.remove(link)# clear out old link
            uv_node = ntree.nodes.new('ShaderNodeUVMap')# add new uv-map node
            uv_node.location = self.location# copy location
            ntree.links.new(uv_node.outputs[0], lskt)# set up new link
            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()
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.