• Godot HelpShaders
  • Writing to a "Buffer" and then reading back the value give different numbers?

First up: I'm using the Compatibility Renderer because I need a web build. I know there are simpler solutions with the other renderers, but they're not options for me. I'm currently using Godot 4.3

I am trying to write a value to a "buffer" (a viewport with a camera that gets the same view as my "main" camera, but writes out different values via an if-statement in the shader), and then in another shader read out that value for use. The viewport is linked to a "Shader Global" sampler2D. I have the Shader Global created in the Project settings, and link them up via code (because linking them through the GUI didn't always seem to work.)

RenderingServer.global_shader_parameter_set("buffer_texture", bufferViewport.get_texture())

That all works fine. I followed the Advanced Post Processing tutorial and have a "post processing" quad so I can see what's in the buffer, and that part seems ok.

Normal View

Post-Processing Overlay

You can see that the spider (on the ground, ignore the one in the upper left for this) is a lighter green than the walls/floor, which is good. I should be writing 255 for the spider and 128 for the walls/floor (and the "sky" is just whatever for now), so that's as expected. (Just using green for now, I have plans for the other values later.)

So, I can write something to the buffer, and read it back out again. Yay!

I also have another shader that does screen-space decals. This also works fine. We can draw with a decal on the spider and floor/walls.

What I would like, is for the decal to draw on the floor/walls but not the spider. That's what the buffer is for, I can look in there and see if that pixel is the floor/walls or spider. I can read a value out of the buffer in my decal shader, but it's not the value I put in.

In the shader that writes TO the buffer I have:

ALBEDO.y = (float(number)/256.0);

The other two values are 0.0. We divide the actual number by 256 to make it fit into the 0-1 that albedo wants (I think?)

Then, in the decal shader we read OUT the value and multiple by 256:

global uniform sampler2D buffer_texture : filter_nearest, repeat_disable;
...
vec2 cull_xy = texture(buffer_texture, SCREEN_UV).xy;
float number = cull_xy.y * 256.0;

If I use "number" in an if-statement to determine if I should draw or discard the decal pixel, have to "guess" at what number I'll get out.

For example, discarding the decal when the buffer is > 76.0 && < 78.0 does what I want:

Discarding if > 34.5 || < 33.5 also works. So 255 seems to be 77 and 128 is seems to be 34. Some sort of scaling going on, but I don't know what. I'd like to be able to just put in a number and get the same number back out, so I don't have to guess each time. Anybody know what's happening here?

I've tried some of the srgb to linear mappings I've found via web-searches, but they also don't seem to give me the number I put in. Maybe this has changed recently? Maybe I'm doing things in the wrong order?

  • xyz replied to this.

    redokapi Probably texture filtering. Use texelFetch() instead of texture() to get the exact value of a pixel.

      xyz texelFetch() seems to return the same value as texture(). So the filtering/scaling must be happening somewhere else?

      • xyz replied to this.

        redokapi No, filtering doesn't affect texelFetch(). You may be doing unwanted linear <-> srgb conversion somewhere in the process.

        I figured out part of it. If I switch my "buffer writing" shader to "unshaded" it works the way I expect. So the difference in values is coming from lighting that is being calculated automatically. (I assumed, incorrectly, that leaving the "light" function blank would just ignore lighting.)

        So, now the issue is, how do I write a light function that gives me the same output as an "unshaded" shader? Just passing the albedo through as diffuse did not work.

        DIFFUSE_LIGHT = ALBEDO;

        Setting shader to "unshaded"

        Passing Albedo through on Diffuse

        (I could set these up as different shaders and do multiple passes, I suppose. But I'm not sure that's any more efficient...running two passes that each have if-statements to check which camera is looking at them seems less efficient than one pass with if-statements?)

        Shlogged through the "built-in" shaders and think I've figured it out. To "ignore" lighting calculations and get the same value out of the buffer, set the ALBEDO in the fragment function, and then use:

        DIFFUSE_LIGHT = linear_to_srgb(ALBEDO)+linear_to_srgb(ALBEDO);

        in the "light" function. Of course, if you have the option of a separate shader, using render_mode: "unshaded" is probably better.
        You can find the linear_to_srgb() function in a few places, but this is the one I'm using (and it's reversal, in case that's useful):

        vec3 linear_to_srgb(vec3 color) {
        // If going to srgb, clamp from 0 to 1.
        color = clamp(color, vec3(0.0), vec3(1.0));
        const vec3 a = vec3(0.055f);
        return mix(
        (vec3(1.0f) + a) * pow(color.rgb, vec3(1.0f / 2.4f)) - a,
        12.92f * color.rgb,
        lessThan(color.rgb, vec3(0.0031308f))
        );
        }

        vec3 srgb_to_linear(vec3 color) {
        return mix(
        pow((color.rgb + vec3(0.055)) * (1.0 / (1.0 + 0.055)), vec3(2.4)),
        color.rgb * (1.0 / 12.92),
        lessThan(color.rgb, vec3(0.04045))
        );
        }