Как получить правильный SourceOver альфа-композитинг в SDL с OpenGL

Я использую FBO (или "Render Texture"), который имеет альфа-канал (32bpp ARGB) и ясно, что с цветом, который не является полностью непрозрачным, например (R=1, G=0, B=0, A=0) (т.е. полностью прозрачный). Затем я рисую полупрозрачный объект, например, прямоугольник с цветом (R=1, G=1, B=1, A=0,5) поверх этого. (Все значения нормированы от 0 до 1)

Согласно здравому смыслу, а также программному обеспечению для обработки изображений, таким как GIMP и Photoshop, а также нескольким статьям о композитинге Портера-Даффа, я ожидал бы получить текстуру, которая

  • полностью прозрачный за пределами прямоугольника
  • белый (1,0, 1,0, 1,0) с непрозрачностью 50 % внутри прямоугольника.

Вот так (вы не увидите этого на сайте SO):

Ожидаемый результат, созданный с GIMP

Вместо этого значения RGB цвета фона, которые (1,0, 0,0, 0,0), взвешиваются в целом с (1 - SourceAlpha) вместо (DestAlpha * (1 - SourceAlpha)). Фактический результат таков:

Фактический результат

Я проверил это поведение, используя OpenGL напрямую, используя API-интерфейс оболочки SDL и используя API-интерфейс оболочки SFML. С SDL и SFML я также сохранил результаты в виде изображения (с альфа-каналом) вместо простого рендеринга на экран, чтобы убедиться, что это не проблема с последним шагом рендеринга.

Что мне нужно сделать, чтобы получить ожидаемый результат SourceOver, либо с SDL, SFML, либо с использованием OpenGL напрямую?

Некоторые источники:

В статье W3 о композитировании указывается co = αs x Cs + αb x Cb x (1 - αs), вес Cb должен быть 0, если αb равен 0, несмотря ни на что.

Английская вики показывает, что пункт назначения ("B") взвешивается в соответствии с αb (а также αs, косвенно).

Немецкий Wiki показывает 50 % -ные примеры прозрачности, ясно, что исходные значения RGB прозрачного фона не мешают ни зеленому, ни пурпурному источнику, а также показывает, что пересечение явно асимметрично в пользу элемента, который находится "сверху".

Есть также несколько вопросов по SO, которые, по-видимому, имеют дело с этим на первый взгляд, но я не смог найти ничего, что говорило бы об этой конкретной проблеме. Люди предлагают разные функции смешивания OpenGL, но общее мнение, кажется, glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)Это то, что SDL и SFML используют по умолчанию. Я также пробовал разные комбинации без успеха.

Другим предложенным моментом является предварительное умножение цвета на целевую альфу, поскольку OpenGL может иметь только 1 фактор, но для корректного SourceOver необходимы 2 фактора. Однако я не могу понять это вообще. Если я предварительно умножаю (1, 0, 0) с целевым альфа-значением, скажем, (0,1), я получаю (0,1, 0, 0) (как предложено здесь, например). Теперь я могу сказать OpenGL фактор GL_ONE_MINUS_SRC_ALPHA для этого (и источник только с GL_SRC_ALPHA), но тогда я эффективно смешиваюсь с черным, что неправильно. Хотя я не специалист по этой теме, я приложил немало усилий, чтобы попытаться понять (и, по крайней мере, дошел до того, что мне удалось запрограммировать работающую чисто программную реализацию каждого режима компоновки). Насколько я понимаю, применение альфа-значения 0,1 "с помощью предварительного умножения" к (1,0, 0,0, 0,0) совсем не то же самое, что правильное отношение к альфа-значению в качестве четвертого компонента цвета.

Вот минимальный и полный пример использования SDL. Для компиляции требуется сам SDL2, опционально SDL2_image, если вы хотите сохранить как PNG.

// Define to save the result image as PNG (requires SDL2_image), undefine to instead display it in a window
#define SAVE_IMAGE_AS_PNG

#include <SDL.h>
#include <stdio.h>

#ifdef SAVE_IMAGE_AS_PNG
#include <SDL_image.h>
#endif

