Модераторы: Rickert
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Создание шейдеров 
:(
    Опции темы
mr.DUDA
  Дата 7.12.2005, 14:56 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


3D-маньяк
****


Профиль
Группа: Экс. модератор
Сообщений: 8244
Регистрация: 27.7.2003
Где: город-герой Минск

Репутация: 7
Всего: 232




M
arilou
Обсуждение статьи тут http://forum.vingrad.ru/topic-118432.html


 Описание

Данная статья охватывает основные понятия, относящиеся к шейдерам вообще, и к HLSL-шейдерам в частности. Доступным языком изложены теоретические основы и практические шаги по созданию вертексных и пиксельных шейдеров. Для практических примеров используется средство разработки шейдеров ATI RenderMonkey.

Содержание

1. Теория шейдеров
1.1 Что такое шейдеры ?
1.2 Они бывают вертексные и пиксельные
1.3 Что можно и чего нельзя сделать на шейдерах
2. Как пишутся шейдеры
2.1 Языки программирования шейдеров
2.2 Структура шейдерной программы
2.3 Ok, что я могу сделать в шейдере, и как ?
2.4 Средства разработки
2.5 Использование шейдеров в 3D-приложении
3. От слов к делу
3.1 Простейший шейдер: анимация текстуры
3.2 Мультитекстурирование: детальные текстуры и металл в камне
3.3 Отражения и вода New!
3.4 Bump mapping
3.5 Ландшафт: горы в снегу, зелёные долины и пустыня
3.6 Пост-фильтры: жаркий летний день
3.7 Пост-фильтры: motion blur


P.S.
Самые нетерпеливые могут начать сразу с пункта 3.1, а затем уже по необходимости читать 2.1-2.5

----

Ссылки на используемые источники


[1] "Shaders for game programmers and artists", Sebastien St-Laurent, ISBN1592004644, 2004

[2] "Microsoft DirectX 9 Programmable pipeline", Kris Gray, MS Press 2003

[3] http://www.dtf.ru

[4] http://www.nvidia.com

[5] http://www.ati.com

----

1. Теория шейдеров

"Не все шейдеры одинаково полезны..." (с) неизв. автор

Для того, чтобы что-то начать делать самостоятельно, необходимо понимать, о чём вообще идёт речь. Слово "шейдер" для многих новичков (и не только новичков) обретает некий магический смысл, становится шаманским заклинанием, открывающим какие-то невероятные по красоте  графические эффекты, непонятным по сути и оттого пугающим. К примеру, как сделана вода в Half-Life или тени в Думе? Чу-у-у, да это же высший пилотаж, куда нам до Кармаков с Гейбами, скажут некоторые. Реальность же такова: всё до безобразия просто. Ну, может быть, чуточку придётся поднапрячься, вспомнить основы алгебры и тригонометрии; ничто в этом мире не даётся задаром. Предположим, что я вас этим не испугал, и вы продолжаете читать статью. Ok. Итак...


1.1 Что такое шейдеры ?

Попробую кратко сформулировать ответ на вопрос, вынесенный в заголовок: шейдер - это программа, исполняющаяся на графическом процессоре видеокарты (GPU). Допустим, что теперь все это знают, и никто не скажет, что шейдер компилируется в EXE или DLL и работает как обычная программа на CPU, или что шейдер - это какая-то хитрая текстура. Шейдер - это программа, выполняющаяся на GPU, и обрабатывающая поток данных, а это значит, что у шейдерной программы есть "вход" и есть "выход", а "посередине" - набор команд, выполняющих обработку. Под "потоком данных" подразумевается информация о координатах вершин полигонов, освещении каждой вершины, нормалях, текстурных координатах, и т.п, непрерывно поступающая блоками в шейдерную программу. Алгоритм работы шейдерной программы можно представить следующим образом:

1) принять со входа блок информации в формате А
2) обработать его по некоему алгоритму
3) выдать на выход блок информации в формате Б
4) далее программа бездействует до следующего прихода блока информации на вход

Это выглядит довольно абстрактно, но именно так всё и работает как для пиксельного, так и для вертексного шейдера.

Любой шейдер "видит деревья, но не видит леса", потому что блоки информации поступают казалось бы в произвольном порядке, безо всякой видимой последовательности. На самом деле, ничего в этом страшного нет, но это нужно помнить, потому что из этого вытекает одно важное следствие: у шейдерной программы нет и быть не может никаких статических или общих переменных; нет ничего похожего на файлы или хранилища информации между вызовами. Во всём остальном шейдерная программа похожа на обыкновенную программу на языке С++: есть входные, выходные и временные переменные, стандартные функции, структуры, массивы, циклы и условные операторы. Она оформляется в виде небольшой функции main(), принимающей и возвращающей значения типа "структура". Как вариант, она может быть оформлена как функция без возвращаемого значения и без структур, тогда все входные и выходные аргументы данные становятся аргументами функции main (выходные аргументы - с модификатором out).

Развивая тему, рассмотрим понятия "вертексный" и "пиксельный шейдер" по отдельности.


1.2 Они бывают вертексные и пиксельные

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

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

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

2) Вертексный шейдер получает на вход информацию об одной вершине одного полигона, обрабатывает её, и выдаёт на выход. Какая именно это информация - зависит от формата вершинных данных (мы настраиваем его в своём приложении), но обязательным является выдача на выход координат вершины (x,y,z). Забегая вперёд, скажу, что вертексный шейдер преобразует координаты из видовых в проекционные.

3) Видеопроцессор получает выходную информацию для всех трёх вершин полигона. Теперь, имея данные о трёх вершинах, GPU проходит по всем пикселам одного полигона, плавно изменяя (интерполируя) значения вершинных данных от одной вершины к другой. Например, если вертексный шейдер вычисляет освещённость каждой вершины полигона, и оказывается, что для вершины А она составляет 1.0, а для вершины Б - 0.0, то для точки ровно посередине между А и Б освещённость будет 0.5. То же самое относится к остальным данным о вершине: координаты (x,y,z), нормаль (nx,ny,nz), текстурные координаты (u,v) и др. Для каждого пиксела, перечисляемого в этом цикле, вызывается программа пиксельного шейдера.

4) Пиксельный шейдер принимает на вход интерполированную версию выходных данных вертексного шейдера (т.е. освещённость, позиция, нормаль и пр. будет соответствовать уже не вершине, а конкретному пикселу полигона), и на основании этих данных вычисляет цвет пиксела. Цвет пиксела является выходными данными пиксельного шейдера, больше никаких выходных данных нет. Именно это значение цвета без какой-либо дополнительной обработки выводится на экран. Это очень важно понимать, так как никакого дополнительного освещения, затенения, затуманивания и смешивания над пикселом производиться нигде не будет, кроме как в пиксельном шейдере. Наш "умный" шейдер обязан все эти операции и вычисления провести самостоятельно.

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


1.3 Что можно и чего нельзя сделать на шейдерах

На шейдерах можно делать почти всё. Только орехи колоть ими нельзя - испортите видеокарту.

Вот лишь краткий перечень:

- анимированные текстуры, свободный полёт мысли разработчика
- анимированные модели, аппаратный skinning на вертексных шейдерах
- смешивание двух и более текстур по любому алгоритму
- текстурирование ландшафта в зависимости от высоты точек
- depth-of-field (слишком близкие и слишком далёкие предметы как бы размыты)
- "чёрно-белое кино"
- размытие изображения фильтром Гаусса или любым другим, смазанные очертания движущихся предметов (motion blur)
- "горячий воздух" (марево над асфальтом, над огнём и др.)
- освещение по любому алгоритму и с любым количеством источников света
- мягкие тени
- bump mapping, parallax bump mapping, displacement mapping, bump любых видов и на любой вкус
- вода и зеркальные отражения, преломление и отражение по Fresnel-у
- HDR, Bloom, Glare и прочие новомодные штучки с солнцем в яркий летний день
- туман любого типа, в том числе объёмный
- анимированные облака в небе
- придумайте сами, не ошибётесь - это можно реализовать с помощью шейдеров

Все перечисленные эффекты подробно описаны в книге [1]

Не буду обсуждать здесь достоинства и недостатки программирования с использованием шейдеров. Скажу лишь то, что время идёт, сейчас видеокарты уже начального уровня поддерживают шейдеры 1.1, и через каких-то пару лет версия шейдеров шагнёт за 5.0 (или переродится в нечто новое), так что все недостатки типа тормознутости "тяжёлых" шейдеров со временем исчезнут, будут вытеснены простотой, гибкостью и элегантностью программирования с их использованием. Те же F.E.A.R, Doom3, FarCry, HalfLife2 бОльшей части красот обязаны шейдерам.

На самом деле, есть ряд вещей, которые можно выполнить только средствами CPU: расчёт геометрии, перемещение источников света, расчёт позиций "костей" для скелетной анимации, и многое другое. С другой стороны, ряд действий разумнее выполнять средствами шейдеров, чем CPU. Сюда в первую очередь относятся всевозможные эффекты над материалами моделей, пост-обработка кадра, анимация, текстурирование больших участков ландшафта, и многое другое. В конечном итоге выбирать приходится из двух меньших зол: скорость отрисовки кадра (тут шейдеры портят картину) и количество данных, обрабатываемых CPU и передаваемых после обработки в видеокарту (чем больше приходится напрягаться процессору и шине AGP, тем больше простаивает без дела GPU).


----

2. Как пишутся шейдеры

2.1 Языки программирования шейдеров

Собственно, языков существует целых 4 на текущий момент (включая низкоуровневый ассемблер), и все они так или иначе транслируются в одни и те же машинные коды, выполняемые на GPU, будь то ATI или NVidia. Стандарт описывает набор машинных команд и регистров, которыми оперируют команды. Пускай гуру пишут шейдер прямо на ASM-е, мы же будем использовать высокоуровневый язык, благо выбор имеется: HLSL (High Level Shader Language, от Microsoft), GLSL (OpenGL Shader Language, от SGI) и Cg от NVidia. Чтобы не запутаться во всём этом многообразии, определим, чем отличаются языки и как они могут быть связаны между собой:

- HLSL является наиболее де-факто общепризнанным языком и используется в Direct3D, синтаксис очень близок к C (не С++)
- GLSL используется в OpenGL-приложениях, о нём я знаю мало, по синтаксису он очень смахивает на HLSL
- Cg повторяет синтаксис HLSL, и является промежуточным языком, который автоматически компилируется в HLSL или GLSL, в зависимости от того, с каким приложением мы работаем: OpenGL или Direct3D

Замечание: Cg, хоть и написан NVidia, работает под ATI-шными видеокартами, "не морщась".

Так как Cg обладает гибкостью синтаксиса HLSL и работает как в Direct3D, так и в OpenGL, имеет смысл начать именно с него. Учитывая особенности сред разработки, в данной статье все примеры написаны на чистом HLSL, хотя по прошествии некоторого времени я так и не нашёл различий между HLSL и Cg.

Что же представляет из себя любой язык, на котором пишутся шейдеры ? Он очень похож на C, но в то же время гибче в плане работы с переменными. В целом, он заточен под конкретную задачу: вычисления с плавающей запятой, матрицами и векторами. Максимальная эффективность (и скорость) шейдерной программы зависит от ряда условий:
- программа должна быть простой и короткой
- чем меньше вычислений, тем быстрее будет работать шейдер (это понятно smile)
- чем более линейная программа, тем быстрее будет работать шейдер
- не приветствуется использование "нелинейных" операторов: циклов и условий

Типы данных, используемые при построении программ шейдеров, делятся на категории:
1) целочисленные и логические (bool, int, half), используются достаточно редко
2) с плавающей запятой (float, double)
3) векторы (float2, float3, float4, double3, bool2 и т.п., или vector<float, 3>)
4) матрицы (float2x2, float3x3, float4x4, double2x2, int4x3, или matrix<float, 2, 2> и т.п.)

В языке существует оператор "точка", позволяющий работать с компонентами векторов и матриц, например к компоненте "x" вектора можно обратиться через "my_var.x".

Все переменные, с которыми работает шейдерная программа, делятся на шесть видов:
1) входные параметры вертексного шейдера
2) интерполированные входные параметры пиксельного шейдера
3) текстурные сэмплеры (о них сказано ниже)
4) переменные, которым GPU автоматически присваивает значение (т.н. "встроенные переменные")
5) врЕменные переменные пользователя
6) входные параметры, переданные из основной программы

Отдельно стоит тип данных "sampler". До сих пор шла речь о вершинных данных, и нигде не упоминалось, откуда пиксельный шейдер получает пикселы текстуры. Исходными данными для операции "получить цвет точки текстуры" являются две переменные: текстурные координаты и переменная специального типа - так называемый сэмплер. Текстурные координаты лежат в интервале (0..1); при выходе за этот интервал происходит wrapping, то есть координата u=0.16 эквивалентна u=102934.16 и так далее. Получить пиксел обычной двумерной текстуры можно с помощью функции tex2D(mySampler, coords), где mySampler - текстурный сэмплер, coords - вектор типа float2 с текстурными координатами. Допускается использование трёхмерных текстур (также известных как многослойные текстуры) и одномерных, представляющих из себя столбик пикселов.

Цвет в шейдере кодируется вектором длины 3 (или 4, если с прозрачностью), в котором каждая компонента лежит в диапазоне (0..1), с точностью зависящей от реальной разрядности цвета. В обычной видеокарте 32-битный цвет кодируется значениями (0..255) для каждой компоненты, соответственно точность компоненты цвета в шейдере будет 1/255. В видеокартах следующего поколения используются 64-битные цвета, что повышает точность вычислений, но реально хватает и того что есть, потому что все промежуточные вычисления с цветом выполняются как с обычными float-переменными. К компоненте цвета можно обратиться двумя способами: myColor.x, myColor.y, myColor.z, myColor.w или myColor.r, myColor.g, myColor.b, myColor.a.

Кроме стандартных и временных переменных существует возможность передать в шейдер произвольные пользовательские  параметры, такие как интенсивность и цвет рассеянного света и т.п. Для таких переменных используется специальный модификатор uniform.

В шейдерном языке используются такие же арифметические операторы, как и в С++, но со своей спецификой. Так, к примеру, операция сложения и умножения по-разному реализуется для float, вектора и матрицы. Тем не менее, это не мешает перемножать переменные разных типов: к примеру, вектор можно умножить на число, а матрицу - на вектор. Более подробно набор доступных операций описывается в книге [2].

