1
$\begingroup$

I want to create a script that reads multiple image files and creates image texture nodes for them in Cycles default ShaderNodeTree.

This script will be housed in it's own NodeTreeCategory called "Import".

The script is exactly like the NodeWrangler addon's Multiple Images import function. In NodeWrangler, executing this program opens up the File Browser, allows the user to select multiple images, and reads/imports them as texture nodes into the Active Node Tree.

Here is the working code:

#NOTE: Run this code first then use SHIFT-A, below, to add Custom Float node type.

import bpy
from bpy.types import NodeTree, Node, NodeSocket
import bpy, blf, bgl
from bpy.types import Operator, Panel, Menu
from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty, FloatVectorProperty, CollectionProperty
from bpy_extras.io_utils import ImportHelper
from mathutils import Vector
from math import cos, sin, pi, hypot
from os import path
from glob import glob
from copy import copy

# Implementation of custom nodes from Python
# Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc.
class MyCustomTree(NodeTree):
    bl_idname = 'CustomTreeType'
    bl_label = 'Custom Node Tree'

# Defines a poll function to enable filtering for various node tree types.
class MyCustomTreeNode :
    @classmethod
    def poll(cls, ntree):
        b = False
        # Make your node appear in different node trees by adding their bl_idname type here.
        if ntree.bl_idname == 'ShaderNodeTree': b = True
        return b

# Derived from the Node base type.
class MyCustomNode(Node, MyCustomTreeNode):
    '''A custom node'''
    bl_idname = 'CustomNodeType'
    bl_label = 'Custom Float'
    bl_icon = 'INFO'

    def update_value(self, context):
        self.outputs["Float"].default_value = self.some_value
        self.update ()

    some_value = bpy.props.FloatProperty(default=0.0, update = update_value)

    def init(self, context):
        self.outputs.new('NodeSocketFloat', "Float")
        self.outputs["Float"].default_value = self.some_value

    def update(self):
        #Review linked outputs.
        try:
            out = self.outputs["Float"]
            can_continue = True
        except:
            can_continue = False
        if can_continue:
            if out.is_linked:
                # I am an ouput node that is linked, try to update my link.
                for o in out.links:
                    if o.is_valid:
                        o.to_socket.node.inputs[o.to_socket.name].default_value = self.outputs["Float"].default_value   #self.some_value

    # Additional buttons displayed on the node.
    def draw_buttons(self, context, layout):
        layout.prop(self, "some_value",text = '')

    # Optional: custom label
    # Explicit user label overrides this, but here we can define a label dynamically.
    def draw_label(self):
        return "My Float"

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

# our own base class with an appropriate poll function,
# so the categories only show up in our target tree type
class MyNodeCategory(NodeCategory):
    @classmethod
    def poll(cls, context):
        b = False
        # Make your node appear in different node trees by adding their bl_idname type here.
        if context.space_data.tree_type == 'ShaderNodeTree': b = True
        return b

# all categories in a list
node_categories = [
    # identifier, label, items list
    MyNodeCategory("SOMENODES", "Import", items=[
        NodeItem("CustomNodeType"),
        ]),
    ]

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)

def pre_frame_change(scene):
    if scene.render.engine == 'CYCLES':
        # Scan materials to see if I have a custom node within any of the trees.
        for m in bpy.data.materials:
            if m.node_tree != None:
                for n in m.node_tree.nodes:
                    if n.bl_idname == 'CustomNodeType':
                        print(n.bl_idname)
                        # One of our custom nodes, let's update it.
                        # When we set the value that will trigger an update inside the node.
                        # Even if we change it to the same value it was.
                        v = n.some_value
                        n.some_value = v

if __name__ == "__main__":
    register()
    bpy.app.handlers.frame_change_pre.append(pre_frame_change)

The part I want to modify is the MyCustomNode class. I plan on creating a class that looks like this:

