Renderowanie 2D część 4.2: Spritebatch

Posted on Wed 21 June 2017 in OpenGL

Cześć. W poprzedniej części napisałem nieco o tym jak przebiega rysowanie a właściwie dokładanie elementów do narysowania. W tym artykule zajmiemy się dokładnie tym jak działa pipeline rysowania SpriteBatchem. Aby narysować jaką teksturę należy napisać taki oto kodzik:

this->_device->clear(Colors::black);
batch->begin(TextureWrap::REPEAT, TextureFilter::POINT_FILTER);

    batch->draw(texture1, glm::vec2(200.f, 200.f), nullptr, Colors::white, 0, glm::vec2(0.f, 0.f), 2, FlipEffect::NONE_FLIP, 2);
    batch->draw(texture2, glm::vec2(300.f, 200.f), nullptr, Colors::white, 0, glm::vec2(0.f, 0.f), 3, FlipEffect::NONE_FLIP, 2);

batch->end();
this->_device->swapBuffers();

Zacznijmy od samej góry. Aby wyczyścić backbuffer trzeba najpierw wywołać funkcje clear() z graphic device. Potem wywołujemy jedno z przeciążeń funkcji begin, która przygotowuje wszystkie elementy SpriteBatcha do rysowania. Przykładowa implementacja wygląda tak:

void SpriteBatch::begin(ShaderProgram * shader, TextureWrap wrap, TextureFilter filter, glm::mat4 viewportTransform)
{
    if (this->_isInitialized == false && this->_isStarted) return;

    this->_customShader = shader;
    this->_viewport = glm::ortho(0.f,
        static_cast<GLfloat>(this->_device->get_viewport().width),
        static_cast<GLfloat>(this->_device->get_viewport().height),
        0.f, -1.f, 1.f);
    this->_viewportTransform = viewportTransform;
    this->_isStarted = true;
    this->_sampler->changeFilter(filter);
    this->_sampler->changeWrap(wrap);
}

Jak możemy zauważyć najważniejsze jest ustalenie Viewportu i samplera. Druga macierz służy do nałożenia transformacji kamery ale to w innym artykule napiszę na ten temat. Po wywołaniu kolejnych funkcji draw() wywołujemy funkcje end ()która wykonuje właściwe rysowanie.

void SpriteBatch::end()
{
    //draw to frame buffer
    if (this->_currentTarget == nullptr)
    {
        this->_currentTarget->begin();
        this->drawBatch();
        this->_currentTarget->end();
    }
    else //draw to backbuffer
    {
        this->drawBatch();
    }
}

Jeżeli mamy nałożony render target to cały batch jest rysowany to niego jeżeli nie to rysujemy na backbuffer. Funkcja drawBatch() która rysuje cały bufor ma następującą implementację:

