Главная страница
Навигация по странице:

  • 6.5. Шейдерная программа

  • 6.6. Реализация

  • Задание: Добавить в тестовое приложение создание шейдерной программы при помощи функций, при- ведённых в данной главе.Подсказка

  • 7. Представление объектов сцены

  • 7.1. Полигональная сетка

  • К. В. Рябинин вычислительная геометрия и алгоритмы компьютерной графики


    Скачать 1.44 Mb.
    НазваниеК. В. Рябинин вычислительная геометрия и алгоритмы компьютерной графики
    АнкорVychislitelnaya_geometriya_i_algoritmy_kompyuternoy_grafiki._Rabota_s_3D-grafikoy_sredstvami_OpenGL
    Дата28.04.2022
    Размер1.44 Mb.
    Формат файлаpdf
    Имя файлаVychislitelnaya_geometriya_i_algoritmy_kompyuternoy_grafiki._Rab.pdf
    ТипДокументы
    #503739
    страница5 из 8
    1   2   3   4   5   6   7   8
    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
    уровня абстракции, который мы используем, говоря об «объекте сце- ны». Если, например, объектом сцены является модель автомоби- ля (обладающая при этом достаточно высокой детализацией), в её
    состав, скорее всего, войдёт несколько полигональных сеток, пред- ставляющих поверхности принципиально разных свойств: металли- ческий корпус, стёкла, светящиеся фары, резиновые колёса и т. д. Для высококачественной визуализации такой модели потребуется осуще- ствить несколько вызовов отрисовки, используя при этом различные шейдеры для различных «деталей».
    Однако в графических приложениях, работающих в реальном времени, как правило, используется диаметрально противополож- ный подход: объединение возможно большего числа объектов в один массив, с целью минимизации числа вызовов отрисовки. Отправка большой порции данных на конвейер за один раз оказывается значительно эффективнее, чем отправка нескольких более мелких пакетов. Более того, иметь один массив вершин, собирающий в себе все используемые атрибуты, эффективнее, чем несколько массивов,
    по одному атрибуту в каждом.
    В данном пособии мы рассмотрим работу лишь с одним масси- вом, но обобщение до работы с несколькими является тривиальным:
    все шаги, необходимые для отправки на конвейер одного массива,
    надо последовательно проделать со всеми имеющимися, а затем вы- звать отрисовку.
    1   2   3   4   5   6   7   8


    написать администратору сайта