3
$\begingroup$

I have created a node with 21 outputs which I want to be connected to RGB combine node and I want some way scroll through ALL the variation with ability to know the variation of inputs. Can anyone suggest a way to solve this problem?

enter image description here

$\endgroup$
  • $\begingroup$ Are those 21 different outputs combined RBG outputs that you want an easy way to cycle through, or are they separate R, G & B components you want to mix in every different possible combinations? Because if it is the second option will yield A LOT of possible combinations. Now math is not my strong suit but I believe that would be 21 x 21 x 21 = 9261 possible combinations and that does not seem trivial to do. Could you add some screenshots of your custom node and the desired output? $\endgroup$ – Duarte Farrajota Ramos Jul 10 '16 at 1:56
  • $\begingroup$ @DuarteFarrajotaRamos I need a variation of 3 values from the 21, I think its around 1300 variations. $\endgroup$ – Denis Jul 10 '16 at 2:04
  • $\begingroup$ @DuarteFarrajotaRamos My midnight combinatorics skills say it should be 21P3=21x20x19=7980 (you can't pick the same socket for all 3). $\endgroup$ – PGmath Jul 10 '16 at 3:37
  • $\begingroup$ @PGmath the order doesn't matter so the number of variations is 1330, and I probably will reduce the number of outputs to 10, depends if there is a good contrast in the outputs. $\endgroup$ – Denis Jul 10 '16 at 3:40
  • $\begingroup$ @Denis I think you are thinking combinations (order of sockets picked doesn't matter) instead of permutations (order matters), which is why you are off by 3!=6. Unless the order doesn't matter, does it? (I.e. should [1,2,3] be different than [2,3,1] etc.?) $\endgroup$ – PGmath Jul 10 '16 at 3:40
4
$\begingroup$

Here's the script I came up with. I called the panel "Socket Arranger", and I put it in a new tab called "Socket" in the node editor.

Here's the main logic behind creating the links:

  • Focus on two groups: 1. the output sockets of one node and 2. the input sockets of another node.
  • I created sets from the output sockets. Each set should be unique (it shouldn't share sockets with other sets). That way, I can consider each set separately when thinking about how they should be connected to the input sockets. Therefore, use itertools.combinations.
  • The size of all sets made from itertools.combinations should be the same as the amount of input sockets. For example, only 3 output sockets can connect to 3 input sockets. (I didn't consider disconnected sockets. I don't know if you need it. Plus it sounds hard to do haha.)
  • For each unique set, I'm looking for as many input setups as possible, so it doesn't matter what order I connect them to the input sockets. When I dealt with the input sockets, I used itertools.permutations.
  • Each unique set made from itertools.combinations contains the same amount of input socket possiblilities (since we're only doing itertools.permutations on the same set of input sockets). So to find the total amount of output/input combinations, I multiplied len(itertools.combinations) of output sockets with len(itertools.permutations) of input sockets. Basically, in this case, "21 C 3" * "3 P 3" = 7980 unique combinations. @PGMath was right.
  • Overall, the code was about mixing a combination of output sockets with a permutation of input sockets.

Code:

import bpy, itertools
from bpy.types import Panel, PropertyGroup, Operator
from bpy.props import StringProperty, IntProperty, PointerProperty, CollectionProperty, BoolProperty
#I make my own term called a 'node map'. I call it a list of tuples with information about how the sockets should be linked. A larger CollectionProperty() stores a list of node maps.'

def updateBeforeFrame(scene):
    '''force update of node setups before the frame is rendered'''
    #This looks repetitive, but apparently this explicit assignment calls the proper "update" functions. Otherwise, it doesn't work, even if you see the index inputs in the Socket Arranger Panel changing
    if scene.inputGroup.combOrIso == True:
        scene.inputGroup.combIndex = scene.inputGroup.combIndex
    else:
       scene.inputGroup.isolateIndex = scene.inputGroup.isolateIndex

bpy.app.handlers.frame_change_pre.append(updateBeforeFrame)

def linkNodes(self,context):
    '''get all the node maps generated from the "Set Up Combinations" button, and interface with "Combination Index" slider to access the maps''' 
    mt = bpy.data.materials[context.scene.inputGroup.mat]
    nOu = mt.node_tree.nodes[context.scene.inputGroup.nodeOut]
    nIn = mt.node_tree.nodes[context.scene.inputGroup.nodeIn]

    intIndex = context.scene.inputGroup.combIndex

    set = context.scene.combSet[intIndex]
    lst = eval(set.miSetString) #convert list earlier converted to a string back to a list
    for output, input in lst:
        mt.node_tree.links.new(nIn.inputs[input], nOu.outputs[output])
    mt.node_tree.update_tag()


def linkIsolatedNodes(self,context):
    '''same as linkNodes, but with the "Isolate String" button and the "Isolate Index" button'''  
    mt = bpy.data.materials[context.scene.inputGroup.mat]
    nOu = mt.node_tree.nodes[context.scene.inputGroup.nodeOut]
    nIn = mt.node_tree.nodes[context.scene.inputGroup.nodeIn]

    intIndex = context.scene.inputGroup.isolateIndex

    set = context.scene.isoCombSet[intIndex]
    lst = eval(set.miSetString)
    for output, input in lst:
        mt.node_tree.links.new(nIn.inputs[input], nOu.outputs[output])        
    mt.node_tree.update_tag()

class CombinationSet(PropertyGroup):
    '''a holder for each list of node maps generated from "the "Set Up Combinations" button''' 

    miSetString = StringProperty()

class IsolatedCombinationSet(PropertyGroup):
    '''same as CombinationSet, but for "Isolate String button'''

    miSetString = StringProperty()

class InputGroup(PropertyGroup):
    '''hold inputs for the script's panel here'''
    #I can set soft_min to 0 for all IntProperties here since when I index any CollectionProperty, it always starts at 0

    combIndex = IntProperty(name="Combination Index", soft_min = 0, update=linkNodes)

    mat = StringProperty(name="Material Name")

    nodeOut = StringProperty(name="Node for Outputs Name",
    description="The name of the node to get output sockets from. (in the Node Panel)")

    nodeIn = StringProperty(name="Node for Inputs Name",
    description="The name of the node to get input sockets from. (in the Node Panel)")

    inOrExclude = BoolProperty(name="Must Contain All Listed?", 
    description = "If yes, isolate only the combinations containing all of the sockets. If no, look for items with at least one of the sockets listed. (The original combination list won't be affected.)",
    default = False)

    isolateString = StringProperty(name="Isolate String", 
    description="Type in the names of either input or output socket node names, separated by commas, to choose specific combinations. (Leave empty to get the original combination list)")

    isolateIndex = IntProperty(name="Isolate Index", soft_min = 0, update=linkIsolatedNodes)

    combOrIso = BoolProperty(name="Render combination keyframes? (else isolated keyframes)",
    description="If yes, the scene will render out keyframes put on the 'Combination Index' slider. If no, it'll render out the 'Isolate Index' slider.")


class SetUpCombinations(Operator):
    '''set up a list of "node maps"'''    
    bl_idname="custom.set_up_combinations"
    bl_label = "Set Up Combinations"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        mt = bpy.data.materials[context.scene.inputGroup.mat]
        nOu = mt.node_tree.nodes[context.scene.inputGroup.nodeOut]
        nIn = mt.node_tree.nodes[context.scene.inputGroup.nodeIn]

        nOuNames = [] 
        nInNames = []
        for socket in nOu.outputs.values():
            nOuNames.append(socket.name)
        for socket in nIn.inputs.values():
            nInNames.append(socket.name)
        miniSetSize = len(nIn.inputs.values())

        comb = list(itertools.combinations(nOuNames, miniSetSize))
        permu = list(itertools.permutations(nInNames, miniSetSize))
        cSet = context.scene.combSet
        cSet.clear()

        for com in comb:
            for per in permu:
                newSet = cSet.add()
                miniSet = list(zip(com, per))
                newSet.miSetString = str(miniSet) #convert to string so can be stored in a StringProperty for long-term usage

        context.scene.inputGroup.maxIndexLabel = str(len(cSet) - 1)
        return {"FINISHED"}

class IsolateCombinations(Operator):
    '''same as SetUpCombinations, but with the option to isolate certain node maps by socket name'''
    bl_idname = "custom.isolate_combinations"
    bl_label = "Isolate Combinations"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        iCoSet = context.scene.isoCombSet
        iCoSet.clear()

        stringLst = context.scene.inputGroup.isolateString.strip('[]').split(',')
        for i in stringLst:
            stringLst[stringLst.index(i)] = i.strip()

        if context.scene.inputGroup.inOrExclude == True:
            for set in context.scene.combSet:
                add = True
                for string in stringLst:
                    if string not in set.miSetString:
                        add = False
                        pass
                if add:
                    newSet = iCoSet.add()
                    newSet.miSetString = set.miSetString            
        else:
            for set in context.scene.combSet:
                add = False
                for string in stringLst:
                    if string in set.miSetString:
                        add = True
                        pass
                if add:
                    newSet = iCoSet.add()
                    newSet.miSetString = set.miSetString
        return {"FINISHED"}        




class SocketArranger(Panel):
    bl_idname = "socket_arranger"
    bl_label = "Socket Arranger"
    bl_space_type = "NODE_EDITOR"   
    bl_region_type = "TOOLS"    
    bl_category = "Sockets"
    bl_context = "objectmode"   


    def draw(self, context):
        lt = self.layout
        iGroup = context.scene.inputGroup

        length = len(context.scene.combSet)
        maxIndex = length - 1
        if length == 0:
            maxIndex = 0

        lt.prop(iGroup, "combIndex")
        lt.label(text="Max Comination Index: " + str(maxIndex))
        lt.label(text="(Max Combinations: " + str(length) + ")")
        lt.prop(iGroup, "mat")
        lt.prop(iGroup, "nodeOut")
        lt.prop(iGroup, "nodeIn")
        lt.operator("custom.set_up_combinations")
        lt.prop(iGroup, "isolateString")
        lt.operator("custom.isolate_combinations")
        lt.prop(iGroup, "isolateIndex")
        lt.prop(iGroup, "inOrExclude")

        length = len(context.scene.isoCombSet)
        maxIndex = length - 1
        if length == 0:
            maxIndex = 0        
        lt.label(text="Max Isolate Index: " + str(maxIndex))
        lt.label(text="(Number of Isolated Combinations: " + str(length) + ")")

        lt.prop(iGroup, "combOrIso")


def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.inputGroup = PointerProperty(type=InputGroup)
    bpy.types.Scene.combSet = CollectionProperty(type=CombinationSet) 
    bpy.types.Scene.isoCombSet = CollectionProperty(type=IsolatedCombinationSet)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.inputGroup
    del bpy.types.Scene.combSet
    del bpy.types.Scene.isoCombSet

if __name__ == "__main__":
    register()

enter image description here

I also added some options to allow you to isolate certain socket set ups to experiment with. I thought it might help you, especially with 21 output sockets. This doesn't work for nodes while you're editing a node group. If I can get it done, I'll try to add that in, maybe along with the "disconnected sockets" scenarios.

| improve this answer | |
$\endgroup$
  • 1
    $\begingroup$ At the end of linkNodes() you can add mt.node_tree.update_tag() and it will trigger a render refresh in the viewport as you change the combination index. $\endgroup$ – sambler Jul 10 '16 at 13:30
  • $\begingroup$ This is perfect! Testing it now, I tried to animate the combinations but it looks like its not updating in the 3D view, is there a way to fix that? $\endgroup$ – Denis Jul 10 '16 at 18:41
  • $\begingroup$ @Denis Cool. What do you mean by "animate"? Like, "is the 3D View updating every time I try a different combination?" The update_tag() function sambler suggested should make it work. In render mode, at least you should see the render info. bar on the top change slightly as you scroll through the combinations. $\endgroup$ – DragonautX Jul 10 '16 at 19:02
  • $\begingroup$ @DragonautX When I keyframe the combination index and hit play the screen is not updating in any mode, not even when i try to render the animation. $\endgroup$ – Denis Jul 10 '16 at 19:11
  • 1
    $\begingroup$ @DragonautX Thanks a lot! all working well now :) $\endgroup$ – Denis Jul 11 '16 at 2:18
1
$\begingroup$

For a simple script, create a combineRGB node for each 3 outputs from the group. That makes 7 different mixes from the 21 outputs you have.

Changing the mixing seed allows different combinations to be made.

import bpy
import random

mixing_seed = 2

node_tree = bpy.context.object.active_material.node_tree
nodes = node_tree.nodes

grp = nodes['Group']

out_idxs = [i for i in range(len(grp.outputs))]
pos_x = grp.location.x + 300
pos_y = grp.location.y

random.seed(mixing_seed)
random.shuffle(out_idxs)
while len(out_idxs):
    n = nodes.new('ShaderNodeCombineRGB')
    n.hide = True
    n.location.x = pos_x
    n.location.y = pos_y
    pos_y -= 70
    for c in range(3):
        node_tree.links.new(n.inputs[c], grp.outputs[out_idxs.pop()])
        if len(out_idxs) == 0: break
| improve this answer | |
$\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.