Search code examples
c++openglblending

How to make fading-to-black effect with OpenGL?


Im trying to achieve fade-to-black effect, but i dont know how to do it. I tried several things but they fail due to how opengl works

I will explain how it would work:

If i draw 1 white pixel and move it around each frame for one pixel to some direction, each frame the screen pixels will get one R/G/B value less (of range 0-255), thus after 255 frames the white pixel will be fully black. So if i move the white pixel around, i would see a gradient trail going from white to black evenly 1 color value difference compared to previous pixel color.

Edit: I would prefer to know non-shader way of doing this, but if its not possible then i can accept shader-way too.

Edit2: Since there is some confusion around here, I would like to tell that i can do this kind of effect already by drawing a black transparent quad over my whole scene. BUT, this does not work as i want it to work; there is a limit on the darkness the pixels can get, so it will always leave some of the pixels "visible" (above zero color value) because: 1*0.9 = 0.9 -> rounded to 1 again, etc. I can "fix" this by making the trail shorter, but i want to be able to adjust the trail lenght as much as possible and instead of bilinear (if thats the right word) interpolation i want linear (so it would always reduce -1 from each r,g,b value in 0-255 scale, instead of using a percent value).

Edit3: Still some confusion left, so lets be clear: i want to improve the effect that is done by disabling GL_COLOR_BUFFER_BIT from glClear(), i dont want to see the pixels on my screen FOREVER, so i want to make them darker in time, by drawing a quad over my scene that will reduce each of the pixels color value by 1 (in 0-255 scale).

Edit4: I'll make it simple, i want OpenGL method for this, the effect should use as little power, memory or bandwidth as possible. this effect is supposed to work without clearing the screen pixels, so if i draw a transparent quad over my scene, the previous pixels drawn will get darker etc. But as explained above few times, its not working very well. The big NO's are: 1) reading pixels from screen, modifying them one by one in a for loop and then uploading back. 2) rendering my objects X times with different darknesses to emulate the trail effect. 3) multiplying the color values is not an option since it wont make the pixels into black, they will stay on the screen forever at certain brightness (see explanation somewhere above).


Solution

  • If i draw 1 white pixel and move it around each frame for one pixel to some direction, each frame the screen pixels will get one R/G/B value less (of range 0-255), thus after 255 frames the white pixel will be fully black. So if i move the white pixel around, i would see a gradient trail going from white to black evenly 1 color value difference compared to previous pixel color.

    Before I explain how to do this, I would like to say that the visual effect you're going for is a terrible visual effect and you should not use it. Subtracting a value from each of the RGB colors will produce a different color, not a darker version of the same color. The RGB color (255,128,0), if you subtract 1 from it 128 times, will become (128, 0, 0). The first color is brown, the second is a dark red. These are not the same.

    Now, since you haven't really explained this very well, I have to make some guesses. I am assuming that there are no "objects" in what you are rendering. There is no state. You're simply drawing stuff at arbitrary locations, and you don't remember what you drew where, nor do you want to remember what was drawn where.

    To do what you want, you need two off-screen buffers. I recommend using FBOs and screen-sized textures for these. The basic algorithm is simple. You render the previous frame's image to the current image, using a blend mode that "subtracts 1" from the colors you write. Then you render the new stuff you want to the current image. Then you display that image. After that, you switch which image is previous and which is current, and do the process all over again.

    Note: The following code will assume OpenGL 3.3 functionality.

    Initialization

    So first, during initialization (after OpenGL is initialized), you must create your screen-sized textures. You also need two screen-sized depth buffers.

    GLuint screenTextures[2];
    GLuint screenDepthbuffers[2];
    GLuint fbos[2]; //Put these definitions somewhere useful.
    
    glGenTextures(2, screenTextures);
    glGenRenderbuffers(2, screenDepthbuffers);
    glGenFramebuffers(2, fbos);
    for(int i = 0; i < 2; ++i)
    {
      glBindTexture(GL_TEXTURE_2D, screenTextures[i]);
      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, SCREEN_WIDTH, SCREEN_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
      glBindTexture(GL_TEXTURE_2D, 0);
    
      glBindRenderbuffer(GL_RENDERBUFFER, screenDepthBuffers[i]);
      glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCREEN_WIDTH, SCREEN_HEIGHT);
      glBindRenderbuffer(GL_RENDERBUFFER, 0);
    
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo[i]);
      glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, screenTextures[i], 0);
      glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, screenDepthBuffers[i]);
      if(glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        //Error out here.
      }
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    }
    

    Drawing Previous Frame

    The next step will be drawing the previous frame's image to the current image.

    To do this, we need to have the concept of a previous and current FBO. This is done by having two variables: currIndex and prevIndex. These values are indices into our GLuint arrays for textures, renderbuffers, and FBOs. They should be initialized (during initialization, not for each frame) as follows:

    currIndex = 0;
    prevIndex = 1;
    

    In your drawing routine, the first step is to draw the previous frame, subtracting one (again, I strongly suggest using a real blend here).

    This won't be full code; there will be pseudo-code that I expect you to fill in.

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbos[currIndex]);
    glClearColor(...);
    glClearDepth(...);
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
    
    glActiveTexture(GL_TEXTURE0 + 0);
    glBindTexture(GL_TEXTURE_2D, screenTextures[prevIndex]);
    glUseProgram(BlenderProgramObject); //The shader will be talked about later.
    
    RenderFullscreenQuadWithTexture();
    
    glUseProgram(0);
    glBindTexture(GL_TEXTURE_2D, 0);
    

    The RenderFullscreenQuadWithTexture function does exactly what it says: renders a quad the size of the screen, using the currently bound texture. The program object BlenderProgramObject is a GLSL shader that does our blend operation. It fetches from the texture and does the blend. Again, I'm assuming you know how to set up a shader and so forth.

    The fragment shader would have a main function that looks something like this:

    shaderOutput = texture(prevImage, texCoord) - (1.0/255.0);
    

    Again, I strongly advise this:

    shaderOutput = texture(prevImage, texCoord) * (0.05);
    

    If you don't know how to use shaders, then you should learn. But if you don't want to, then you can get the same effect using a glTexEnv function. And if you don't know what those are, I suggest learning shaders; it's so much easier in the long run.

    Draw Stuff As Normal

    Now, you just render everything you would as normal. Just don't unbind the FBO; we still want to render to it.

    Display the Rendered Image on Screen

    Normally, you would use a swapbuffer call to display the results of your rendering. But since we rendered to an FBO, we can't do that. Instead, we have to do something different. We must blit our image to the backbuffer and then swap buffers.

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, fbos[currIndex]);
    glBlitFramebuffer(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, SCREEN_WDITH, SCREEN_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
    //Do OpenGL swap buffers as normal
    

    Switch Images

    Now we need to do one more thing: switch the images that we're using. The previous image becomes current and vice versa:

    std::swap(currIndex, prevIndex);
    

    And you're done.