Шейдеры — это очень легко и просто, но некоторые почему-то их боятся. Самое важное в процессе понимания шейдеров — понимать, как работает вызов функций Draw без шейдеров и с ними. Для начала попробую объяснить на пальцах рендеринг с Фиксированым Конвеером (без шейдеров).
При вызове функции Draw идёт цикл по каждой вершине, каждая вершина попадает в вершинный фиксированный конвеер.
Сначала каждая вершина из 3-D координат преобразуется в экранные 2-D координаты умножением на матрицу
Затем каждая входящая в конвейер нормаль (если такие имеются) умонжается на мировую (матрицу объекта) матрицу и нормализуется (приводится к единичной длине) для правильного расчёта освещения.
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-текстуры, большой — для карты цветов), чтобы не засорять вершинный буфер лишними данными.
Всё, теперь вершины готовы для дальнейшей отрисовки. Даже сейчас, если провести цикл по каждому полигону, и отрисовать от каждой вершины в полигоне до каждой вершины(3 линии), то мы получим сетку(wireframe) объекта(правда однотонную).
Теперь самое интересное — как далее рисуется объект по пикселам.
По каждому полигону (3-м индексам) видеокарта проходит циклом (если брать упрощёную схему без отсечения невидимых граней, и сборки примитива).
Далее самое важное, что люди обычно не понимают. Интерполятор, Пиксельный конвейер.
Видеокарта проходит циклом по всем пикселям, принадлежащим выводимому полигону. (получается чем объект дальше - тем меньше выводимых пикселов, тем меньше работы у пиксельного конвеера). К каждому пикселю видеокарта интерполирует (т.е линейно усредняяет по 3 значениям из вершин) т.е. данные, которые дал вершинный конвеер (это цвет и/или ТексКоорд). Делает он это примерно так(не совсем конечно, но похоже).
Так для каждого пикселя генерируются средние значения вершинных данных в данной координате пиксела на экране.
Далее тоже сложно и занятно. Как объект текстурируется, при наличии текстурных координат.
Цвет текселя для данного текстурного слоя по данным текс. координатам находится при помощи текстурной фильтрации.
//
При точечной фильтрации из текстуры для пикселя берётся ближайший цвет к координатам(поэтому она такая и резкая).
При три/биллинейной фильтрации цвет пикселя интерполируется между тремя ближайшими пикселами. Трилинейной фильтрацией интерполируется чётче, засчёт более сложного алгоритма.
При анизотропной фильтрации(в сочетании с би/трилинейной) интерполируются сразу несколько текселей(2,4,6,8,16), засчёт чего мы можем получить более чёткую картинку при слишком "сжатых" текстурных координатах (обычно у полигонов слишком круто повёрнутых к камере).
Для билинейной фильтрации цвет пикселя из текстуры по текстурным координатам будет получаться примерно так:
при наличии мультитекстурирования вызовов "interpolate" будет столько, сколько текстурных слоёв(самплеров).
А смешиваться они будут так, как вы укажете перед рендером.
Допустим для 2-х самплеров и цвета вершины со смешиванием MULTPLE-(вершина и самплер1)=результат1, и MULTIPLE2X-(результат1 и самплер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. очень важно понимать процесс рендеринга, хотябы схематически. Во всех начинаниях геймдева это очень поможет.