К. В. Рябинин вычислительная геометрия и алгоритмы компьютерной графики
Скачать 1.44 Mb.
|
6.4. Взаимодействие шейдеров При запуске шейдеры получают доступ на чтение к данным той сущности, с которой они работают. Помимо этого, однако, меж- ду шейдером и основным приложением или между шейдерами смеж- ных этапов конвейера может быть протянута односторонняя инфор- мационная связь: основное приложение может сообщать дополни- тельную информацию шейдерам, а шейдеры более ранних этапов конвейера могут передавать данные шейдерам более поздних этапов. Данные вершин называются атрибутами (англ. Attribute ). Они доступны в вершинном шейдере. Для их описания в языке GLSL используется ключевое слово in , а в языке ESSL – ключевое слово attribute Например, в GLSL описание выглядит так: in vec3 a position; В ESSL этому соответствует: attribute vec3 a position; Семантически такая запись описывает один трёхкомпонент- ный атрибут вершины, названный a position Следует отметить, что шейдер не обязан описывать все имею- щиеся у вершины атрибуты: он может работать только с каким-то их подмножеством. Однако нужно избегать ситуаций, когда в шейдере присутствует описание атрибутов, фактически отсутствующих в вер- шине, так как это может привести к некорректным результатам из-за влияния неинициализированных переменных. Число атрибутов вершин, поддерживаемых графическим про- цессором, ограничено, но достаточно велико, чтобы покрыть боль- шую часть потребностей программистов. Атрибуты предлагается именовать, начиная с префикса a , чтобы подчеркнуть их назначение в программе. Для того чтобы связать конкретные области массива данных вершин с атрибутами, описанными в вершинном шейдере, в основ- ном приложении необходимо получить их дескрипторы. В случае ис- пользования ESSL единственной возможностью получить дескрип- тор является использование функции glGetAttribLocation , при- 52 нимающей в качестве аргумента символьное имя атрибута (напри- мер, “a position” ) и возвращающей его дескриптор. В GLSL нор- мальной практикой является задание дескрипторов в самом шейдере в явном виде при помощи ключевого слова layout Например, нижеприведённая запись означает, что указанному атрибуту назначен дескриптор 0 : layout(location = 0) in vec4 a position; Следует обратить внимание, что у каждого атрибута дескрип- тор должен быть уникальным. Вершинный шейдер в результате своей работы должен как минимум означить системную переменную gl Position типа vec4 , записав туда однородные координаты соответствующей вершины после перемножения со всей имеющейся у соответству- ющего объекта сцены цепочкой матриц. Значение этой переменной используется на дальнейших этапах конвейера. Матрицы преобразования, как правило, попадают в шейдер из основной программы (как часть «настроечной» информации, в которую могут входить самые разные параметры, например скорость анимации, коэффициенты плавности переходов и т. п.). Для этого используются специальные переменные, называемые юниформами (англ. Uniform ). Они описываются при помощи ключевого слова uniform , например uniform mat4 u mvp; Юниформы могут быть описаны как в вершинном, так и во фрагментном шейдере, таким образом оба типа шейдеров могут по- лучать из объемлющей программы настроечную информацию. Их предлагается именовать, начиная с префикса u , чтобы подчеркнуть их назначение в программе. Значение каждого юниформа должно быть задано в основной программе. Для этого, так же как и в случае атрибутов, используются дескрипторы. Аналогично, при использовании ESSL или GLSL вер- сии ниже 430 единственным способом получения дескриптора юни- форма является функция glGetUniformLocation , принимающая символьное имя юниформа и возвращающая его дескриптор. 53 В GLSL версии 430 и выше есть возможность задать дескрип- тор вручную: layout(location = 0) uniform mat4 u mvp; При этом, однако, как и в случае атрибутов, необходимо пом- нить об уникальности дескрипторов. Уникальность должна быть со- блюдена в пределах всего конвейера, т. е. если вершинный и фраг- ментный шейдер имеют семантически одинаковые юниформы (на- пример, отвечающий за один и тот же параметр алгоритма, исполь- зуемого в обоих шейдерах), дескрипторы этих юниформов тем не ме- нее должны различаться. Для взаимодействия друг с другом шейдеры используют ещё один тип переменных, называемых вэриинги (англ. Varying ). Для их описания в языке ESSL используется ключевое слово varying , а в языке GLSL – связка out в «отправляющем» шейдере и in в «при- нимающем». Важно отметить, что число и тип выходов вершинного и входов фрагментного шейдера должны совпадать, иначе конвейер не сможет функционировать. Пример описания вэриинга в вершинном и фрагментном шей- дерах на ESSL: varying vec3 v color; varying vec3 v color; Аналогичный пример для GLSL: out vec3 v color; in vec3 v color; Чаще всего фрагментный шейдер запускается большее число раз, чем вершинный, поскольку одному треугольнику, имеющему всего три вершины, может соответствовать целое множество фраг- ментов. При этом с каждым фрагментом связывается некоторое значение каждого вэриинга, отправленного вершинным шейдером в результате обработки соответствующих вершин. Значение, прихо- дящее в вэриинг фрагментного шейдера есть результат билинейной интерполяции значений соответствующего вэриинга вершинного шейдера, полученных в результате обработки вершин растеризуе- 54 мого в данный момент примитива. Интерполяция осуществляется с учётом перспективных искажений примитива (которые могут возникнуть в случае перспективной проекции), что гарантирует правильное изменение параметра вдоль поверхности примитива. Функция интерполяции имеет линейный характер и не может быть переопределена. Вэриинги предлагается именовать, начиная с префикса v , чтобы подчеркнуть их назначение в программе. Конечной целью фрагментного шейдера является определе- ние данных для всех целей рендеринга. В ESSL цель рендеринга по умолчанию (буфер цвета) отображена на системную переменную gl FragColor типа vec4 (выражающую цвет фрагмента в RGBA). В GLSL системной переменной цвета фрагмента нет, и програм- мист должен задать отображение на цель рендеринга в явном виде вручную путём объявления выходной переменной при помощи ключевого слова out : layout(location = 0) out vec4 o color; Нулевой дескриптор здесь означает цель рендеринга по умол- чанию (буфер цвета). Если используется лишь цель рендеринга по умолчанию, в принципе, layout(location = 0) можно опустить. Выходные переменные, отображаемые на цели рендеринга, предлагается именовать, начиная с префикса o , чтобы подчеркнуть их назначение в программе. 6.5. Шейдерная программа После того как шейдер написан, он должен быть скомпили- рован. По спецификации OpenGL компиляция шейдеров происхо- дит «налету», т. е. во время выполнения основного приложения. По- скольку код шейдера невелик, компиляция происходит относительно быстро (счёт идёт на миллисекунды), тем не менее этот этап рекомен- дуется выносить в блок подготовительной работы (создания и загруз- ки графических ресурсов), т. е. отделить от процесса визуализации сцены. В процессе компиляции могут возникнуть ошибки (как при компиляции любой программы), как правило связанные с синтакси- 55 сом. Наличие ошибок не позволяет далее работать с данным шей- дером, поэтому успешность компиляции обязательно должна прове- ряться, а содержание возникших ошибок имеет смысл выводить в какой-либо выходной поток (например, stderr ). Согласно спецификации OpenGL для встраивания в конвейер шейдеры всех этапов предварительно собираются в единый объект, называемый шейдерной программой (англ. Shader Program ). На эта- пе сборки ( линковки , англ. Linking ) шейдерной программы также мо- гут возникнуть ошибки, связанные с неверным набором шейдеров или несовпадение выходов шейдеров более ранних этапов конвейе- ра со входами шейдеров более поздних этапов. Успешность линковки также необходимо проверять и при возникновении ошибок логиро- вать их. Собранная шейдерная программа должна быть активирована до отправки данных на конвейер. Обычно в графическом приложении используется много раз- личных шейдерных программ для обеспечения всех необходимых эффектов, и перед отправкой очередной порции данных активную программу переключают. Однако в один момент времени активной может быть только одна программа, шейдеры из которой должны полностью определять алгоритм конвейера. 6.6. Реализация Предлагается расширить тестовое приложение, добавив в него вершинный и фрагментный шейдеры. Для упрощения запишем код шейдеров в строковые константы, а не в отдельные файлы. Следует отметить, что такой способ хранения шейдеров не всегда плох да- же в реальных проектах: часто отсутствие дополнительных внешних ресурсов является желательным. Для создания шейдера и шейдерной программы удобно заве- сти отдельные функции. Начнём с создания шейдера. Предлагается следующая сигнатура: GLuint createShader( const GLchar *code, GLenum type) – code – строка, содержащая код шейдера; 56 – type – константа, определяющая тип создаваемого шейдера (в данном случае – вершинный или фрагментный); – возвращаемое значение – ненулевой дескриптор шейдера, если со- здание прошло успешно; 0 , если при создании произошла ошибка. Тело функции приведено в листинге 1. Листинг 1. Функция создания шейдера 1 GLuint createShader( const GLchar ∗ code, GLenum type) 2 { 3 GLuint result = glCreateShader(type); 4 5 glShaderSource(result, 1, &code, NULL); 6 glCompileShader(result); 7 8 GLint compiled; 9 glGetShaderiv(result, GL COMPILE STATUS, 10 &compiled); 11 if (!compiled) 12 { 13 GLint infoLen = 0; 14 glGetShaderiv(result, GL INFO LOG LENGTH, 15 &infoLen); 16 if (infoLen > 0) 17 { 18 char infoLog[infoLen]; 19 glGetShaderInfoLog(result, infoLen, 20 NULL, infoLog); 21 cout << ”Shader compilation error” << 22 endl << infoLog << endl; 23 } 24 glDeleteShader(result); 25 return 0; 26 } 27 return result; 28 } 57 В строке (3) происходит создание шейдерного объекта задан- ного типа (объект в данном случае – это структура данных, связанная с графическим контекстом OpenGL, а не экземпляр какого-либо клас- са, поскольку OpenGL является полностью императивным ). Систе- ма резервирует память под управляющие структуры данных и воз- вращает дескриптор в виде целого беззнакового числа. При этом ва- лидными являются ненулевые дескрипторы, ноль же означает ошиб- ку создания. В строке (5) загружается массив строк исходного кода. Приведённая функция работает со всем исходным кодом как с одной строкой. В строке (6) отдаётся команда на компиляцию шейдера. В строке (9) запрашивается статус компилятора, чтобы проверить, не возникли ли ошибки. Если ошибки имеются, в строках (13–25) за- прашивается и выводится на консоль их описание, затем созданный шейдер удаляется и возвращается 0 . В противном случае возвраща- ется дескриптор созданного и успешно скомпилированного шейдера. Следует отметить, что как и во многих других стандартах и спецификациях, в OpenGL принято соглашение именовать функции, создающие что-либо, используя слово Create , а функции, запраши- вающие что-либо, созданное другими, – используя слово Get . При этом ответственность за объект всегда остаётся на создателе, т. е. тот, кто создал какой-либо объект, обязан затем уничтожить его при помощи соответствующей функции, содержащей в названии слово Delete , а тот, кто запросил какой-то объект, должен просто забыть о нём по ненадобности. С целью более глубокого понимания смысла использованных функций, читателю рекомендуется обратиться к документации OpenGL [11]. Для создания шейдерной программы предлагается использо- вать функцию следующей сигнатуры: GLuint createProgram(GLuint vsh, GLuint fsh) – vsh – дескриптор вершинного шейдера; – type – дескриптор фрагментного шейдера; – возвращаемое значение – ненулевой дескриптор программы, ес- ли создание прошло успешно; 0 , если при создании произошла ошибка. 58 Тело функции приведено в листинге 2. Листинг 2. Функция создания шейдерной программы 1 GLuint createProgram(GLuint vsh, GLuint fsh) 2 { 3 GLuint result = glCreateProgram(); 4 5 glAttachShader(result, vsh); 6 glAttachShader(result, fsh); 7 8 glLinkProgram(result); 9 10 GLint linked; 11 glGetProgramiv(result, GL LINK STATUS, 12 &linked); 13 14 if (!linked) 15 { 16 GLint infoLen = 0; 17 glGetProgramiv(result, GL INFO LOG LENGTH, 18 &infoLen); 19 if (infoLen > 0) 20 { 21 char infoLog[infoLen]; 22 glGetProgramInfoLog(result, infoLen, 23 NULL, infoLog); 24 cout << ”Shader program linking error” 25 << endl << infoLog << endl; 26 } 27 glDeleteProgram(result); 28 return 0; 29 } 30 31 return result; 32 } 59 В строке (3) происходит создание объекта шейдерной про- граммы. Затем, в строках (5–6) к этому объекту присоединяются шейдеры. В строке (8) происходит линковка программы, а в строке (11) – запрос статуса компоновщика. Строки (16–28), аналогично предыдущей функции, посвящены выводу на консоль описания потенциальных ошибок. В случае же успешной линковки функция возвращает дескриптор созданной и готовой к использованию шейдерной программы. Важно отметить, что после сборки программы сами шейдер- ные объекты, участвовавшие в её создании, оказываются более не нужны, и их можно удалить функцией glDeleteShader . Шейдер- ная программа, в свою очередь, должна существовать до тех пор, по- ка сцене нужны те эффекты, которые она реализует. После этого она также должна быть удалена функцией glDeleteProgram В данном примере, чтобы не усложнять структуру тестового приложения, дескриптор шейдерной программы имеет смысл объ- явить как глобальную переменную, создать программу в функции init и удалить в функции cleanup Строковые константы кодов вершинного и фрагментного шей- дера приведены в листингах 3 и 4 соответственно. Листинг 3. Код вершинного шейдера 1 const GLchar vsh[] = 2 ”#version 330 \ n” 3 ”” 4 ”layout(location = 0) in vec2 a position;” 5 ”layout(location = 1) in vec3 a color;” 6 ”” 7 ”out vec3 v color;” 8 ”” 9 ”void main()” 10 ” { ” 11 ” v color = a color;” 12 ” gl Position = vec4(a position, 0.0, 1.0);” 13 ” } ”; 60 Листинг 4. Код фрагментного шейдера 1 const GLchar fsh[] = 2 ”#version 330 \ n” 3 ”” 4 ”in vec3 v color;” 5 ”” 6 ”layout(location = 0) out vec4 o color;” 7 ”” 8 ”void main()” 9 ” { ” 10 ” o color = vec4(v color, 1.0);” 11 ” } ”; В первой строке указывается директива компилятору каса- тельно используемой версии GLSL. Поскольку, как уже отмечалось выше, этот язык по мере своего развития претерпевал ощутимые изменения, указывать версию необходимо в явном виде. Версия 330 соответствует стандарту OpenGL 3.3. Для упрощения дескрипторы атрибутов заданы в коде шей- дера в явном виде. Вершинный шейдер имеет два атрибута – vec2 a position (пространственные координаты) и vec3 a color (цвет), т. е. ожидает данные в формате (x, y, r, g, b) . Кроме того, он готовит данные для фрагментного шейдера: vec3 v color . В своей функции main он передаёт данные из атрибута цвета в соответ- ствующий вэриинг, а также означивает позицию текущей вершины, собирая четырёхкомпонентный вектор однородных координат из принятых на вход пространственных данных. Как можно видеть, здесь не используется трансформация вершин: шейдер ожидает, что координаты выводимого объекта уже приведены к NDC. Фрагментный шейдер определяет входной канал vec3 v color , соответствующий выходному каналу вершинного шей- дера, а также выходной канал vec4 o color для нулевой цели рендеринга (буфера цвета). Единственное, что происходит в его функции main – означивание выходного канала пришедшими 61 с предыдущих ступеней конвейера данными, дополняя их до четырёхкомпонентного вектора (добавляя альфа-канал). Задание: Добавить в тестовое приложение создание шейдерной программы при помощи функций, при- ведённых в данной главе. Подсказка: Обратить внимание на корректную и своевременную очистку всех созданных объектов. 62 7. Представление объектов сцены Объекты сцены в компьютерной графике вообще и в OpenGL в частности представляются совокупностью геометрической модели (англ. Model ) и материала (англ. Material ). Геометрическая модель – это совокупность свойств формы объекта. Материал – это сово- купность визуальных свойств объекта. Обе составляющих объекта сцены определяются связанными с объектом данными и конкретной программой графического конвейера (шейдерами). 7.1. Полигональная сетка Данные объекта могут храниться в виде достаточно сложной структуры, включая в себя описание его вершин и связей между ни- ми, описание оптических свойств его поверхности и т. д. Вершины и их связи называются полигональной сеткой объекта (англ. Mesh ). В самом простом случае эта сетка представлена одним масси- вом, в котором вершины последовательно объединяются в примити- вы. Однако, если представить составленный из двух треугольников четырёхугольник, становится очевиден недостаток такого подхода: идущие строго последовательно вершины, описывающие некоторую сетку, с высокой вероятностью приведут к дублированию данных. На сложных моделях такое дублирование может привести к серьёзным накладным расходам. Эта проблема решается введением индексации: отдельного массива, элементами которого являются номера вершин. Этот массив задаёт порядок следования вершин и позволяет переиспользовать данные, хранящиеся однократно. В более сложном случае один объект может быть представ- лен несколькими массивами, хранящими разные атрибуты его вер- шин (при этом все они отправляются на конвейер одновременно за один вызов отрисовки), или же несколькими принципиально различ- ными полигональными сетками (которые отправляются на конвей- ер последовательно, разными вызовами отрисовки). Это зависит от 63 уровня абстракции, который мы используем, говоря об «объекте сце- ны». Если, например, объектом сцены является модель автомоби- ля (обладающая при этом достаточно высокой детализацией), в её состав, скорее всего, войдёт несколько полигональных сеток, пред- ставляющих поверхности принципиально разных свойств: металли- ческий корпус, стёкла, светящиеся фары, резиновые колёса и т. д. Для высококачественной визуализации такой модели потребуется осуще- ствить несколько вызовов отрисовки, используя при этом различные шейдеры для различных «деталей». Однако в графических приложениях, работающих в реальном времени, как правило, используется диаметрально противополож- ный подход: объединение возможно большего числа объектов в один массив, с целью минимизации числа вызовов отрисовки. Отправка большой порции данных на конвейер за один раз оказывается значительно эффективнее, чем отправка нескольких более мелких пакетов. Более того, иметь один массив вершин, собирающий в себе все используемые атрибуты, эффективнее, чем несколько массивов, по одному атрибуту в каждом. В данном пособии мы рассмотрим работу лишь с одним масси- вом, но обобщение до работы с несколькими является тривиальным: все шаги, необходимые для отправки на конвейер одного массива, надо последовательно проделать со всеми имеющимися, а затем вы- звать отрисовку. |