ПрограммированиеСтатьиГрафика

Альтернативная замена glDrawPixels в версиях OpenGL 3.x и выше или фундаментальные основы визуализации на OpenGL для новичков

Внимание! Этот документ ещё не опубликован.

Автор:

Как известно, функция glDrawPixels является устаревшей (deprecated), но иногда необходимо просто вывести набор пикселей на экран. Например в целях отладки или там, где не важна скорость вывода. В этом сообщении я приведу аналог данной функции, принимающей в качестве аргументов все тоже самое, что и оригинальная glDrawPixels. Данное сообщение также поможет новичкам в OpenGL поверхностно ознакомиться с тем, как создаются текстуры, шейдеры, вершинный буфер (Vertex Buffer Object) и VertexArrayObject. Не смотря на свою простоту код, описанный ниже охватывает фундаментальные основы визуализации на OpenGL 3.x и выше.
Далее построчно будет рассмотрено, что, как и зачем в этой функции.

1. Шейдеры
2. Создание текстуры
3. Создание шейдеров
4. Привязка текстуры к шейдеру
5. Создание вершинного буфера (Vertex Buffer Object или сокращенно VBO)
6. Создание Vertex Array Object или сокращенно VAO
7. Вершинные атрибуты для привязки нашего вершинного буфера к атрибутам вершинного шейдера
8. Вывод
9. Завершение
10. Полный исходный код
11. Бонус

1. Шейдеры

Вершинный шейдер принимает на вход двухмерные координаты (position) в диапазоне -1,+1,
описывающие Screen-aligned quad в однородных координатах. На выходе - текстурные координаты изображения.

        attribute vec2 position;
        varying vec2 texCoord;
        void main() {
            gl_Position = vec4( position, 0.0, 1.0 );
            texCoord = position*0.5 + 0.5;
        }

Фрагментный шейдер принимает на вход текстурные координаты, полученные в вершинном шейдере и использует их для выборки пикселей изображения из текстуры.

        varying vec2 texCoord;
        uniform sampler2D image;
        void main() {
            gl_FragColor = texture2D( image, texCoord );
        }

Вершинный и фрагментный шейдеры компилируются и исполняются на GPU. Теперь перейдем непосредственно к вызовам функций OpenGL.

Для справки: существуют также, не рассмотренные здесь: compute shader, geometry shader, tessellation control shader, tessellation evaluation shader.

2. Создание текстуры

Создание текстуры в нашем примере не отличается от создания текстур в ранних версиях OpenGL. Итак, для того чтобы создать текстуру, необходимо сгенерировать ее идентификатор:

    glGenTextures( 1, &texid );

Далее необходимо указать OpenGL-ю, с какой текстурой мы в данный момент работаем. Для этого служит glBindTexture:

    glBindTexture( GL_TEXTURE_2D, texid );

Здесь первый аргумент тип нашей текстуры (GL_TEXTURE_2D), второй - идентификатор, полученный на предыдущем шаге.

Следующая строчка не обязательна, если ширина исходного изображения (точнее размер в байтах ширины исходного изображения) выровнена по 4-ем байтам. В нашем случае мы указываем выравнивание по 1-му байту, т.к. изображение не выравнено.

    glPixelStorei( GL_UNPACK_ALIGNMENT, 1 ); // set 1-byte alignment

Теперь необходимо загрузить массив пикселей изображения в текстуру. В нашем примере для этого подходит функция glTexImage2D.

    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, _Width, _Height, 0, _Format, _Type, _Pixels );

Здесь нас интересует второй, третий и шестой аргумент. Второй аргумент - это mip-уровень текстуры (у нас он нулевой и единственный), третий аргумент - это в каком формате изображение будет храниться в видео памяти, шестой - бордер. Аргументы _Width, _Height, _Format, _Type и _Pixels полностью соответствуют спецификации glDrawPixels.

Ну и, наконец, устанавливаем фильтрацию текстуры. Так как мы пишем аналог glDrawPixels, мы устанавливаем фильтрацию GL_NEAREST (округление к ближайшему пикселю при выборке из текстуры в шейдере).

    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );

3. Создание шейдеров

Создание шейдерной программы состоит из нескольких этапов:
- создание идентификаторов шейдерной программы, а также идентификаторов самих шейдеров (в нашем случае только два - вершинный и фрагментный);
- задание исходного кода шейдерам;
- компиляция шейдеров;
- присоединение шейдеров к шейдерной программе;
- удаление шейдеров;
- линковка и валидация шейдерной программы.

