Space Invaders «с нуля» — Часть 2: настраиваем шейдеры OpenGL и рисуем спрайт пришельца
Как написать собственный клон Space Invaders на C++ с помощью OpenGL: создание буфера, работа с шейдерами, текстурами и спрайтами. Подробное руководство для начинающих гейм-девелоперов и энтузиастов графики.
103 открытий1К показов
Это перевод статьи автора Nick Tasios. Перевод первой части мы уже публиковали: там настроили окно и контекст. Во второй части настроим шейдеры OpenGL, чтобы отрисовать спрайт пришельца! Спрайт (sprite) — это двумерное графическое изображение, которое используется для отображения объектов в играх и интерфейсах. Если проще — картинка персонажа, предмета или эффекта, которая выводится на экран поверх фона. Код для действий этой части можно посмотреть в репозитории на GitHub.
Рендеринг на стороне CPU
GPU отлично справляется с обработкой больших объёмов данных, но программировать графический процессор сложнее, чем CPU. Для простого клона Space Invaders, который я создаю здесь, проще выполнять всю отрисовку на центральном процессоре (CPU), используя буфер — участок памяти, представляющий собой пиксели игрового экрана. Затем этот буфер можно передать на GPU в виде текстуры и вывести на экран компьютера.
Буфер имеет определённую ширину и высоту. Мы представляем каждый пиксель в виде значения типа uint32_t, что позволяет хранить четыре 8-битных компонента цвета для каждого пикселя. В нашем случае мы будем использовать только 24 бита — по 8 бит для красного (red), зелёного (green) и синего (blue) каналов. Можно возразить, что логичнее было бы использовать uint8_t, но я применяю 32-битное значение, потому что так проще работать с индексами при обращении к элементам массива. Чтобы использовать эти типы целых чисел с фиксированной разрядностью, необходимо подключить стандартный заголовочный файл:
Чтобы упростить определение цветов в виде значений типа uint32_t, мы создадим следующую функцию:
Эта функция устанавливает три старших байта (24 бита) в значения r, g и b соответственно.
Оставшиеся 8 младших битов заполняются значением 255 — хотя, как упоминалось ранее, альфа-канал (прозрачность) в нашем случае не используется. Далее мы создаём функцию, которая очищает буфер, заполняя его указанным цветом.
Эта функция проходит по всем пикселям буфера и устанавливает для каждого заданный цвет.
Теперь, в главной функции (main), мы инициализируем буфер — то есть создаём область памяти, где будут храниться все пиксели экрана:
Этот код создаст буфер с шириной buffer_width и высотой buffer_height, а также заполнит его цветом clear_color, то есть зелёным.
Шейдеры OpenGL
После того как мы создали буфер, нам нужно настроить OpenGL, чтобы иметь возможность вывести его содержимое на экран. В современном OpenGL большая часть обязанностей, которые раньше выполнял драйвер, теперь ложится на разработчика. Он должен сам писать небольшие программы, которые выполняются на видеокарте (GPU) — такие программы называются шейдерами (shaders).
OpenGL определяет конвейер рендеринга — последовательность этапов, через которые проходит изображение перед выводом на экран. Шейдеры могут выполняться на разных стадиях этого конвейера.
Старый OpenGL иногда называют fixed pipeline — «фиксированный конвейер», поскольку его этапы были заранее определены и не могли быть изменены программистом.
В современном OpenGL почти все этапы программируемы, и основные среди них два:
- Vertex Shader (вершинный шейдер) — обрабатывает данные вершин, обычно выполняет трансформацию объектов в координаты экрана.
- Fragment Shader (фрагментный шейдер) — работает с пикселями, полученными после растеризации. Он определяет их цвет, глубину и, при необходимости, трафаретные значения (stencil).
Эти два шейдера — минимально необходимые для работы любого рендеринга в OpenGL.
Хотя с помощью шейдеров можно создавать невероятно сложные визуальные эффекты, в этом проекте мы построим два простых шейдера, которые просто выведут содержимое нашего буфера (из предыдущего шага) на экран.
Обычно для отображения изображения нужно создать квад (quad), который покрывает весь экран. Однако существует известный трюк — можно сгенерировать полноэкранный треугольник, не передавая в шейдер вообще никаких вершинных данных.
Для фрагментного шейдера нам просто нужно считать значение из текстуры буфера и вывести результат этого выборочного чтения.
Обратите внимание, что выходное значение вершинного шейдера TexCoord теперь используется как вход для фрагментного шейдера. Хотя вершинному шейдеру не требуется передавать какие-либо данные о вершинах, мы всё же должны указать, что будем рисовать три вершины. Для этого создаётся объект вершинного массива (VAO).
Если упростить, VAO (Vertex Array Object) — это структура в OpenGL, которая хранит формат вершинных данных вместе с самими данными вершин. Наконец, оба шейдера нужно скомпилировать в код, понятный GPU, а затем связать их в единый шейдерный программный объект.
В приведённом выше фрагменте кода шейдерная программа сначала создаётся с помощью функции glCreateProgram. Отдельные шейдеры создаются функцией glCreateShader и компилируются с помощью glCompileShader. После этого они присоединяются к программе через glAttachShader, после чего сами объекты шейдеров можно удалить.
Программа связывается вызовом glLinkProgram. Во время компиляции OpenGL выводит различную информацию — аналогично тому, как это делает компилятор C++. Однако, чтобы получить эти сообщения, их нужно перехватывать вручную. Для этого созданы две простые функции: validate_shader и validate_program.
Текстура буфера
Для передачи данных изображения на GPU используется текстура OpenGL. Как и в случае с VAO, текстура также объект, который вместе с данными изображения содержит информацию о формате этих данных. Сначала текстура создаётся с помощью функции glGenTextures.
Затем задаём формат изображения, а также некоторые стандартные параметры, определяющие поведение выборки текстуры.
Здесь мы указываем, что изображение должно использовать 8-битный RGB-формат для внутреннего представления текстуры. Последние три параметра вызова glTexImage2D определяют формат пикселей данных, которые мы передаём в текстуру: каждый пиксель находится в формате RGBA и представлен четырьмя беззнаковыми 8-битными целыми числами.
Два первых вызова glTexParameteri сообщают GPU не применять фильтрацию (сглаживание) при чтении пикселей. Два последних указывают, что при попытке чтения за пределами текстуры будет использоваться значение на границе.
Теперь нам нужно привязать текстуру к uniform-переменной sampler2D во фрагментном шейдере. В OpenGL существует несколько текстурных единиц, к которым может быть привязан uniform. Мы получаем расположение uniform-переменной в шейдере (его можно рассматривать как своего рода «указатель») с помощью glGetUniformLocation и устанавливаем её на текстурную единицу 0 с помощью вызова glUniform1i.
Отображение буфера
Наконец, мы настроили всё необходимое, чтобы вывести буфер на экран. Непосредственно перед запуском игрового цикла мы отключаем тест глубины и привязываем созданный ранее объект вершинного массива (VAO).
Если теперь вызвать:
То содержимое созданного окна должно отобразиться зелёным цветом.
Отрисовка спрайта
В предыдущем посте мы уже создали окно и вывели на экран цвет. Заканчивать эту часть на том же этапе было бы скучно — так что теперь давайте что-нибудь нарисуем.
Сначала я определяю простой спрайт:
Это просто участок данных, размещённых в куче, вместе с указанием ширины и высоты спрайта. Сам спрайт представлен в виде битовой карты (bitmap) — то есть каждый пиксель кодируется одним битом, где значение 1 означает, что пиксель спрайта включён (“on”).
Далее мы создаём функцию, которая отрисовывает спрайт в буфере с заданным цветом.
Функция просто проходит по пикселям спрайта и рисует активные (“on”) пиксели по указанным координатам, если они находятся в пределах границ буфера.
Альтернативно можно представить спрайт так же, как и буфер — то есть каждый пиксель хранится как 32-битное RGBA-значение, где альфа-канал используется для прозрачности или смешивания.
В главной функции (main) мы создаём спрайт пришельца (alien sprite).
Чтобы нарисовать красный спрайт в позиции (112, 128), вызываем:
Чтобы подготовиться к следующим шагам, мы очищаем буфер и рисуем спрайт на каждом кадре игрового цикла. Для обновления текстуры OpenGL вызываем функцию glTexSubImage2D.
Если скомпилировать и запустить финальный код из этого поста, то вы увидите красную текстуру в центре зелёного фона.
В этом посте мы подготовили всё необходимое, чтобы отрисовывать спрайты на CPU и выводить их в окно, созданное ранее. В современном OpenGL это требует определённого объёма работы — мы затронули такие темы, как объекты вершинных буферов, шейдеры и текстуры.
Теперь, когда базовые функции отрисовки готовы, можно переходить к более интересной части — программированию игровой логики. Разберём это в следующей статье.
103 открытий1К показов