HLSL предоставляет разработчику массу встроенных функций, никаких include-ов при этом делать не нужно. Вот лишь некоторые, часто используемые функции:

sin, cos, tan, atan, asin, acos - надеюсь, их представлять не надо smile
mul - перемножает между собой векторы, матрицы и числа
length (vector) - вычисляет длину вектора
distance (a, b) - расстояние между двумя точками, заданными векторами
dot - вычисляет dot product (что это такое, рассмотрим ниже), очень важная функция
lerp - линейная интерполяция
tex1D, tex2D, tex3D - получение пиксела из текстурного сэмплера для одномерной, двумерной и трёхмерной текстуры (да, бывают и такие)

Все стандартные функции и типы данных описаны в книге [2].

Наконец, синтаксис HLSL допускает такие операции, как построение из N-мерного вектора M-мерного (аналога в С++ не существует). Пример:

Код
float4 my4Dvector = my2DVector.yyxx;




2.2 Структура шейдерной программы

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

Вспомним теорию: на вход вертексного шейдера поступает набор данных об одной вершине одного полигона. Что это за данные - мы определяем в своём приложении, когда описываем формат вершины. Стопроцентную гарантию можно дать на наличие во входных данных информации о позиции вершины (x,y,z), причём позиция задана в видовых координатах (т.е. относительно камеры). Помимо позиции, на вход обычно подаётся нормаль к вершине (nx,ny,nz) и координаты в текстуре (u,v). В шейдерах каждый компонент входных данных имеет свою так называемую "семантику", или подтип, если хотите: POSITION, NORMAL, TEXCOORD и пр. Учитывая то, что к примеру текстурных координат может быть не одна а две, к семантике иногда добавляется номер: POSITION0, NORMAL0, NORMAL1, TEXCOORD2 и т.п. Семантика определяет, через какие регистры передаются входные данные в указанную переменную, либо через какие регистры обработанные данные передаются на выход.

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

Код
struct VS_INPUT
{
   float4 Pos: POSITION;
   float4 Normal: NORMAL;
};


Здесь мы объявили структуру входных данных VS_INPUT (имя структуры может быть произвольным), содержащую информацию о координате и нормали вершины. Входные данные поступают в функцию vs_main:

Код
VS_OUTPUT vs_main(VS_INPUT input)
{
   // код вертексного шейдера
}


Тип выходного параметра определяется через структуру VS_OUTPUT, которую также необходимо определить в шейдерной программе. Выходные данные попадут после интерполяции к пиксельному шейдеру. Обязательным условием является наличие в VS_OUTPUT переменной с семантикой POSITION. Кроме того, в выходной структуре вертексного шейдера не разрешается использование любой семантики кроме COLOR и TEXCOORD, т.к. пиксельный шейдер имеет регистры только такого типа. Последнее правило: вертексный шейдер обязан заполнить все поля выходной структуры, незаполненные поля приводят к ошибкам на этапе компиляции шейдера. Пример описания выходной структуры вертексного шейдера:

Код
struct VS_OUTPUT
{
   float4 Pos: POSITION;
   float2 TexCoord: TEXCOORD0;
}


Всё то же самое можно написать без использования структур:

Код
void vs_main(
   float4 inPos :     POSITION,
   float2 inTexCoord: TEXCOORD,
   float4 inNormal:   NORMAL,
   out float4 outPos: POSITION,
   out float2 outTexCoord: TEXCOORD
)
{
   // код вертексного шейдера
}


До сих пор шла речь о вертексном шейдере, теперь рассмотрим пиксельный. Аналогично, он оформляется в виде функции, принимающей входные данные (интерполированная версия выходных данных вертексного шейдера) и возвращающей итоговый цвет пиксела. Пример описания пиксельного шейдера:

Код
float4 ps_main(
    float4 inPos: POSITION
    float2 inTexCoord: TEXCOORD) : COLOR0
{
   // код пиксельного шейдера
}


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

Код
void ps_main(float4 inPos
    float4 inPos: POSITION
    float2 inTexCoord: TEXCOORD
    out outColor : COLOR0)
{
   // код пиксельного шейдера
}


Что именно пишется вместо "код пиксельного/вертексного шейдера", будет рассказано в следующей главе.



2.3 Ok, что я могу сделать в шейдере, и как

Найдём "точку опоры". Итак, мы знаем:

1) что такое шейдеры
2) из каких компонентов состоит шейдерная программа

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



2.4 Средства разработки

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

- FX Composer
- DirectX Effect Edit
- ATI Render Monkey

Все три бесплатны. DirectX Effect Edit к тому же встроен в DX SDK, но довольно беден по возможностям как IDE. FX Composer больше заточен под разработку материалов, чем эффектов, хотя в этом я могу и ошибаться. В общем, если вы найдёте что-то получше, чем RenderMonkey, дайте мне знать smile

