Can you use #defines like method parameters in HLSL?

364 views Asked by At

In HLSL, is there a way to make defines act like swap-able methods? My use case is creating a method that does fractal brownian noise with a sampling function(x, y). Ideally I would be able to have a parameter that is a method, and just call that parameter, but I can't seem to do that in HLSL in Unity. It wouldn't make sense to copy+paste the entire fractal brown method and change just the one sampler line, especially if I'm using multiple layers of different noise functions for a final output. But I can't seem to find out how to do it.

Here is what I've tried:

#define NOISE_SAMPLE Random(x, y)

float FBM()
{
    ...
    float somevalue = NOISE_SAMPLE;
    ....
}

And in a compute buffer, I have something like this:

void CSMain(uint3 id : SV_DispatchThreadID)
{
    ...
    #undef NOISE_SAMPLE 
    #define NOISE_SAMPLE Perlin(x, y)
    float result = FBM();
    ...
}

However this doesn't seem to work. If I use NOISE_SAMPLE in the CSMain function, it uses the Perlin version. However, calling FBM() still uses the random version. This doesn't seem to make sense as I've read elsewhere that all functions are inline, so I thought the FBM function would 'inline' itself below the redefinition with the Perlin version. Why is this the case and what are some options for my use case?

1

There are 1 answers

0
Bizzarrus On

This doesn't work, as a #define is a preprocessor instruction, and the preprocessor does its work before any other part of the HLSL compiler. So, even though your function is eventually inlined, this inlining only happens long after the preprocessor has run. In fact, the preprocessor is basically doing a purely string-based find-and-replace (just slightly smarter) before the actual compiler even sees your code. It isn't even aware of the concept of a function.

Out of my head, I can think of two options for your use case:

  1. You could pass an integer as a parameter to your FBM() method, which identifies your noise function, and then have a switch (or an if-else-chain) inside your FBM() method, which selects the proper noise function based on this integer. Since the integer is passed as a compile-time constant, I'd expect that the compiler optimizes that branching away (and even if it doesn't, the cost of such a branch is fairly low, since all threads are always taking the same path through the code):
float FBM(uint noise)
{
    ...
    float somevalue = 0.0f;
    if(noise == 0)
        somevalue = Random(x, y);
    else
        somevalue = Perlin(x, y);
    ...
}

void CSMain(uint3 id : SV_DispatchThreadID)
{
    ...
    float result = FMB(1);
    ...
}
  1. You could write your whole FBM() method as a preprocessor macro instead of a function (you can end a line in a #define with \ to have the macro spanning multiple lines). This is a bit more cumbersome, but your #undef and #define would work, as the inlining is then actually done by the preprocesor as well.
#define NOISE_SAMPLE Random(x, y)

#define FBM { \
    ... \
    float somevalue = NOISE_SAMPLE; \
    ... \
    result = ...; \
}

void CSMain(uint3 id : SV_DispatchThreadID)
{
    float result = 0.0f;
    ...
    #undef NOISE_SAMPLE 
    #define NOISE_SAMPLE Perlin(x, y)
    FBM;
    ...
}

(Note that, with this approach, the compiler errors/warnings will never reference a line inside the FBM macro, but only ever the line(s) where the FBM macro is being called, so debugging these errors/warnings is slightly harder)