← All ideas

Logic and Flow Control for Nodes (looping, if...then...else, etc.)

Problem


Blender's current node implementation provides no simple way of altering the linear flow of a node tree, which forces the user to manually express the behavior in an inefficient and verbose way.

Proposal


A few primitive nodes that follow the NodeGroup pattern can be added to achieve this functionality. In the following section, I list common problematic scenarios and my proposed solution for each.

Looping


Goal

Perform the same node operation many times.

Old Workaround

Explicitly duplicate the node many times like this: Imgur

Proposed Solution

Create a "LoopGroup" and put the repeated nodes inside. A LoopGroup is identical in every way to a NodeGroup, except that it has an additional output node called "Loop Output". A LoopGroup is capable of executing its nodes multiple times before outputting its final values. Here is how the inside of a LoopGroup equivalent to the above operation looks: Imgur Notice how much more straightforward and succinct this is.

The "Loop Output" node's inputs match the configuration of the Group Input node. During the first iteration, the nodes execute normally until the Loop Output node is reached, at which point execution jumps back to the beginning. Each time the LoopGroup repeats itself, Group Input will contain the values sent to Loop Output in the previous cycle instead of the original inputs attached to the LoopGroup on the top level. Any inputs on the Loop Output that are left unconnected simply default to the original top-level inputs.

Finally, when the last iteration is completed, execution is allowed to continue past the Loop Output. Beyond this point, the behavior is exactly the same as a NodeGroup -- connections to Group Output are sent out of the group and back to the top level. Here is what a LoopGroup looks like on the top level: Imgur The "Iterations" field controls how many times the LoopGroup executes before moving on. It is also possible to attach an input to the Iterations field for a variable number of iterations. It might be wise to include a "max iterations" option to prevent variable iterations from exploding to a ridiculous amount and potentially freezing the program.

Recursion


Goal

Perform an iterative operation that requires fractal or tree structure.

Old Workaround

There is currently no way to do this natively in Blender without explicitly building the whole process with static nodes.

Proposed Solution

Allow a NodeGroup to contain itself. Add "Recursive Input" node that can only be created within a NodeGroup: Imgur The Recursive Input node has an extra input called "break" in addition to the existing inputs. Also, when a NodeGroup references itself, the input node gains an input called "Recursion Depth" and the top level interface gains an input called "Max Iterations":

Imgur

When a Recursive Input node is present, the contents of the NodeGroup will run normally until the Recursive Input node is reached. The Recursive Input is treated as an instance of all the nodes currently in the group, so the contents of the NodeGroup begin to run indefinitely.

This will continue until one of two conditions is met:

  • The value of Break is nonzero
  • The number of iterations executed exceeds Max Iterations

When recursion is skipped using the Break input, the execution point rides back up the layers of recursion until the value of Break is zero or the top is reached.

When recursion is finished, the nodes feed the output through the Group Output node like normal.

Conditional Statements (if...then...else)


Goal

Follow a different node path depending on an input. For example, an artist might want to have a mountainous terrain material to appear as snow IF the world Z is above a certain altitude, and stone or grass below that point.

Old Workaround

Use a Mix Shader or Math node to control the contribution of multiple inputs: Imgur This approach isn't particularly messy or cluttered, but it doesn't allow Blender to skip execution of large sections of nodes that are not needed at the time.

Proposed Solution

Create a "LogicGroup" and move the two cases inside. A LogicGroup node is identical in every way to a NodeGroup node, except that it accepts two NodeGroup datablocks instead of one. Only one of these two NodeGroups will be executed, and which one is determined by a "Condition" input. Here is how a LogicGroup equivalent to the above material looks: Imgur "FalseGroup" and "TrueGroup" contain different node trees. Only one of these groups will actually be executed, which is determined by the value connected to Condition. In this case, if Condition is greater than 4.000, TrueGroup will be substituted in; otherwise, FalseGroup will be used.

This approach is slightly cleaner than the existing method and should, in theory, improve the efficiency of node tree execution by skipping over nodes that don't affect anything.

Conclusion