Итак, скачиваем RenderMonkey с сайта ATI (http://www.ati.com/developer/rendermonkey, примерно 35Мб), устанавливаем, открываем любой пример в папке samples, и... расстраиваемся, глядя на обилие значков и непонятных обозначений в окне RM. Конечно нет! На самом, интерфейс программы прост и удобен, а возможностей для разработчика очень много и все они под рукой, хотя это становится понятно не сразу а лишь через некоторое время возни с RM.

Окно RM состоит из трёх основных частей: слева располагается дерево проекта, содержащее узлы эффектов и прочего, снизу - окно output, куда выводятся ошибки компиляции, в центре - окна (которые можно вывести на закладки, если развернуть тек. окно), содержащие preview шейдера и редактор исходного текста. Компилируется шейдер по нажатию F7.

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

Можно считать каждый эффект папкой, содержащей шейдеры, "накладываемые друг на друга". Внутри эффекта располагаются значки переменных, 3D-моделей, текстур и самое главное - Pass'ов (проходов рендеринга). В самом простом эффекте всего лишь один Pass. Внутри Pass-а находятся значки "Pixel shader" и "Vertex shader", кликнув на которые, открывается окно редактора шейдерной программы, с подсветкой синтаксиса и прочими удобствами. Кроме собственно шейдеров, в Pass входят ссылки на тестовые текстуры и модели. 

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


2.5 Использование шейдеров в 3D-приложении

Небольшое лирическое отступление перед тем, как начать погружение в практическое создание шейдеров: в данной статье вы нигде не найдёте рецепта, как "прикрутить" шейдер к вашему 3D-приложению, потому, что в зависимости от того, что вы используете - OpenGL, Direct3D, или какой-нибудь универсальный движок типа Ogre, - вариант подключения шейдера будет сильно (если не полностью) различным в каждом конкретном случае. Например, в Ogre шейдер помещается в скрипт с материалом или в отдельный .cg-файл, в OpenGL используются ф-ции из специального расширения, в D3D - интерфейсы. Если позволит время, в следующей статье я опишу способ подключения шейдеров в движке Ogre, а пока если вы хотите попробовать хотя бы освоить технику их разработки, увидеть их вживую - добро пожаловать в следующий раздел, где мы создадим наш первый шейдер.


----

3. От слов к делу

3.1 Простейший шейдер: анимация текстуры

Запустите RenderMonkey, откроется окно пустого проекта. Можно сохранить его сразу, либо позже. Файлы проектов в RM имеют расширение .rfx, и содержат помимо кода шейдеров ещё ссылки на используемые 3D-ресурсы: текстуры и модели.

Правкликом нажимаем на узел "Effect Workspace", выбираем "Add effect group", "Effect group w/ DirectX Effect". В дерево проекта добавляется эффект, содержащий следующие узлы:

user posted image

- Effect Group 1 - "папка", содержащая наши эффекты
- Effect1 - вложенная "папка", содержащая шейдеры, тестовые текстуры и модели
- matViewProjection - стандартная переменная, в которую автоматически передаётся матрица view-projection
- Stream Mapping - здесь задаётся формат входных данных для вертексного шейдера (в реальном приложении мы самостоятельно описываем формат этих данных; RM позаботился о нас и вынес эти настройки в отдельный узел эффекта)
- Model - тестовая модель (шарик)
- Pass0 - единственный проход рендеринга

Внутри Pass0 содержатся следующие узлы:

- Model (со стрелкой) - ссылка на модель шарика
- Vertex Shader и Pixel Shader - кликнув два раза, откроется редактор исходного текста вертексного или пиксельного шейдера
- Stream Mapping (со стрелкой) - ссылка на настройки входных данных вертексного шейдера

Начнём с того, что определим что у нас имеется: тестовая модель, какие-то дефолтные настройки входных данных, и какой-то дефолтный исходный код в пиксельном и вертексном шейдерах. Модель рендерится в окне Preview, и вся залита ровным красным цветом. Нашей первой задачей будет изменить модель: пусть вместо шарика будет чайник (фанаты OpenGL меня поймут smile). Итак, кликаем два раза по значку "Model", выбираем файл "Teapot.3ds". Те, у кого слабая видеокарта, не увидят изменений в preview пока не нажмут Space (таковы издержки программной эмуляции шейдеров, но результат тот же, хоть и не в real-time). Убеждаемся, что вместо сферы у нас теперь чайник.

Следующий этап: давайте наложим на нашу модель какую-нибудь текстуру. Начать нужно с того, что добавить текстуру в эффект, а ссылку на текстуру - в Pass0. Кликаем правой кнопкой по значку "Effect1", выбираем пункт меню "Add Texture", подменю "Add 2D Texture", выбираем Media/Textures/Earth.jpg, в проект добавится значок с именем "Earth". Что, ничего не изменилось в preview ? Всё правильно, так и должно быть smile. Теперь нужно добавить ссылку на текстуру в наш Pass0, чтобы пиксельный шейдер мог наложить текстуру на модель чайника. Кликаем правой кнопкой по значку "Pass0", и выбираем - ВНИМАНИЕ - "Add Texture Object" (не "Add Texture"). В результате, внутри Pass0 появится значок "Texture0", по виду напоминающий коробку, перечёркнутый красной линией. Красная линия означает, что ссылка на текстуру не заполнена, т.е. нужно указать, какая именно текстура из имеющихся в проекте будет использована в ссылке. Последнее действие: раскрываем значок "Texture0", кликаем правой кнопкой по значку "baseMap", выбираем пункт "Reference Node", и в появившемся подменю выбираем нашу текстуру "Earth" (кроме неё там сейчас ничего и нету). Всё, текстуру мы добавили.

Следующее действие: прежде чем мы перейдём непосредственно к программированию шейдеров, определимся с тем, какие входные данные вертексного шейдера нам нужны. Кликаем два раза по значку "Stream Mapping" (по тому, который без стрелки), и в открывшемся окне видим, что на вход поступает переменная типа float3 с семантикой POSITION. Вспоминаем, что наличие такой переменной является обязательным. Теперь давайте подумаем: чего нам ещё не хватает для того, чтобы наложить текстуру на модель ? Не хватает текстурных координат для каждой вершины. Быть посему, нажимаем "Add", и меняем семантику второй переменной с POSITION на TEXCOORD. Внимание: поле Index не меняем, пусть там будет 0. Нажав OK, закрываем окно.

Переходим непосредственно к кодированию. Начнём, что логично, с вертексного шейдера. Кликаем два раза по значку "Vertex shader", можем для удобства развернуть окно редактора на всю величину. Что мы видим:

Код
float4x4 matViewProjection;

struct VS_INPUT 
{
   float4 Position : POSITION0;
   
};

struct VS_OUTPUT 
{
   float4 Position : POSITION0;
   
};

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.Position = mul( matViewProjection, Input.Position );
   
   return( Output );
   
}


Если вы пропустили главы с теорией, то вкратце опишу, какие действия выполняет этот код:

- вначале объявлена стандартная переменная matViewProjection (матрица 4x4 преобразования из видовых координат в проекционные), значение в эту переменную подставляется автоматически; как вы могли заметить, в дереве проекта эта переменная присутствует. Если попробовать удалить её из дерева проекта, код шейдера компилироваться (F7) не будет
- далее следует описание входных данных вертексного шейдера, в виде структуры, содержащей переменную Position в семантике POSITION. Через эту переменную в шейдер попадают видовые координаты вершины (т.е. координаты относительно позиции камеры)
- описание выходных данных вертексного шейдера содержит ту же переменную, но уже в проекционных координатах (т.е. в координатах экрана)
- главная функция вертексного шейдера vs_main получает на вход и выдаёт на выход структуру входных и выходных данных, соответственно

Обратимся к коду vs_main, что там происходит. Вначале объявляется переменная-структура типа VS_OUTPUT, которую возвращает оператор return в конце функции. Вспомним, что вертексный шейдер обязательно должен заполнить все свои выходные данные, что он и делает, присваивая переменной-полю структуры Output.Position значение, получаемое с пом. функции mul (умножение). Вектор, содержащий 3D-координаты вершины (Input.Position) умножается на матрицу matViewProjection, что аналогично проецированию вершины на экран.

Если всё с этим понятно, открываем пиксельный шейдер - для этого достаточно просто перейти на закладку "Pixel Shader" в том же окне. Код пиксельного шейдера:

Код
float4 ps_main() : COLOR0
{   
   return( float4( 1.0f, 0.0f, 0.0f, 1.0f ) );
}


Итак, как мы видим, пиксельный шейдер и возвращает константу: float4(1, 0, 0, 1). Вспомним, что результат функции пиксельного шейдера - это итоговый цвет пиксела. Следовательно, рассматриваемый пиксельный шейдер возвращает цвет пиксела, у которого R-компонента равна максимальному возможному значению (1.0), G- и B-компоненты нулю, канал прозрачности - максимальному значению (полностью непрозрачный цвет).

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

Код
float4x4 matViewProjection;

struct VS_INPUT 
{
   float4 Position : POSITION0;
   float2 TexCoords : TEXCOORD;
};

struct VS_OUTPUT 
{
   float4 Position : POSITION0;
   float2 TexCoords : TEXCOORD;
};

VS_OUTPUT vs_main(VS_INPUT Input)
{
   VS_OUTPUT Output;

   Output.Position = mul(matViewProjection, Input.Position);
   Output.TexCoords = Input.TexCoords;
   
   return Output;
}


