Суперсовременный OpenGL. Часть 2



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

    Возможно повезет и я смогу весь оставшийся материал впихнуть в эту статью, это не точно…

    Array Texture


    Текстурные массивы были добавлены еще в OpenGL 3.0, но почему-то мало кто пишет о них (информация надёжно прячется масонами). Все вы знакомы с программированием и знаете что такое масcив, хотя лучше я «подойду» с другой стороны.

    Для уменьшения количества переключений между текстурами, а как следствие и снижению операций переключения состояний, люди используют текстурные атласы(текстура которая хранит в себе данные для несколько объектов). Но умные ребята из Khronos разработали нам альтернативу — Array texture. Теперь мы можем хранить текстуры как слои в этом массиве, то есть это альтернатива атласам. На OpenGL Wiki немного другое описание, про mipmaps и т.д., но мне оно кажется слишком сложным (ссылка).

    Преимущества использования этого подхода по сравнению с атласами в том, что каждый слой рассматривается как отдельная текстура с точки зрения wrapping и mipmapping.

    Но вернемся к нашим баранам… Текстурный массив имеет три вида таргета:

    • GL_TEXTURE_1D_ARRAY
    • GL_TEXTURE_2D_ARRAY
    • GL_TEXTURE_CUBE_MAP_ARRAY

    Код создания текстурного массива:

    GLsizei width = 512;
    GLsizei height = 512;
    GLsizei layers = 3;
    glCreateTextures(GL_TEXTURE_2D_ARRAY, 1, &texture_array);
    glTextureStorage3D(texture_array, 0, GL_RGBA8, width, height, layers);

    Самые внимательные заметили, что мы создаем хранилище для 2D текстур, но почему-то используем 3D массив, тут нет ошибки или опечатки. Мы храним 2D текстуры, но так как они расположены «слоями» получаем 3D массив (на самом деле, хранятся пиксельные данные, а не текстуры. 3D массив имеет 2D слои с данными пикселей).

    Тут легко понять на примере 1D текстуры. Каждая строчка в 2D массиве пикселей представляет собой отдельный 1D слой. Также автоматически могут создаваться mipmap текстур.

    На этом все сложности заканчиваются и добавление изображения на определенный слой довольно простое:

    glTextureSubImage3D(texarray, mipmap_level, offset.x, offset.y, layer, width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

    При использование массивов нам надо немного поменять шейдер

    #version 450 core
    
    layout (location = 0) out vec4 color;
    layout (location = 0) in vec2 texture_0;
    
    uniform sampler2DArray texture_array;
    uniform uint diffuse_layer;
    
    float getCoord(uint capacity, uint layer)
    {
    	return max(0, min(float(capacity - 1), floor(float(layer) + 0.5)));
    }
    
    void main()
    {
    	color = texture(texture_array, vec3(texture_0, getCoord(3, diffuse_layer)));
    }

    Самым лучшим вариантом будет расчитывать нужный слой за пределами шейдера, для этого мы можем использовать UBO / SSBO (используется также для передачи матриц, да и многих других данных, но это как-то в другой раз). Если уж кому не терпится тык_1 и тык_2, можете почитать.

    Что касается размеров, то есть GL_MAX_ARRAY_TEXTURE_LAYERS который равен 256 в OpenGL 3.3 и 2048 в OpenGL 4.5.

    Cтоит рассказать про Sampler Object (не относиться к Array texture, но полезная вещь) — это объект который используется для настройки состояний текстурного юнита, независимо от того, какой объект сейчас привзяан к юниту. Он помогает отделать состояния сэмплера от конкретного текстурного объекта, что улучшает абстракцию.

    GLuint sampler_state = 0;
    glGenSamplers(1, &sampler_state);
    glSamplerParameteri(sampler_state, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glSamplerParameteri(sampler_state, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glSamplerParameteri(sampler_state, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glSamplerParameteri(sampler_state, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glSamplerParameterf(sampler_state, GL_TEXTURE_MAX_ANISOTROPY_EXT, 16.0f);

    Я только что создал объект сэмплера, включил линейную фильтрацию и 16-кратную анизотропную фильтрацию для любого текстурного юнита.

    GLuint texture_unit = 0;
    glBindSampler(texture_unit, sampler_state);

    Тут мы просто биндим сэмплер к нужному текстурному юниту, а когда он нам перестает быть нужным биндим 0 к данному юниту.

    glBindSampler(texture_unit, 0);

    Когда мы привязали сэмплер его настройки имеют приоритет над настройками текстурного юнита. Результат: нет необходимости изменять существующую кодовую базу для добавления объектов сэмплера. Вы можете оставить создание текстур как есть (со своими собственными состояниями сэмплера) и просто добавить код для управления и использования объектов сэмплера.

    Когда настало время удалить объект, просто вызываем эту функцию:

    glDeleteSamplers(1, &sampler_state);

    Texture View


    Я переведу это как «текстурный указатель(может правильнее ссылки, я хз)», так как не знаю лучшего перевода.

    Что же такое указатели в перспективе OpenGL?

    Все очень просто, это указатель на данные immutable(именно изменяемой) текстуры, как видим на картинке нижу.



    По факту это объект, который расшаривает данные текселей определенного текстурного объекта, для аналогии можно привести std::shared_ptr из С++. Пока существует хоть один указатель на текстуру, исходная текстура не будет удалена драйвером.

    В wiki более детально описано, а так же стоит почитать о типах текстуры и таргета (они не обязательно должны совпадать)

    Для создания указателя нам надо получить дескриптор текстуры вызвав glGenTexture(никаких инициализаций не нужно) и потом glTextureView.

    glGenTextures(1, &texture_view);
    glTextureView(texture_view, GL_TEXTURE_2D, source_name, internal_format, min_level, level_count, 5, 1);

    Текстурные указатели могут указывать на N-й уровень mipmap'a, довольно полезно и удобно. Указатели могут быть как текстурными массивами, частями массивов, определенным слоем в этом массиве, а может быть срезом 3D текстуры как 2D текстура.

    Single buffer for index and vertex


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

    Вот как бы мы это сделали:

    GLint alignment = GL_NONE;
    glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &alignment);
    
    const GLsizei ind_len = GLsizei(ind_buffer.size() * sizeof(element_t));
    const GLsizei  vrt_len = GLsizei(vrt_buffer.size() * sizeof(vertex_t));
    
    const GLuint  ind_len_aligned = align(ind_len, alignment);
    const GLuint  vrt_len_aligned = align(vrt_len, alignment);
    
    GLuint buffer 	= GL_NONE;
    glCreateBuffers(1, &buffer);
    glNamedBufferStorage(buffer, ind_len_aligned + vrt_len_aligned, nullptr, GL_DYNAMIC_STORAGE_BIT);
    
    glNamedBufferSubData(buffer, 0, ind_len, ind_buffer.data());
    glNamedBufferSubData(buffer, ind_len_aligned, vrt_len, vrt_buffer.data());
    
    GLuint vao 	= GL_NONE;
    glCreateVertexArrays(1, &vao);
    glVertexArrayVertexBuffer(vao, 0, buffer, ind_len_aligned, sizeof(vertex_t));
    glVertexArrayElementBuffer(vao, buffer);


    Tessellation and compute shading


    Я не буду вам рассказывать про шейдер тесселяции, так как материала в гугле по этому поводу очень много(на русском), вот пару уроков: 1, 2, 3. Приступим к рассмотрению шейдера для расчетов (блииин, тоже много материала, расскажу вкратце).

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

    Картинка, я не знаю как ее назвать (типа потоки группируются).



    Для чего можем использовать?

    • Обработка изображения
      1. Блур
      2. Алгоритмы на основе плиток (отложенное затенение)
    • Симуляции
      1. Частицы
      2. Вода

    Дальше не вижу смысла писать, тоже есть много инфы в гугле, вот простой пример использования:

    //биндим пйплайн с расчетным шейдером
    glUseProgramStages( pipeline, GL_COMPUTE_SHADER_BIT, cs);
    //биндим текстуру, как изображение для чтения/записи
    glBindImageTexture( 0, tex, 0, GL_FALSE, 0, GL_WRITE_ONLY,
    GL_RGBA8);
    //запускаем 80x45 потоковых групп (достаточно для 1280х720)
    glDispatchCompute( 80, 45, 1);


    Вот пример пустого compute shader:
    #version 430
    layout(local_size_x = 1, local_size_y = 1) in;
    layout(rgba32f, binding = 0) uniform image2D img_output;
    void main() {
      // base pixel color for image
      vec4 pixel = vec4(0.0, 0.0, 0.0, 1.0);
      // get index in global work group i.e x,y position
      ivec2 pixel_coords = ivec2(gl_GlobalInvocationID.xy);
      
      //
      // interesting stuff happens here later
      //
      
      // output to a specific pixel in the image
      imageStore(img_output, pixel_coords, pixel);
    }


    Вот несколько ссылок для более глубокого ознакомления 1, 2, 3, 4.

    Path rendering


    Это новое(уже не новое) расширение от NVidia, его основная цель — векторный 2D рендеринг. Мы его можем использовать для текстов или UI, а поскольку графика векторная, она не зависит от разрешение, что несомненно большой плюс и наш UI'чик будет прекрасно смотреться.

    Основной концепцией является — трафарет, затем покрытие(cover в оригинале). Устанавливаем трафарет пути, затем визуалезируем пиксели.

    Для менеджмента используются стандартный GLuint, а так же функции создания и удаления имеют стандартное именование.

    glGenPathsNV // генерация
    glDeletePathsNV // удаление


    Вот немного о том, как мы можем получить путь:
    • SVG или PostScript в string'e
      glPathStringNV
    • массив команд с соответствующими координатами
      glPathCommandsNV
      и для обновления данных
      glPathSubCommands, glPathCoords, glPathSubCoords
    • шрифты
      glPathGlyphsNV, glPathGlyphRangeNV
    • линейные комбинации существующих путей (интерполирование одного, двух и более путей)
      glCopyPathNV, glInterpolatePathsNV, glCombinePathsNV
    • линейное преобразование существующего пути
      glTransformPathNV

    Список стандартных команд:

    • move-to (x, y)
    • close-path
    • line-to (x, y)
    • quadratic-curve (x1, y1, x2, y2)
    • cubic-curve (x1, y1, x2, y2, x3, y3)
    • smooth-quadratic-curve (x, y)
    • smooth-cubic-curve (x1, y1, x2, y2)
    • elliptical-arc (rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y)

    Вот как выглядит строка пути в PostScript:

    "100 180 moveto 40 10 lineto 190 120 lineto 10 120 lineto 160 10 lineto closepath” //звезда
    "300 300 moveto 100 400 100 200 300 100 curveto 500 200 500 400 300 300 curveto
    closepath” //сердце

    А вот в SVG:

    "M100,180 L40,10 L190,120 L10,120 L160,10 z” //звезда
    "M300 300 C 100 400,100 200,300 100,500 200,500 400,300 300Z” //сердце

    Еще есть много всяких плюшек с видами заполнений, краев, изгибов:



    Я не буду тут все описывать, так как материала очень много и это займет целую статью (если будет интересно, то как-нибудь напишу).

    Вот список примитивов для отрисовки

    • Cubic curves
    • Quadratic curves
    • Lines
    • Font glyphs
    • Arcs
    • Dash & Endcap Style

    Вот немного кода, а то уж очень много текста:

    //Компилирование SVG пути
    glPathStringNV( pathObj, GL_PATH_FORMAT_SVG_NV,
     strlen(svgPathString), svgPathString);
    //заполняем трафарета
    glStencilFillPathNV( pathObj, GL_COUNT_UP_NV, 0x1F);
    //конфигурация
    //покрываем трафарет (визуализируем пикселями)
    glCoverFillPathNV( pathObj, GL_BOUNDING_BOX_NV);

    Вот и все.

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

    Как и обещал, напишу следующую статью про оптимизацию и уменьшение вызовов отрисовки. Хотелось бы попросить написать в комментариях, о чем бы вы еще хотели почитать и что вам интересно:
    • Написание игры на cocos2d-x (Только практика, без воды)
    • Перевод цикла статей по Vulkan
    • Какие-то темы по OpenGL (кватернионы, новый функционал)
    • Алгоритмы компьютерной графики (освещение, space screen ambient occlusion, space screen reflection)
    • Ваши варианты


    Всем спасибо за внимание.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 16

      +1

      Возможно вас заинтересует цикл статей о практической стороне создания игр на cocos2d-x или ещё каком-то движке(будем обучаться вместе), а можно и вообще на чистом ogl

        0
        Очень интересуют аспекты современного opengl кода и оптимизаций. Мобилы (gles) и десктопы (gl3,4).
          0
          Я бы тоже с удовольствием почитал о техниках оптимизаций для OpenGL 3,4
        0

        Очень рад был бы ознакомиться именно с алгоритмами. И буду благодарен если посоветуете с чем можно ознакомиться по этой тематике в районе начального/среднего уровня. Спасибо

          0

          На втором скриншоте, судя по всему, должно быть написано Depth, вместо второго Width.

            0

            Скажите, а какой сакральный смысл кроется в присваивании переменным значения GL_NONE перед вызовом функций?

              +1
              если поменяется стандарт и 0 больше не будет инциализацией по умолчанию (что нереально) поменяют и GL_NONE. Приывчка (сразу видно, что переменная которая инициализирована GL_NONE используется в OpenGL)
                –2

                Про обратную совместимось что-нибудь слышали?

                  –2

                  Кто аам такую чушь сказал?

                –2

                *Чушь.

                  0

                  Если я не прав, поправьте. Какое это имеет отношение к обратной совместимости ?

                    –1

                    Прямое отношение.
                    Какое отношение имеет присваивание переменным значений перед их вызовом?

                      0

                      Где тут обратная совместимость ?

                    0

                    Возможно, это осталось со старого стандарта и не удалась, так как ленами код, но в данном случае это не имеет отношения к обратной совместимости

                      0

                      Нигде нет информации, что это старый стандарт и его не надо использовать, а функционал оставили лишь для совместимости. Следует, что это не точно

                      0
                      • перед вызовом функций.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое