Шейдеры это очень легко, порсто все их почемуто боятся.
Самое важное в процессе понимания шейдеров - понимать как работает вызов ф-и 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 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 Out.texC[i]=mul(In.texC[0],texMatrix[i]); |
Всё, теперь вершины готовы для дальнейшей отрисовки. Даже сейчас,
если провести цикл по каждому полигону, и отрисовать от каждой вершины
в полигоне до каждой вершины(3 линии), то мы получим сетку(wireframe)
объекта(правда однотонную).
Теперь самое интересное - как далее рисуется объект по пикселам.
По каждому полигону(3-м индексам) видеокарта проходит циклом
(если брать упрощёную схему без отсечения невидимых граней, и сборки
примитива).
for(i=0;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. очень важно понимать процесс рендеринга, хотябы схематически. Во всех начинаниях геймдева это очень поможет.
http://www.gamedev.ru/faq/?id=142
Внимание! При цитировании ссылка на страницу с материалом обязательна!