Войти
ПрограммированиеСтатьиГрафика

Cg

Автор:

Современная компьютерная графика вступает в новую эру. На лозунгах, которые зовут нас вперед, написано: "вперед к программируемости!". OpenGL 2.0 выглядит великолепным миражом на горизонте. Этот интерфейс  предоставляет возможность компиляции с языка высокого уровня на стороне графического сервера. Однако, полная реализация этого интерфейса кажется пока несбыточной, и нам лишь предстоит мечтать о светлом будущем.

Сейчас же мы вынуждены  работать с legacy (англ. "наследие") интерфейсами, очень ограничительными по своей природе и зачастую несовместимыми.  Это вершинные и пиксельные (правильнее - фрагментные) шейдеры,  представленные в OpenGL целой линейкой.

Наиболее значимые расширения для обработки вершин:

0. GL_ARB_vertex_program  Расширение, одобренное ARB. Работает на достаточно широкой линейке видеокарт. Предпочтительно в использовании.

1. GL_NV_vertex_program  NV20 базовая функциональность.

2. GL_NV_vertex_program2  NV30 базовая функциональность,  описание пока не доступно в реестре OpenGL.

3. GL_EXT_vertex_shader  Расширение, более известное под названием GL_ATI_vertex_shader. Работает на чипах ATI, начиная с R200 (и, видимо, еще где-то, но где, я не знаю).

Расширения 0-1-2 базируются на asm подобных языках (если забыть про маргинальное расширение под номером 3). Программирование превращается в настоящий кошмар - обычно после написания пяти строк совершенно забываешь, что делает предыдущий код, что в каких динамических регистрах лежит, и куда, черт побери, мы поместили глобальные константы.

Необходимо учитывать  то, что драйверы от разных производителей "железа" поддерживают разные расширения.  Любой же достаточно серьезный проект должен поддерживать широкий спектр  оборудования.  Упомянутые расширения различаются по синтаксису (большей частью в мелочах, но и не только), так что картинка в части обработки вершин складывается достаточно безрадостная. Если вы думаете, что в части обработки фрагментов (пикселов) дела обстоят радужнее, вы крупно ошибаетесь. Там царит форменный разброд. Доступны расширения:

0. GL_ARB_fragment_program  Расширение, одобренное ARB. Весьма мощное. Поддерживается чипами ATI R300 и NV30.

1. GL_NV_texture_shader (с вариациями GL_NV_texture_shader2 и GL_NV_texture_shader3)  + GL_NV_register_combiners (с вариацией GL_NV_register_combiners2) В сумме NV20 базовая функциональность.

2. GL_NV_fragment_program    NV30 базовая функциональность,  описание пока не доступно в реестре OpenGL.

3. GL_ATI_fragment_shader Расширение от ATI. Работает на R200.

Указанные расширения и вовсе не содержат ничего общего в плане синтаксиса.

Однако, весьма пространное введение я посвятил оплакиванию судьбы программиста трехмерной графики. Теперь должна прозвучать некая конструктивная нота и мажорный аккорд. Вместе с объявлением собственно темы этой заметки. Которая называется:

" Язык Cg от NVIDIA"

Использование языка высокого уровня  (Cg) для написания шейдеров преследует следующие цели:

1.)  Упрощение процесса разработки за счет  использования прозрачного, простого и мощного языка, сходного с C, RenderMan или шейдерным языком OpenGL 2.0.

2.)  Унификация процесса разработки. "Написали однажды, исполняем всюду". Транслятор способен интерпретировать Cg программы с помощью разнообразных "профилей" (profiles в терминологии Cg).  На настоящий момент Cg поддерживает все три базовых расширения для обработки вершин, упомянутые выше. И три расширения обработки фрагментов.  В этом пункте надо выделить один момент. Это фактор кросс-платформенности. Если вы захотите мигрировать с DX на OpenGL или обратно (не знаю, правда, зачем это вам надо), то в вашем распоряжении и DX профили.

Вам уже захотелось попробовать свои силы в программировании на Cg?

Тогда вперед. Через тернии к звездам! Вам потребуется компилятор MS VC 6.0 и последняя версия пакета Cg ToolKit  с сайта NVIDIA, включающая подробную документацию, библиотеки и h-файлы, а также множество примеров.

Статья будет посвящена разбору одного небольшого примера, поскольку я сторонник концепции, что лучше 1 раз увидеть, чем 100 раз услышать. Пример весьма несложен, но, тем не менее,  достаточно показателен.  Исходники доступны: (20021117.zip - 366 кБ), собранное приложение должно работать на ускорителях от NV не младше gf3. Запустив его, вы увидите текстурированный бублик и сможете повращать его мышкой.

Несколько слов о выбранной схеме текстурирования. Используются 3 уникальные текстуры. Первая проектируется в XY плоскости, вторая проектируется в YZ плоскости, третья - в ZX плоскости. Текстуры смешиваются с весами n.z*n.z, n.x*n.x, n.y*n.y соответственно, где n- нормаль. Такой подход позволяет избежать появления областей, где текстура визуально "растянута". Текстурируемая поверхность выглядит очень однородно и привлекательно.

