23

I'm looking to parallelize some complex math, and WebGL looks like the perfect way to do it. The problem is, you can only read 8 bit integers from textures. I would ideally like to get 32 bit numbers out of the texture. I had the idea of using the 4 color channels to get 32 bits per pixel, instead of 4 times 8 bits.

My problem is, GLSL doesn't have a "%" operator or any bitwise operator!

TLDR: How do I convert a 32bit number to 4 8bit numbers by using the operators in glsl.

Some extra info on the technique (using bitwise operators):

How to store a 64 bit integer in two 32 bit integers and convert back again

Yves M.
  • 29,855
  • 23
  • 108
  • 144
william malo
  • 2,480
  • 2
  • 18
  • 18
  • OpenGL ES has no bitwise operators because graphics hardware doesn't implement integer ops. – Matt Randell Aug 26 '13 at 21:33
  • 4
    @randmat11: That is not true at all. All modern GPUs have ALUs. OpenGL ES does not implement bitwise operators because older shader models did not expose the functionality. Starting with Shader Model 4.0 (dx10), bitwise operators were introduced. GLSL in OpenGL 3.0+ has bitwise operators, it is only OpenGL ES's watered down GLSL that does not. – Andon M. Coleman Aug 27 '13 at 04:03

3 Answers3

33

You can bitshift by multiplying/dividing by powers of two.

As pointed out in the comments the approach I originally posted was working but incorrect, here's one by Aras Pranckevičius, note that the source code in the post itself contains a typo and is HLSL, this is a GLSL port with the typo corrected:

const vec4 bitEnc = vec4(1.,255.,65025.,16581375.);
const vec4 bitDec = 1./bitEnc;
vec4 EncodeFloatRGBA (float v) {
    vec4 enc = bitEnc * v;
    enc = fract(enc);
    enc -= enc.yzww * vec2(1./255., 0.).xxxy;
    return enc;
}
float DecodeFloatRGBA (vec4 v) {
    return dot(v, bitDec);
}
LJᛃ
  • 7,655
  • 2
  • 24
  • 35
  • 1
    That is very smart and I wonder why I haven't thought of just using multiplication to bitshift. – william malo Aug 27 '13 at 00:51
  • 1
    What would be the Javascript equivalent to this glsl code? I need to share bit data between javascript <-> glsl as texture and cant make it work in javascript :/ – Buksy Nov 25 '14 at 21:19
  • This wasn't really working for me, unless I moved some stuff around, `return vec4(comp.yzw, floor(depth))` to recover all the four components. – verma Feb 12 '16 at 20:52
  • Edit after a bit of testing: This wasn't really working for me, unless I moved some stuff around, `return vec4(comp.yzw, floor(depth)/256.0)` to recover all the four components. Also, it should be noted that for me the color components go in as [0, 256) but come out as [0, 1). May be just screwing things up on my end, but with these changes things seem to work for me. – verma Feb 12 '16 at 21:03
  • 1
    I think this is not going to be very accurate for the first component because of the way floats are stored. Assuming IEEE754 single-precision, where you get 24 bits of precision and 8 bits for the exponent, as you start getting to numbers higher than 2^24 (16,777,216) then you will start losing integral precision. So as your w increases, you lose precision in your x. if w > 1/256, x error is up to 1/256; if w > 1/128, x error is up to 1/128; ... if w > 0.25, maximum x error is 0.25; if w > 0.5, maximum x error is 0.5! – Wivlaro Jul 28 '16 at 21:04
  • 5
    Great solution, thanks! However on iOS platform i had to add this code at the beginning of DecodeFloatRGBA function: `v = floor(v * 255.0 + 0.5) / 255.0;` to make sure v contains precise 0, 1/255, 2/255, … ,1 value. It was necassary because usually “vec4 v” comes from texture2D function, and on some GPUs (for example in iOS devices) texture2D returns for example “0.000001” instead of exact 0.0. – Slyv Mar 15 '18 at 12:25
  • Made one working without issues in full IEEE 754 range. It's hard, but doable. Much more complex than your three-liner. – Brian Cannard May 08 '20 at 08:22
4

In general, if you want to pack the significant digits of a floating-point number in bytes, you have consecutively to extract 8 bits packages of the significant digits and store it in a byte.

Encode a floating point number in a predefined range

In order to pack a floating-point value in 4 * 8-bit buffers, the range of the source values must first be specified.
If you have defined a value range [minVal, maxVal], it has to be mapped to the range [0.0, 1.0]:

