Гtr>
NeHe Tutorials Народный учебник по OpenGL
Урок 47. OpenGL

CG Vertex Shader

 

Использование вершинных и фрагментных (или пиксельных) шейдеров имеет массу преимуществ. Наиболее очевидное – разгрузка ЦПУ (CPU) за счет загрузки ГПУ (GPU). Gg – это простой и понятный язык для написания мощных шейдеров. (Примечание переводчика: шейдеры – это программы, которые исполняются графическим процессором (ГПУ или GPU)).

 

Этот урок преследует несколько целей. Во-первых, демонстрирует простой вершинный шейдер, который что-то делает, без всяких лишних и  не очень-то нужных эффектов типа освещения и тому подобного. Во-вторых, показывает простой механизм для запуска вершинных шейдеров с понятными и видимыми результатами, используя OpenGL. Таким образом, этот урок предназначен для новичков, которые интересуются Cg, и имеют не много опыта работы с OpenGL.

 

Этот урок основан на базовом коде NeHeGL. Если Вам нужна более подробная информация о Cg сходите на сайт NVidia (developer.nvidia.com), а на сайте www.cgshaders.org можно найти множество крутых шейдеров.

 

ПРИМЕЧАНИЕ: Этот урок не предназначен, для того чтобы рассказать Вам все об Cg. Я намерен преподать Вам только то, как загрузить и запустить вершинные шейдеры с помощью OpenGL.

 

Настройка:

 

Вначале (если Вы это не сделали), надо скачать компилятор Cg с сайта NVidia. Загружайте версию 1.1, так как между версией 1.1 и 1.0 есть ряд существенных отличий (разные имена переменных, замена функций и т.д.), и если код компилировался под одной версией, то необязательно он будет компилироваться с другой.

 

Далее поместите заголовочные файлы и библиотеки Cg в соответствующие директории Visual Studio. Поскольку я подозрительно отношусь к работе инсталляторов, я копирую библиотеки:

 

Из: C:\Program Files\NVIDIA Corporation\Cg\lib

В:   C:\Program Files\Microsoft Visual Studio\VC98\Lib

 

и заголовочные файлы (поддиректория Cg и Gљext.h в поддиректорию GL)...

 

Из: C:\Program Files\NVIDIA Corporation\Cg\include

В:   C:\Program Files\Microsoft Visual Studio\VC98\Include

 

Теперь все готово для урока.

 

Урок по Cg:

 

Большая часть информации в этом уроке взята из Руководства пользователя по инструментарию Cg.

 

Если несколько важных моментов, которые вы должны помнить при работе с вершинными (и позже с фрагментными) программами. Прежде всего, надо понимать, что вершинная программа выполняется полностью на КАЖДОЙ вершине. Единственный способ запустить вершинную программу на выбранной вершине состоит в том, что надо загружать/выгружать программу для каждо отдельной вершины, или обработать вершины в потоке, на который воздействует вершинная программа, и поток, которые не обрабатывается.

 

Выход вершинной программы передается во фрагментный шейдер, независимо от того имеется ли фрагментный шейдер или нет.

 

И, наконец, помните, что вершинная программа выполняется на вершине до того, как обрабатывается примитив, в то время как фрагментная программа выполняется после растеризации.

 

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

 

Для начала создадим текстовый файл “wave.cg”, который будет содержать определение структуры для передачи нашей Cg программе необходимой информации для ее работы:

 

struct appdata

{

  float4 position : POSITION;

  float4 color  : COLOR0;

  float3 wave  : COLOR1;

};

 

Все три переменные имеют предопределенный тип (POSITION, COLOR0 и COLOR1, соответственно). Эти предопределенные имена используются для связывания. В OpenGL, эти предопределенные имена неявно отображают входа с соответствующими аппаратными регистрами. Основная программа должна обеспечит передачу данных для каждой из этих переменных. Переменная position ТРЕБУЕТСЯ, так как она используется для растеризации. Это единственная переменная, которая требуется для работы вершинной программы.

 

