crenderingsdltext-rendering

SDL_RenderCopy-ing to another texture results in subtly darker image


I've been chasing this visual bug for a while, and I've concluded that the process of renderCopying from one texture to another results in a slightly darkened image.

#include "basics.h"

#include <SDL.h>
#include <SDL_ttf.h>

#ifdef WIN32
#define _WIN32_WINNT 0x0500
#include <windows.h>
#endif



//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~O~~~~~~~~~~| M A I N |~~~~~~~~~~~O~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
int main(int argc, char *argv[]){

    SDL_Window *window;
    SDL_Renderer *renderer;
    int width = 640;
    int height = 480;
    int loop = 1;
    int mouseX, mouseY, pmouseX, pmouseY;


    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s", SDL_GetError());
        return 3;
    }
    if (SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED, &window, &renderer)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create window and renderer: %s", SDL_GetError());
        return 3;
    }

    SDL_GetWindowSize( window, &width, &height );


    const SDL_Color black = {0, 0, 0, 255};
    const SDL_Color white = {255, 255, 255, 255};

    char font_filename [] = "fonts/FreeSerif.ttf";//AveriaLibre-Regular
    int size = 20;

    if(TTF_Init() == -1){
        printf("TTF_Init: %s\n", TTF_GetError());
    }
    TTF_Font *font = TTF_OpenFont( font_filename, size );
    
    char str [] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
    
    SDL_Rect dst1 = (SDL_Rect){ 100, 100, 400, 200 };
    SDL_Rect dst2 = (SDL_Rect){ 515, 100, 400, 200 };

    
    SDL_Texture *buffer = SDL_CreateTexture( renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, width, height );
    SDL_SetTextureBlendMode( buffer, SDL_BLENDMODE_BLEND );

    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255);
    SDL_RenderClear( renderer );

    SDL_Surface *surf = TTF_RenderText_Blended_Wrapped( font, str, white, dst1.w );
    dst1.w = surf->w; dst1.h = surf->h;
    SDL_Texture *texture = SDL_CreateTextureFromSurface( renderer, surf );
    SDL_RenderCopy( renderer, texture, NULL, &dst1 );


    SDL_SetRenderTarget( renderer, buffer );
    dst2.w = surf->w; dst2.h = surf->h;
    SDL_RenderCopy( renderer, texture, NULL, &dst2 );
    SDL_SetRenderTarget( renderer, NULL );
    SDL_RenderCopy( renderer, buffer, NULL, &(SDL_Rect){0, 0, width, height} );
    

    SDL_FreeSurface( surf );
    SDL_DestroyTexture( texture );

    char ascii [] = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
    
    surf = TTF_RenderText_Blended( font, ascii, white );
    texture = SDL_CreateTextureFromSurface( renderer, surf );
    SDL_RenderCopy( renderer, texture, NULL, &(SDL_Rect){100, 500, surf->w, surf->h} );

    
    SDL_SetRenderTarget( renderer, buffer );
    SDL_RenderCopy( renderer, texture, NULL, &(SDL_Rect){100, 530, surf->w, surf->h} );
    SDL_SetRenderTarget( renderer, NULL );
    SDL_RenderCopy( renderer, buffer, NULL, &(SDL_Rect){0, 0, width, height} );


    SDL_RenderPresent(renderer);

    while( 1 ) {
        SDL_Event event;
        while( SDL_PollEvent(&event) ){
            if(event.type == SDL_QUIT) {
                goto exit;
            }
        }
    }
    exit:

    SDL_DestroyTexture( buffer );
    SDL_DestroyTexture( texture );
    SDL_FreeSurface( surf );
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);

    SDL_Quit();

    return 0;
}

this program results in this: text rendering output

Like I say, it's a subtle effect, you might need to zoom in, but for my purposes it's really awful. Anyone know what could be the cause, or some workaround perhaps? thanks.


Solution

  • You recursively apply blend operation multiple times on the same data.

    Your render texture have the same size as your window. You didn't clear it, so let's presume it is zeroed by default, including alpha (it may be implementation-dependant, i don't see documentation states that anywyere; explicit clear with required colour may be a good idea).

    SDL_ttf gives you surface with alpha channel. Then you render your first text fragment to that texture with blendmode=blend, blending it atop of zeroes. If you look at the formulas for blend operations - alpha values of your render target gets overwritten by your text texture (dstA = srcA + (dstA * (1-srcA)), and dstA is 0, so it is just srcA), but colour values are multiplied by your source alpha, so srcRGB = srcRGB * srcA.

    Then you blend this target texture on top of your screen, which is cleared to (0, 0, 0, 255), applying the same blend operation again, resulting in another blending by the same srcA, so your colours are now srcRGB = srcRGB * srcA * srcA. This is obviously incorrect.

    It gets worse with your second text fragment, because you do all that again, but never clear your render target. So when you use your render target again, you do another rendering of entire texture, including what was rendered at first text fragment, applying another blend on top of that.

    You need to do blending only once. There are multiple ways to do so, and you'll have to decide what's best for your case. I don't know how you want to use your render texture, so presuming you want to keep alpha values, you need to do blending at last stage. That can be achieved by drawing into render target with blendmode=none, and then blitting your render texture onto screen, but only once. For example:

    SDL_Texture *buffer = SDL_CreateTexture( renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, width, height );
    SDL_SetTextureBlendMode( buffer, SDL_BLENDMODE_BLEND );
    
    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255);
    SDL_RenderClear( renderer );
    
    // first text fragment, rendering directly to screen
    SDL_Surface *surf = TTF_RenderText_Blended_Wrapped( font, str, white, dst1.w );
    dst1.w = surf->w; dst1.h = surf->h;
    SDL_Texture *texture = SDL_CreateTextureFromSurface( renderer, surf );
    SDL_RenderCopy( renderer, texture, NULL, &dst1 );
    
    // first text fragment, rendering to render texture. blendmode=none to avoid
    // pre-blending
    SDL_SetRenderTarget( renderer, buffer );
    dst2.w = surf->w; dst2.h = surf->h;
    SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE);
    SDL_RenderCopy( renderer, texture, NULL, &dst2 );
    SDL_SetRenderTarget( renderer, NULL );
    // NOT copying render target to screen. It is not finished yet, need to do
    // that only once
    
    SDL_FreeSurface( surf );
    SDL_DestroyTexture( texture );
    
    char ascii [] = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
    
    // second text fragment, rendering directly to screen
    surf = TTF_RenderText_Blended( font, ascii, white );
    texture = SDL_CreateTextureFromSurface( renderer, surf );
    SDL_RenderCopy( renderer, texture, NULL, &(SDL_Rect){100, 500, surf->w, surf->h} );
    
    // second text fragment, rendering to render texture, blendmode=none
    SDL_SetRenderTarget( renderer, buffer );
    SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE);
    SDL_RenderCopy( renderer, texture, NULL, &(SDL_Rect){100, 530, surf->w, surf->h} );
    SDL_SetRenderTarget( renderer, NULL );
    
    // render texture finished, now it is time to blend it with screen contents
    SDL_RenderCopy( renderer, buffer, NULL, &(SDL_Rect){0, 0, width, height} );
    
    SDL_RenderPresent(renderer);