ПрограммированиеFAQ

Как новичку понять шейдеры?

Шейдеры — это очень легко и просто, но некоторые почему-то их боятся. Самое важное в процессе понимания шейдеров — понимать, как работает вызов функций Draw без шейдеров и с ними. Для начала попробую объяснить на пальцах рендеринг с Фиксированым Конвеером (без шейдеров).

При вызове функции Draw идёт цикл по каждой вершине, каждая вершина попадает в вершинный фиксированный конвеер.

Сначала каждая вершина из 3-D координат преобразуется в экранные 2-D координаты умножением на матрицу

//
mat=мировая(матрица объекта)*МатВида*МатПроекции; 
Out.pos=mul(In.Position,mat);
//

Затем каждая входящая в конвейер нормаль (если такие имеются) умонжается на мировую (матрицу объекта) матрицу и нормализуется (приводится к единичной длине) для правильного расчёта освещения.

Out.nor=normalize(mul(In.normal,World)); 

В итоге получаем трансформированную нормаль.

Дальше видеокарта считает суммарное освещение (если нормали есть в формате вершин, и установлены источники света). Освещение вершины считается очень просто, буквально за несколько тактов — с помощью скалярного произведения (чем меньше угол между вектором света и вектором нормали вершины, тем темнее. Максимум освещения — 1.0 (полное освещение) минимум — 0.0 (полное затенение)).

Out.lighting=0.0f;
for(i=0;i<CountLights;i++)
  Out.lighting += max(dot3(nor,vecLight[i]),0.0f)*(Light.Diffuse*objMaterial.Diffuse)
                + (Light.Ambient*objMaterial.Ambient);
  //здесь для Dir-light без расчёта отражающего(specular) света

Текстурные координаты подаются на выход обычно без изменения. Хотя можно выходные ТК умножить на матрицы масштабирования (маленький размер для Detail-текстуры, большой — для карты цветов), чтобы не засорять вершинный буфер лишними данными.

 for(i=0;i<OutTexC;i++)
    Out.texC[i]=mul(In.texC[0],texMatrix[i]);

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

Теперь самое интересное — как далее рисуется объект по пикселам.

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

for(i=0;i<CountIndexes;i++)
{
   vertex1=indexeVertex[i];
   vertex2=indexesVertex[i];
   vertex3=indexesVertex[i];
};

Далее самое важное, что люди обычно не понимают. Интерполятор, Пиксельный конвейер.

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

//
outInterpolatedValue=(1-distanse(pixelScreenPos,vertex1.pos))*vertex1.value+(1-distanse(pixelScreenPos,vertex2.pos))
*vertex2.value+(1-distanse(pixelScreenPos,vertex2.pos))*vertex2.value;
//

Так для каждого пикселя генерируются средние значения вершинных данных в данной координате пиксела на экране.
Далее тоже сложно и занятно. Как объект текстурируется, при наличии текстурных координат.
Цвет текселя для данного текстурного слоя по данным текс. координатам находится при помощи текстурной фильтрации.
//
При точечной фильтрации из текстуры для пикселя берётся ближайший цвет к координатам(поэтому она такая и резкая).
При три/биллинейной фильтрации цвет пикселя интерполируется между тремя ближайшими пикселами. Трилинейной фильтрацией интерполируется чётче, засчёт более сложного алгоритма.
При анизотропной фильтрации(в сочетании с би/трилинейной) интерполируются сразу несколько текселей(2,4,6,8,16), засчёт чего мы можем получить более чёткую картинку  при слишком "сжатых" текстурных координатах (обычно у полигонов слишком круто повёрнутых к камере).

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

function tex2D(Sampler tex,vector2 Tcoord)
{
color1=TextureBuffer[int(Tcoord.x*tex.Width)][int(Tcoord.y*tex.Height)];
color2=TextureBuffer[int(Tcoord.x*tex.Width)+1][int(Tcoord.y*tex.Height)];
color3=TextureBuffer[int(Tcoord.x*tex.Width)+okrug(Tcoord.x*tex.Width-tex.Width)][int(Tcoord.y*tex.Height)+
+okrug(Tcoord.y*tex.Height-tex.Height)];

return pixelOut=interpolateBiLine(color1,color2,color3,Tcoord);
};