class NWAddMultipleImages(Operator, ImportHelper):
    """Add multiple images at once"""
    bl_idname = 'node.nw_add_multiple_images'
    bl_label = 'Open Selected Images'
    bl_options = {'REGISTER', 'UNDO'}
    directory = StringProperty(subtype="DIR_PATH")
    files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})

    def execute(self, context):
        nodes, links = get_nodes_links(context)
        nodes_list = [node for node in nodes]
        if nodes_list:
            nodes_list.sort(key=lambda k: k.location.x)
            xloc = nodes_list[0].location.x - 220  # place new nodes at far left
            yloc = 0
            for node in nodes:
                node.select = False
                yloc += node_mid_pt(node, 'y')
            yloc = yloc/len(nodes)
        else:
            xloc = 0
            yloc = 0

        if context.space_data.node_tree.type == 'SHADER':
            node_type = "ShaderNodeTexImage"
        elif context.space_data.node_tree.type == 'COMPOSITING':
            node_type = "CompositorNodeImage"
        else:
            self.report({'ERROR'}, "Unsupported Node Tree type!")
            return {'CANCELLED'}

        new_nodes = []
        for f in self.files:
            fname = f.name

            node = nodes.new(node_type)
            new_nodes.append(node)
            node.label = fname
            node.hide = True
            node.width_hidden = 100
            node.location.x = xloc
            node.location.y = yloc
            yloc -= 40

            img = bpy.data.images.load(self.directory+fname)
            node.image = img

        # shift new nodes up to center of tree
        list_size = new_nodes[0].location.y - new_nodes[-1].location.y
        for node in new_nodes:
            node.select = True
            node.location.y += (list_size/2)
        return {'FINISHED'}

How do I create this class so that it shows up in the Import NodeCategory of the ShaderNodeTree?

$\endgroup$
2
  • $\begingroup$ I think you may be a bit confused here. You appear to be creating existing nodes within the cycles shader node tree. You don't use custom nodes to add a menu item that creates existing nodes to a cycles material. Custom nodes are to create entirely new nodes that are only used in their own nodetree structure, like animation nodes or sverchok. $\endgroup$
    – sambler
    Jun 5, 2016 at 8:15
  • $\begingroup$ You're correct. However with the custom node it doesn't seem to work in the node category correctly $\endgroup$
    – lvxejay
    Jun 9, 2016 at 7:04

1 Answer 1

1
$\begingroup$

Script now works correctly with this code:

import bpy
from bpy.types import NodeTree, Node, NodeSocket
import bpy, blf, bgl
from bpy.types import Operator, Panel, Menu
from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty, FloatVectorProperty, CollectionProperty
from bpy_extras.io_utils import ImportHelper
from mathutils import Vector
from math import cos, sin, pi, hypot
from os import path
from glob import glob
from copy import copy


def node_mid_pt(node, axis):
    if axis == 'x':
        d = node.location.x + (node.dimensions.x / 2)
    elif axis == 'y':
        d = node.location.y - (node.dimensions.y / 2)
    else:
        d = 0
    return d


def get_nodes_links(context):
    space = context.space_data
    tree = space.node_tree
    nodes = tree.nodes
    links = tree.links
    active = nodes.active
    context_active = context.active_node
    # check if we are working on regular node tree or node group is currently edited.
    # if group is edited - active node of space_tree is the group
    # if context.active_node != space active node - it means that the group is being edited.
    # in such case we set "nodes" to be nodes of this group, "links" to be links of this group
    # if context.active_node == space.active_node it means that we are not currently editing group
    is_main_tree = True
    if active:
        is_main_tree = context_active == active
    if not is_main_tree:  # if group is currently edited
        tree = active.node_tree
        nodes = tree.nodes
        links = tree.links

    return nodes, links



# Implementation of custom nodes from Python
# Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc.
class MyCustomTree(NodeTree):
    bl_idname = "CustomTreeType"
    bl_label = "Custom Node Tree"
    bl_icon = "NODE"

# Defines a poll function to enable filtering for various node tree types.
class MyCustomTreeNode:
    @classmethod
    def poll(cls, ntree):
        b = False
        # Make your node appear in different node trees by adding their bl_idname type here.
        if ntree.bl_idname == 'ShaderNodeTree': b = True
        return b