Следующее, нам надо создать структуру, которая будет содержать результат для последующей работы фрагментной программы.

 

struct vfconn

{

  float4 HPos  : POSITION;

  float4 Col0  : COLOR0;

};

 

Как в случае входа, каждая выходная переменная связана с предопределенным именем. Hpos представляет позицию преобразованную в гомогенное пространство отсечения. Col0 представляет цвет вершины после изменения сделанного вершинной программой.

 

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

 

vfconn main(appdata IN,  uniform float4x4 ModelViewProj)

{

  vfconn OUT;        // Переменная для передачи данных во фрагментный шейдер

 

Также как в Си, мы задаем нашу функцию, которая возвращает структуру vfconn, имя функции (“main”, но может быть другим), и параметры. В нашем примере, структура appdata – это входная переменная (содержит позицию текущей вершины, цвет вершины и значения для формирования волны на нашей модели).

 

Примечание переводчика: за размещение фактических значений в эту структуру отвечает драйвер GPU.

 

Мы также принимаем постоянный параметр – матрицу вида, модели и проекции из OpenGL. Это значение неизменно, поскольку мы манипулируем нашими вершинами, и поэтому упоминается постоянное. Эта матрица требуется для преобразования позиции вершины в гомогенное пространство отсечения.

 

Далее мы определяем переменную, в которой хранятся наши модифицированные значения из вершинного шейдера. Эти значения возвращаются в конце функции, и передаются во фрагментный шейдер (если он существует).

 

Теперь надо выполнить модификацию данных вершины.

 

  // Изменим позицию Y вершины по синусу.

  IN.position.y = ( sin(IN.wave.x + (IN.position.z / 4.0) ) +

                    sin(IN.wave.x + (IN.position.x / 5.0) ) ) * 2.5f;

 

Мы изменяем координату Y в зависимости от текущих координат X и Z. Координаты X и Z делим на 4.0 и 5.0 соответственно, чтобы сгладить их (чтобы понять, что я имею в виду можно заменить эти значения на 1.0).

 

В переменной IN.wave находится постоянно увеличивающееся значение, которое производит синусоидальную волну на нашей модели. Эта переменная задается в основной программе. Поэтому, мы вычисляем координату Y как синус от X и Z. Наконец, мы масштабируем результат на 2.5, чтобы сделать волны, более высокими.

 

Далее мы делаем необходимые операции для вычисления выходного значения, которое будет использоваться фрагментной программой.

 

  // Трансформация позиции вершины в гомогенном пространстве отсечения (требуется)

  OUT.HPos = mul(ModelViewProj, IN.position);

 

  // Зададим цвет из IN.color

  OUT.Col0.xyz = IN.color.xyz;

 

  return OUT;

}

 

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

 

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

Урок по OpenGL:

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

 

Вначале необходимо кое-что настроить. Мы должны включить необходимые заголовочные файлы для выполнения шейдеров Cg с OpenGL. В числе других операторов #include, мы должны включить заголовочные файлы Cg и CgGL.

 

#include <cg\cg.h>    // НОВОЕ: заголовок Cg

#include <cg\cggl.h>  // НОВОЕ: заголовок Cg для OpenGL

 

Сейчас мы готовы настроить наш проект и начать работать. До старта мы должны убедиться в том, что Visual Studio найдет корректные библиотеки. Следующий код позволит нам добиться этой цели!

 

#pragma comment( lib, "cg.lib" )    // Искать Cg.lib во время линковки

#pragma comment( lib, "cggl.lib" )  // Искать CgGL.lib во время линковки

 

Затем мы создадим несколько глобальных переменных для нашей модели и переключатель для включения и выключения программы CG.

 

#define    SIZE  64                // Зададим размер модели по оси X/Z

bool    cg_enable = TRUE, sp;      // Переключатель Вкл/Выкл программы Cg, Нажат пробел?

GLfloat    mesh[SIZE][SIZE][3];    // Наша статическая модель

GLfloat    wave_movement = 0.0f;   // Наша переменная для смещения волн вдоль модели

 