при наличии мультитекстурирования вызовов "interpolate" будет столько, сколько текстурных слоёв(самплеров).
А смешиваться они будут так, как вы укажете перед рендером.
Допустим для  2-х самплеров и цвета вершины со смешиванием MULTPLE-(вершина и самплер1)=результат1, и MULTIPLE2X-(результат1 и самплер2) это будет выглядеть так.

return PixelColorOut = colorInterpolated*tex2D(sampler1,texC0)*tex2D(sampler2,texC1)*2;

И вот так видеокарта, сделав цикл по всем полигонам и пикселам, рисует объект на экран.

Я конечно описал всё очень схематично и примитивно (кстати, при рендере все функции ассемблероподобны=) ), и без описания блока пиксельных тестов (z-test - для того чтобы не рисовать дальние пикселы поверх ближних, stensil-test, alpha-test - чтобы не рисовать почти прозрачные пикселы , alpha-blending- эфекты полупрозрачности, fog-blend - туман), но крайне важно понять хотябы схему работы рендеринга

1. для оценки производительности приложения(из-за чего тормозит, а из-за чего нет, что почему, как работает)
2. конечно же для понимания замены фиксированного вершинного и пиксельного конвеера соответственно вершинными и пиксельными шейдерами
(хотя можно и третий пункт приписать - чтобы не думать что видеокарта это магическая штуковина, которая преобразовывает циферки в реальность =) ).

Справка. Шейдеры — это програмы, которые выполняются соответственно для каждой вершины (VShader) и выводимого пиксела (PShader) объекта.

Вот полная схема вызова рендера.

Изображение


Как видно из картинки, вершинный шейдер заменяет вершинный конвеер, то есть:

ВШ заменяет
1. Расчёт экранной позиции вершины.
2. Расчёт освещения.
3. Расчёт передаваемых в пиксельный шейдер текстурных координат.

Пиксельный шейдер заменяет фиксированое смешивание текстурных слоёв и цветов вершин.

Основные "непонимания" при начале работы с шейдерами такковы:

1. Зачем нужны вершинные шейдеры, если фиксированный вершинный конвеер прекрасно справляется с повершинным освещением, расчётом позиции и ТК?
2. Зачем нужны вершинные шейдеры, если изменения позиции, цвета, можно вручную сделать Lock/UnLock VertexBuffer.

Ответы:
а) Основное предназначение вершинного шейдера - это не расчёт позиции, цвета, освещения, а подготовка данных для пиксельного шейдера, т.е. передача своих данных в линейный интерполятор. Например вершинный шейдер может
легко передать в интерполятор нормаль. Получится что каждый пиксел будет иметь свою нормаль, и пиксельный шейдер влёгкую организует perpixel-освещение, освещение по Фонгу, bump-mapping, ... Фииксированный вершинный конвеер так не умеет.
б) "Lock/UnLock VertexBuffer" каждый раз, вызывая эти методы вы гоняете по шине весь вершинный буффер, что достаточно замедляет программу.

Непонимание пиксельного шейдера в основном исходят из непонимания рендеринга. Некоторые думают, что PS проходит не по каждому пикселю выводимого объекта, а по каждому текселю текстуры(тогда бы производительность программы напрямую бы зависела от скаляции и разрешения текстуры), отсюда полное непонимание что происходит :( (и подобные непонятки).

Вручную интерполяцию и фильтрацию текстуры считать не придётся в PShader, так как во входные данные подаются уже интерполированные текстурные координаты, нормали, цвета; а получение цвета текстуры(то что я называл фильтрацией) обеспечивают внудренние текстурные функции пикс. шейдера. В PS остаётся только смешать всё как вам надо.

В вершинный шейдер же подаётся то что находится в вершинном буфере для данной вершины - для FFP стандатно 32 байта (позиция вершины, нормаль вершины, текстурные координаты[0]).

Также в шейдер могут передаваться шейдерные константы - наборы чисел которые не меняются для шейдера во время отрисовки одного объекта(вызова Draw). Обычно это матрицы преобразований(WorldViewProject), направление и свойства света, и материалы.

Основная доля графических эфектов делается в пиксельном шейдере. Можно например из текстуры сделать карту нормалей и поставить на второй текстурный слой, затем прочитать эту нормаль из текстуры при рендере. У нас получится что к каждой точке(пикселу) поверхности объекта своя нормаль, уже не интерполированная - эфект микро-неровностей и рельефа.

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

#HLSL, #шейдеры

23 августа 2007 (Обновление: 11 мар 2024)

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