These changes will improve node workflow efficiency in cases where the flow of a node tree isn't entirely linear. They would also allow quick changes and tinkering, as changing the number of iterations in a LoopGroup or swapping out the body of a conditional section in a LogicGroup is much quicker and simpler than it is using the old methods. In addition, these flow control primitives have the potential to enhance the performance of nodes altogether when used effectively.

Let me know if there's anything I've missed or any other forms of flow control that Blender's node editor could benefit from.

P2c6u26.png

97.4%

Development Status: Open

Upvote 37 Downvote 1
Quargzon
20 May 2018

19 Comments

Draise

Handy. Animation Nodes has had looping a while, having this in compositing and shading would be awesome.

3
o.mangold

I couldn't agree more. Exactly what I was thinking. I even would wish to go further than that. What I would like to have on top of it, would be two more features:

  • Callbacks from groups to nodes placed outside of it. For example, you want to apply a filter to a texture.

    • Currently you have to:
      • Create a separate copy of the filtering node for each texture you want to apply it to.
      • As the filter needs to evaluate the texture at several different coordinate locations, you have to duplicate the texture node/group multiple times inside the filter group.
    • This feature would allow you to:
      • Separate the filter group from the texture node, by placing both on the same level. The idea is to connect the texture to some special, new plugin sockets on the filter group. As it is a 'in between' connection, a node with callback sockets (the filter node here), would maybe have to be U-shaped or something instead of rectangular (yeah, funny, I know).
      • The duplication of the texture node vanishes, if the callback can be referenced multiple times from within inside the filter node. The idea is that duplicating the 'callback reference' node inside the filter allows to evaluate it multiple times with different input values.
  • Recursive evalution of groups nodes, by allowing to place a copy of the group into itself. This is not the same as the recursion in the sense of iteration mentioned above. It is the equivalent to recursive function calls. An example, I would like to implement badly, is a procedural texture looking like a tree or crack with nested forks spreading out from it, e.g. like this:

    Crack example

    • I believe currently it cannot reasonably be done at all, as the number of evaluations of the basic building block would grow exponentially with the nesting depth, or in short would be way to large for any sane person to create the whole mess by hand and also likely freaking slow to compute.
    • The idea is, that forking can be done by placing multiple copies of the iteratively called group into itself.
    • For efficiency reasons, as many recursive 'calls' can be dropped, there should likely be an input flag to signal whether the nested evaulation needs to be done or not. If set to false, it would just return some default values.
    • As a safety measure, there should likely also be a hard recursion limit (configurable), to prevent accidential infinite recursion. This would also allow to compute a memory usage limit in advance.
    • Concerning this special case of procedural trees/cracks texture, I tried to create one outside of blender, to see if it was practical. It seems the answer is yes, as I found an efficient algorithm (which could be implemented in material nodes with this recursion feature). It is quite fun to play around with, and can produce lots of very differently-looking trees. If someone wants to try it, or use it to create image textures, it can be found here . Sample image:

Tree example

2
Quargzon Author

@o.mangold: There is currently already basic functionality to avoid duplication (I just discovered this a minute ago) -- if you duplicate a node group, the duplicate references the same datablock, so all duplicates benefit from edits to the group.

On the other hand, recursive behavior would be awesome. That's exactly how I would implement it. Just allow nodes to internally reference themselves, and allow a "break" condition to be triggered early if desired.

This could even be extended to allow cyclic dependencies between NodeGroups (A calls B, B calls A, and so on). I see a lot of interesting possibilities with this.

The only concern I have about this is that I'm not sure how well this would work with material nodes. Generally, materials need to act like a continuous function, such that the renderer can throw some arbitrary coordinates at it and have it come back with the material properties at that location. In the case of a recursive procedural material, it would need to evaluate all the recursion steps for EVERY sample on EVERY point, which would explode exponentially in complexity.

The only way I can think of to handle that would be to cache the results in a lookup table (i.e. an image texture), at which point it might be better to just use texture nodes to tell it how to generate textures beforehand, and then just use those textures in the material.

EDIT: I've edited a section on recursion into the proposal.

o.mangold

