Tools for Cycles PyNodes

Secrop - would this loop node help simplify any of your complex iridescent materials?

Hmm…this might be naive, but are the custom nodes created by this addon limited to only behaving the way nodes currently do? Or can you do operations to the outputs of nodes that would normally be impossible?

For instance something like: use the output of a non-displayed glossy shader to drive the height value of displacement applied to a displayed shader, so the object is bumpier where it is hit by light.

Or for something more likely to work: perform logical operations on the output of texture nodes. This is technically possible with stock nodes as I’ve seen some pretty complex “if-then-else” custom nodegroups, but this could be simpler and more powerful if you can open up the .py and add conditions there.

EDIT: I realize that the early posts in the thread do cover this kind of conditional/switch/interpolation node, but I’m wondering if that’s done identically to how a stock nodegroup would do it, or it’s handled behind the scenes in a cleaner way.

I think it would only simplify the process to build them… not really in making a small footprint in render time.

It’s an addon, not really a cycles’ patch. It turns customization of your node editor easier. What the nodes can do in terms of rendering is basically the same as you do with nodegroups. But in editing time, nodes can perform actions in the blender data, or change the node’s interface, check if some node is on the nodetree, etc.

For instance something like: use the output of a non-displayed glossy shader to drive the height value of displacement applied to a displayed shader, so the object is bumpier where it is hit by light.

No. That won’t work. (and it would require some deep changes in Cycles’ source code)

Or for something more likely to work: perform logical operations on the output of texture nodes. This is technically possible with stock nodes as I’ve seen some pretty complex “if-then-else” custom nodegroups, but this could be simpler and more powerful if you can open up the .py and add conditions there.

Again, we are limited to what the builtin nodes do.
You can use 'if-then-else’s for looking up blender data, and other stuff; but not to perform actions in render time over the data that will flow through the nodes. In this case, you need to perform the same steps that those nodegroups you mentioned do. (this is also a kind of GPU limitation, as conditionals are not very parallel computation friendly)

Yep - that’s kinda what I was getting at.

In your “complex approach to iridescence” thread, you make reference to a diagram that shows the various orders of refraction. Could these refraction orders be solved recursively using your loop node and thereby simplify your original node setup.

There are two problems that need to be solved in the ‘diffraction grating shader’… One is the output direction, which is only available in the closure’s code (I get this vector by controlling the normals); And the second are the orders, which can overlap and produce another kind of interference (and I’m still stuck in the best way to calculate this). I’ve tried, with no success, to use the Wigner’s Function for the wave/color calculations; but that’s still some steps above my current math knowledge.
The loop node could simplify the process of building nodes to deal with those points, but it’s still a very delicate process, and very difficult to optimize (at least for doing it accuratelly). :frowning:

Just a quick question - does this node work with 2.79?

I installed the addon, then loaded a file I had been experimenting with previously, but all i’m getting is a black output.

1 Like

I have to update some stuff for the latest builds… It’s supposed to work with the 2.79 but not with 2.79a. Remember that gpu rendering (viewport material shading) is still not working as my patch didn’t went to master.
There are some stuff that changed in the api (along with the new python version), and I haven’t got the time to digest the changes and how to adapt it.
I hope the next version will be more user friendly, specially in the organization of the Add menu, but this needs to change the actual nodeitems_builtins and nodeitems_utils modules.

ok thanks.

When I saw this, I got all excited. I poked it with a stick and I’ve made a few handy things, but what I really wanted is tough to figure out since I’m not sure where to dig into the docs.
Here’s my drool bucket idea in a nutshell and for the life of me, I can’t figure out why nobody found a way to make this happen since texture sets are extremely common use and you nearly always want their mapping to match anyway:

Is it even possible? Other Render engines have it so there’s something, but this is cycles.

FWIW, I got this but the file path is the roadblock to making this useful.

import bpy
from ShaderNodeBase import ShaderNodeBase