Мы определяем размер в 64 точки по каждому краю нашей модели (оси X и Z). Затем создаем массив для каждой вершины модели. Последняя переменная требуется для создания перемещения синусоидальной волны вдоль модели.

 

Теперь надо задать глобальные переменные для Cg.

 

CGcontext  cgContext;  // Контекст сохранения нашей Cg программы

 

Первая переменная, которая нам нужна CGcontext. Переменная CGcontext - контейнер для множества программ Cg. В общем, необходима только одна переменная CGcontext независимо от числа используемых вершинных и фрагментных программ. Можно выбирать различные программы из контекста CGcontext, используя функций cgGetFirstProgram и cgGetNextProgram.

 

Далее определяем переменную CGprogram для нашей вершинной программы.

 

CGprogram  cgProgram;  // Наша вершинная программа Cg

 

Переменная CGprogram используется для сохранения вершинной программы. CGprogram - по существу указатель на вершинную (или фрагментную) программу. Она добавляется к CGcontext.

 

Кроме этого, надо иметь переменную для сохранения профиля шейдера.

 

CGprofile  cgVertexProfile;  // Профиль использования нашего вершинного шейдера

 

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

 

CGparameter  position, color, modelViewMatrix, wave; // Параметры необходимые в шейдере

 

Каждый CGparameter - по существу указатель на соответствующий параметр в шейдере.

 

Теперь, когда мы разобрались в глобальных переменных, пришло время подобраться к настройке модели и шейдера.

 

В функции Initialize до вызова “ return TRUE;” мы должны добавить новый код.

 

  glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);          // Рисовать каркасную модель

 

  // Создать модель

  for (int x = 0; x < SIZE; x++)

  {

    for (int z = 0; z < SIZE; z++)

    {

      mesh[x][z][0] = (float) (SIZE / 2) - x; // Центр модели в начале координат

      mesh[x][z][1] = 0.0f;            // Задать все значение Y для всех точек равным 0

      mesh[x][z][2] = (float) (SIZE / 2) - z; // Центр модели в начале координат

    }

  }

 

Сначала вызываем glPolygonMode для задания каркасного отображения (без хорошего освещения сплошная заливка выглядит ужасно). Затем проходим по модели, задавая значения X и Z относительно начала координат. Значение Y для каждой точки задается равным 0.0f. Интересно отметить, что значения сгенерированные на этом шаге ни изменяются во время выполнения.

 

С инициализацией модели покончено, теперь надо инициализировать Cg.

 

  // Настройка Cg

  cgContext = cgCreateContext(); // Создание нового контекста для наших Cg программ

 

  // Проверка нашего контекста, что все успешно

  if (cgContext == NULL)

  {

    MessageBox(NULL, "Failed To Create Cg Context", "Error", MB_OK);

    return FALSE; // Дальше нельзя продолжать

  }

 

Сначала пробуем создать новый CGcontext для хранения программ Cg. Если возвращено значение равное NULL, то создание контекста потерпело фиаско. Это  обычно происходит из-за ошибок распределения памяти.

 

  cgVertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX); // Взять последний профиль GL

 

  // Проверить, что все хорошо

  if (cgVertexProfile == CG_PROFILE_UNKNOWN)

  {

    MessageBox(NULL, "Invalid profile type", "Error", MB_OK);

    return FALSE; // Дальше нельзя продолжать

  }

 

  cgGLSetOptimalOptions(cgVertexProfile); // Задать текущий профиль

 

Теперь определим последний использованный профиль. Для определения последнего фрагментного профиля вызываем cgGLGetLatestProfile с типом профиля CG_GL_FRAGMENT. Если возвращаемое значение - CG_PROFILE_UNKNOWN, то это значит, что нет соответствующего доступного профиля. С корректным профилем, выполняем вызов cgGLSetOptimalOptions. Эта функция задает параметры компилятора, которые основаны на доступных параметрах компилятора, GPU и драйвера. Эти функции используются с каждой новой программой Cg. (Это необходимо для оптимизации компиляции шейдера в зависимости от видеокарты и драйверов).

 

  // Загрузка и компиляция шейдера из файла

  cgProgram = cgCreateProgramFromFile(cgContext, CG_SOURCE, "CG/Wave.cg", cgVertexProfile, "main", 0);

 

  // Все успешно?

  if (cgProgram == NULL)

  {

    // Определим, что не так

    CGerror Error = cgGetError();

 

    // Вывод сообщения с тем, что произошло.

    MessageBox(NULL, cgGetErrorString(Error), "Error", MB_OK);

    return FALSE; // Дальше нельзя продолжать

  }

 

