3

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

Denis
  • 13,059
  • 7
  • 60
  • 81
  • 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? – Duarte Farrajota Ramos Jul 10 '16 at 01:56
  • @DuarteFarrajotaRamos I need a variation of 3 values from the 21, I think its around 1300 variations. – Denis Jul 10 '16 at 02:04
  • @DuarteFarrajotaRamos My midnight combinatorics skills say it should be 21P3=21x20x19=7980 (you can't pick the same socket for all 3). – PGmath Jul 10 '16 at 03:37
  • @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. – Denis Jul 10 '16 at 03:40
  • @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.?) – PGmath Jul 10 '16 at 03:40
  • @Denis You answered my question telepathically! – PGmath Jul 10 '16 at 03:43
  • @Denis two more questions. What kind of input do you need? Would an approach that does generate permutations be acceptable, or does it have to weed out duplicate combinations? – PGmath Jul 10 '16 at 03:49
  • @PGmath I would prefer that its not generated randomly but in a sequence and duplicates are not really important. – Denis Jul 10 '16 at 04:02
  • @denis I don't have time to do it tonight, but my idea is to take an input number and convert it to a 3-digit base-N number (using modulo math nodes), each digit of which is sent to a homemade N-way mix node. Clunky, but doable. (I will see if I can find a way of weeding out the duplicate combos mathematically.) – PGmath Jul 10 '16 at 04:25
  • @PGmath I think this way its not very productive and too complex to make, maybe to put something on each color that scrolls quickly through the outputs will be better. I will do some more tests and probably will change the question. – Denis Jul 10 '16 at 04:32

2 Answers2

4

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.

DragonautX
  • 1,306
  • 1
  • 12
  • 18
  • 1
    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. – sambler Jul 10 '16 at 13:30
  • 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? – Denis Jul 10 '16 at 18:41
  • @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. – DragonautX Jul 10 '16 at 19:02
  • @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. – Denis Jul 10 '16 at 19:11
  • @Denis Oh ok. Apparently, the update functions linkNodes() and linkIsolatedNodes() aren't called when the frame changes. What I found from here though is that I can explicitly call a handler function to run those update functions before a frame. I called it updateBeforeFrame(). I also found that assigning a Property to itself calls an update function. It sounds odd, but if it works, it works haha. I've updated the script with edits. – DragonautX Jul 10 '16 at 22:22
  • @Denis Also, I added a checkbox at the bottom of the panel to say whether to animate using the "Combination Index" keyframes or the "Isolate Index" keyframes. – DragonautX Jul 10 '16 at 22:30
  • @DragonautX I'm not sure if I'm doing something wrong but keyframed animation is not updating or rendered. – Denis Jul 11 '16 at 00:20
  • @Denis Did you check the new checkbox I made? Or is your node setup complete, like is everything connected to a Diffuse BSDF or Material Output ? Maybe clicking "Set Up Combinations" will help. If you'd like, I can look at your Blender file. – DragonautX Jul 11 '16 at 01:02
  • @DragonautX the node setup is complete and I tried to keyframe all the variations, but its stuck in one combination that was set manually. And the file is too big to upload. – Denis Jul 11 '16 at 01:17
  • @Denis What do you mean by "set manually"? You put a keyframe on 1 and 20 for example to run through all variations, and then what did you "set manually"? Does it involve keyframes? And how big is the file? – DragonautX Jul 11 '16 at 01:33
  • @DragonautX Just for testing I set the frames equal to the combinations and set the keyframes from 0 to the end, but the frames rendered only the combination that was initially in the node setup even though i can see the keyframed index is changing every frame but not the node connections. The file is about 700mb – Denis Jul 11 '16 at 01:39
  • @DragonautX I dont really need to render the animation, but it would help to play the animation instead of scrolling through the combination one by one – Denis Jul 11 '16 at 01:41
  • @Denis Oh ok, so like from frame 0 to 7979, and for each frame, show a specific node setup. And even when you press play, you dont see anything in the node editor changing, only what was originally there. Odd. Adding the handler function should've fixed this. It did for me. Wow thats a big file haha. Was it that size before you ran the script? If no, maybe you could run a clear () to reduce file size. If the animation is for just looking at different settings, you can set the 3d view to render and you can click and hold the "Combination Index" Panel and move the mouse to scroll comfortably. – DragonautX Jul 11 '16 at 01:59
  • @DragonautX I reloaded the script and it solved the problem, Im not sure why it didnt work before – Denis Jul 11 '16 at 02:12
  • @Denis Oh, it's all working now, socket changes and animation? Awesome! Oh ya that's right. When I try to reimport scripts into the console, it doesn't update, so the last option I always see is to save the blend file and scripts and reopen the same file. Anyways, well, if everything works for you, then happy Blending then! – DragonautX Jul 11 '16 at 02:17
  • 1
    @DragonautX Thanks a lot! all working well now :) – Denis Jul 11 '16 at 02:18
  • Oops, had to add a missing mt.node_tree.update_tag() to linkIsolatedNodes(). Just a self note. – DragonautX Jul 11 '16 at 09:17
1

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
sambler
  • 55,387
  • 3
  • 59
  • 192