Работа приложения управляется двумя Cg программами, которые сохранены в отдельные файлы и компилируются в момент запуска приложения.

Вершинная программа:

void main(
float4 in  inpos      : POSITION, //in  координата вершины
float3 in  innor      : NORMAL,   //in  нормаль   
float4 out outpos     : POSITION, //out координата 
float3 out outcol     : COLOR,    //out цвет
float2 out outtx0     : TEXCOORD0,//out текстурная координата №0
float2 out outtx1     : TEXCOORD1,//out текстурная координата №1
float2 out outtx2     : TEXCOORD2,//out текстурная координата №2
uniform float4x4 ModelViewProj //матрица полного преобразования
)
{
  outpos = mul(ModelViewProj, inpos);//производим преобразования координаты
  outtx0 =inpos.xy*0.1f;//проекция на XY плоскость
  outtx1 =inpos.yz*0.1f;//проекция на YZ плоскость
  outtx2 =inpos.zx*0.1f;//проекция на ZX плоскость
  outcol =innor*innor;//color=(n.x*n.x,n.y*n.y,n.z*n.z) 
}

Отметим следующие моменты. Язык Cg скрипта очень похож на C. Он имеет встроенные скалярные (float) и векторные (float2,float3,float4) типы данных. Наличествуют очень удобные возможности маскирования и перестановки. Так, inpos.yz эквивалентно float2(inpos.y, inpos.z). Доступны арифметические операции. Имеется встроенная математическая библиотека (в примере представленная командой матричного умножения mul). Собственно, типов и операций данных много больше. Перечисляя, боюсь что-то упустить - смотрите документацию.

Программа main имеет входные параметры, которые подаются для каждой вершины с помощью обычных средств OpenGL. Из примера видно, как осуществляется привязка таких параметров.

Имеются выходные параметры, которые подаются на блок установки треугольников и растеризации. Кроме того, имеется один матричный параметр, который является статическим, на что указывает ключевое слово uniform. Это матрица, которая позже будет привязана к OpenGL матрице GL_MODELVIEW*GL_PROJECTION. Можно также определять uniform векторные параметры.

Язык Cg теоретически очень богат и содержит в частности мощные средства для управления исполнением (возможность организовывать циклы и подпрограммы). На практике возможности скрипта ограничены профилем, под который тот компилируется. Вполне возможна ситуация, что сложный скрипт не будет исполняться под малофункциональным профилем.

Фрагментная программа:

void main(
float3 in incol: COLOR,     //in цвет фрагмента. 
float2 in intx0: TEXCOORD0, //in текстурная координата №0 
float2 in intx1: TEXCOORD1, //in текстурная координата №1 
float2 in intx2: TEXCOORD2, //in текстурная координата №2
float3 out outcol:COLOR,    //out цвет. результат
uniform sampler2D tex0,     //текстура  №0
uniform sampler2D tex1,     //текстура  №1
uniform sampler2D tex2      //текстура  №2
)
{ 
  float3 r1,r2,r3,r4;         //переменные
  r1=tex2D(tex0,intx0);       //первое чтение из текстуры
  r2=tex2D(tex1,intx1);
  r3=tex2D(tex2,intx2);
  r4=incol.b*r1+
    incol.r*r2+
    incol.g*r3;
  outcol=r4;                  //записываем результат
}

Статическими переменными являются текстуры. Точнее, текстурные сэмплеры.  Результирующий цвет фрагмента определяется переменной outcol.

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

Разберем способ компиляции Cg скриптов. Во-первых,  в *.cpp файле определяются следующие переменные.

1. Контекст Cg

static CGcontext Context = NULL;

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

static CGprogram ProgramVP = NULL;

3. Uniform параметр вершинной программы, соответствует uniform float4x4 ModelViewProj, определенной выше.

static CGparameter ModelViewProjParam = NULL;

4. Идентификатор фрагментной программы. Можно загрузить много фрагментных программ и переключаться между ними по мере надобности.

static CGprogram ProgramFP = NULL;

5. Идентификаторы текстурных сэмплеров: uniform sampler2D tex0,tex1,tex2

static CGparameter tex0    = NULL;

static CGparameter tex1    = NULL;
static CGparameter tex2    = NULL;

6. Имена вершинного и фрагментного профилей. Если изменить CG_PROFILE_FP20 на CG_PROFILE_ ARBFP1, то приложение запустится на видеоускорителях ATI R300.

static CGprofile ProfileVP = CG_PROFILE_ARBVP1;

static CGprofile ProfileFP = CG_PROFILE_FP20;

Далее, самая важная функция. "Какая функция самая важная?" - удивленно спросите вы. Та, которая сообщает нам об ошибках, конечно! И не просто сообщает: в случае ошибки она вызывает аварийное завершение работы приложения.

