Search code examples
c++openglglslshadersfml

How can I pull back float data, not color, generated in a shader to the CPU?


I wrote a shader that generates noise, it uses gl_TexCoord[0].xy (a thing generated by SFML) as input and outputs a float for every pixel.

I want the data on the CPU, as an array of 32bits float.

Here is what I'm doing:

//using opengl 3.0 (I also tried 3.2)
ContextSettings settings(0, 0, 0, 3, 0);
sf::RenderWindow window(sf::VideoMode(windowsize.x, windowsize.y),
    "yada", 7, settings
);

Here I overwrite the texture after the texture is created. I'm using windows which only has opengl1.1 headers, so I have to manually set GL_R32F (found in glad done by dav1d):

#ifndef GL_R32F 
#define GL_R32F 0x822E // found this in glad.h for opengl 3.0, easy guess is that this value doesn't change across opengl versions
#endif
auto handle = sprite_perlin.getTexture()->getNativeHandle(); 
glBindTexture(GL_TEXTURE_2D, handle);
auto size = sprite_perlin.getTexture()->getSize();
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, size.x, size.y, 0, GL_RED, GL_FLOAT, NULL);
        }

Here I read the texture data into values:

float values[4096];
// whole_size is the size of the texture, here it is 60
if(whole_size*whole_size < 4096)
    glGetTexImage(GL_TEXTURE_2D, 0, GL_RED, GL_FLOAT, values);

Image brought_back;
brought_back.create(whole_size, whole_size, Color::Red);

float mi = 1e20, ma = -1e20;

for (int i = 0; i < whole_size; ++i) {
    for (int j = 0; j < whole_size; ++j) {
        brought_back.setPixel(i, j, { 0,Uint8(255.f*values[whole_size*i + j]),0 });
        ma = max(values[whole_size*i + j], ma);
        mi = min(values[whole_size*i + j], mi);
    }
}

mi and ma are both to 0.0f;

Here is the noise generated when showed on a sprite with the shader (with some color function that takes a float) The shader works as intended, I would just like to retrieve the data as float on the CPU.

noise example

Here is a simplified version of the shader:

uniform int texture_size;

float get_value_at(ivec2 pixel_coord) {
    //dummy value
    return 0.34f;
}

void main(){
    ivec2 pix_pos = ivec2(gl_TexCoord[0].xy*texture_size);

    float val = get_value_at(pix_pos);

    gl_FragColor = vec4(val,0,0,1);
}

Solution

  • How can I pull back float data, [...]

    The output of the fragment shader is written to the framebuffer. The precision depends on the format of the framebuffer. The format of the Default Framebuffer is (probably) 1 byte for each color channel. So it doesn't matter which type you use when you read the color from the buffer. The precision is lost when the color is written into the buffer.

    If you want to store the colors with a higher precision, then you've to render to a Framebuffer, that uses a Renderbuffer or Texture with a format that can store the values with an higher precision.
    See Image Format.

    If you want to render to a framebuffer, then I recommend to read a tutorial like Framebuffers or Render To Texture respectively Khronos wiki - Framebuffer Object Extension Examples.

    Anyway, I'll provide an example. You've to setup a framebuffer with provides the internal format GL_R32F (or GL_RGBA32F for 4 color channels).

    Either setup a render buffer with the internal format GL_R32F and attach it to the framebuffer:

    // render buffer with internal format `GL_RGBA32F`
    GLuint rbo32F = 0;
    glGenRenderbuffers(1, &rbo32F);
    glBindRenderbuffer(GL_RENDERBUFFER, rbo32F);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_R32F, size.x, size.y);
    
    // framebuffer with attached render buffer
    GLuint fbo = 0;
    glGenFramebuffers(1, &fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo32F);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    

    Or setup a texture with the format GL_R32F and attach it to the framebuffer. If you just to want to create a texture, which is used for further rendering, then is is the approach for you used to be. There is no need to upload the image to the CPU, because the image is already rendered to a texture, which can be used further.

    // texture with internal format `GL_R32F`
    GLuint tex32F = 0;
    glGenTextures(1, &tex32F);
    glBindTexture(GL_TEXTURE_2D, tex32F);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, size.x, size.y, 0, GL_RED, GL_FLOAT, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
    // framebuffer with attached texture
    GLuint fbo = 0;
    glGenFramebuffers(1, &fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex32F, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    

    In both cases you should validate the completeness of the frambuffer glCheckFramebufferStatus:

    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    int status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (status != GL_FRAMEBUFFER_COMPLETE) {
        // error handling
        // [...]
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    

    Bind the framebuffer before rendering:

    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    

    After you've draw the geometry, the texture can be read by glGetTexImage. e.g.:

    std::vector<GLfloat> buffer(size.x * size.y * 1); // widht * height * 1 color channel
    glBindTexture(GL_TEXTURE_2D, tex32F);
    glGetTexImage(GL_TEXTURE_2D, 0, GL_RED, GL_FLOAT, buffer.data());