Как видно, мы добавили переменную с семантикой TEXCOORD в выходные данные вертексного шейдера. Кроме того, по правилам необходимо заполнять все выходные данные - это делает строка после mul(), копируя текстурные координаты из входных данных вертексного шейдера в выходные. Соответственно, TEXCOORD-переменная была добавлена во входные данные вертексного шейдера. Возвращаясь назад, напомню, что мы добавили TEXCOORD в Stream Mapping; если бы мы этого не сделали, компилятор сообщил бы нам об ошибке, т.к. переменная в VS_INPUT не была бы связана ни с одним реальным регистром GPU. Кроме того, все имена типа VS_INPUT, VS_OUTPUT, TexCoords и т.п. (кроме семантик) - чисто условные, можете менять их как вздумается.

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

Код
sampler Texture0;

float4 ps_main(float2 texCoord: TEXCOORD) : COLOR0
{   
   return tex2D(Texture0, texCoord);
}


Вначале была объявлена переменная-сэмплер, связанная с текстурой. Имя переменной должно совпадать с любой ссылкой на текстуру (Texture Object), из имеющихся в Pass0. В нашем случае, это ссылка Texture0, указывающая на текстуру Earth. В функции ps_main, как видим, появился аргумент с семантикой TEXCOORD, в который придут данные от вертексного шейдера (VS_OUTPUT.TexCoords). Эти данные содержат пару текстурных координат для пиксела. Далее, функция ps_main содержит вызов стандартной ф-ции tex2D, в которую передаётся сэмплер и текстурные координаты. Здесь всё просто: tex2D просто выбирает точку из текстуры Earth.jpg, по координатам из texCoords.

Компилируем шейдер (клавиша F7), и - вот оно, свершилось! Казалось бы, простая операция "наложить текстуру", а сколько при этом потребовалось хитрых манипуляций... Но ничего, во-первых вы скоро к этому привыкнете, а во-вторых, шейдер умеет не только натягивать текстуры на модель... Что мы теперь и докажем: сделаем нашу текстуру анимированной!

user posted image

Простая анимация может просто передвигать текстуру в одном направлении, либо по какому-то периодическому закону. Выберем наиболее простой вариант анимации - первый. Наша анимация текстуры сводится к простому сдвигу текстурных коодринат (texCoords) на величину, зависящую от времени. Для работы со временем, используются встроенные переменные, такие как fTime0_X и др. Чтобы шейдер мог работать со встроенной переменной, необходимо добавить её в дерево переменных. Кликаем правой кнопкой по значку "Effect1", выбираем "Add Variable", 

Это сообщение отредактировал(а) arilou - 26.10.2006, 19:23


--------------------
user posted image
PM MAIL WWW   Вверх
mr.DUDA
Дата 24.12.2005, 12:04 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


3D-маньяк
****


Профиль
Группа: Экс. модератор
Сообщений: 8244
Регистрация: 27.7.2003
Где: город-герой Минск

Репутация: 7
Всего: 232



 3.2 Мультитекстурирование: детальные текстуры и металл в камне

В предыдущем примере был описан довольно-таки тривиальный эффект, который легко воспроизводится и без шейдеров. Что ж, давайте возьмём пример чуть посложнее. Попробуем реализовать эффект "детальная текстура". Если в двух словах, данный эффект состоит в следующем: на полигон накладываются две текстуры, одна из которых является основной и накладывается всегда, а вторая - детальная текстура, - повторяющаяся текстура с мелкими деталями, показывающая характер поверхности полигона: камень, дерево, снег и т.п. Детальная текстура, как правило, "проступает" на каком-то небольшом расстоянии до полигона.

Создайте новый проект RenderMonkey, как в предыдущем примере. Измените модель с шарика на "Teapot.3ds' и добавьте в проект две текстуры (обе есть в папке Media) - "Distortion2.jpg" и "Noise.tga". Переименуйте их в проекте на Stone и Noise, соответственно. Это у нас будет мрамор и его детальная текстура (шершавая поверхность мрамора). Добавьте в Pass0 текстурные объекты для обеих текстур и также переименуйте из в Stone и Noise. Последнее, что осталось сделать - добавьте в Stream Mapping семантику TEXCOORD (по дефолту, напомню, там есть только POSITION). Мы готовы к программированию.

В этот раз мы попробуем работать без структур. Отредактируйте код вертексного шейдера следующим образом:

Код
float4x4 matViewProjection;

void vs_main(
   in float4 iPosition  : POSITION0,
   in float2 iTexCoord  : TEXCOORD0,
   
   out float4 oPosition : POSITION0,
   out float2 oTexCoord : TEXCOORD0,
   out float  oDistance : TEXCOORD1
)
{
   oPosition = mul(matViewProjection, iPosition);
   oTexCoord = iTexCoord;
   oDistance = oPosition.z;
}


Здесь мы отдаём на выход координаты вершины после проекции, текстурные координаты вершины (переменная oTexCoord) и вводим дополнительную выходную переменную в семантике TEXCOORD1 (для пиксельного шейдера она будет, соответственно, входной) для передачи пиксельному шейдеру расстояния от камеры до вершины. Эта переменная будет задавать расстояние, по которому определяется, "проступает" ли детальная текстура или же нет (если расстояние слишком велико).

Отредактируйте пиксельный шейдер следующим образом:
Код
sampler Noise;
sampler Stone;
const float detailDistance = 15;

float4 ps_main(
   float2 TexCoord : TEXCOORD0,
   float Distance : TEXCOORD1
   ) : COLOR0
{   
   float4 stdTex = tex2D(Stone, TexCoord);
   float4 detTex = tex2D(Noise, TexCoord * 4);
   return stdTex + detTex * detailDistance / Distance;
}


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

Функция ps_main берёт пиксел из текстуры Stone (наш "мрамор") и пиксел из текстуры Noise (шершавая поверхность), причём умножив текстурную координату на некий коэффициент 4. Умножая текстурные координаты на коэффициент N, мы тем самым искуственно повышаем разрешающую способность текстуры в N раз. Естественно, при этом никакие промежутки между точками текстуры не заполняются автоматически - текстура не растягивается, а просто повторяется в ширину и длину N раз. В нашем случае с шумовой текстурой, этого вполне достаточно.

Полученные два цвета stdTex и detTex затем складываются по нехитрому алгоритму: к цвету мрамора прибавляется цвет "шершавой поверхности", умноженный на частное от двух чисел. Первое число - это константа, определяющая максимальное расстояние, на котором видна детальная текстура. Второе число - расстояние до пиксела. Чем больше расстояние, тем меньше видна детальная текстура. Скомпилируйте эффект и наслаждайтесь результатом.

Рассматривая наш мраморный чайник с очень близкого расстояния, вы можете заметить, что детальная текстура становится светлее, чем ближе мы к ней, и на минимальном расстоянии всё становится залитым белым цветом. Правильно, ведь чем меньше переменная Distance, тем больше получается частное (detailDistance / Distance), и в конце концов оно становится больше 1, что приводит к выходу итогового цвета за границы 0...1. Чтобы избежать этого, измените последнюю строку ps_main на:

Код
   return stdTex + detTex * saturate(detailDistance / Distance);


Стандартная функция saturate "обрезает" переданный аргумент, если он выходит за границы 0...1. Например: для 0 результат saturate будет 0, для -0.5 и меньше результат будет также 0, для 0.6 будет 0.6, для 1 будет 1, для 1.3 и выше функция вернёт 1.

Вот что у нас получилось при приближении к мраморному чайнику:
user posted image
user posted image
user posted image