void cgErrorCallback(void)
{
  CGerror LastError = cgGetError();
  if(LastError)
  {
    printf("%s\n\n", cgGetErrorString(LastError));
    printf("Cg error, exiting...\n");
    exit(0);
  }
}

Предварительно мы устанавливаем эту функцию как обработчик ошибок (очень суровый, надо признать):

cgSetErrorCallback(cgErrorCallback);

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

Context = cgCreateContext();
  ProgramVP = cgCreateProgramFromFile(Context,
                                    CG_SOURCE, 
                                    "..//CG//vp.cg",
                                    ProfileVP,
                                    NULL, NULL);
  
  fprintf(stderr, "LAST LISTING----%s----\n", cgGetLastListing(Context));
  fprintf(stderr, "---- PROGRAM BEGIN ----\n%s---- PROGRAM END ----\n",
  cgGetProgramString(ProgramVP, CG_COMPILED_PROGRAM));
  
  cgGLLoadProgram(ProgramVP);
  ModelViewProjParam = cgGetNamedParameter(ProgramVP, "ModelViewProj");
  
  ProgramFP = cgCreateProgramFromFile(Context,
                                    CG_SOURCE, 
                                    "..//CG//fp.cg",
                                    ProfileFP,
                                    NULL, NULL);
  
  fprintf(stderr, "LAST LISTING----%s----\n", cgGetLastListing(Context));
  fprintf(stderr, "---- PROGRAM BEGIN ----\n%s---- PROGRAM END ----\n",
  cgGetProgramString(ProgramFP, CG_COMPILED_PROGRAM));
  cgGLLoadProgram(ProgramFP);

  GLuint handle;
  tex0=cgGetNamedParameter(ProgramFP,"tex0");
  handle=loadjpg("..//TEX//texture0.jpg");
  cgGLSetTextureParameter(tex0,handle);

  tex1=cgGetNamedParameter(ProgramFP,"tex1");
  handle=loadjpg("..//TEX//texture1.jpg");
  cgGLSetTextureParameter(tex1,handle);

  tex2=cgGetNamedParameter(ProgramFP,"tex2");
  handle=loadjpg("..//TEX//texture2.jpg");
  cgGLSetTextureParameter(tex2,handle);

Ничего сложного, все действия разумны и вряд ли нуждаются в обосновании. Необходимо создать контекст. Загрузить из файла Cg программу. Смотрим листинг и откомпилированный код в консоли (это, впрочем, не обязательно). Загружаем программу. Привязываем к переменной ModelViewProj интерфейс ModelViewProjParam. Uniform параметры скриптов можно получить с помощью функции cgGetNamedParameter.  Единственная нетривиальная часть здесь - это привязка текстур. Функция loadjpg загружает текстуру из файла и возвращает OpenGL идентификатор текстуры. Потом он привязывается к текстурному сэмплеру из фрагментной программы. И так для всех трех текстур (сам код загрузки jpg файлов написан автором на основе библиотеки IJL - может, кому понадобится).

Теперь собственно рендер.

cgGLEnableProfile(ProfileVP);
  cgGLEnableProfile(ProfileFP);    
  cgGLEnableTextureParameter(tex0);
  cgGLEnableTextureParameter(tex1);
            cgGLEnableTextureParameter(tex2);

  cgGLBindProgram(ProgramVP);
  cgGLBindProgram(ProgramFP);
   
  cgGLSetStateMatrixParameter(ModelViewProjParam,
                                CG_GL_MODELVIEW_PROJECTION_MATRIX,
                                CG_GL_MATRIX_IDENTITY);

  glutSolidTorus(6,10,64,64);

  cgGLDisableProfile(ProfileVP);
  cgGLDisableProfile(ProfileFP);
  cgGLDisableTextureParameter(tex0);
  cgGLDisableTextureParameter(tex1);
  cgGLDisableTextureParameter(tex2);

Опять ничего сложного. Включаем поддержку профилей. Привязываем программы. Привязываем их uniform параметры. Разрешаем текстурные сэмплеры и назначаем матрицу трансформации. Рисуем тор с помощью библиотеки  glut. Можно использовать обычные способы подачи геометрии - glBegin/glEnd, массивы, списки. Но в реальных условиях предпочтительнее использование специализованных расширений типа GL_NV_vertex_array_range  и GL_ATI_vertex_array_object. После мы выключаем все, что было включено ранее. Вроде как подчищаем. Хотя настоятельной нужды делать сие в нашем случае нет.

Voila! На экране крутится текстурированный тор. Вот собственно и все. Можно было бы рассказывать более подробно о языке Cg, о дополнительных возможностях интерфейса, но я скромно остановлюсь на достигнутом.

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

PS. Желаю вам успехов на конкурсе http://contest.ixbt.com ! Время поджимает - быстрее :)!

Полезные ссылки:
http://developer.nvidia.com/view.asp?PAGE=cg_main
http://www.cgshaders.org/

#Cg, #OpenGL

19 февраля 2002 (Обновление: 15 июня 2009)