class Box_MapSet(ShaderNodeBase):

    bl_name='Box_MapSet'
    bl_label='Box_MapSet'
    bl_icon='NONE'

    def defaultNodeTree(self):
        self.addNode('NodeFrame', {'name':'BoxMap Set'})
        self.addNode('ShaderNodeTexImage', {'name':'Roughness', 'color_space':'NONE', 'projection':'BOX', 'projection_blend':0.1})
        self.addNode('ShaderNodeTexImage', {'name':'Metallic', 'color_space':'NONE', 'projection':'BOX', 'projection_blend':0.1})
        self.addNode('ShaderNodeTexImage', {'name':'AO', 'projection':'BOX', 'projection_blend':0.1})
        self.addNode('ShaderNodeTexImage', {'name':'Base Color', 'projection':'BOX', 'projection_blend':0.1})
        self.addNode('NodeReroute', {'name':'Reroute'})
        self.addNode('ShaderNodeTexImage', {'name':'Displacement', 'color_space':'NONE', 'projection':'BOX', 'projection_blend':0.1})
        self.addNode('ShaderNodeTexImage', {'name':'Normal', 'color_space':'NONE', 'projection':'BOX', 'projection_blend':0.1})
        self.addInput('NodeSocketVector', {'name':'Vector', 'default_value':[0.000,0.000,0.000], 'min_value':0.000, 'max_value':1.000})
        self.addOutput('NodeSocketColor', {'name':'Base Color', 'default_value':[0.000,0.000,0.000,0.000]})
        self.addOutput('NodeSocketColor', {'name':'AO', 'default_value':[0.000,0.000,0.000,0.000]})
        self.addOutput('NodeSocketColor', {'name':'Metallic', 'default_value':[0.000,0.000,0.000,0.000]})
        self.addOutput('NodeSocketColor', {'name':'Roughness', 'default_value':[0.000,0.000,0.000,0.000]})
        self.addOutput('NodeSocketColor', {'name':'Normal', 'default_value':[0.000,0.000,0.000,0.000]})
        self.addOutput('NodeSocketColor', {'name':'Displacement', 'default_value':[0.000,0.000,0.000,0.000]})
        self.addLink('nodes["Reroute"].outputs[0]', 'nodes["Base Color"].inputs[0]')
        self.addLink('nodes["Displacement"].outputs[0]', 'nodes["Group Output"].inputs[5]')
        self.addLink('nodes["Roughness"].outputs[0]', 'nodes["Group Output"].inputs[3]')
        self.addLink('nodes["Metallic"].outputs[0]', 'nodes["Group Output"].inputs[2]')
        self.addLink('nodes["Base Color"].outputs[0]', 'nodes["Group Output"].inputs[0]')
        self.addLink('nodes["Reroute"].outputs[0]', 'nodes["Metallic"].inputs[0]')
        self.addLink('nodes["Reroute"].outputs[0]', 'nodes["Roughness"].inputs[0]')
        self.addLink('nodes["Reroute"].outputs[0]', 'nodes["Displacement"].inputs[0]')
        self.addLink('nodes["Reroute"].outputs[0]', 'nodes["Normal"].inputs[0]')
        self.addLink('nodes["Normal"].outputs[0]', 'nodes["Group Output"].inputs[4]')
        self.addLink('nodes["Reroute"].outputs[0]', 'nodes["AO"].inputs[0]')
        self.addLink('nodes["AO"].outputs[0]', 'nodes["Group Output"].inputs[1]')
        self.addLink('nodes["Group Input"].outputs[0]', 'nodes["Reroute"].inputs[0]')

    def init(self, context):
        self.setupTree()

    #def copy(self, node):

    #def free(self):

    #def socket_value_update(self, context):

    #def update(self):

    #def draw_buttons(self, context, layout):

    #def draw_buttons_ext(self, contex, layout):

    #def draw_label(self):

@sirmaxim, adding that buttom requires you to have a TextProperty for the path, and a layout.template_image or any other way to expose the property, in the draw_buttons function… The property can then have an update function that sets all the internal ShaderNodeTexImage nodes.

On the topic, I like this idea!:+1:
I’d probably add some kind of regex for determine textures names, while having the hability to add and remove outputs, etc… perhaps even read all textures in a folder and automatically create the outputs…
:slight_smile:

Yep! Exactly where I want to get the node. I’d also want to change the node label to match the texture name.

I’ve got some pretty complex node groups and it’s a hassle to have
pre-img group > images > post-img group.
There are some addons that can do this sort of thing, but I don’t really want to build addons out of my node groups. There’s still a lot of sockets to hook up. It doesn’t really fix the UX issue of not being able to expose ImageTexture controls to group input.

This method can which makes you awesome. So want this in trunk. Would give every tech artist the ability to make custom nodes for their team out of node groups the team makes.

Honestly, I would’ve been happy with just being able to expose the controls, but the fact that I can do more (one file button for all images, etc) is a really nice option, even though it’s a little extra work to write code.

Changing the node label is easy: on the draw_label(), just return a string with the wanted label.