int main(int argc, char **argv)
{
    if (SDL_Init(SDL_INIT_VIDEO) != 0)
    {
        printf("init failed %s\n", SDL_GetError());
        return 1;
    }
#ifdef SAVE_IMAGE_AS_PNG
    if (IMG_Init(IMG_INIT_PNG) == 0)
    {
        printf("IMG init failed %s\n", IMG_GetError());
        return 1;
    }
#endif

    SDL_Window *window = SDL_CreateWindow("test", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
    if (window == NULL)
    {
        printf("window failed %s\n", SDL_GetError());
        return 1;
    }

    SDL_Renderer *renderer = SDL_CreateRenderer(window, 1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);
    if (renderer == NULL)
    {
        printf("renderer failed %s\n", SDL_GetError());
        return 1;
    }

    // This is the texture that we render on
    SDL_Texture *render_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, 300, 200);
    if (render_texture == NULL)
    {
        printf("rendertexture failed %s\n", SDL_GetError());
        return 1;
    }

    SDL_SetTextureBlendMode(render_texture, SDL_BLENDMODE_BLEND);
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

    printf("init ok\n");

#ifdef SAVE_IMAGE_AS_PNG
    uint8_t *pixels = new uint8_t[300 * 200 * 4];
#endif

    while (1)
    {
        SDL_Event event;
        while (SDL_PollEvent(&event))
        {
            if (event.type == SDL_QUIT)
            {
                return 0;
            }
        }

        SDL_Rect rect;
        rect.x = 1;
        rect.y = 0;
        rect.w = 150;
        rect.h = 120;

        SDL_SetRenderTarget(renderer, render_texture);
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 0);
        SDL_RenderClear(renderer);
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 127);
        SDL_RenderFillRect(renderer, &rect);

#ifdef SAVE_IMAGE_AS_PNG
        SDL_RenderReadPixels(renderer, NULL, SDL_PIXELFORMAT_ARGB8888, pixels, 4 * 300);
        // Hopefully the masks are fine for your system. Might need to randomly change those ff parts around.
        SDL_Surface *tmp_surface = SDL_CreateRGBSurfaceFrom(pixels, 300, 200, 32, 4 * 300, 0xff0000, 0xff00, 0xff, 0xff000000);
        if (tmp_surface == NULL)
        {
            printf("surface error %s\n", SDL_GetError());
            return 1;
        }

        if (IMG_SavePNG(tmp_surface, "t:\\sdltest.png") != 0)
        {
            printf("save image error %s\n", IMG_GetError());
            return 1;
        }

        printf("image saved successfully\n");
        return 0;
#endif

        SDL_SetRenderTarget(renderer, NULL);
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
        SDL_RenderClear(renderer);
        SDL_RenderCopy(renderer, render_texture, NULL, NULL);
        SDL_RenderPresent(renderer);
        SDL_Delay(10);
    }
}

1 ответ

Решение

Благодаря @HolyBlackCat и @Rabbid76 я смог пролить некоторый свет на все это. Я надеюсь, что это может помочь другим людям, которые хотят знать, как о правильном альфа-смешении и деталях, лежащих в основе предварительно умноженной альфы.

Основная проблема заключается в том, что корректное альфа-смешивание "Source Over" в действительности невозможно со встроенной функциональностью смешивания OpenGL (то есть glEnable(GL_BLEND), glBlendFunc[Separate](...), glBlendEquation[Separate](...)) (кстати, для D3D то же самое). Причина в следующем:

При вычислении результирующего цвета и альфа-значений операции смешивания (в соответствии с корректным Source Over) необходимо использовать следующие функции:

Каждое значение цвета RGB (нормализовано от 0 до 1):

RGB_f = (alpha_s x RGB_s + alpha_d x RGB_d x (1 - alpha_s)) / alpha_f

Значение альфа (нормализовано от 0 до 1):

alpha_f = alpha_s + alpha_d x (1 - alpha_s)

куда

  • sub f - результат color / alpha,
  • sub s это источник (что сверху) color / alpha,
  • d - обозначение (что внизу) цвет / альфа,
  • альфа это альфа-значение обработанного пикселя
  • и RGB представляет одно из значений красного, зеленого или синего цвета пикселя

Тем не менее, OpenGL может обрабатывать только ограниченное множество дополнительных факторов, соответствующих исходным или целевым значениям (RGB_s и RGB_d в уравнении цвета) ( см. Здесь), причем в этом случае соответствующие GL_ONE, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, Мы можем правильно указать альфа-формулу, используя эти параметры, но лучшее, что мы можем сделать для RGB:

RGB_f = alpha_s x RGB_s + RGB_d x (1 - alpha_s)