float mapVal = clamp((value-minVal)/(maxVal-minVal), 0.0, 1.0);

The function Encode packs a floating point value in the range [0.0, 1.0] into a vec4:

vec4 Encode( in float value )
{
    value *= (256.0*256.0*256.0 - 1.0) / (256.0*256.0*256.0);
    vec4 encode = fract( value * vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    return vec4( encode.xyz - encode.yzw / 256.0, encode.w ) + 1.0/512.0;
}

The function Decode extracts a floating point value in the range [0.0, 1.0] from a vec4:

float Decode( in vec4 pack )
{
    float value = dot( pack, 1.0 / vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    return value * (256.0*256.0*256.0) / (256.0*256.0*256.0 - 1.0);
}

The following functions packs and extracts an floating point value in and from the range [minVal, maxVal]:

vec4 EncodeRange( in float value, flaot minVal, maxVal )
{
    value = clamp( (value-minVal) / (maxVal-minVal), 0.0, 1.0 );
    value *= (256.0*256.0*256.0 - 1.0) / (256.0*256.0*256.0);
    vec4 encode = fract( value * vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    return vec4( encode.xyz - encode.yzw / 256.0, encode.w ) + 1.0/512.0;
}

float DecodeRange( in vec4 pack, flaot minVal, maxVal )
{
    value = dot( pack, 1.0 / vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    value *= (256.0*256.0*256.0) / (256.0*256.0*256.0 - 1.0);
    return mix( minVal, maxVal, value );
}

Encode a floating point number with an exponent

Another possibility is to encode the the significant digits to 3 * 8-bits of the RGB values and the exponent to the 8-bits of the alpha channel:

vec4 EncodeExp( in float value )
{
    int exponent  = int( log2( abs( value ) ) + 1.0 );
    value        /= exp2( float( exponent ) );
    value         = (value + 1.0) * (256.0*256.0*256.0 - 1.0) / (2.0*256.0*256.0*256.0);
    vec4 encode   = fract( value * vec4(1.0, 256.0, 256.0*256.0, 256.0*256.0*256.0) );
    return vec4( encode.xyz - encode.yzw / 256.0 + 1.0/512.0, (float(exponent) + 127.5) / 256.0 );
}

float DecodeExp( in vec4 pack )
{
    int exponent = int( pack.w * 256.0 - 127.0 );
    float value  = dot( pack.xyz, 1.0 / vec3(1.0, 256.0, 256.0*256.0) );
    value        = value * (2.0*256.0*256.0*256.0) / (256.0*256.0*256.0 - 1.0) - 1.0;
    return value * exp2( float(exponent) );
}

Note, since a standard 32-bit IEEE 754 number has only 24 significant digits, it is completely sufficient to encode the number in 3 bytes.

See also How do I convert between float and vec4,vec3,vec2?

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
0

Everyone is absolutely correct in how to handle something like this in WebGl, but I wanted to share a trick for getting the values in and out.

Assuming you want to do some comparison on two values that fit in 16 bits:

// Generate a list of random 16bit integers
let data16bit = new Uint16Array(1000000);
for(let i=0; i < data16bit.length; i+=2){
    data16bit[i]   = Math.random()*(2**16);
    data16bit[i+1] = Math.random()*(2**16);
}
// Read them one byte at a time, for writing to 
// WebGL
let texture = new Uint8Array(data16bit.buffer);

Now when you get your values in your fragment shader, you can pick up the numbers for manipulation:

vec4 here = texture2D(u_image, v_texCoord);
// Read the "red" byte and the "green" byte together (as a single thing) 
// as well as the "blue" byte and the "alpha" byte together as a single
// thing
vec2 a = here.rg;
vec2 b = here.ba;
// now compare the things
if(a == b){
    here.a = 1;
}
else{
    here.a = 0;
}
// return the boolean value
gl_FragColor = here;

The point is just a reminder that you can treat the same block of JavaScript memory as different sizes: Uint16Array, and Uint8Array (rather than trying to do bit shifting and breaking it up).

Update

In response to requests for more detail, that code is very close to a cut/paste directly from this code and explanation.

The exact use of this can be found on the corresponding samples on GitLab (two parts of the same file)

Jefferey Cave
  • 2,507
  • 1
  • 27
  • 46
  • Could you expand on your answer to a running example? – mix3d Sep 28 '20 at 23:03
  • 1
    @mix3d The code was originally cut/paste/tweak from a larger program I was working on. I later wrote a smaller demonstration up for classroom use. I am linking to the smaller demonstration. – Jefferey Cave Oct 01 '20 at 11:56