Can't read a FORMAT_R8 uniform sampler2D texture correctly from within a shader in Godot

846 views Asked by At

I need to pass an array of integers to a shader as a uniform. As uniform arrays are not yet supported, I'm using a FORMAT_R8 texture/isampler2D to achieve this.

If I'm correctly reading the bytes from the texture, they should be in the range [0..7] and then by normalizing that value, I should get different shades of red, but that's not happening.

In the screenshot you can see that when the value is read as 0, it's correctly displaying it black, but when it's other than 0, it's full red, no shades. I tried comparing the value read and it's always greater than any value I can try, up to the maximum value of a 32 bit signed integer, so no idea what value I'm reading back from the texture.

Any ideas?

The full project is here: https://github.com/Drean64/clash-of-the-colors/tree/simple

GDScript:

extends ViewportContainer

func _ready():
    var attr_ink = PoolByteArray([])
    attr_ink.resize(32*24)
    for i in range(0, 32*24):
        attr_ink[i] = i % 8 # fill array with [0..7]
    
    var image = Image.new()
    image.create_from_data(32, 24, false, Image.FORMAT_R8, attr_ink)

    var ink = ImageTexture.new()
    ink.create_from_image(image, 0)
    
    (self.material as ShaderMaterial).set_shader_param("ink", ink)

Shader:

shader_type canvas_item;

uniform isampler2D ink;

void fragment() {
    COLOR = vec4(0., 0., 0., 1.); // Black
    ivec2 cell = ivec2(UV / TEXTURE_PIXEL_SIZE) / 8; // 8x8 pixel cell coordinate
    int ink_color = texelFetch(ink, cell, 0).r; // Should be [0..7]
    COLOR.r = float(ink_color) / 7.; // Should normalize the red value to [0..1]
    // Line above is a kind of debug to get a hint at what the ink_color value is
}
1

There are 1 answers

3
Theraot On BEST ANSWER

This is what worked for me:

  1. Use sampler2D instead of isampler2D (this seems to be the culprit).
  2. Normalize by multiplying by 32.0 (that is 256.0 / 8.0).

That is:

COLOR.r = texelFetch(ink, cell, 0).r * 32.0;

Update: Apparently the values are there when using isampler2D, but encoded wrong.

I began by confirming that the texture was being generated correctly. The problem is when binding the texture for the shader.

I had a look at Godot source, but didn't anything specific to biding the texture for isampler2D. Which made me think that it was handled the same as sampler2D (or I'm not very good at looking, in either case I should try to figure things messing with the shader code).

First thing to try was to figure out how large the values where. And they came around 1000000000. So, it appears we are getting 32 bits values. After trying each one, this gets pretty close to a solution:

int x = texelFetch(ink, cell, 0).r;
COLOR.r = mod(float(x / (256 * 256)), 256.0) / 256.0;

That almost works. For reasons unclear to me, where this should had the second shade of red, it is black. That made me think that perhaps the values are encoded as float.

Under that idea, I decided to divide the values by 2097152 (2²¹ = 256 * 256 * 32). This would get rid of most of the fractional part.

float                    binary                                integer
5.877471754111438e-39 -> 0 00000000 01000000000000000000000 -> 2097152
                    1 -> 0 01111111 00000000000000000000000 -> 1065353216
                    2 -> 0 10000000 00000000000000000000000 -> 1073741824
                    3 -> 0 10000000 10000000000000000000000 -> 1077936128
                    4 -> 0 10000001 00000000000000000000000 -> 1082130432
                    5 -> 0 10000001 01000000000000000000000 -> 1084227584
                    6 -> 0 10000001 10000000000000000000000 -> 1086324736
                    7 -> 0 10000001 11000000000000000000000 -> 1088421888
                                  X XX

I got these binary representation of floating point values using https://baseconvert.com/ieee-754-floating-point

My idea was to take the bits I marked an X above. After dividing by 2097152, they would have moved to the three least significant bits. That leaves us this code:

int x = texelFetch(ink, cell, 0).r;
COLOR.r = mod(float(x / 2097152), 8.0) / 8.0;

Does that work? Again, almost. It is the same result as earlier. Except this one comes with a (partial) explanation of the result by virtue of the table above, since the value 2 is 0 on the bits I marked an X above. In fact, the values for 0 and 1 would also need patching.

Now, I could only patch the values, somehow, we would have working code, right? I just need to figure out what the values ended up being.

If I try dividing the value for 2 which is 1073741824 by 2097152 I get 512. Which… Didn't work (And I'll blame baseconvert.com). But served as good starting place to search the actual values.

I ended with following code, which will give the expected result with isampler2D:

int x = texelFetch(ink, cell, 0).r;
int y = x / 2097152;
COLOR.r = mod(float(y), 8.0) / 8.0;
if (y == 476)
{
   COLOR.r = 1.0 / 8.0;
}
if (y == 480)
{
   COLOR.r = 2.0 / 8.0;
}
if (y == 482)
{
    COLOR.r = 3.0 / 8.0;
}

So there, my explanation is that it is passing the values as floating point numbers, but reading them as integers.