Рассмотрим другой эффект, основанный на смешивании текстур. В игре Half-Life2 на первом уровне "вокзал" есть интересный эффект - каменная плитка пола с металлическими вставками. Если присмотреться к нему, особенно при отключенной поддержке DirectX9.0, становится понятно, что этот эффект сделан на текстуре а не на нескольких моделях ("плитка" и "вставки"). Давайте попробуем сделать что-то подобное своими силами.

Переименуйте текстуру Noise в Metal, так нам будет понятнее (не забудьте переименовать также и текстурный объект). Даблкликом по текстуре Stone выбираем файл "Hex.dds" (это будет плитка), для Metal выбираем файл "chrome.tga" в папке Media/Chrome. Добавьте в Stream Mapping семантику NORMAL, поставьте Index=0, а DataType измените на FLOAT3. Кроме всего прочего, нам понадобится стандартная переменная ViewPosition (чтобы по позиции камеры вычислять параметры "бликования" металлических вставок). Нажмите правкликом по Pass0, выберите "Add Variable / float / Predefined / vViewPosition" (в самом низу списка переменных).

Отредактируйте вертексный шейдер:
Код
float4x4 matViewProjection;
float4 vViewPosition;

void vs_main(
   in float4 iPosition  : POSITION0,
   in float2 iTexCoord  : TEXCOORD0,
   in float4 iNormal    : NORMAL0,

   out float4 oPosition : POSITION0,
   out float2 oTexCoord : TEXCOORD0,
   out float  oDotProduct : TEXCOORD1
)
{
   oPosition = mul(matViewProjection, iPosition);
   oTexCoord = iTexCoord;
   oDotProduct = dot(iNormal, normalize(vViewPosition - iPosition));
}


В пояснениях нуждается последняя строка. Функция normalize выполняет нормализацию вектора, т.е. делит все компоненты вектора на одно и то же число так, чтобы длина вектора стала равна 1. Нормализуем мы вектор (vViewPosition - iPosition), что логически даёт вектор направления от вершины на камеру. Затем вызывается стандартная ф-ция dot. Данная функция вычисляет косинус угла между двумя нормализованными векторами. Чем на больший угол раздвинуты 2 вектора, тем меньшее значение вернёт dot. Если оба вектора совпадают, dot возвращает 1. Фактически, мы берём вектор нормали вершины и вычисляем косинус между ним и направлением на камеру. Чем ближе между собой нормаль и направление на камеру, тем ближе результат к 1. В результате всех этих вычислений, мы получаем значение oDotProduct, которое можно считать коэффициентом освещённости металла при условии, что свет идёт из камеры. Таким способом мы организуем блик на металлической поверхности: максимально повёрнутая к нам поверхность максимально освещена.

Редактируем пиксельный шейдер:
Код
sampler Metal;
sampler Stone;

float4 ps_main(
   float2 TexCoord : TEXCOORD0,
   float DotProduct: TEXCOORD1
   ) : COLOR0
{   
   float4 texStone = tex2D(Stone, TexCoord);
   float4 texMetal = tex2D(Metal, TexCoord);
   float key = (texStone.a < 0.2) ? 0 : 1;
   return texStone * key + pow(DotProduct, 3) * texMetal * (1 - key);
}


Вначале всё просто. Получаем два пиксела из текстур (камень и метал). Затем мы применяем небольшую хитрость. Чтобы определить, какую часть текстуры "Stone" надо заменить на металлическую вставку, мы используем слой прозрачности текстуры (вот почему мы выбрали "Hex.dds", потому что она содержит слой прозрачности), как маску. Этот способ ничем не лучше и не хуже прочих. Мы, например, могли бы создать ещё одну текстуру "Mask", в которой белые пикселы означают "камень", а чёрные - "металл".

Задача по смешиванию двух текстур решается следующим образом. Проверяем значение маски в данной точке (это значение прозрачности, texStone.a) и в случае, если оно не превышает некоторый порог (взяли к примеру 0.2), тогда в этой точке будет металл (key=0), иначе в этой точке - камень (key=1). Получили значение key=0 или =1. Затем берём цвет камня в данной точке и умножаем на key, складываем с цветом металла умноженным на обратное значение key и умноженным на DotProduct. Для придания более реалистичного эффекта металла, мы дополнительно возводим DotProduct в куб (числа, меньшие 1, при возведении в положительную степень уменьшаются тем сильнее, чем меньше было значение числа).

Для усиления эффекта можно освещать также и каменную часть текстуры, возведя DotProduct в меньшую степень:
Код
   return DotProduct * texStone * key + pow(DotProduct, 3) * texMetal * (1 - key);


Окончательный результат:
user posted image
user posted image 


--------------------
user posted image
PM MAIL WWW   Вверх
mr.DUDA
Дата 10.9.2006, 16:17 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


3D-маньяк
****


Профиль
Группа: Экс. модератор
Сообщений: 8244
Регистрация: 27.7.2003
Где: город-герой Минск

Репутация: 7
Всего: 232



3.3 Отражения и вода

Что такое вода с точки зрения рендеринга ? Если брать самый примитивный вариант, водой можно назвать анимированную текстуру, по цвету более-менее совпадающую с нашими представлениями о воде, и натянутую на плоскую поверхность. Таким образом, можно взять к примеру текстуру синего цвета с более светлыми и тёмными разводами и применив к ней анимацию - например, скроллинг в одну сторону - сказать что это "вода". Но выглядеть это будет как движущаяся текстура, не более того. Что же делает воду водой ?

Вода отражает падающий на неё свет. Кроме того, она преломляет всё что под поверхностью. Отставим пока что преломление в сторону и обратим внимание на отражение. С этой точки зрения, вода очень напоминает зеркало. Единственное отличие между водой и зеркалом - отсутствие анимации у последнего (т.е. зеркало так же отражает свет, как и вода в безветренную погоду при отсутствии течения). 

Разберём отражение во всех подробностях. В современных играх эффект отражения на поверхности реализуется одним из двух способов:

- Рендеринг всей сцены в один проход, с наложением кубической текстуры на поверхность модели;
- Рендеринг всей сцены в два прохода с двумя камерами (для N отражающих поверхностей – в N+1 проходов с N+1 камерами).
- Третий способ (raytracing) не рассматриваем. На современных видеокартах он не поддерживается из-за большой сложности обсчёта сцены. 

Обратимся ко второму способу рендеринга отражений - в два прохода. Отражение формируется за счёт съемки сцены с определённой точки и наложения результата на поверхность во втором проходе. Как можно догадаться, такой точкой является пересечение вектора обзора камеры (v_camera) с отражающей поверхностью. При рендеринге первого прохода камера переносится в точку пересечения, и направление обзора меняется на вектор отражения v_reflect = reflect (v_camera, N_water) где reflect - функция отражения в 3D-пространстве, N_water - нормаль отражающей поверхности. Таким образом, получается снимок отражения для наблюдателя, находящегося в исходной точке, и на втором проходе остаётся наложить полученный снимок как текстуру на отражающую поверхность. Недостатки этого способа: низкая производительность (два прохода с рендерингом всей видимой части сцены) и проблемы в случае криволинейных отражающих поверхностей - вместо одного вектора нормали их множество, поэтому приходится выбрать какой-то один для рендеринга первого прохода, что даёт нереалистичный снимок отражения.

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

