Djemon
Djemon

Reputation: 105

Render from HDRi LatLong map directly to floating point cubemap - not HDR

I have an HDR radiance environment map as a LatLong 2D texture image that I want to convert to a cubemap. I do this by loading the HDR map as a 2D float texture, project it onto a cube and then render the scene inside this cube from 6 different directions, directly filling a cubemap with glFramebufferTexture2D with the respective cubemap faces as the function's texture target.

The generated cubemap is a floating point cubemap generated as follows:

glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

for (unsigned int i = 0; i < 6; ++i)
{
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_FLOAT, NULL);
}
if (mipmap)
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

Note that the type parameter is GL_FLOAT so it should properly accept HDR values. The HDR image is loaded using stb_image.h as follows:

if (stbi_is_hdr(path.c_str()))
{
    stbi_set_flip_vertically_on_load(true);

    int width, height, nrComponents;
    float *data = stbi_loadf(path.c_str(), &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        Bind();
            glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_FLOAT, data);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            if (Mipmapping)
                glGenerateMipmap(GL_TEXTURE_2D);
        Unbind();
        stbi_image_free(data);
    }
}

I also tried iterating over this array and retrieving the max float value to see if the HDR loaded correctly and the highest float value of my current HDR map is 288 which is far above 1.0 which I would expect.

Here's where things get tricky, based on the input texture (HDR float map) and the output cubemap (as a float) I'd expect that the cubemap faces get properly treated as floating point textures and directly copy the HDR values. However, the cubemap appears LDR as the moment I add tonemapping (w/ a variable exposure) I get quite a lot of banding and I'm clearly missing the precision of HDR as the following image shows (with an exposure of ~7.5)

LDR image of what should be HDR

I'm not sure whether there's something I'm missing and I couldn't find much on OpenGL's docs regarding rendering directly to floating point framebuffers; I assume this is possible as it wouldn't make sense if it wasn't.

For completeness' sake, here is the relevant code that generates the cubemap from the LatLong image (with renderCustomCommand rendering the cube with proper samplers set):

glGenFramebuffers(1, &m_FramebufferCubemap);
glGenRenderbuffers(1, &m_CubemapDepthRBO);

Camera faceCameras[6] = {
    Camera(position, vec3( 1.0f,  0.0f,  0.0f), vec3(0.0f, -1.0f,  0.0f)),
    Camera(position, vec3(-1.0f,  0.0f,  0.0f), vec3(0.0f, -1.0f,  0.0f)),
    Camera(position, vec3( 0.0f,  1.0f,  0.0f), vec3(0.0f,  0.0f,  1.0f)),
    Camera(position, vec3( 0.0f, -1.0f,  0.0f), vec3(0.0f,  0.0f,- 1.0f)),
    Camera(position, vec3( 0.0f,  0.0f,  1.0f), vec3(0.0f, -1.0f,  0.0f)),
    Camera(position, vec3( 0.0f,  0.0f, -1.0f), vec3(0.0f, -1.0f,  0.0f))
};

glBindFramebuffer(GL_FRAMEBUFFER, m_FramebufferCubemap);
glBindRenderbuffer(GL_RENDERBUFFER, m_CubemapDepthRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_CubemapDepthRBO);

glViewport(0, 0, width, height);
glBindFramebuffer(GL_FRAMEBUFFER, m_FramebufferCubemap);

for (unsigned int i = 0; i < 6; ++i)
{
    Camera *camera = &faceCameras[i];
    camera->SetPerspective(90.0f, width/height, 0.1f, 100.0f);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, cubeTarget->ID, 0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    for (unsigned int i = 0; i < renderCommands.size(); ++i)
    {
        renderCustomCommand(&renderCommands[i], camera);
    }
}

glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, m_RenderSize.x, m_RenderSize.y);

And here's the code for sampling the LatLong 2D image -> cube:

#version 330 core
out vec4 FragColor;
in vec3 wPos;

#include sample.glsl

uniform sampler2D environment;

void main()
{
    vec2 uv = SampleLatLong(normalize(wPos));
    vec3 color = texture(environment, uv).rgb;
    FragColor = vec4(color, 1.0);
}

Note that the LatLong to Cubemap conversion goes well, as the 2D environment is properly rendered on a cubemap, but simply clamped to the [0,1] range the moment it's rendered as a skybox, as if somewhere along the process it lost its floating point data.

I've been stuck on this problem for a while now and was hoping any one of you could shed some insight (is it even possible to render directly to float cubemaps like this?). Thank you.


EDIT: Here is the same picture with a high exposure set from Photoshop, as you can see a lot of details emerge which I've lost in the renderer.

HDR image from Photoshop with high exposure

Upvotes: 0

Views: 1467

Answers (1)

kakTuZ
kakTuZ

Reputation: 562

The third parameter of your glTexImage2D call needs to be GL_RGB16F or GL_RGB32F. It specifies the internal format.

The two parameters GL_RGB and GL_FLOAT at the end are only used to specify memory layout of the optional data pointer. They do not influence the internal format.

Upvotes: 2

Related Questions