В котором полностью отсутствует альфа-компонент места назначения (alpha_d). Обратите внимание, что эта формула эквивалентна правильной, если \alpha_d = 1. Другими словами, при рендеринге в кадровый буфер, который не имеет альфа-канала (например, буфер окна), это нормально, иначе это приведет к неверным результатам.

Чтобы решить эту проблему и добиться правильного альфа-смешения, если alpha_d НЕ равен 1, нам нужны некоторые грубые обходные пути. Исходная (первая) формула выше может быть переписана в

alpha_f x RGB_f = alpha_s x RGB_s + alpha_d x RGB_d x (1 - alpha_s)

если мы примем тот факт, что значения цвета результата будут слишком темными (они будут умножены на результат альфа-цвета). Это уже избавляет от разделения. Однако, чтобы получить правильное значение RGB, нужно разделить результирующее значение RGB на альфа-значение результата, поскольку оказывается, что преобразование обычно никогда не требуется. Мы вводим новый символ (pmaRGB), который обозначает значения RGB, которые обычно слишком темные, потому что они были умножены на значение альфа соответствующего пикселя.

pmaRGB_f = alpha_s x RGB_s + alpha_d x RGB_d x (1 - alpha_s)

Мы также можем избавиться от проблемного фактора alpha_d, убедившись, что ВСЕ значения RGB целевого изображения были умножены на их соответствующие значения альфа в некоторой точке. Например, если нам нужен цвет фона (1.0, 0.5, 0, 0.3), мы не очищаем буфер кадров с этим цветом, а вместо этого (0.3, 0.15, 0, 0.3). Другими словами, мы делаем один из шагов, которые GPU должен был бы сделать уже заранее, потому что GPU может обрабатывать только один фактор. Если мы рендерим существующую текстуру, мы должны убедиться, что она была создана с предварительно умноженной альфа. Результатом наших операций смешивания всегда будут текстуры, которые также имеют предварительно умноженную альфу, поэтому мы можем продолжать рендеринг объектов и всегда быть уверенными, что у конечного пункта есть предварительно умноженная альфа. Если мы отрисовываем полупрозрачную текстуру, полупрозрачные пиксели всегда будут слишком темными, в зависимости от их альфа-значения (0 альфа означает черный, 1 альфа означает правильный цвет). Если мы выполняем рендеринг в буфер, который не имеет альфа-канала (например, обратный буфер, который мы используем для фактического отображения вещей), alpha_f неявно равен 1, поэтому предварительно умноженные значения RGB равны правильно смешанным значениям RGB. Это текущая формула:

pmaRGB_f = alpha_s x RGB_s + pmaRGB_d x (1 - alpha_s)

Эта функция может использоваться, когда источник еще не имеет предварительно умноженного альфа-канала (например, если источником является обычное изображение, полученное из программы обработки изображений, с альфа-каналом, который правильно смешан без предварительно умноженного альфа).

Есть причина, по которой мы могли бы также избавиться от \ alpha_s и использовать предварительно умноженную альфу для источника:

pmaRGB_f = pmaRGB_s + pmaRGB_d x (1 - alpha_s)

Эту формулу необходимо использовать, если в исходном случае предварительно умножена альфа - потому что тогда все значения исходного пикселя равны pmaRGB, а не RGB. Это всегда будет так, если мы будем разыскивать внеэкранный буфер с альфа-каналом, используя вышеуказанный метод. Также может быть разумным, чтобы все текстурные активы сохранялись с предварительно умноженным альфа-каналом по умолчанию, чтобы всегда можно было использовать эту формулу.

Напомним, что для вычисления значения альфа мы всегда используем следующую формулу:

alpha_f = alpha_s + alpha_d x (1 - alpha_s)

что соответствует (GL_ONE, GL_ONE_MINUS_SRC_ALPHA). Чтобы вычислить значения цвета RGB, если источник не имеет предварительно умноженного альфа, примененного к его значениям RGB, мы используем

pmaRGB_f = alpha_s x RGB_s + pmaRGB_d x (1 - alpha_s)

что соответствует (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA). Если к нему применяется предварительно умноженная альфа, мы используем

pmaRGB_f = pmaRGB_s + pmaRGB_d x (1 - alpha_s)

что соответствует (GL_ONE, GL_ONE_MINUS_SRC_ALPHA).


