Search Unity

Is there an easier way to do this? (Encoding 3 low-precision values into one using modulo)

Discussion in 'Shaders' started by asqewfcq2egf, Oct 28, 2020.

  1. asqewfcq2egf

    asqewfcq2egf

    Joined:
    Nov 16, 2018
    Posts:
    15
    So i have this set of 3 functions here https://www.desmos.com/calculator/36sztettqv that take a 0-1 floating point value and pull 3 unique low-precision values out of it using mod().

    Is there any function in cg/HLSL that would simplify this? Maybe cast to int or something?
    It seems like a clever way to pack data and i'm surprised i haven't seen it come up anywhere as i've been learning about shaders.

    I'll be doing these ops inside the vertex shader, using values sampled from an uncompressed ARGB32 texture, targeting shader model 3.5
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Packing data is super common, but tends to come fairly late in shader / graphics programming at which point it’s not anything people consider terribly novel so it gets glossed over frequently. In the AAA world, probably almost every game is doing some form of data packing somewhere in the graphics pipeline. Even Unity does this! In the past Unity kind of did the reverse, and packed a single float into 4 channels of an ARGB32 texture to support floating point precision data on platforms that didn’t support floating point render textures.

    More recently for the HDRP, Unity added a bunch of functions for packing and unpacking data.
    https://github.com/Unity-Technologi...der-pipelines.core/ShaderLibrary/Packing.hlsl
    With an explanation of some of them here:
    https://seblagarde.wordpress.com/2018/09/02/gbuffer-helper-packing-integer-and-float-together/

    For something like trying to pack multiple values into an 8 bit channel, the easiest solution is to multiply the 0.0 to 1.0 value you get from sampling the texture, multiply it by 255, and convert it to a uint. Then you can do basic bit masking and shift operations to extract any part of that you want.

    Code (csharp):
    1. // packs 3 floats into 8 bits with 3, 3, and 2 bits of precision
    2. float packFloat3To8(float3 in)
    3. {
    4.   uint3 abc = uint3(saturate(in) * float3(8, 8, 4));
    5.   return float(abc.x | abc.y >> 3 | abc.z >> 6) / 255.0;
    6. }
    7.  
    8. float3 unpack8ToFloat3(float in)
    9. {
    10.   uint byte = uint(in * 255.0 + 0.5);
    11.    return float3(
    12.     float(byte & 8) / 8.0,
    13.     float((byte << 3) & 8) / 8.0,
    14.     float((byte << 6) & 4) / 4.0
    15.     );
    16. }
     
    andywatts, ShiftedClock and Olmi like this.
  3. asqewfcq2egf

    asqewfcq2egf

    Joined:
    Nov 16, 2018
    Posts:
    15
    Interesting! Lots of good stuff in that hlsl file. The int casting makes things so much easier.

    Would these kinds of encoding still work if integers were not available? from my understanding, there was a point where GPUs couldn't really do proper integer math or bitwise stuff.

    and uh, btw, thank you for all the work you do on these forums. I've learned so much about unity just from the answers you've posted, haha
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I mean, you always have integers. You might not have bitwise operations, but integers are supported pretty much universally. You can replicate a lot of the bitwise operations being done here with other math.

    But yes, you can do it without integers. See the functions in the Packing.hlsl I linked to above. No bitwise operations in most of them. Even the pack and unpack functions detailed in that blog post, the actual functions in the Packing.hlsl don't use them! Those
    float maxi = float(1 << numBitI);
    , that's just a fancy way of writing
    float maxi = pow(2, numBitI);
    .
     
  5. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    Ok, i feel a bit dumb but am I missing something? I tried using the functions above from @bgolus, packed up a value, rendered it onto a rgba32 unorm texture and tried to unpack it, but the values are very much screwed-

    I also tried the PackFloat2To8 and the corresponding unpack from the hdrp hlsl and the unpacked results also are also pretty wrong.

    Has someone a working example of this? I want to pack a bunch of data into my gbuffer and for a lot of those values I don't need the full 8-bit precision.

    Edit: If it makes it more clear what I mean, I can post some examples of the weird results.
     
  6. flogelz

    flogelz

    Joined:
    Aug 10, 2018
    Posts:
    142
    Okidoki, so I sat down and learned the basics about bitshifting and I came up with a version that works for me now. I changed a few things up, so if you have any feedback on that I would appreciate it! Especially since I changed a few things up in comparison to @bgolus code.

    Code (CSharp):
    1. // packs 3 floats into 8 bits with 3, 3, and 2 bits of precision
    2. float Pack3FloatsTo8Bit(float a, float b, float c)
    3. {
    4.    a = min(a, 0.99999);
    5.    b = min(b, 0.99999);
    6.    c = min(c, 0.99999);
    7.  
    8.    uint x = uint(a * 8);
    9.    uint y = uint(b * 8);
    10.    uint z = uint(c * 4);
    11.  
    12.    return float(x | y << 3 | z << 6) / 255.0;
    13. }
    14.  
    15. void Unpack3FloatsFrom8Bit(float packed, out float a, out float b, out float c)
    16. {
    17.    uint byte = uint(packed * 255);
    18.  
    19.    a = float( byte       & 7) / 8.0;
    20.    b = float((byte >> 3) & 7) / 8.0;
    21.    c = float((byte >> 6) & 3) / 4.0;
    22. }
    1. Added a min to the input values, so that they are never truly 1. Reason for that is, that values that are exactly one get wrongly unpacked. As far as I understand this happens, because a pure 1 ( or 8 as uint) would become 00001000, which then later doesn't work with the bitshift & operator for the first 3 bits.
    2. Mirrored the bithsift ops, which would make more sense i believe, because otherwise the bits would overwrite themselves?
    3. And instead of using & and 8 8 4, I used 7 7 3 as those seem more like the correct masks? For example 7 being 00000111, which would give me the result of the first 3 bits, like I'd expect.

    (and also removed the + 0.5, because it didn't make any difference during my tests)

    I'm pretty new to bit operations, so take this whole paragraph with a grain of salt:vD
     
    DonCornholio and bgolus like this.