It’s possible to ‘expose’ ImageTexture attributes, exactly the same way you expose the path, in the draw_buttons() function; not with sockets but with layout properties. For example, nodes[‘Image Texture.xxx’].color_space=‘NONE’ does the same as setting that node to ‘Non-Color Data’. And you can use enums directly copied from the node’s class (so if later versions of blender have more options, the script will still work seamlessly.

About this being in the trunk, I’m working on it; I already contact some members of the dev team, but nobody knows exactly how the node systems will work in 2.8… :confused:

About the nodes… i think it was mentioned that Lucas is supposed to come to code quest this or the next week… prolly will now more after.

I don’t think Lucas is going to work with nodes… But I’ll poke him when he starts CQ.
[Edited: I was thinking wrong :roll_eyes:!! Just opened the email from Ton’s (from 15/2)… :slight_smile:]

@sirmaxim, another feature you can use is the draw_buttons_ext() function. It will draw a panel in the Properties sidebar, instead of doing it in the node. I use this option in some nodes that require some configuration that can be too much for displaying inside the node… for example, options that once setted, won’t probably ever change in the respective scene; this is the kind of thing that you don’t need to have being displayed while setting your materials.

Ok, so I can get filepath to work, display the name and change the label, but I wanted it to behave like node wrangler’s Principled import which is using

files = CollectionProperty(
                    type=bpy.types.OperatorFileListElement,
                    options={'HIDDEN', 'SKIP_SAVE'})

to get an array of files. I gave it a whack, but the addon doesn’t dynamically (and for good reason) register random operator classes contained in the node files. I was trying to avoid having complex parsing of files because everyone has different naming/storage schemes for their textures that vary by source/project/etc. Selecting the textures and having it detect type is greatly preferred. Any suggestions?

I think you can register the operator externally (i.e as another addon), and access it from the node… It might also be possible to include the operator in the node code, but it needs to be called by the register function, that sits in the addon and not in the node and needs to be changed in order to give registering control to the nodes…

Another option, could be just to retrieve the directory path, display a list of all (or filtered) image files inside, and let user select which ones to use…
There’s probably other ways to do this… must take a closer look and experiment a bit.

First working prototype, basically directly hijacked from Node Wrangler:

Loaded textures stay that way so it’s a one shot.

1 Like

Ok. So this is bad:

def copy(self, node):
      self.node_tree=node.node_tree.copy()

Causes a custom group to spawn a whole lot of .### copies of itself on redraw. Like 100+ in just a few minutes of messing around.
console error it produces:

ID user decrement error: NTShaderNodeInterpolate_nodetree (from '[Main]'): 0 <= 0

Still working out some bugs in my texture node dreams before I take a crack at writing the EnumProperty changes, but it’s now able to load textures from a different directory repeatedly.

Pull requests interest you by chance? I’ve got at least a couple nodes that might prove useful to people as well as this image set when it’s ready. (I would do one PR per node to keep it simple if it were me).

That’s strange… the copy isn’t supposed to be called from the draw calls (only by duplicating a node)… unless you explicitly tell it so. :confused:
What is the blender version (and hash) you’re working with?

In this particular case, the node needs a private nodetree for each instance (because each instance will reference a different set of textures)… this means that you need to clear the used nodetree in the free() function.
In the end, there should be one nodetree for each node instance.

About Pull requests, I think the github page already allows it, but if not just tell me your github username, and I’ll add you to the Collaborators.

Yeah, it is strange. I currently have

def init(self, context):
        self.width = 220
        self.setupTree()

    # def copy(self, node):
    #     self.node_tree=node.node_tree.copy()

    def free(self):
        if self.node_tree.users==1:
            bpy.data.node_groups.remove(self.node_tree, do_unlink=True)

    #def socket_value_update(self, context):

    #def update(self):

    def draw_buttons(self, context, layout):
        col=layout.column()
        col.prop(self, 'filepath', text="Base Color")

    #def draw_buttons_ext(self, contex, layout):

    def draw_label(self):
        node_label = path.basename(self.filepath)
        return node_label

    def draw_menu():
        return 'SH_NEW_TexTools' , 'Texture Group'

But if I un-comment the def copy it makes a bunch of hidden node groups every time I do anything with the node. Some of the included nodes do the same. version:

2.79b
2018-03-22 14:10
Hash: f4dc9f9d68b

built on ArchLinux, so there might be some minor changes for packaging purposes. I can check the pkgbuild, but I don’t expect anything unusual. It might very well be one of my addons doing something. I’ve got a fair few that affect the node editor panels.

edit: decided to just throw this in here so you can see the whole thing: https://github.com/sirmaxim/ShaderNodesExtra/commit/6927ae5402bf7d5f9afc633a71d1473859dca69e