Shaders and Defines

Hi there,

I am developing some shader but I would like to use preprocessor macros.
Here is what I am doing in my shaders, nothing special.

#if defined(MY_DEFINE)
    // something
#else
    // other thing
#endif

And in my scripts I am enabling and disabling keywords this way. Again nothing fantastic.

Shader.EnableKeyword("MY_DEFINE");

But there is a tricky thing. I am adding those defines in some Unity built-in include files (.cginc) that I override.
Because of that I am not able to add multi_compile or shader_feature otherwise I would have to change all the default Unity built-in shaders to add those pragma.

Is there a way to make defines work without using multi_compile. Or maybe there are some other ways to do this.

Thank you very much !

Not that I know of.

Those #if statements are processed when the shaders are compiled, which happens before any script runs. Some platform specific preprocessor macros are set by Unity’s own shader compiler system before the shaders are compiled, which is why some of them don’t have multi_compiles, like those listed in that link. Any #if that you need to swap between while the game is running needs to be precompiled ahead of time as a variant and the multi_compile and shader_feature pragmas are the only way Unity knows it needs to do that.

You’ll need to make copies of the built in shaders with #pragma multi_compile _ MY_DEFINE added for this to work. Alternatively, you could just use Shader.SetGlobalFloat("_myValue", value) and if (_myValue == 1) { // do stuff } if you’re doing this for a desktop project. Modern GPUs are surprisingly efficient at dealing with if statements these days, especially if you can use UNITY_BRANCH before the if statement.

1 Like

Thank you for your great answer as always @bgolus

Hmm and I think that there’s no way to insert custom preprocessor macros at this step…

Yep, that’s what I did but I wanted to avoid branching. As I’ve heard bad things about if statement in shaders. I’ve also read about UNITY_BRANCH and UNITY_FLATTEN (Unity documentation, Microsoft documentation) but I don’t really get the consequences and implications of those attributes.

Using if statements in shaders for mobile isn’t great, both “paths” get calculated then one is chosen. For something simple like choosing between two values it’s not a big deal, but if you’re using it to avoid a large calculation then, well, you’re not.

On desktop and consoles however, GPUs have gotten really, really good at dealing with if statements, especially if it’s testing against a uniform value (either set on the material or a shader global). A few years ago I found a blog post where someone tested out the “common knowledge” of avoiding if statements by having a shader if between two fairly expensive operations. Unfortunately I can’t find it now. The “common knowledge” was that the result would be the same as or more expensive than just doing both operations and choosing between them with a lerp.

Instead he found that using a uniform or a checkboard pattern was almost indistinguishable from unique shaders (and the checkboard, while slower than a single unique shader across the screen, was probably faster than doing that with a checkerboard mesh). The only case that was close to as slow as the “common knowledge” was when he tested against a high frequency noise, but still faster than the worse case. He tested it again more recently (like 3~4 years ago if my memory serves me) and found the performance using noise had nearly matched the uniform case.

It’s pretty easy to test too on your own. Write a shader that does a silly expensive operation like for(i=0; i<100; i++) value = acos(value); inside an if statement and put it inside an if() you toggle on and off via a uniform.

My understanding is UNITY_BRANCH (which is HLSL’s [branch]) is a hint to tell the shader compiler you really want it to treat that particular if block as a dynamic branch, but modern shader compilers will often do it for if statements you even if you don’t have it.

UNITY_FLATTEN is a hint to do the old “common knowledge” way, which might be needed for “flow control” reasons. Basically if you’re using discard, or clip(), or ddx() / ddy() / fwidth() derivative functions on the result of a value modified by an if it can’t be a branch.

4 Likes