# Derived from the Node base type.
class MyCustomNode(Node, MyCustomTreeNode):
    '''A custom node'''
    bl_idname = 'CustomNodeType'
    bl_label = 'Import PBR Textures'
    bl_icon = 'NODETREE'

    # Additional buttons displayed on the node.
    def draw_buttons(self, context, layout):
        layout.operator("node.nw_add_multiple_images", text="Select Images")

    # Optional: custom label
    # Explicit user label overrides this, but here we can define a label dynamically.
    def draw_label(self):
        return "Test Float"


class NWAddMultipleImages(Operator, ImportHelper):
    """Add multiple images at once"""
    bl_idname = "node.nw_add_multiple_images"
    bl_icon = 'NODETREE'
    bl_label = 'Open Selected Images'
    bl_options = {'REGISTER', 'UNDO'}
    directory = StringProperty(subtype="DIR_PATH")
    files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})

    def execute(self, context):
        nodes, links = get_nodes_links(context)
        nodes_list = [node for node in nodes]
        if nodes_list:
            nodes_list.sort(key=lambda k: k.location.x)
            xloc = nodes_list[0].location.x - 220  # place new nodes at far left
            yloc = 0
            for node in nodes:
                node.select = False
                yloc += node_mid_pt(node, 'y')
            yloc = yloc/len(nodes)
        else:
            xloc = 0
            yloc = 0

        if context.space_data.node_tree.type == 'SHADER':
            node_type = "ShaderNodeTexImage"
        elif context.space_data.node_tree.type == 'COMPOSITING':
            node_type = "CompositorNodeImage"
        else:
            self.report({'ERROR'}, "Unsupported Node Tree type!")
            return {'CANCELLED'}

        new_nodes = []
        for f in self.files:
            fname = f.name

            node = nodes.new(node_type)
            new_nodes.append(node)
            node.label = fname
            node.hide = True
            node.width_hidden = 100
            node.location.x = xloc
            node.location.y = yloc
            yloc -= 40

            img = bpy.data.images.load(self.directory+fname)
            node.image = img

        # shift new nodes up to center of tree
        list_size = new_nodes[0].location.y - new_nodes[-1].location.y
        for node in new_nodes:
            node.select = True
            node.location.y += (list_size/2)
        return {'FINISHED'}

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

# our own base class with an appropriate poll function,
# so the categories only show up in our target tree type
class MyNodeCategory(NodeCategory):
    @classmethod
    def poll(cls, context):
        b = False
        # Make your node appear in different node trees by adding their bl_idname type here.
        if context.space_data.tree_type == 'ShaderNodeTree': b = True
        return b

# all categories in a list
node_categories = [
    # identifier, label, items list
    MyNodeCategory("SOMENODES", "Import", items=[
        NodeItem("CustomNodeType"),
        #NodeItem("node.nw_add_multiple_images"),
        ]),
    ]


#def multipleimages_menu_func(self, context):
    #col = self.layout.column(align=True)
    #col.operator(NWAddMultipleImages.bl_idname, text="Import PBR")
    #col.separator()

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

    #bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
    #bpy.types.NODE_PT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
    #bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
    #bpy.types.NODE_PT_category_CMP_INPUT.prepend(multipleimages_menu_func)

    bpy.utils.register_module(__name__)

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

    #bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
    #bpy.types.NODE_PT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
    #bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
    #bpy.types.NODE_PT_category_CMP_INPUT.remove(multipleimages_menu_func)

    bpy.utils.unregister_module(__name__)

def pre_frame_change(scene):
    if scene.render.engine == 'CYCLES':
        # Scan materials to see if I have a custom node within any of the trees.
        for m in bpy.data.materials:
            if m.node_tree != None:
                for n in m.node_tree.nodes:
                    if n.bl_idname == 'CustomNodeType':
                        print(n.bl_idname)
                        # One of our custom nodes, let's update it.
                        # When we set the value that will trigger an update inside the node.
                        # Even if we change it to the same value it was.
                        v = n.some_value
                        n.some_value = v

if __name__ == "__main__":
    register()
    bpy.app.handlers.frame_change_pre.append(pre_frame_change)
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .