Can you branch to do nothing?

I have the following shader, the character is broken into an armor, head, and weapon section.
If the texture for the weapon section is null I want that entire branch to just stop. Don’t output anything.

That’s because some animations, like Idle, don’t have a secondary texture for the weapon. Thus the weapon emission section of the shader should not get added to the sprite.
(The easiest solution is to add a blank image as the weapon secondary texture in the idle animation, but I want to know if it can be done in the graph.)

I have been trying for a while to use branch and compare and other logic operators, and I can get it close to working, but then I get confused as to how (if possible) can I just end the branch.

If I compare a vector4 to RGBA of Weapon Emission Sample Texture, I can get a true/false output from a branch node… but then the branch node has to go somewhere. If it’s false, I don’t want it to be going anywhere. I don’t want it passing colors, or floats, or anything. In the final add node of my shader it should add nothing.

Does that makes sense? Is it possible?

No, you can’t.

It may twist your mind, but think of all of the things in a shadergraph as being run from right to left.

Start with the “Fragment Base Color” socket. Let’s say it has a wire feeding in from the output of Add. Okay, it MUST run the Add node to supply something to that Fragment Base Color. But when it runs the Add node, the Add node has two input sockets. It MUST run the things that feed into those sockets. And so on.

When an input socket is fed from a Branch, it will run the Branch. And the Branch will decide on one of its inputs, it MUST decide on an answer to feed downstream. It’s mandatory. A node cannot just refuse to give anything on its output, because the stuff downstream has already begun to run its own code and is demanding an input value.

Instead, your Branch will just have to supply black or white or something simple, and you will also want to supply a 0 to the “Fragment Alpha” socket under the same circumstances. If the situation requires you choose no tile, then the same situation should resolve to a clear alpha so whatever ends up in the Base Color just won’t be drawn.

1 Like

Thanks that helped a lot, this is what I came up with.
I’m not sure if it’s sound, but it seems to work how I imagine in my head.
A null texture compared to a 0,0,0,0 vector4 seems to work.
If the empty texture = 0,0,0,0 then pass out the empty texture, otherwise apply the color, and intensity multipliers and pass that out.

9802437--1407195--upload_2024-4-29_0-38-36.png

9802437--1407201--upload_2024-4-29_0-41-20.png

When creating shaders you must change your way of thinking. Those are not conditional logic with different flow paths, but instead they are mathematical formulas. You calculate stuff and output the result.
Yes, you can compare and do stuff like you tried above, however when possible you should try to avoid it, mainly for performence reasons ( also there are different kind of branching ).

Let’s break down your shader.
First of all you might notice that connection between texture and comparison turns from pink to blue, it means the first component (x) is used in comparison. That means that you only check if red channel is zero - what if your emission is green only?
Second, equality comparison with floating point numbers is not good idea, it might work in case of zeros, but otherwise it might fail because numbers are not precise.
Third, your branch is actually “slow” branch, because you are testing value that comes from texture and it’s not uniform for all pixels.

In such a case local keyword is better way to handle this. It will compile variants of this shader for provided inputs, and you can enable or disable that keyword via code or with checkbox in material inspector.
This is how I understand “null” texture, but if you need to check whatever it’s black or something else, then you could use this texture as mask by multiplying by it. Also can use lerp instead of branch.

I am not sure if there is something I misunderstood, because I don’t understand why you need this branch at all.
If you multiply your emission colors by black (0) you always get zero, so if there is blank, missing, empty image, then you get zero, and if you add zero to any number it’s still the same number.

1 Like

Two multiplies are very nearly free on a GPU.

One thing to try and wrap your head around is a shader is code that runs separately for every pixel the mesh is visible on screen. That compare isn’t comparing “the texture”, it’s comparing the color of that texture at the point that was sampled for the current on screen pixel. The texture could be a single pixel, but it’s still going to get sampled by every on screen pixel separately. That’s slightly cheaper than if there was a real full size texture there, but probably not as much cheaper as you think. Humorously, sampling a 1 pixel texture is the most costly thing in the graph. Everything afterwards is very nearly free in comparison.

However, doing a branch based on data that is sampled from a texture is the slowest kind of branch possible, as it both has to wait for the texture to be sampled, and then as to do a real branch between code paths which has a cost of its own. Also, because GPUs are SIMD processors that work on multiple pixels in parallel, if two pixels choose separate branches, you end up paying the cost of doing both paths in series, and then the extra cost of the branch itself.

However, a real branch costs more than those two multiplies, and shader compilers are smart enough to know that, so the shader compiler will never actually use a real branch for this. Instead for simple cases like above it’s more likely to use a “fast branch”, aka a swap. It’ll just run the code on both paths and choose the wanted results afterwards. This means you’re paying the cost of “both paths”, but since one path doesn’t actually do anything it’s fine, and a swap is faster than a real branch.

Except a swap also costs more than two multiplies, and shader compilers are smart enough to know that, and that the above “branch” doesn’t actually do anything useful. So, the funny thing is it’s very unlikely that it’ll even include a swap. It’ll just always run the two multiplies and ignore the branch and comparison entirely, striping that from the compiled shader.

TLDR: The branch is not only not needed, it doesn’t actually do anything at all in this specific case and will be entirely ignored.

You still shouldn’t add the nodes though, as even though it doesn’t do anything in the final shader, it does mean it’s more for the shader generator and compiler to do. There’s also no guarantee the shader compiler will make these smart decisions. It may leave a real branch in there sometimes.

2 Likes