Search Unity

Encode float4 RGBA to uint

Discussion in 'Shaders' started by zhutianlun810, Jun 14, 2019.

  1. zhutianlun810

    zhutianlun810

    Joined:
    Sep 17, 2017
    Posts:
    168
    Hello,

    I am reading an implementation about encoding float4 RGBA into a uint. But I cannot undersatnd it. This i sthe code:
    Code (CSharp):
    1.                 float3 rgb2hsv(float3 c)
    2.                 {
    3.                     float4 k = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    4.                     float4 p = lerp(float4(c.bg, k.wz), float4(c.gb, k.xy), step(c.b, c.g));
    5.                     float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.r));
    6.  
    7.                     float d = q.x - min(q.w, q.y);
    8.                     float e = 1.0e-10;
    9.  
    10.                     return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
    11.                 }
    12.                 uint EncodeRGBAuint(float4 color)
    13.                 {
    14.                     //7[HHHHHHH] 7[SSSSSSS] 11[VVVVVVVVVVV] 7[AAAAAAAA]
    15.                     float3 hsv = rgb2hsv(color.rgb);
    16.                     hsv.z = pow(hsv.z, 1.0 / 3.0);
    17.  
    18.                     uint result = 0;
    19.  
    20.                     uint a = min(127, uint(color.a / 2.0));
    21.                     uint v = min(2047, uint((hsv.z / 10.0) * 2047));
    22.                     uint s = uint(hsv.y * 127);
    23.                     uint h = uint(hsv.x * 127);
    24.  
    25.                     result += a;
    26.                     result += v * 0x00000080; // << 7
    27.                     result += s * 0x00040000; // << 18
    28.                     result += h * 0x02000000; // << 25
    29.  
    30.                     return result;
    31.                 }
    Is there any resource to explain the algorithm of such encoding online?
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,341
    I'm not sure of a good single resource for explaining the entirety of what's going on, but I can break it down in to a simplified explanation of the general idea.

    Computers store data using binary, ie: everything is stored as a series of 0s or 1s, or bits. Different kinds of data use those 0s and 1s differently, but one of the most common examples is simple whole numbers. A number can be stored as a series of bits where each successive bit is a power of 2 value. For example, a 4 bits can store the values 0 through 15.

    0000 = 0
    0001 = 1
    0010 = 2
    0011 = 3
    ...
    1000 = 8
    1001 = 9
    ...
    1111 = 15

    https://www.mathsisfun.com/binary-number-system.html

    If you want bigger numbers, you just need more bits. Floating point numbers are a lot more complicated, so I'm not going to go into that, especially as it's not actually important to be able to understand what's happening.

    A uint is a binary number with 32 bits, so can store values between 0 and 4,294,967,295. (2^32-1). But with some math, or some bit fiddling, you could use those 32 bits to store several separate numbers. A common case is packing an 8 bit per channel (aka 32 bit) RGBA color into a uint. Since the data is already only 32 bits, you just need to take each "0.0-1.0" (actually 0 - 255) value and make their values be those for the bit range of the full uint.

    So, for example:
    Code (csharp):
    1. float4 color = // color value with 0.0 - 1.0 component value ranges
    2.  
    3. // converts the 0.0 - 1.0 range to 0 to 255, using the first 8 bits;
    4. uint packedR = uint(color.r * 255);
    5.  
    6. // converts the 0.0 - 1.0 range to 255 to 65,535, with steps of 256, which uses the second 8 bits
    7. uint packedG = uint(color.g * 255) * 256 - 1;
    8.  
    9. // converts the 0.0 - 1.0 range to 65,535 to 16,777,215, with steps of 65,536, which uses the third 8 bits
    10. uint packedB = uint(color.b * 255) * 65536 - 1;
    11.  
    12. // converts the 0.0 - 1.0 range to 16,777,215 to 4,294,967,295, with steps of 16,777,216, which uses the last 8 bits
    13. uint packedA = uint(color.a * 255) * 16777216 - 1;
    14.  
    15. // add them together.
    16. uint packedColor = packedR + packedG + packedB + packedA;
    note: I'm pretty sure I have this math a little off

    Because each packed color channel is only using a specific range of bits, they can be added together while still being possible to extract later and get the original numbers. Now really, no one uses the above code for this kind of thing because usually if you have a uint it's way easier to use bitwise operations, ie: directly manipulate the bits by masking them and shifting them around.
    Code (csharp):
    1. uint packedR = uint(color.r * 255);
    2. uint packedG = uint(color.g * 255) << 8; // shift bits over 8 places
    3. uint packedB = uint(color.b * 255) << 16; // shift bits over 16 places
    4. uint packedA = uint(color.a * 255) << 24; // shift bits over 24 places
    5. uint packedColor = packedR + packedG + packedB + packedA
    That produces functionally identical values to the above example, but is a lot more concise and usually a lot faster as it requires the CPU/GPU to do less work. My first example is what you would use on platforms that don't support bitwise operations, though those platforms likely also don't support uint values, or are very slow when handling them.

    The code you posted is very similar, but is packing an RGB value by first converting into HSV and using only 7 bits for the hue, saturation, and alpha, and the remaining 11 bits for the value. The human eye is far more sensitive to variations in brightness (or "value" in the HSV parlance), so if you need to pack a floating point color with a greater than 8 bit per channel precision into only 32 bits, it makes sense to drop some precision in the color hue and saturation. Many image and video formats do similar things, like JPG, or most video compression formats, where the "color" is stored at a lower precision, and often even a different resolution, than the brightness. The code above also applies a gamma correction to the brightness; the human eye's perception of brightness isn't linear, so gamma curves exist partially to correct for this and make sure there's adequate precision in the ranges it matters. Roughly speaking the human eye can discern between different dark values better than different brighter values, especially when there aren't other bright values nearby.
     
    Last edited: Jun 14, 2019
    Findeh, SugoiDev and halley like this.
  3. zhutianlun810

    zhutianlun810

    Joined:
    Sep 17, 2017
    Posts:
    168
    Thanks for you explanation!!!!
     
  4. AlexHell

    AlexHell

    Joined:
    Oct 2, 2014
    Posts:
    167
    rgb2hsv is conversion from RGBA to HSV (another color model).. it simplifies encoding as I see, due to it ranges like 0-360 degree of H (hue), etc
    it's lossy encoding any way: float is 4 byte, float4 RGBA is 4*4 = 16 byte, byt uint is 4 byte total