@quargzon: About the exponential explosion, yes, that has worried me, too. Thus my experimenting which resulted in the linked tree texture code. It exactly does that, evaluate the recursion at each point. But the number of evaluations, when used as supposed to, doesn't explode exponentially with the tree depth (check for yourself, it prints out the sum at the end). The trick is to intelligently prune away unneeded recursive calls early. Sure, not for any recursively defined texture this can be done easily, but I tried to show that there are use cases where it works.

About the callbacks, yes, you are right, duplication of the texture inside the filter can already be avoided by wrapping it into a group. But having this feature still would allow to create a generic filter group, applicable to any texture without changing the filter group. So it avoids duplication of that group also.

1
o.mangold

@quargzon: One more remark, baking textures into images likely is a very common workaround for such issues. Unfortunately there isn't a feature to update the image and read it back automatically as needed, which makes it a painful process. And of course the is the matter of memory usage for hires textures.

1
Quargzon Author

@o.mangold: Ah, I didn't realize you were specifically talking about textures. To be honest, I was a bit lost on the first concept because I'd never actually considered filtering textures that way. I always just assumed it would be too many taps/nodes being thrown around to be practical instead of actually trying it, heh.

As for the recursionsplosion, this is something I'd need a Blender dev's perspective on. I'm not familiar with how Cycles in implemented on CPU or GPU, so for all I know, the nodes/samples/tiles might already employ some kind of recursive function in their makeup. There might not even be much wiggle room left in the stack to allow any meaningful recursion... Can't say for sure.

Also, I didn't realize that algorithm was "smart"; that's clever. Still, I think the usefulness of any such thing is contingent on how much stack space devs are willing to share for this feature. I have a hunch that modifiers and the scene graph probably use up a good portion of it. Who knows, maybe there's a reason recursive nodes aren't already in Blender... I hope that's not the case, but I can't deny the possibility.

Quargzon Author

@o.mangold: Yes, it would be nice if there was an entire node section dedicated to procedural textures. You could build a procedural texture completely out of nodes, then have it auto-bake to an image whenever necessary. Arbitrary changes in resolution would be incredibly simple since the generator functions could be resolution-independent.

It would be great to be able to sidestep the whole "make a plane, make a texture, unwrap the plane, stick an ortho camera over it, adjust render settings, and render" process and just have them when you want them. All the while, you're having to disrupt your creative process to build this "texture rig" and absolutely NOT getting instant feedback. You couldn't, for example, design a procedural emissive map and see how it affects the scene in a real render while you're designing it (assuming the reason you're baking it to a texture is that rendering it vanilla is impractical). Definitely an area that is lacking.

o.mangold

@quargzon: Watching som etutorials, It seems in fact quite a few artists build ridiculously complex nodal materials, including low-pass or edge filters and whoknowswhat.