Ну и сами,за себя говорящие функции:

    shaderProgram = glCreateProgram();

    handle = glCreateShader( GL_VERTEX_SHADER );
    glShaderSource( handle, 1, &vertexShader, 0 );
    glCompileShader( handle );
    glAttachShader( shaderProgram, handle );
    glDeleteShader( handle );

    handle = glCreateShader( GL_FRAGMENT_SHADER );
    glShaderSource( handle, 1, &fragmentShader, 0 );
    glCompileShader( handle );
    glAttachShader( shaderProgram, handle );
    glDeleteShader( handle );

    glLinkProgram( shaderProgram );
    glValidateProgram( shaderProgram );

Желательно на этапах компиляции, линковки и валидирования проверять на соответствующие ошибки. В данном примере для чистоты и ясности кода эти моменты опущены.

Чтобы установить шейдерную программу, как используемую (текущую), необходимо сделать следующим образом:

    glUseProgram( shaderProgram );

4. Привязка текстуры к шейдеру

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

    glActiveTexture( GL_TEXTURE0 );
    glUniform1i( glGetUniformLocation( shaderProgram, "image" ), 0 );

Для ясности то же самое можно написать следующим образом:

    GLint unit = 0;
    glActiveTexture( GL_TEXTURE0 + unit );
    glUniform1i( glGetUniformLocation( shaderProgram, "image" ), unit );

Текстуру биндить не нужно, так как это уже было сделано в пункте 2.

5. Создание вершинного буфера (Vertex Buffer Object или сокращенно VBO)

Теперь необходимо сгенерировать геометрию для вершинного шейдера.

Чтобы избежать умножения на ортогональную матрицу в вершинном шейдере, вершины сразу можно задать в однородных координатах:

    const float verts[4][2] =
    {
        {-1.0f, -1.0f},
        { 1.0f, -1.0f},
        {-1.0f,  1.0f},
        { 1.0f,  1.0f}
    };

Если вы обратили внимание, порядок вершин задан в соответствии с GL_TRIANGLE_STRIP. На мой взгляд, это оптимальный способ задать Screen-Aligned Quad (наименьшее количество вершин и без необходимости в индексном буфере).

Сгенерируем идентификатор для вершинного буфера.

    glGenBuffers( 1, &vertBuffer );

Биндим полученный индентификтор, чтобы OpenGL знал с каким из вершинных буферов он работает в данный момент.

    glBindBuffer( GL_ARRAY_BUFFER, vertBuffer );

Запишем вершины в буфер.

    glBufferData( GL_ARRAY_BUFFER, sizeof( verts ), &verts[0][0], GL_STATIC_DRAW );

6. Создание Vertex Array Object или сокращенно VAO

Аналогично создаем и биндим VAO:

    glGenVertexArrays( 1, &vao );
    glBindVertexArray( vao );

7. Вершинные атрибуты для привязки нашего вершинного буфера к атрибутам вершинного шейдера

Аналогично юниформам получаем расположение атрибута "position" в шейдере.

    GLint attribLocation = glGetAttribLocation( shaderProgram, "position" );

Включаем использование данного атрибута.

    glEnableVertexAttribArray( attribLocation );

Устанавливаем позицию (смещение) в вершинном буфере для данного атрибута.

    glVertexAttribPointer( attribLocation, 2, GL_FLOAT, GL_FALSE, sizeof(float)*2, 0 );

8. Вывод

Устанавливаем область вывода через glViewport:

    glViewport( 0,0,_Width,_Height );

Рисуем наши 4 вершинки:

    glDrawArrays( GL_TRIANGLE_STRIP, 0, 4 );

9. Завершение

После завершения работы с созданными объектами (текстуры, буферы, шейдеры и т.п.) их необходимо удалить, чтобы освободить ресурсы.

    glBindBuffer( GL_ARRAY_BUFFER, 0 );
    glDeleteBuffers( 1, &vertBuffer );
    glUseProgram( 0 );
    glDeleteProgram( shaderProgram );
    glDeleteTextures( 1, &texid );
    glDeleteVertexArrays( 1, &vao );

10. Полный исходный код

#define STRINGIFY(text)              #text