Теперь пытаемся создать нашу программу из исходного файла. Мы вызываем функцию cgCreateProgramFromFile, которая загрузит и скомпилирует Cg программу из указанного файла. Первый параметр определяет, к какой переменной CGcontext программа будет присоединена. Второй параметр определяет, что код Cg находится в файле, который содержит исходный текст (CG_SOURCE), или файл, который содержит объектный код от заранее компилированной программы (CG_OBJECT). Третий параметр - название файла с программой Cg. Четвертый параметр - последний профиль для заданного типа программы (использовать вершинный профиль для вершинных программ, фрагментный профиль для фрагментных программ). Пятый параметр определяет функцию входа Cg программы. Эта функция может быть названа произвольно, и часто называется по-другому, чем "main". Последний параметр задает добавочные параметры, которые будут переданы к компилятору Cg. Их часто задают как NULL.

 

Если cgCreateProgramFromFile выполняется с ошибкой по какой-то причине, мы получаем последнюю ошибку, вызывая cgGetError. Тогда можно получить строку с ошибкой из CGerror, вызывая cgGetErrorString.

 

Инициализация почти закончена.

 

  // Загрузка программы

  cgGLLoadProgram(cgProgram);

 

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

 

  // Получить дескрипторы для каждого из наших параметров, так как

  // мы будем изменять их в нашем коде

  position  = cgGetNamedParameter(cgProgram, "IN.position");

  color    = cgGetNamedParameter(cgProgram, "IN.color");

  wave    = cgGetNamedParameter(cgProgram, "IN.wave");

  modelViewMatrix  = cgGetNamedParameter(cgProgram, "ModelViewProj");

 

  return TRUE; // Вернуть TRUE (Инициализация успешна)

}

 

На заключительном шаге инициализации надо получить дескрипторы переменных, которыми мы будем управлять из нашей Cg программы. Для каждого CGparameter мы получаем дескриптор соответствующего параметра программы Cg. Если параметр не существует, cgGetNamedParameter возвратит NULL.

 

Если параметры в программе Cg неизвестны, может использовать функции cgGetFirstParameter и cgGetNextParameter, чтобы найти все параметры в заданной CGprogram.

 

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

 

В функции Deinitialize, мы должны очистить нашу Cg программу.

 

  cgDestroyContext(cgContext); // Уничтожить контекст Cg и все программы в нем

 

Надо просто вызывать cgDestroyContext для каждой из переменных CGcontext (их можем иметь несколько, но обычно есть только одина). Можно индивидуально удалять каждую CGprograms, вызывая cgDestoryProgram, однако, вызывая cgDestoryContext, мы удаляем все CGprograms, которые содержались в CGcontext, и затем удаляем сам CGcontext.

 

Теперь мы добавим код к функции Update. Следующий код проверяет, нажат ли пробел и не удерживается. Если пробел нажат и не удерживается, мы меняем значение cg_enable из true в false или из false в true.

 

  if (g_keys->keyDown [' '] && !sp)

  {

    sp=TRUE;

    cg_enable=!cg_enable;

  }

 

Далее проверим, был ли пробел отжат, и если так, то sp (нажат пробел?) равно false.

 

  if (!g_keys->keyDown [' '])

    sp=FALSE;

 

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

 

Заключительная функция, которую мы должны изменить - функция Draw. Мы добавим наш код после glLoadIdentity и до glFlush.

 

  // Позиция камеры для просмотра нашей модели с расстояния

  gluLookAt(0.0f, 25.0f, -45.0f, 0.0f, 0.0f, 0.0f, 0, 1, 0);

 