Разберём понятие "кубическая текстура" (cube map). Кубическая текстура - это набор из шести квадратных текстур, объединённых вместе и составляющих грани одного куба (не путать с 3D-текстурами). Cube map создаётся путём фотографирования окружающей обстановки с одной точки в шести направлениях, и объединения полученных изображений в один файл (в DirectX для таких текстур используется формат dds). Иногда кубические текстуры называют environment map, т.к. cube map замечательно подходит для создания трёхмерного background-а: находясь внутри куба или многогранника с натянутой на него кубической текстурой, практически невозможно различить стыки текстур-граней, что создаёт эффект нахождения внутри сферы. Это позволяет использовать cube map как трёхмерный background (или environment - окружение). Environment mapping используется практически во всех современных играх для изображения статичного неба и эффекта отражений.

Работать с кубической текстурой проще, чем может показаться. На полигон можно наложить текстуру, зная текстурные координаты вершин полигона. Для обычных двумерных текстур, пара текстурных координат (U, V) передаётся в функцию tex2D, возвращая цвет точки текстуры. Для кубических текстур  используется не tex2D а функция texCUBE, также возвращающая цвет точки текстуры, но принимающая три текстурные координаты. Смысл трёх координат - это единичный вектор, выходящий из центра воображаемого куба и пересекающий одну из его граней; точка пересечения определяет грань и пару координат, аналогичных (U, V). Всю работу выполняет GPU, от нас требуется лишь правильно указать вектор из центра куба. Очень просто представить себе, как рендерится environment map: берём модель в виде куба или многогранника с обращёнными внутрь гранями, камера фиксируется в центре куба. Вертексный шейдер вычисляет вектор "камера -> вершина", нормализует его и передаёт в пиксельный шейдер. Пиксельный шейдер выполняет единственное действие – вызывает texCUBE с полученным вектором:

Код
sampler skyBox;

float4 ps_main(float3 vec: TEXCOORD0) : COLOR 
{
   return texCUBE(skyBox, vec);
}


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

Если же поместить камеру вне модели, и вектор vec вычислять как отражённый от поверхности, то с минимальными изменениями мы получим эффект отражения.

Лирическое отступление. Естественно, что "отражаться" в нашей модели будет не сцена, а кубическая текстура, но при грамотно подготовленной текстуре заметить разницу будет не так легко. Для примера возьмём раллийный симулятор Collin McRae Rally, где основное действие происходит, например, в пустыне или лесу. Что будет отражаться в стекле машины ? Деревья, небо, трава – всё это можно как бы "усреднить", создав статичную картину отражения в виде cube map, и пользователь не заметит разницы, находясь за машиной и глядя в заднее стекло. То же самое можно отнести к отражениям на моделях оружия, стекла в 3D-шутерах и т.п.

Вернёмся к шейдерам и создадим простой эффект металлического чайника (модель Teapot.3ds), отражающего горную панораму (кубическая текстура Snow.dds). В новом workspace измените модель на Teapot.3ds из папки Media/Models, добавьте стандартные переменные vViewPosition (вектор) и matViewProjection (матрица 4 на 4), текстуру Snow.dds из папки Media/Textures. Отредактируйте Stream mapping, добавив элемент NORMAL с размерностью FLOAT3. Создайте текстурный объект с именем texSnowCUBE для текстуры Snow.dds. Даблкликом откройте свойства текстурного объекта и отредактируйте настройки фильтрации:

D3DSAMP_MAGFILTER = D3DTEXF_LINEAR
D3DSAMP_MINFILTER = D3DTEXF_LINEAR
D3DSAMP_MIPFILTER = D3DTEXF_LINEAR

Это придаст текстуре сглаженный вид при рендеринге. Теперь собственно наберите исходник вертексного и пиксельного шейдеров:
Код
float4x4 matViewProjection;
float4 vViewPosition;

void vs_main(
   float4 inPos          : POSITION,
   float3 inNormal       : NORMAL,
   out float4 outPos     : POSITION,
   out float3 outReflect : TEXCOORD1
)
{
   outPos = mul(matViewProjection, inPos);
   outReflect = reflect(normalize(inPos - vViewPosition), inNormal);
}


Код
sampler texSnowCube;

float4 ps_main(float3 vReclect: TEXCOORD1) : COLOR 
{
   return texCUBE(texSnowCube, vReclect);
}


Вертексный шейдер, как обычно, вычисляет позицию вершины в проекционных координатах outPos, а также вектор отражения outReflect. Для последнего, вычисляется вектор, выходящий из камеры по направлению к вершине (inPos – vViewPosition), нормализуется и передаётся в стандартную функцию reflect, вместе с нормалью вершины. Функция reflect принимает входящий вектор, нормаль, и возвращает отражённый вектор. Пиксельный шейдер принимает вектор отражения для конкретной точки полигона и берёт точку из cube map.

Результат:
user posted image

Усложним задачу, добавив к сцене background и сделав поверхность лакированным деревом. Для первой части, нужно добавить в workspace модель Spere.x и ещё один проход рендеринга (Add Pass). Чтобы бэкграунд рендерился до чайника, перетяните существующий pass на новый – они поменяются местами. Итак, есть два pass-а, назовём их background_pass и teapot_pass соответственно. Добавим в background модель Sphere, текстурный объект texSnowCube (не забудьте настроить фильтрацию для сглаживания текстуры) и элемент Render State (пункт меню Add Render State Block). Откройте Render State даблкликом и отредактируйте параметры:

D3DRS_ZWRITEENABLE = FALSE
D3DRS_CULLMODE = NONE

Это приведёт к тому, что результаты первого прохода не будут влиять на z-buffer (тем самым бэкграунд будет бесконечно удалён от камеры и не будет пересекаться с чайником), а отключение culling приведёт к тому, что на первом проходе полигоны будут рендериться с обеих сторон (нужно, т.к. мы видим модель Sphere изнутри). Чтобы render state из background_pass не влиял на teapot_pass (а это не нужно, т.к. чайник должен рендериться только снаружи и с записью в z-buffer), добавьте Render State в teapot_pass и поставьте:

D3DRS_ZWRITEENABLE = TRUE
D3DRS_CULLMODE = D3DCULL_CCW


Шейдер для background_pass:
Код
float4x4 matViewProjection;
float4 vViewPosition;

void vs_main(
      float4 inPos  : POSITION,
      out float4 outPos : POSITION,
      out float3 texCoord : TEXCOORD0
)
{
   outPos = mul(matViewProjection, float4(inPos.xyz + vViewPosition, 1));
   texCoord = inPos.xyz;
}


Код
sampler texSnowCube;

float4 ps_main(float3 pos: TEXCOORD0) : COLOR 
{
   return texCUBE(texSnowCube, pos);
}



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

Результат:
user posted image


Добавим эффект лакированного дерева. Для этого нужно добавить в эффект текстуру Wood.dds, в Stream mapping добавить элемент TEXCOORD (FLOAT2), а teapot_pass добавить текстурный объект texWood (всё те же настройки фильтрации), после чего отредактировать шейдеры в teapot_pass:

Код
float4x4 matViewProjection;
float4 vViewPosition;

