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:
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:
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:
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:
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":
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:
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:
"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.
19 Comments
Handy. Animation Nodes has had looping a while, having this in compositing and shading would be awesome.
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.
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:
@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.
@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.
@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.
@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.
@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.
@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.
@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.
@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.
@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.
@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.
@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,
@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.
@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.
@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...
+1. A select or switch node could also be nice. Select a geometry based on a string value, or a boolean.
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.
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