Сначала, мы хотим сдвинуть нашу точку зрения достаточно далеко от начала координат, для того чтобы увидеть всю модель. Мы перемещаем камеру на 25 единиц вверх, на 45 единиц от экрана, и направляем фокус на начало координат.

 

  // Задать матрицу вида и модели шейдера такой же, как матрица OpenGL

  cgGLSetStateMatrixParameter(modelViewMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);

 

Далее надо присвоить матрице вида и модели шейдера матрицу вида и модели OpenGL. Это необходимо сделать, так как позиция, которая изменяется вершинном шейдере, необходимо преобразовать в новую позицию в гомогенном пространстве отсечения. Для этого надо умножить новую позицию на матрицу вида и модели.

 

  if (cg_enable)

  {

    cgGLEnableProfile(cgVertexProfile); // Разрешить профиль вершинного шейдера

 

    // Связать вершинный шейдер с текущим состоянием

    cgGLBindProgram(cgProgram);

 

Здесь мы должны разрешить вершинный профиль. cgGLEnableProfile разрешает заданный профиль, делая вызовы OpenGL релевантными. cgGLBindProgram связывает программу с текущим состоянием. Это по существу активизирует нашу программу, и приводит к тому, что наша программа будет выполняться на каждой вершине, которая передана в GPU. Эта программа будет выполняться на каждой вершине, пока мы не отключим этот профиль.

 

    // Задать цвет рисования ярко-зеленным (может быть изменен в шейдере, и так далее...)

    cgGLSetParameter4f(color, 0.5f, 1.0f, 0.5f, 1.0f);

  }

 

Следующее, мы задаем цвет модели. Это значение может быть динамически изменено при визуализации модели, например, чтобы создать эффект циклически повторяющих цветов.

 

Обратите внимание на условие cg_enable равно true ? Если это условие не выполняется, то не выполняется ни одна из команд Cg выше. Это препятствует коду Cg  выполняться.

 

Теперь мы готовы визуализировать нашу модель!

 

  // Начать рисование модели

  for (int x = 0; x < SIZE - 1; x++)

  {

    // Рисовать полоску треугольников для каждой колонки нашей модели

    glBegin(GL_TRIANGLE_STRIP);

    for (int z = 0; z < SIZE - 1; z++)

    {

      // Задать параметр волны для нашего шейдера.

      // Значение wave увеличивается в основной программе

      cgGLSetParameter3f(wave, wave_movement, 1.0f, 1.0f);

      glVertex3f(mesh[x][z][0], mesh[x][z][1], mesh[x][z][2]);  // Рисовать вершину

      glVertex3f(mesh[x+1][z][0], mesh[x+1][z][1], mesh[x+1][z][2]);  // Рисовать вершину

      wave_movement += 0.00001f; // Увеличить движение волны

      if (wave_movement > TWO_PI) // Предотвратить крах программы

        wave_movement = 0.0f;

    }

    glEnd();

  }

 

Для визуализации нашей модели, мы просто делаем цикл по оси Z для каждого значения X (по существу мы работаем по столбцам от одной стороны нашей модели до другой). Для каждого столбца, мы начинаем новую полосу треугольников.

 

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

 

Затем мы передаем вершину в GPU и во время обработки ее, автоматически происходит выполнение нашей вершинной программы на этой вершине. Мы медленно увеличиваем переменную wave_movement, чтобы получить медленное и гладкое движение.

 

Если значение wave_movement достигает TWO_PI, то мы сбрасываем ее в 0, чтобы предотвратить крах программы. Константа TWO_PI задана вначале программы.

 

  if (cg_enable)

    cgGLDisableProfile(cgVertexProfile); // Запрет вершинного профиля

 

Как только мы закончили визуализацию, мы смотрим, если cg_enable равно true, то мы отключаем наш вершинный профиль и продолжаем визуализацию, если нам хочется еще что-то нарисовать.

© Оуэн Боурн

PMG  13 апреля 2007 (c)  Сергей Анисимов