The GPU implementation might indeed be an issue. There certainly is limited stack space, and performance could seriously suffer. Looking at the built-in procedural texture nodes (voronoi, musgrave ...) these are ridiculously simple to implement without much nontrivial control flow (musgrave has a fixed-length loop, depending on the dimension, but that's all). There is probably reason for it.

Still I would benefit from more options to create procedural textures. When I render on GPU I run more quickly out of memory than getting unrealistically low rendering speeds. Thus I love procedural textures, as they give arbitrarily high resolutions for no memory usage.

1
o.mangold

@quargzon: Yes, that would help. But often it would be necessary to bake the texture for each object separately, because the textures are used three-dimensional (object or generated coordinates, instead of UV). I do that whenever possible, as it avoids visible seams.

Quargzon Author

@o.mangold: If we're talking about purely procedural textures without any lighting/raytracing calculations going on, the baking should be over in mere milliseconds (assuming a reasonable amount of textures). I was thinking it would update every time the node tree was altered or a render was started. Taking this further, rasterized procedural textures could be animated and the renderer would simply update them before rendering every frame.

And yes, in the case where 3D coordinates are to be preserved, meshes would need to be unwrapped. The only reason I wouldn't recommend dynamically unwrapping meshes and allocating the necessary texture space before render is because this would likely frequently fail and end up allocating something absurd for distant objects where the detail isn't even visible. For that reason, I lean towards the traditional style of setting up a bake and creating the image textures manually.

o.mangold

@quargzon: I could live with the requirement of having to unwrap meshes before this kind of image texture generation will work. Maybe it would be the easiest way to go, to create an image for every texture that would be troublesome to set up with nodes. All a question of memory and workflow optimization then. For the former, I'm curious how well the announced feature of keeping textures in host memory, in case GPU memory runs out. For the latter, would have to search for plugins. Something to trigger stuff like rendering of a texture to an image or generating one externally, followed by an automatic update of the in-memory copy. would be really cool.

Quargzon Author

@o.mangold: I was thinking having the procedural texture updates integrated into the loading system might be a good idea. For example, at times when an image-based texture would typically be read off the hard drive, an equivalent procedural texture would bake itself. Similarly to an animated image/video texture, a procedural texture with keyframed values would update every frame.

About the host memory offloading, I suspect it only works on a per-texture basis. If a scene contained a massive 32768x32768 texture or something, I don't know if it could break that texture up across VRAM and RAM. Runaway massive textures were my main concern with auto-calculating texture space for procedural bakes. An algorithm might get too excited and allocate something impractical if it works similarly to the adaptive subdivision (allocating texture pixels based on screen surface area). Maybe it could have minimum and maximum pixel density limits, also like adaptive subdivision.

But I agree that doing it manually wouldn't be too bad. Besides, that seems more like something an add-on could fix after the core functionality is exposed to Python.

o.mangold

@quargzon: Automatic texture baking as needed certainly would be a step forward. After all one doesn't want to think about that while creating a scene. Finding the right resolution for baking the image texture is of course another such thing. The great thing about procedural textures in the material definition is, that you can get instant previews while tuning parameters. I often put the 3d editor in rendered mode beside the node editor. But is there currently something like a time when images are reloaded? I mean other then when opening a file manually or clicking on reload. I was under the impression that Blender otherwise doesn't feel a need to do that.

About the adaptive subdivision, as it currently works, I also feel that it's problematic. It seems when it is enabled in the 3d editor, the subdivision is still done down to the finest level everywhere. Obviously there are reasons why it is still experimental.

Not sure how the texture offloading will be done, but if I read this

devblogs.nvidia.com/maximizing-unified-memory-performance-cuda/

correctly, then Nvidias since Pascal are able to swap mapped host memory in and out as needed. So swapping in partial textures should be possible. Render performance would likely suffer a lot with Progressive Refine enabled,

Quargzon Author

@o.mangold: That was probably a bad example. I meant it more in the sense that one would reload an image when it was modified on external storage (which isn't automatic). However, a procedural texture should do the same automatically when its nodes are modified.

As for the adaptive subdivision, it actually attempts to optimize the number of polygons per output pixel. Because of this, when rendering with a perspective camera, more distant (i.e. smaller) objects end up with fewer polygons, as they take up less screen space. There do seem to be scenarios where the adaptive subdivision freaks out and generates incorrect results, but in my experience with it, it generally works in the expected way if the dicing settings are set sanely. The main problem arises when a part of the object is extremely close to the camera. The algorithm will happily subdivide the surface a million times to achieve the appropriate polygon density for that camera distance. That's why the "Max Subdivisions" setting is absolutely needed.

And yes, partial textures would definitely be slower. I hope Blender has some system in place to index what textures or parts of textures are needed for each tile so it can better optimize the VRAM allocation.

o.mangold

@quargzon: I guess we could quite a bit sidetracked from the original proposal, huh :) But in any case we seem to be quite in line about the usefulness of the mentioned features. Improvements related to nodes and in particular textures.

1
Quargzon Author

@o.mangold: Haha, we really did. It was a very interesting discussion, though. Maybe a Blender developer will come by some time and read through it all. We can only hope...

elys1

+1. A select or switch node could also be nice. Select a geometry based on a string value, or a boolean.

1
artturi85

I very much hope this will be added. Also I hope the "loop group" would actually be little bit similar to the mapping node. So that you could edit the angle and location of each iteration.

1

Looking at your comment I think adding a logic node / or a script (costomizable) node is good way to do both

may be like these in pyflow github project

this could be a sympolic to text editor script to esecute with an opthional