Что это практически означает в OpenGL: при рендеринге в кадровый буфер с альфа-каналом, соответственно, переключитесь на правильную функцию смешивания и убедитесь, что для текстуры FBO всегда предварительно умноженная альфа применяется к его значениям RGB. Обратите внимание, что правильная функция смешивания может потенциально отличаться для каждого визуализированного объекта в зависимости от того, имеет ли источник предварительно умноженную альфа. Пример: нам нужен фон [1, 0, 0, 0.1], и мы наносим на него объект с цветом [1, 1, 1, 0.5].

// Clear with the premultiplied version of the real background color - the texture (which is always the destination in all blending operations) now complies with the "destination must always have premultiplied alpha" convention.
glClearColor(0.1f, 0.0f, 0.0f, 0.1f); 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//
// Option 1 - source either already has premultiplied alpha for whatever reason, or we can easily ensure that it has
//
{
    // Set the drawing color to the premultiplied version of the real drawing color.
    glColor4f(0.5f, 0.5f, 0.5f, 0.5f);

    // Set the blending equation according to "blending source with premultiplied alpha".
    glEnable(GL_BLEND);
    glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glBlendEquationSeparate(GL_ADD, GL_ADD);
}

//
// Option 2 - source does not have premultiplied alpha
// 
{
    // Set the drawing color to the original version of the real drawing color.
    glColor4f(1.0f, 1.0f, 1.0f, 0.5f);

    // Set the blending equation according to "blending source with premultiplied alpha".
    glEnable(GL_BLEND);
    glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    glBlendEquationSeparate(GL_ADD, GL_ADD);
}

// --- draw the thing ---

glDisable(GL_BLEND);

В любом случае полученная текстура имеет предварительно умноженную альфа. Вот 2 варианта того, что мы могли бы сделать с этой текстурой:

Если мы хотим экспортировать его как изображение, которое правильно альфа-смешано (согласно определению SourceOver), нам нужно получить его данные RGBA и явно разделить каждое значение RGB на альфа-значение соответствующего пикселя.

Если мы хотим визуализировать его в буфер (чей цвет фона должен быть (0, 0, 0,5)), мы поступим так, как обычно (для этого примера, мы дополнительно хотим модулировать текстуру с помощью (0, 0, 1, 0.8)):

// The back buffer has 100 % alpha.
glClearColor(0.0f, 0.0f, 0.5f, 1.0f); 
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// The color with which the texture is drawn - the modulating color's RGB values also need premultiplied alpha
glColor4f(0.0f, 0.0f, 0.8f, 0.8f);

// Set the blending equation according to "blending source with premultiplied alpha".
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquationSeparate(GL_ADD, GL_ADD);

// --- draw the texture ---

glDisable(GL_BLEND);

Технически, результат будет иметь предварительно умноженную альфу, примененную к нему. Тем не менее, поскольку результат альфа всегда будет равен 1 для каждого пикселя, предварительно умноженные значения RGB всегда равны правильно смешанным значениям RGB.

Чтобы добиться того же в SFML:

renderTexture.clear(sf::Color(25, 0, 0, 25));

sf::RectangleShape rect;
sf::RenderStates rs;
// Assuming the object has premultiplied alpha - or we can easily make sure that it has
{
    rs.blendMode = sf::BlendMode(sf::BlendMode::One, sf::BlendMode::OneMinusSrcAlpha);
    rect.setFillColor(sf::Color(127, 127, 127, 127));
}

// Assuming the object does not have premultiplied alpha
{
    rs.blendMode = sf::BlendAlpha; // This is a shortcut for the constructor with the correct blending parameters for this type
    rect.setFillColor(sf::Color(255, 255, 255, 127));
}

// --- align the rect ---

renderTexture.draw(rect, rs);

И аналогично рисовать renderTexture на задний ход

// premultiplied modulation color
renderTexture_sprite.setColor(sf::Color(0, 0, 204, 204));
window.clear(sf::Color(0, 0, 127, 255));
sf::RenderStates rs;
rs.blendMode = sf::BlendMode(sf::BlendMode::One, sf::BlendMode::OneMinusSrcAlpha);
window.draw(renderTexture_sprite, rs);

К сожалению, это невозможно с SDL afaik (по крайней мере, не на GPU как часть процесса рендеринга). В отличие от SFML, который предоставляет пользователю детальный контроль над режимом наложения, SDL не позволяет устанавливать отдельные компоненты функции наложения - он имеет только SDL_BLENDMODE_BLEND жестко glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA),

Другие вопросы по тегам