void DrawPixels( int _Width, int _Height, GLenum _Format, GLenum _Type, const void * _Pixels ) {
    GLuint texid;
    GLuint shaderProgram;
    GLuint handle;
    GLuint vertBuffer;
    GLuint vao;

    const char * vertexShader = STRINGIFY(
        attribute vec2 position;
        varying vec2 texCoord;
        void main() {
            gl_Position = vec4( position, 0.0, 1.0 );
            texCoord = position*0.5 + 0.5;
        }
    );

    const char * fragmentShader = STRINGIFY(
        varying vec2 texCoord;
        uniform sampler2D image;
        void main() {
            gl_FragColor = texture2D( image, texCoord );
        }
    );

    glGenTextures( 1, &texid );
    glBindTexture( GL_TEXTURE_2D, texid );
    glPixelStorei( GL_UNPACK_ALIGNMENT, 1 ); // set 1-byte alignment
    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, _Width, _Height, 0, _Format, _Type, _Pixels );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );

    shaderProgram = glCreateProgram();

    handle = glCreateShader( GL_VERTEX_SHADER );
    glShaderSource( handle, 1, &vertexShader, 0 );
    glCompileShader( handle );
    glAttachShader( shaderProgram, handle );
    glDeleteShader( handle );

    handle = glCreateShader( GL_FRAGMENT_SHADER );
    glShaderSource( handle, 1, &fragmentShader, 0 );
    glCompileShader( handle );
    glAttachShader( shaderProgram, handle );
    glDeleteShader( handle );

    glLinkProgram( shaderProgram );
    glValidateProgram( shaderProgram );
    glUseProgram( shaderProgram );

    glActiveTexture( GL_TEXTURE0 );
    glUniform1i( glGetUniformLocation( shaderProgram, "image" ), 0 );

    const float verts[4][2] =
    {
        {-1.0f, -1.0f},
        { 1.0f, -1.0f},
        {-1.0f,  1.0f},
        { 1.0f,  1.0f}
    };

    glGenBuffers( 1, &vertBuffer );
    glBindBuffer( GL_ARRAY_BUFFER, vertBuffer );
    glBufferData( GL_ARRAY_BUFFER, sizeof( verts ), &verts[0][0], GL_STATIC_DRAW );

    glGenVertexArrays( 1, &vao );
    glBindVertexArray( vao );

    GLint attribLocation = glGetAttribLocation( shaderProgram, "position" );
    glEnableVertexAttribArray( attribLocation );
    glVertexAttribPointer( attribLocation, 2, GL_FLOAT, GL_FALSE, sizeof(float)*2, 0 );
    
    glViewport( 0,0,_Width,_Height );

    glDrawArrays( GL_TRIANGLE_STRIP, 0, 4 );

    glDisableVertexAttribArray( attribLocation );

    glBindBuffer( GL_ARRAY_BUFFER, 0 );
    glDeleteBuffers( 1, &vertBuffer );
    glUseProgram( 0 );
    glDeleteProgram( shaderProgram );
    glDeleteTextures( 1, &texid );
    glDeleteVertexArrays( 1, &vao );
}

11. Бонус

В качестве бонуса реализация аналога glDrawPixels для OpenGL 1.1 и выше:

void DrawPixels11( int _Width, int _Height, GLenum _Format, GLenum _Type, const void * _Pixels ) {
    GLuint texid;

    glGenTextures( 1, &texid );
    glBindTexture( GL_TEXTURE_2D, texid );
    glPixelStorei( GL_UNPACK_ALIGNMENT, 1 ); // set 1-byte alignment
    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, _Width, _Height, 0, _Format, _Type, _Pixels );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );

    glActiveTexture( GL_TEXTURE0 );

    const float verts[4][2] =
    {
        {-1.0f, -1.0f},
        { 1.0f, -1.0f},
        {-1.0f,  1.0f},
        { 1.0f,  1.0f}
    };

    const float texCoord[4][2] =
    {
        { 0.0f,  0.0f},
        { 1.0f,  0.0f},
        { 0.0f,  1.0f},
        { 1.0f,  1.0f}
    };

    glEnableClientState( GL_VERTEX_ARRAY );
    glEnableClientState( GL_TEXTURE_COORD_ARRAY );

    glVertexPointer( 2, GL_FLOAT, sizeof( float ) * 2, verts );
    glTexCoordPointer( 2, GL_FLOAT, sizeof( float ) * 2, texCoord );

    glEnable( GL_TEXTURE_2D );

    glViewport( 0,0,_Width,_Height );

    glPushMatrix();
    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();
    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();

    glDrawArrays( GL_TRIANGLE_STRIP, 0, 4 );

    glPopMatrix();

    glDisableClientState( GL_VERTEX_ARRAY );
    glDisableClientState( GL_TEXTURE_COORD_ARRAY );

    glDeleteTextures( 1, &texid );
}

#glDrawPixels, #OpenGL

5 сентября 2015