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

Реализация динамики флага с использованием Cg.

Автор:

В начале 2003 года сайт www.ixbt.com объявил конкурс по шейдерам, написанным на новоиспеченном языке Cg. Так как призы были очень заманчивыми, а опыт в программировании шейдеров я уже кое-какой имел, то грешно было не принять участия.

Итак, мой замысел заключался в изображении флага, который колышется на ветру. Кроме того, флаг должен освещаться точечным источником, движущимся по кругу (рис.1).

Таким образом, задача автоматически разделилась на две:

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

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

Вот полный текст вершинного шейдера:

struct appdata
{
  float4 position : POSITION;
  float4 tex0 : TEXCOORD0;
  float4 light_pos : COLOR0;
};

struct vfconn
{
  float4 HPOS : POSITION;
  float4 TEX0 : TEXCOORD0;
  float4 TEX1 : TEXCOORD1;
  float4 TEX2 : TEXCOORD2;
  float4 COL0 : COLOR0;
};

vfconn main(appdata IN,
    uniform float4x4 ModelViewProj,
    uniform float time,
    uniform float amplitude)
{
  vfconn OUT;

  float4 tempPOS = IN.position;
  
  tempPOS.z=sin(IN.tex0.x*20+time)/100*amplitude*IN.tex0.x;  
  
  OUT.HPOS = mul(ModelViewProj, tempPOS);
  
  OUT.TEX0 = IN.tex0;
  OUT.TEX1 = IN.tex0;
  OUT.TEX2 = IN.tex0;
      
  OUT.COL0 = IN.light_pos;
  return OUT;
} // main

Структура appdata отвечает за входные данные в шейдер. Рассмотрим ее подробнее. C помощью переменной position мы передаём позицию вершины в мировых координатах, tex0 - координаты UV текстуры. А вот на переменную light_pos следует обратить особое внимание! Одним из достоинств Cg, является то, что в шейдер можно передавать данные совместимого типа через регистры, которые имеют совсем другое назначение. Как и в случае использования light_pos. На первый взгляд, переменная light_pos используется для передачи цвета. Однако, на самом деле мы ее используем для передачи позиции источника света. Это очень удачный финт, на который следует обратить внимание и при дальнейшем использовании.

Затем, в программе следует описание структуры vfconn. Она отвечает за передачу данных уже непосредственно точкам внутри полигона.

С позицией и цветом, вроде бы, все понятно (помним, что с помощью цвета мы передаём позицию источника света). А вот для текстурных координат пришлось выделить ещё 2 доп.регистра. Зачем это потребовалось я объясню чуть позже. А сейчас, перейдем непосредственно к функциии main().

Как видите, в нее передается 3 переменных:
1. Матрица, которая представляет собой произведение видовой и проекционной матриц.
2. Время, которое прошло от начала работы программы. С помощью этой переменной мы заставим флаг колыхаться.
3. Максимальная амплитуда колыхания флага.

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

Следующие 3 строчки отвечают за расчет позиции вершины в пространстве. Как видим, координаты Х и Y остаются неизменными, а Z координата изменяется по закону синуса, что и создает эффект колыхания. Помните общий вид колебательного процесса? Если нет, то я напомню: Y=A*sin(B*X+T), где A - амплитуда, B - период, T - сдвиг по фазе (ну, эту-то фразу вы должны знать :-)).

Вот таким образом вычисляется координата Z флага. Единственное, что отличает движение моего флага от движения по синусоиде, так это уменьшение амплитуды колыхания флага возле флагштока. Этот эффект достигается путём умножения величины амплитуды на X-координату текстуры. Остается умножить полученные координаты на матрицу трансформации.

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

void main(float4 in tex_coord0 : TEXCOORD0,
    float4 in tex_coord1 : TEXCOORD1,
    float4 in tex_coord2 : TEXCOORD2,
    float4 in light_pos : COLOR0,
    float4 out color : COLOR,
    uniform sampler2D tex1,
    uniform sampler2D tex2)
{
  float4 TexColor1 = tex2D(tex1, tex_coord0), TexColor2 = tex2D(tex2, tex_coord1),a1;

  a1=(tex_coord2-light_pos)*float4(1.0,1.0,0.0,0.0);

  float c=4*dot(a1,a1) + light_pos.z*light_pos.z;

  color= TexColor2.x*TexColor2.y*TexColor2.z<0.001 ? 1 : (1-c)*TexColor1;
}

Первое, что может броситься в глаза - это отсутствие описания структур ввода и вывода данных. Дело в том, что для ввода/вывода мы будем использовать несколько другой способ, нежели тот, что использовался в вершинном шейдере. Но, обо всем по порядку.

В первых строчках мы объявляем саму функцию и переменные. Если в описании переменной стоит in, это значит, что в этой переменной мы передаём данные шейдеру, если out - возвращаем из него. Пиксельный шейдер может возвращать только цвет (COLOR) или глубину (DEPTH). В данном случае шейдер возвращает цвет. Переменные tex1 и tex2 содержат указатели на плоские текстуры. tex1 - основная текстура (рис.1), а tex2 - текстура с отмеченными "точками свечения" карты (рис.2).

Изображение
Рис 1.
Изображение
Рис 2.

Переходим к телу функции. В первой строчке я объявляю 3 переменные: TexColor1, TexColor2 и a1. В TexColor1 и TexColor2 мы сразу заносим цвет соответствующим точкам текстур. Это делается с помощью функции tex2D(). У нее есть одна особенность - после ее выполнения, переменная, в которой находились текстурные координаты, "портится", то есть не несет тех значений, которые она несла до выполнения tex*() (осторожно, не попадитесь на эту удочку в будущем!) Поэтому мы и размножили текстурные координаты ещё в вершинном шейдере.

В следующей строчке мы находим вектор от источника света до точки на полигоне (с помощью векторной разности) и выделяем из полученного результата x и y составляющие.

Далее, в переменную c мы записываем нечто похожее :-) на расстояние от источника света до точки. Оно находится довольно просто. Если кто-то забыл, то напоминаю, что скалярное произведение вектора самого на себя даёт квадрат его длины. Запись dot(a1,a1) говорит сама за себя (кстати, использовать такой метод мне посоветовал сам IronPeter, победитель этого конкурса, за что ему отдельная благодарность). Правда, я ещё умножил полученное значение на 4, дабы увеличить площадь освещаемого участка. А для того, чтобы размер освещаемой площади зависел от расстояния от источника света до флага, я прибавил к расстоянию z-координату позиции источника света (тоже в квадрате, дабы соблюдать размерность).

И, наконец, пришло время непосредственного расчета цвета точки флага. Здесь нельзя забыть про фишку, которую мы запланировали ещё в самом начале, а именно: необходимо заставить светиться определенные участки на флаге, которые отмечены на отдельной текстуре (к счастью, в пиксельных шейдерах можно создавать несложные условия, по типу C++'ого  оператора " ? : "). В условии мы проверяем наличие черного цвета на второй текстуре. Если цвет - черный (произведение rgb компонентов сравнимо с 0), то шейдер возвращает белый цвет (RGB==1). Если это не так, то возвращается цвет текстуры, умноженный на значение расстояния ("1-с" означает, что, чем больше расстояние, тем темнее, ближе к 0).

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

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

#Cg

9 марта 2003 (Обновление: 15 июня 2009)

Комментарии [2]