void SpriteBatch::drawBatch()
{

    switch (this->_sortMode)
    {
    case SortMode::FRONT_TO_BACK:
        std::sort(this->_items.begin(), this->_items.end(),
            [=](SpriteBatchItem* item1, SpriteBatchItem* item2) { return item1->drawOrder < item2->drawOrder; });
        break;
    case SortMode::BACK_TO_FRONT:
        std::sort(this->_items.begin(), this->_items.end(),
            [=](SpriteBatchItem* item1, SpriteBatchItem* item2) { return item1->drawOrder > item2->drawOrder; });
        break;
    }

    if (this->_viewport == glm::mat4())
    {
        GLint display[4];
        glGetIntegerv(GL_VIEWPORT, display);
        this->_viewport = glm::ortho(0.f, (float)display[2],
            (float)display[3], 0.f);
    }

    GLuint currentProgram;
    if (this->_customShader != nullptr)
    {
        this->_customShader->use();
        currentProgram = this->_customShader->get_programId();
    }
    else
    {
        this->_defaultShader->use();
        currentProgram = this->_defaultShader->get_programId();
    }

    glm::mat4  full_viewProj =  this->_viewport * this->_viewportTransform;
    auto viewport = glm::value_ptr(full_viewProj);
    glUniformMatrix4fv(glGetUniformLocation(currentProgram, "projection"), 1, GL_FALSE, viewport);

    glBindVertexArray(this->_vao);

    glBindBuffer(GL_ARRAY_BUFFER, this->_vbo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this->_ebo);

    size_t spritesCount = this->_items.size();
    size_t batchesToDraw = spritesCount / MAX_BATCH_ITEMS;

    this->_sampler->bind();
    for (size_t batchNumber = 0; batchNumber < batchesToDraw + 1; ++batchNumber)
    {
        size_t spriteNumber = spritesCount - (batchNumber * MAX_BATCH_ITEMS);
        if (spriteNumber > MAX_BATCH_ITEMS)
            spriteNumber = MAX_BATCH_ITEMS;

        for (size_t i = batchNumber * MAX_BATCH_ITEMS; i < batchNumber * MAX_BATCH_ITEMS + spriteNumber; ++i)
        {
            auto item = this->_items[i];

            size_t newI = (i - (i / MAX_BATCH_ITEMS * MAX_BATCH_ITEMS)) * 32;

            //top left
            this->_vertexBuffer[newI + 0] = item->postions[0].x;
            this->_vertexBuffer[newI + 1] = item->postions[0].y;
            this->_vertexBuffer[newI + 2] = item->texcoords[0].x;
            this->_vertexBuffer[newI + 3] = item->texcoords[0].y;
            this->_vertexBuffer[newI + 4] = item->color.x;
            this->_vertexBuffer[newI + 5] = item->color.y;
            this->_vertexBuffer[newI + 6] = item->color.z;
            this->_vertexBuffer[newI + 7] = item->color.w;
            //top right
            this->_vertexBuffer[newI + 8] = item->postions[1].x;
            this->_vertexBuffer[newI + 9] = item->postions[1].y;
            this->_vertexBuffer[newI + 10] = item->texcoords[1].x;
            this->_vertexBuffer[newI + 11] = item->texcoords[1].y;
            this->_vertexBuffer[newI + 12] = item->color.x;
            this->_vertexBuffer[newI + 13] = item->color.y;
            this->_vertexBuffer[newI + 14] = item->color.z;
            this->_vertexBuffer[newI + 15] = item->color.w;
            //top left
            this->_vertexBuffer[newI + 16] = item->postions[2].x;
            this->_vertexBuffer[newI + 17] = item->postions[2].y;
            this->_vertexBuffer[newI + 18] = item->texcoords[2].x;
            this->_vertexBuffer[newI + 19] = item->texcoords[2].y;
            this->_vertexBuffer[newI + 20] = item->color.x;
            this->_vertexBuffer[newI + 21] = item->color.y;
            this->_vertexBuffer[newI + 22] = item->color.z;
            this->_vertexBuffer[newI + 23] = item->color.w;
            //top right
            this->_vertexBuffer[newI + 24] = item->postions[3].x;
            this->_vertexBuffer[newI + 25] = item->postions[3].y;
            this->_vertexBuffer[newI + 26] = item->texcoords[3].x;
            this->_vertexBuffer[newI + 27] = item->texcoords[3].y;
            this->_vertexBuffer[newI + 28] = item->color.x;
            this->_vertexBuffer[newI + 29] = item->color.y;
            this->_vertexBuffer[newI + 30] = item->color.z;
            this->_vertexBuffer[newI + 31] = item->color.w;
        }

        size_t fullSize = spritesCount - batchNumber * MAX_BATCH_ITEMS;

        if (fullSize > MAX_BATCH_ITEMS)
            fullSize = MAX_BATCH_ITEMS;

        glBufferSubData(GL_ARRAY_BUFFER, 0, fullSize* (32 * sizeof(GLfloat)),
            &this->_vertexBuffer[0]);

        auto lastTex = this->_items[batchNumber * MAX_BATCH_ITEMS]->texture;
        int offset = 0;

        SpriteBatchItem* spriteItem;
        for (size_t i = 0; i < spriteNumber; ++i)
        {
            spriteItem = this->_items[batchNumber * MAX_BATCH_ITEMS + i];
            if (spriteItem->texture->get_textureId() != lastTex->get_textureId())
            {
                lastTex->bind();
                glDrawElements(GL_TRIANGLES, (i - offset) * 6,
                    GL_UNSIGNED_INT, (GLvoid*)(offset * 6 * sizeof(GLuint)));
                offset = i;
                lastTex = spriteItem->texture;
            }
        }

        lastTex->bind();
        glDrawElements(GL_TRIANGLES, (spriteNumber - offset) * 6,
            GL_UNSIGNED_INT, (GLvoid*)(offset * 6 * sizeof(GLuint)));
    }

    glBindTexture(GL_TEXTURE_2D, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    this->_sampler->bind();

    glBindVertexArray(0);
    glUseProgram(0);

    this->_isStarted = false;
    this->clearBatchItems();
}

Metoda drawBatch na początku sortuje tekstury, oblicza viewport włącza customowy shader jeżeli jest on ustawiony. Następnie obliczana jest macierz projekcji i wszystkie dane pakowane są do bufora oraz wysyłane są one do rysowania. Po wszystkim batch jest czyszczony i cały proces zaczyna się od nowa. To chyba na tyle jeżeli chodzi o samego batcha z grafiki została nam jeszcze kamera, którą opiszę w następnym artykule.