Найти тему
ZDG

OpenGL #3: Зелёный сло... треугольник и наш первый шейдер

Оглавление

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

Предыдущие части: Рисуем треугольник, Приступим, помолясь, OpenGL на пальцах

Только мы пока не увидим шикарный разноцветный треугольник. Компиляция и связывание шейдеров требуют нудной подготовки, и нам придётся пройти сначала этот этап.

Исходный код шейдера

Начнём с вершинного (вертекс) шейдера. Он занимается подготовкой вершин треугольника для дальнейшей растеризации. Тот шейдер, который работает по умолчанию, нас полностью устраивает – он просто ничего не делает с вершинами, потому что с ними в общем-то ничего и не надо делать.

Наш вершинный шейдер тоже не будет ничего делать. Но так как наш треугольник будет с цветными вершинами, мы передадим в шейдер цвета вершин. А он эти цвета передаст дальше по конвейеру, для пиксельного (фрагментного) шейдера. То есть шейдер будет работать просто как посредник по передаче данных.

Шейдер я сделаю в отдельном файле vertex_shader.glsl, он будет читаться главной программой из файла, чтобы в случае изменения шейдера не надо было перекомпилировать всю программу.

Посмотрим на исходный код:

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

  • version 430: означает, что мы используем версию OpenGL 4.3. Это важно, так как шейдеры разных версий несовместимы.
  • ключевое слово in назначает входные параметры шейдера. В данном случае мы видим, что в шейдер с конвейера поступают структуры VertexPosition (координаты вершины) и VertexColor (цвет вершины). У них тип vec3. Это встроенный тип GLSL, который обозначает вектор из трёх элементов. Для координат точки это, очевидно, {x, y, z}, а для цвета {R, G, B}.
  • ключевое слово out назначает выходные параметры шейдера. Это будет vec3 Color.

Далее мы видим наш привычный void main(), где происходит собственно работа шейдера. Первым делом выходному параметру Color присваивается входной параметр VertexColor. Это значит, что мы взяли цвет на входе и просто передали его на выход. Всё.

Затем мы видим ранее не объявленный нигде параметр gl_Position. Секрет в том, что он является встроенным выходным параметром для координат вершин. Мы его не объявляем, но он есть.

Выходной параметр gl_Position также получает данные из входного параметра VertexPosition. Но эти данные на лету расширяются из vec3 в vec4. Если vec3 это вектор из трёх элементов, то нетрудно догадаться, что vec4 это вектор из четырёх элементов.

Его можно задать допустим так: vec4(1, 2, 3, 4), а можно так, как описано выше: vec4(vec3(), 1), то есть передаём vec3 и добавляем ещё один элемент. GLSL понимает такие преобразования.

Таким образом, мы приняли на вход структуры vec3 VertexColor и vec3 VertexPosition, и передали на выход структуры vec3 Color и vec4 gl_Position. Хотя gl_Position не заявлена как out, она вшита по умолчанию.

Зачем мы добавили 1 элемент вектора к gl_Position? Пока не важно, этого требует внутренняя кухня, с которой ещё предстоит разобраться.

Простейший шейдер готов, переходим к его компиляции.

Компиляция шейдера

Читаем текст шейдера из файла в символьный буфер. Я просто выделил под буфер string_buffer килобайт памяти:

-2

Прочитав строку из файла, по правилам языка C нужно после неё записать нулевой байт – конец строки.

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

-3

Сначала мы получаем "номерок" шейдера (vertex_shader) от функции glCreateShader(). В функцию передаётся константа GL_VERTEX_SHADER, таким образом мы поясняем, что хотим создать вершинный шейдер.

Затем мы из текста, ранее загруженного из файла, готовим массив parts[]. Зачем нужен массив? Как я понял, если у нас будет много шейдеров, то у них будут какие-то общие места. Эти общие места можно вынести в отдельные кусочки. Тогда мы сможем составить нужный нам шейдер из кусочков – какие-то взять, какие-то пропустить. И вот массив нужен как раз для того, чтобы собрать в него все кусочки. У нас сейчас только один кусочек, поэтому мы просто инициализируем массив из одного элемента.

Затем с помощью функции glShaderSource() мы загружаем массив с кусочками в шейдер. Первый параметр – номерок vertex_shader, затем количество частей в массиве (1), затем массив (parts), и далее должен быть ещё один массив, в котором хранятся длины кусочков. Но к счастью, если вместо этого массива указать NULL, то длина кусочков будет считаться до нулевого байта, в общем, как обычно со строками.

Итак, мы получили номерок для вершинного шейдера, сделали массив строк, состоящий из одной строки, и загрузили этот массив в шейдер.

Наконец, самая лёгкая часть – компилируем шейдер с помощью glCompileShader(vertex_shader).

Лёгкая она исключительно тогда, когда шейдер компилируется нормально. По-хорошему здесь требуется ещё примерно 10 строк кода, чтобы запросить статус компиляции и получить ошибки, если они есть. Эти строки есть в проекте на github, но здесь я их пока пропущу, чтобы не застревать.

Программа

Хорошо, теперь у нас есть скомпилированный вершинный шейдер. Но шейдер это лишь подпрограмма. А в конвейере OpenGL используется аж целая программа. Но её не надо писать или компилировать. Её суть в том, что мы связываем (линкуем) несколько шейдеров, и получается программа. Шейдеры в ней будут выполняться по порядку.

-4

Тут все действия достаточно типичны.

  • Получаем номерок для программы от glCreateProgram()
  • Добавляем наш вершинный шейдер в программу с помощью glAttachShader()
  • Линкуем шейдеры внутри программы (пока только один) с помощью glLinkProgram()
  • И наконец подключаем программу к конвейеру OpenGL с помощью glUseProgram()

Здесь после glLinkProgram() также нужно запросить статус линковки и узнать об ошибках, если они есть, но мы пока это тоже пропускаем, чтобы не грузиться.

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

Свой пиксельный шейдер

Чтобы наша программа заработала как надо, нужно освоить ещё много информации. Пока же, просто для утешения, напишем свой пиксельный шейдер. Он не будет принимать никаких входных параметров, а будет просто рисовать пикселы зелёным цветом. Если мы увидим на экране не белый, а зелёный треугольник, то будем знать, что это работает наше детище, и это нас хоть немного порадует.

Создаём файл fragment_shader.glsl:

-5

Как видно, я убрал входные параметры, а выходной параметр FragColor просто выдаёт зелёный цвет.

Не парясь, потому что это временно, методом copy-paste создаём код для загрузки и компиляции этого шейдера (номерок у него будет fragment_shader). Нужно только в функции glCreateShader() использовать константу GL_FRAGMENT_SHADER.

Теперь надо изменить вершинный шейдер, чтобы он тоже не принимал и не слал ничего лишнего:

-6

После компиляции подключаем к программе оба шейдера:

-7

Ну и собственно всё, запускаем:

-8

Ура! Мы нарисовали свой собственный зелёный треугольник с блекджеком и своими собственными шейдерами!

Казалось бы, от разноцветных пикселов нас отделяет совсем немного. Но поверьте, это не так :) Впереди ещё достаточно боли, хотя вершина уже видна.

Текущий код лежит на github в ветке shaders.

Читайте дальше: Раскрашиваем вершины треугольника