void vs_main(
   float4 inPos          : POSITION,
   float3 inNormal       : NORMAL,
   float2 inTexCoord     : TEXCOORD0,
   out float4 outPos     : POSITION,
   out float2 outTexCoord: TEXCOORD0,
   out float3 outReflect : TEXCOORD1
)
{
   outPos = mul(matViewProjection, inPos);
   outReflect = reflect(normalize(inPos - vViewPosition), inNormal);
   outTexCoord = inTexCoord;
}


Код
 sampler texSnowCube;
sampler texWood;

float4 ps_main(
   float2 texCoord: TEXCOORD0,
   float3 vReflect: TEXCOORD1
) : COLOR 
{
   return 0.6f * tex2D(texWood, texCoord) + 0.4f * texCUBE(texSnowCube, vReflect);
}


Пиксельный шейдер смешивает текстуру дерева с отражением в пропорции 6:4. Изменяя эти значения в сторону текстуры отражения или дерева, получим более металлический или более "деревянный" вид соответственно.

Результат:
user posted image

Полностью эффект можно скачать по этой ссылке

Замечу, что здесь практически не используется никакой модели освещения, и ещё лучших результатов можно добиться при использовании diffuse+specular освещения (как в примере по смешиванию текстур).


Эффект воды

Перейдём от простого к анимированному отражению – именно этим и является вода. Используем следующий способ анимации: из 3D-текстуры, содержащей шум, выбирается слой в зависимости от текущего времени – таким образом получаем маску, плавно изменяющуюся во времени, которую будем считать рисунком волн на поверхности воды. Для каждой точки поверхности воды, цвет шумовой текстуры используем как вектор отклонения от текстурных координат точки. Например, чёрный цвет шумовой текстуры даст максимальное отрицательное отклонение, белый – максимальное положительное, серый 0.5 – нулевое отклонение. Полученное значение цвета шумовой текстуры прибавляется к текстурным координатам точки, создавая искажение, подобное волнам (при правильно сформированной шумовой текстуре).

Для усложнения эффекта, добавляется "течение" – одна из текстурных координат постоянно изменяется во времени. 

В качестве основы используем эффект, описанный выше. Переименуйте teapot_pass в water_pass. Замените модель teapot на Cube.x, а также добавьте 3D-текстуру NoiseVolume.dds в эффект, и текстурный объект texNoise в water_pass. Последнее, что нужно сделать – добавить стандартную переменную fTime0_X в эффект. Редактируем пиксельный шейдер в water_pass:

Код
sampler texSnowCube;
sampler texNoise;
float fTime0_X;

float4 ps_main(
   float2 texCoord: TEXCOORD0,
   float3 vReflect: TEXCOORD1
) : COLOR 
{
   // настройки эффекта
   // скорость течения воды
   const float waterSpeedX = 0.2f;
   // скорость ветра - как быстро изменяется рисунок волн
   const float windSpeed = 0.1f;
   // высота волн - как сильно рисунок волн искажает текстуру отражения воды
   const float waveHeight = 0.2f;
   // частота волн - с какой периодичностью повторяется искажающая текстура
   const float waveRepeatScale = 2.0f;

   // получаем координату в шумовой текстуре, используя частоту волн
   float2 texCoordRepeated = waveRepeatScale * texCoord;
   
   // добавляем смещение по одной из осей (x)
   // другую ось оставляем без изменений (y)
   // выбираем слой шумовой 3D-текстуры в зависимости от времени (fTime0_X)
   // и скорости ветра (windSpeed)
   float3 texCoordNoise = float3(
         texCoordRepeated.x + waterSpeedX * fTime0_X,
         texCoordRepeated.y,
         windSpeed * fTime0_X);
         
   // получаем цвет точки шумовой текстуры 0..1, сдвигаем его в диапазон -0,5..0,5
   // и уменьшаем в waveHeight раз
   float4 noiseVector = (tex3D(texNoise, texCoordNoise) - 0.5f) * waveHeight;
   
   // получаем отражённый цвет, смещённый на noiseVector   
   return texCUBE(texSnowCube, vReflect + noiseVector);
}


Пояснения даны в тексте шейдера. Как видно, кроме того что добавляется анимация (в виде сдвига vReflect на noiseVector), отличий от простого отражения – никаких. Результат (здесь не видно анимации):
user posted image


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

Код
   // получаем отражённый цвет, смещённый на noiseVector   
   float4 reflectedColor = texCUBE(texSnowCube, vReflect + noiseVector);

   // собственный цвет воды   
   const float4 waterColor = float4(0.1961, 0.3459, 0.6842, 1.0f);
   
   // соотношение "вода-отражение"
   const float waterColorAmount = 0.5f;

   // окончательный цвет
   return lerp(waterColor, reflectedColor, waterColorAmount);


Результат:
user posted image

Полностью эффект можно скачать по этой ссылке.

Аналогичный эффект (Ocean) можно найти в стандартном примере Reflections Refrations.rfx, единственным серьёзным отличием является зависимость соотношения вода-отражение от нормали к поверхности – т.е. чем больше угол между направлением камеры и нормалью к воде, тем больше waterColor в результирующем цвете.

Два слова о способе рендеринга отражений с двумя камерами. В этом случае, работа шейдера заключается в вычислении позиции, с которой необходимо отрендерить первый pass, и собственно в получении результата рендеринга с этой точки. В терминах Render Monkey, в эффект добавляется объект Camera и Render Target, а в workspace – объект Renderable Texture (RT), настраиваемые таким образом, чтобы render target от одного pass-а был связан с текстурой RT, используемой как обычная текстура в следующем pass-е. Пример можно посмотреть в Reflections Refractions, эффект Refraction.

Эффект преломления реализуется практически так же как и эффект отражения при использовании двух камер: подводная часть сцены рендерится с точки, находящейся на векторе нормали, восстановленном в точке пересечения направления камеры и поверхности воды; полученный результат искажается и накладывается на поверхность воды. Единственное нетривиальное действие - разбить сцену на подводную и надводную части (или наоборот, зависит от того, где находится наблюдатель) и вычислить точку, с которой нужно рендерить часть сцены.


--------------------
user posted image
PM MAIL WWW   Вверх
casterTroy
Дата 16.1.2015, 18:06 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 1
Регистрация: 16.1.2015

Репутация: нет
Всего: нет



Вот что получаетсья когда пытаюсь сделать мультитекстурирование. В чём проблема?

Присоединённый файл ( Кол-во скачиваний: 21 )
Присоединённый файл  shader.png 308,56 Kb
PM MAIL   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "Программирование игр, графики и искуственного интеллекта"
Rickert

НА ЗЛОБУ ДНЯ: Дорогие посетители, прошу обратить внимание что новые темы касающиеся новых вопросов создаются кнопкой "Новая тема" а не "Ответить"! Любые оффтопиковые вопросы, заданные в текущих тематических темах будут удалены а их авторы, при рецедиве, забанены.

  • Литературу, связанную с программированием графики, обсуждаем здесь
  • Действия модераторов можно обсудить здесь
  • С просьбами о написании курсовой, реферата и т.п. обращаться сюда
  • Вопросы связанные с программированием графики и мультимедии на языках С++ и Delphi
  • Вопросы по реализации алгоритмов рассматриваются здесь

Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Rickert.

 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Программирование игр, графики и искусственного интеллекта | Следующая тема »


 




[ Время генерации скрипта: 0.3096 ]   [ Использовано запросов: 20 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.