Вступление.
Нынешнее графическое железо, такое, например, как GeForce FX5900 или Radeon9800 предоставляет пользователю возможность замены некоторых частей графического конвейера шейдером. Шейдер — это специальная программа, которая использует определенные программируемые регистры видеокарты для создания различных графических эффектов. Более новые карты (GeForce 6800) обеспечивают расширенные возможности программируемости шейдеров: огромное количество инструкций, динамическое ветвление и т.д.
Для начала немного теории. На рисунке показана упрощенная модель графического конвейера, которая условно делится на две части: обработка геометрии и обработка фрагментов.
Рисунок 1. Обработка геометрии.
На первой стадии графического конвейера выполняется преобразование координат (применение мировой, видовой и проекционной матриц) вершины, перевод вершины в пространство отсечения, расчет освещения, применение материалов, определение цвета каждой вершины с учетом всех источников света и генерация текстурных координат.
После выполнения этих операций наступает компоновка примитива. В этой части конвейера, вершины группируются в треугольники и подаются в растеризатор.
Рисунок 2. Обработка фрагментов.
Растеризатор делит треугольник на фрагменты (пиксели), для которых интерполируются текстурные координаты и цвет. Затем для каждого фрагмента происходит выполнение следующих операций: проверка принадлежности пикселя, наложение текстур (заданные для фрагмента координаты текстуры определяют интерполированный цвет из элементов текстурного изображения — текселей, значение этого цвета комбинируется с цветом фрагмента), применение эффектов тумана, альфа-тест, тест шаблона (stencil-test), тест глубины, смешивание, дизеринг и логические операции. После обработки всех этих методов полученный фрагмент помещается в буфер кадра, который впоследствии выводиться на экран.
В каждую из этих частей графического конвейера можно вставить свой определенный шейдер. Существует два различных шейдера: вершинный шейдер (vertex shader) и пиксельный шейдер (pixel shader).
Вершинный шейдер выполняется для каждой вершины (в данном контексте — это структура, состоящая из нескольких вершинных атрибутов, одним из которых должна быть позиция вершины), проходящей по графическому конвейеру, и его нужно установить в разделе обработки вершин. В нем можно изменять атрибуты, такие как нормаль, позиция, текстурные координаты, цвет вершины и т.д. Эти данные вершинный шейдер получает из вершинного буфера.
Во время использования вершинного шейдера следующие части графического конвейера не выполняются: трансформация из мирового пространства в пространство отсечения, нормализация, освещение и материалы, генерация текстурных координат. Соответственно и устанавливаемые render states тоже не будут оказывать никакого эффекта на вершинный шейдер.
Пиксельные шейдеры в свою очередь предоставляют широкие возможности по обработке фрагментов. Они позволяют пользователю по шагам управлять процессом наложения текстур, определения глубины и вычисления цвета фрагментов. Что это дает? Во-первых, можно создавать в играх per-pixel lighting т.е. реальное освещение (в статье рассматриваются самые известные методы освещения). Во-вторых, позволяет создавать красивые эффекты с частицами (например, огонь, дым, капли дождя). И многое другое.
Пиксельные шейдеры выполняются для каждого фрагмента в фазе растеризации треугольников. Фрагмент (или пиксель) — точка, с оконными координатами, полученная растеризатором после выполнения над ней ряда операций. Проще говоря, результирующая точка буфере кадра, совокупность этих точек потом формирует изображение. Пиксельные шейдеры оперирует над фрагментами до заключительных стадий, т.е. до тестов глубины, альфы и stencil. Кстати, возможно в будущем и этими операциями будет управлять шейдер, кто знает… Пиксельный шейдер получает интерполированные данные (цвет, текстурные координаты) из вершинного шейдера.
Обзор HLSL.
Tired of writing shaders in assembly language?
Try the high–level shader language!
SDK help (с)
Один из наиболее серьёзных компонентов в DirectX® 9, который предоставляет программирование шейдеров на языке высокого уровня (похожий на язык C), является HLSL(High Level Shading Language), разработанный Microsoft.
Зачем нужен HLSL? Область применения языка программирования графики высокого уровня весьма широка. Такой язык облегчает разработку графических эффектов и создание впечатляющих графических приложений. Кроме того, использование HLSL повышает читаемость кода шейдера и снижает время разработки тяжелого алгоритма. Приведем пример фрагментного шейдера на asm–языке:
ps_2_0
def c0, 1, 1, 0.5, 0.5
mov r0, c0
mov oC0, r0
и тоже самое на HLSL:
float4 main(): COLOR0{
return float4(1.0,1.0,0.5,0.5);
}
|
Можно сделать вывод, что HLSL очень удобен и понятен.
Описание языка HLSL.
Типы данных.
HLSL поддерживает разнообразные типы данных: от простых скаляров до комплексных типов - векторов и матриц.
Скалярные типы:
Тип | Значение
| | bool | true или false
| | int | 32-bit signed integer
| | half | 16-bit floating point value
| | float | 32-bit floating point value
| | double | 64-bit floating point value
|
|
Не все графические процессоры в настоящее время поддерживают определенные типы данных (кроме float, конечно же). Например, целые числа могут эмулироваться через тип float.
Векторные типы:
vector <type, size>
— означает вектор размерностью size, и скалярным типом type.
Можно объявлять еще так:
typeN
– означает N компонентный вектор с данным типом type. Например:
float4 Vector;
или тоже самое можно объявить так:
float Vector[4];
или
vector <float, 4> Vector;
Матрицы:
Подобно скалярам и векторам, матрицы могут иметь все скалярные типы.
typeMxN
— матрица с типом type и размерностью M на N.
matrix<type, M,N>
— матрица с типом type и размерностью M на N.
Можно контролировать ориентацию:
#pragma pack_matrix (row_major);
Доступ к элементам и строкам матрицы:
mat._m00, mat._11, mat [0][0]
mat._m00_m01_m02_m03, mat [0]
Переменные:
Переменная может быть объявлена как static или extern. Любая нестатическая переменная (записывается с приставкой extern или без приставки), которая объявлена за шейдером, может быть изменена через API. Статическая переменная используется только шейдером и не управляется API.
Например:
extern float a;
const float b;
static float c;
float d;
Переменные a и d должны выставляться через API-функцию Set*ShaderConstant*() и их может изменять шейдер. Переменная b тоже задается через Set*ShaderConstant*(), но шейдер не может изменить ее значение. И переменная c не задается через API, но может быть изменена в шейдере.
Инициализация переменных выполняется также как и на языке C.
Например:
float2x2 mat = {1.0f, 0.0f, // ряд 1
1.0f, 2.0f}; // ряд 2
float4 pos = {1.0f, 0.5f, 12.0f, 1.0f};
float f = 0.01f;
|
Структуры:
HLSL поддерживает структуры. Например:
struct VS_OUTPUT
{
float4 Pos;
float3 View;
};
|
Структуры могут быть приведены к/из скалярных величин, векторов, матриц и других структур.
Операторы
Существуют операторы для следующих операций:
Операции | Операторы
| | Арифметические | -, +, *, /, %
| | Инкремент, декремент | ++, --
| | Логические | &&, ||, ?:
| | Унарные | !, -, +
| | Сравнения | <, >, <=, >=, ==, !=
| | Назначение | =, -=, +=, *=, /=
| | Привидение типов | (тип)
| | Комма | ,
| | Член структуры | .
| | Член массива | [индекс]
|
|
Оператор извлечения остатка деления (%) работает как с целыми числами, так и с числами с плавающей точкой.
Сравнение векторов производится покомпонентно.
Ветвление
if (expr) then statement [else statement]
Циклы
do statement while (expr);
while (expr) statement;
for (expr1;expr2;expr3) statement
Функции
Похоже на функции в C, за исключением того, что не поддерживается рекурсия. Параметры функции могут быть семантически связаны с данными.
Некоторые математические инструкции, которые можно применять в HLSL:
abs(x) | абсолютная величина (per-component).
| | acos(x) | возвращает арккосинус каждого компонента x. Каждый компонент должен быть в диапазоне [-1, 1].
| | asin(x) | возвращает арксинус каждого компонента x. Каждый компонент должен быть в диапазоне [-pi/2, pi/2].
| | atan(x) | возвращает арктангенс каждого компонента x. Каждый компонент должен быть в диапазоне [-pi/2, pi/2].
| | ceil(x) | Возвращает наименьшее целое число, которое больше чем или равно x.
| | cos(x) | Возвращает косинус x.
| | cosh(x) | Возвращает гиперболический косинус x.
| | ddx(x) | Возвращает частную производную x относительно screen-space x-координаты.
| | ddy(x) | Возвращает частную производную x относительно screen-space y-координаты.
| | degrees(x) | Конвертирование x с радианы в градусы.
| | distance(a, b) | Возвращает расстояние между двумя точками a и b.
| | dot(a, b) | Возвращает dot product двух векторов a и b.
| | floor(x) | Возвращает самое большое целое число, которое является меньше чем или равным x.
| | fwidth(x) | Возвращает abs(ddx(x))+abs(ddy(x)).
| | len(v) | Векторная длина.
| | length(v) | Возвращает длину вектора v.
| | lerp(a, b, s) | Возвращает a + s (b - a).
| | log(x) | Возвращает логарифм x.
| | log10(x) | Возвращает десятичный логарифм x.
| | mul(a, b) | Делает матричное умножение между a и b.
| | normalize(v) | Возвращает нормализированный вектор v.
| | pow(x, y) | Возвращает xy.
| | radians(x) | Конвертирует x из градусов в радианы.
| | reflect(i, n) | Возвращает вектор отражения.
| | refract(i, n, eta) | Возвращает вектор преломления.
| | rsqrt(x) | Возвращает 1 / sqrt(x).
| | sin(x) | Возвращает синус x.
| | sincos(x, out s, out c) | Возвращает синус и косинус x.
| | sinh(x) | Возвращает гиперболический синус x
| | sqrt(x) | Возвращает квадратный корень(per-component).
| | step(a, x) | Возвращает (x = a) ? 1 : 0.
| | tan(x) | Возвращает тангенс x
| | tanh(x) | Возвращает гиперболический тангенс x
|
|
Использование текстур во фрагментном шейдере.
Если вы хотите использовать текстуры во фрагментном шейдере, то необходимо объявить extern переменную типа sampler. Фактически sampler определяет текстуру, с которой можно читать текущие значение цвета во фрагментном шейдере. Ниже описаны функции, которые возвращают цвет текстуры, исходя из заданных параметров:
tex1D(s, t) | Чтение из одномерной текстуры. s - sampler. t - скаляр.
| | tex1D(s, t, ddx, ddy) | Чтение из одномерной текстуры, с производными. s - sampler. t, ddx, и ddy - скаляры.
| | tex1Dproj(s, t) | Чтение из одномерной проективной текстуры. s - sampler. t - 4D вектор. t делиться на t.w перед выполнением функции.
| | tex1Dbias(s, t) | Чтение из одномерной текстуры со смещением, s - sampler, t - 4-х мерный вектор, Мип-уровень смещается на t.w до того, как производится поиск.
| | tex2D(s, t) | Чтение из двухмерной текстуры. s - sampler. t – 2D вектор.
| | tex2D(s, t, ddx, ddy) | Чтение из двухмерной текстуры, с производными. s - sampler. t – 2D текстурные координаты. ddx, ddy- 2D вектора.
| | tex2Dproj(s, t) | Чтение из двумерной проективной текстуры. s - sampler. t - 4D вектор. t делиться на t.w перед выполнением функции.
| | tex2Dbias(s, t) | Чтение из двумерной текстуры со смещением, s - sampler, t - 4-х мерный вектор, Мип-уровень смещается на t.w до того, как производится поиск.
| | tex3D(s, t) | Чтение из трехмерной текстуры. s - sampler. t – 3D вектор.
| | tex3D(s, t, ddx, ddy) | Чтение из трехмерной текстуры, с производными. s - sampler. t – 2D текстурные координаты. ddx, ddy - 3D вектора.
| | tex3Dproj(s, t) | Чтение из трехмерной проективной текстуры. s - sampler. t - 4D вектор. t делиться на t.w перед выполнением функции.
| | tex3Dbias(s, t) | Чтение из трехмерной текстуры со смещением, s - sampler, t - 4-х мерный вектор, Мип-уровень смещается на t.w до того, как производится поиск.
| | texCUBE(s, t) | [i]Чтение из кубической текстуры. s – sampler, t – 2D текстурные координаты.
| | texCUBE(s, t, ddx, ddy) | Чтение из кубической текстуры. s - sampler. t – 3D текстурные координаты. ddx, ddy - 3D вектора.
| | texCUBEproj(s, t) | Чтение из кубической проективной текстуры. s – sampler, t - 4D вектор. t делиться на t.w перед выполнением функции.
| | texCUBEbias(s, t) | Чтение из кубической текстуры. s – sampler, t - 4D вектор. Мип-уровень смещается на t.w до того, как производится поиск.
|
|
t – текстурные координаты. ddx, ddy – производные.
Есть полезное свойство констант в HLSL это связывание с регистрами констант, например:
sampler tex : register (s0);
Определяет текстуру в 0 stage. А в API это задается так:
RenderDevice->SetTexture(0,texture0);
Входные и исходящие параметры вершинного и фрагментного шейдеров.
Вершинные и фрагментные шейдера имеют два типа входящих данных: varying и uniform.
Uniform — данные, которые постоянны для многократного использования в шейдере. Объявление uniform данных в HLSL можно сделать двумя способами:
1)Объявить данные как extern переменную. Например:
float f;
float4 main () : COLOR
{
return f;
} |
2)Объявить данные через определитель uniform. Например:
float4 main (uniform float4 f) : COLOR
{
return f;
} |
Uniform переменные задаются через таблицу констант. Таблица констант содержит все регистры, которые постоянно используются в шейдере.
Varying — данные, которые являются уникальными для каждого вызова шейдера. Например: позиция, нормаль и т.д. В вершинном шейдере такая семантика описывает varying данные, которые передаются из вершинного буфера, а во фрагментом шейдере — интерполированные данные, полученные из вершинного шейдера. Основные входящие семантические типы:
POSITIONn | Позиция.
| | BLENDWEIGHTn | Весовой коэффициент
| | BLENDINDICESn | Индекс весовой матрицы
| | NORMALn | Нормаль.
| | PSIZEn | Размер точки.
| | COLORn | Цвет.
| | TEXCOORDn | Текстурные координаты.
| | TANGENTn | Тангент.
| | BINORMALn | Бинормаль.
| | TESSFACTORn | Фактор тесселяции.
|
|
Использование varying данных во фрагментном шейдере определяет состояние одного фрагмента. Основные входящие семантические типы:
COLORn | Цвет.
| | TEXCOORDn | Текстурные координаты.
|
|
n – определяет номер семантического типа. Например: TEXCOORD1, NORMAL0.
Исходящие данные шейдера определяют способ линковки с входящими данными следующего этапа графического конвейера. Например, исходящая семантика для вершинного шейдера связывает исходящие данные с интерполянтами в растеризаторе, который генерирует входящие данные для фрагментного шейдера.
Исходящие данные для вершинного шейдера:
POSITION | Позиция.
| | PSIZE | Размер точки.
| | FOG | Коэффициент “туманности” для вершины.
| | COLORn | Цвет.
| | TEXCOORDn | Текстурные координаты.
|
|
Исходящие данные для фрагментного шейдера:
COLORn | Цвет.
| | DEPTH | Значение глубины.
|
|
Теперь, когда мы обсудили синтаксис языка и связывание с графическим конвейером входящих/исходящих данных шейдеров, напишем простой пример на HLSL.
Dizzy эффект.
Первый эффект, который мы рассмотрим, называется dizzy (с английского — головокружительный). Смысл эффекта заключается в отображении анимированных спирально-закрученных колец.
Алгоритм для вершинного шейдера:
1) Применяем видовую, мировую и проекционную матрицы на позицию вершины.
2) Сохраняем в текстурные координаты (TEXCOORD0) позицию нетрансформированной вершины для фрагментного шейдера. Т.е. входящими данными у нас будут: POSITION, а исходящими - TEXCOORD0.
Алгоритм для фрагментного шейдера:
1) Получаем интерполированные данные (TEXCOORD0 – т.е. наши текстурные координаты).
2) Вычисляем арктангенс между текстурными координатами x,y.
3) Вычисляем радиус между текстурными координатами. По уравнению окружности:
R2=(x-a)2+(y-b)2. Для удобства примем a = 0, b = 0.
4) Возвращаем цвет, по общей формуле sin(угол + радиус). Т.е. некое смещение, которое из-за функции синуса будет закручивать кольца.
Для того, что бы получить анимированную картинку необходимо еще ввести параметр time, который своим приростом (или наоборот, что повлияет на направление вращения) будет закручивать кольца. Еще можно ввести параметр num_ring, который определяет количество закручиваемых колец. Его надо умножить на радиус, ведь чем больше значение радиуса, тем больше расстояние между кольцами.
При подстановке этих параметров конечная формула примет вид:
цвет=sin(угол + количество колец * радиус + время).
Вот фрагментный шейдер этого эффекта:
float4 main(float2 texCoord: TEXCOORD0) : COLOR
{
float ang = atan2 (texCoord.x, texCoord.y);
float rad = dot (texCoord, texCoord);
return 0.5*(1 + sin (ang + rings * rad + time));
}; |
И вершинный шейдер:
float4x4 mvp;
//Структура для исходящих данных. Эти данные будут передаваться в растеризатор,
// а потом во фрагментный шейдер.
struct VS_OUTPUT
{
float4 Pos: POSITION;
float2 TexCoord: TEXCOORD0;
};
//Для входящих данных нам нужна только позиция объекта, которая задается в вершинном буфере.
VS_OUTPUT main(float4 Pos: POSITION)
{
VS_OUTPUT Out;
//применение видовой, мировой и проекционной матрицы на позицию вершины
Out.Pos = mul(mvp,Pos);
//Приравниваем координаты вершины в текстурные координаты.
Out.TexCoord = normalize(Pos.xy);
return Out;
} |
В качестве более трудных примеров рассмотрим модели освещения.
Модели освещения.
Наверное, каждый слышал о моделях освещения, они окружают нас повсюду: например, возьмем любой объект, тот же стакан, стоящий на столе. На него падает некий свет, если по точнее, то свет от солнца или любого другого источника освещения, это и называется некоторой моделью освещения. На практике обычно никто не пытаются запрограммировать все физические принципы света, поскольку их вычисление очень дорого обходится. Однако придумано множество моделей дающих реалистичный результат при малых расчетах.
Рассмотрим все подробнее, создадим простую модель освещения — равномерное освещение.
1. Равномерное освещение.
Равномерное освещение (ambient lighting) обеспечивает постоянное начальное освещение для всей сцены. Оно освещает все вершины объектов одинаково, потому что не зависит ни от каких других факторов освещения. Это самый простой и быстрый тип освещения, но при этом дает наименее реалистичный результат. Формула для вычисления этой модели освещения так же очень проста, т.к. там всего одна арифметическая операция — умножение. Для ее вычисления достаточно перемножить цвет материала на интенсивность освещения.
Iambient=ka×Ia
Вершинный шейдер для расчета равномерной модели освещения:
//Произведение мировой, видовой и проекционной матриц.
float4x4 mat_mvp;
// Входящие данные.
struct VS_INPUT_STRUCT
{
float4 position: POSITION;
};
// Исходящие данные.
struct VS_OUTPUT_STRUCT
{
float4 position: POSITION;
};
VS_OUTPUT_STRUCT main (VS_INPUT_STRUCT In_struct)
{
VS_OUTPUT_STRUCT Out_struct;
Out_struct.position = mul (mat_mvp, In_struct.position);
return Out_struct;
}
|
Этот простейший шейдер просто вычисляет позицию объекта в мировом пространстве, но в данной модели освещения можно его и не использовать, поскольку от вершины нам не надо каких то других компонентов (нормаль и т.д.) для расчета.
float4 ambient_color; // Цвет материала.
float ambient_intensity; // Интенсивность цвета.
float4 main(): COLOR0
{
// Возвратим Результат из формулы AI*AC.
return ambient_color * ambient_intensity;
}
|
Здесь тоже нет ничего сложного, просто возвращаем компилятору произведение вектора, на какое то число в данном случае ambient_intensity. Делает он это так:
ambient_intensity * ambient_color.x;
ambient_intensity * ambient_color.y;
ambient_intensity * ambient_color.z;
ambient_intensity * ambient_color.w;
Модель равномерного освещения:
Для того чтобы вычислить бликовый и диффузный компоненты света, необходимо найти три вектора:
• Нормаль N к фрагменту.
• Видовой вектор V — вектор, который направлен на наблюдателя.
• Позицию источника света L.
Углы между этими векторами составляют интенсивность освещения.
2. Диффузная модель освещения.
Диффузная модель освещения (diffuse lighting model) — модель освещения, которая зависит от положения источника освещения и от объектной нормали поверхности. Поскольку излучение света одинаково во всех направлениях, видовой вектор не имеет значения, т.е. v=0. Такой метод требует большего вычисления, так как изменяется для каждой вершины объекта, однако неплохо затеняет объекты и придает им объем. Свет падает, не заполняя всю поверхность одинаковым цветом (как в случае с раномерным освещением), а создается впечатление, что, свет направлен на какую либо поверхность.
Если вектор позиции источника освещения перпендикулярен поверхности, то никакой матовости не будет наблюдаться, потому что интенсивность света зависит от угла α. Для расчета диффузной модели освещения используется формула (по закону Ламберта):
Idiffuse=kd×Id×(N•L)
Подведем итоги – создадим шейдеры для расчета освещения.
Вершинный шейдер:
float4x4 mat_mvp; //Произведение мировой, видовой и проекционной матриц.
float4x4 mat_world; //Мировая матрица.
float4 vec_light; //Позиция источника света
struct VS_INPUT_STRUCT //Входящие данные
{
float4 position: POSITION;
float3 normal: NORMAL;
};
struct VS_OUTPUT_STRUCT //Исходящие данные
{
float4 position: POSITION;
float3 light: TEXCOORD0;
float3 normal: TEXCOORD1;
};
VS_OUTPUT_STRUCT main(VS_INPUT_STRUCT In_struct)
{
VS_OUTPUT_STRUCT Out_struct;
//Вычисляем позицию вершины.
Out_struct.position = mul(mat_mvp,In_struct.position);
//Сохраняем позицию источника для передачи во фрагментный шейдер в виде
//3D текстурных координат.
Out_struct.light = vec_light;
//Рассчитываем нормаль поверхности и сохраняем для фрагментного шейдера.
Out_struct.normal = normalize(mul(mat_world,In_struct.normal));
//*под словом “сохраняем” имеется ввиду посылание данных в растеризатор,
//а только потом в вершинный шейдер.
return Out_struct;
}
|
И пиксельный шейдер:
float diffuse_intensity;
float4 diffuse_color;
float4 main(float3 light: TEXCOORD0, float3 normal: TEXCOORD1):COLOR0
{
return diffuse_color * diffuse_intensity * dot(normal,light);
}
|
Диффузная модель освещения:
3.Бликовая модель освещения.
Сложно, представить такую модель освещения не увидев ее. А на самом деле эту модель мы можем увидеть почти везде. Например, чисто отполированную (круговыми движениями) прямую металлическую поверхность, направив на нее источник света, и посмотрев под неким углом, который не перпендикулярен поверхности. В результате мы увидим блики на поверхности, которые существенно увеличивают реалистичность изображения. Эти блики являются отражением источника света от поверхности. В этой модели освещения помимо векторов позиции источника освещения и нормали (как в случае с диффузной моделью освещения) используются еще два вектора: видовой вектор и вектор отражения. Бликовую модель освещения (specular lighting model) предложил Буи-Туонг Фонг.
Угол между видовым вектором и вектором отражения – β. Чем больше угол β, тем ярче бликовое освещение. Поэтому бликовая модель освещения вычисляется по следующей формуле:
Ispecular=ks×Is×(V•R)n
где R=reflect (–norm(V), N)
n — коэффициент яркости свечения.
Зависимость яркости свечения от угла β:
С ростом параметра n отражение становиться все более бликовым и все более концентрируется вдоль направления вектора отражения R.
Вершинный шейдер:
float4x4 mat_mvp; //Произведение мировой, видовой и проекционной матриц.
float4x4 mat_world; //Мировая матрица.
float4 vec_light; //Позиция источника света
float3 vec_view_pos; //Видовой вектор
float4 vec_eye; //Позиция наблюдателя
struct VS_INPUT_STRUCT //Входящие данные
{
float4 position: POSITION;
float3 normal: NORMAL;
};
struct VS_OUTPUT_STRUCT //Исходящие данные
{
float4 position: POSITION;
float3 light: TEXCOORD0;
float3 normal: TEXCOORD1;
float3 view: TEXCOORD2;
};
VS_OUTPUT_STRUCT main(VS_INPUT_STRUCT In_struct)
{
VS_OUTPUT_STRUCT Out_struct;
//Трансформируем позицию вершины
Out_struct.position = mul(mat_mvp,In_struct.position);
//Сохраняем позицию источника для передачи в пиксельный шейдер в виде
//3D текстурных координат.
Out_struct.light = vec_light;
//Рассчитываем нормаль поверхности и сохраняем для пиксельного шейдера.
Out_struct.normal = normalize(mul(mat_world,In_struct.normal));
//Вычисляем видовой вектор и сохраняем для пиксельного шейдера.
Out_struct.view = vec_eye - vec_view_pos;
return Out_struct;
}
|
Необязательно вычислять видовой вектор в шейдере, можно вычислить в программе и занести в вершинный шейдер. Для этого нужно инвертировать видовую матрицу и умножить на вектор D3DXVECTOR4(0.0,0.0,0.0,1.0) — позиция при которой вектор перпендикулярен поверхности (смотрит на нас). Выглядит это так:
D3DXMATRIXA16 mat_temp,mat_view_inverse;
D3DXVECTOR4 view_pos;
mat_temp = mat_world * mat_view;
D3DXMatrixInverse(&mat_view_inverse,NULL,&mat_temp);
D3DXVec4Transform(&view_pos,(D3DXVECTOR4*)&D3DXVECTOR4(0.0f,0.0f,0.0f,1.0f));
Пиксельный шейдер:
float4 specular_color;
float4 specular_intensity;
struct PS_INPUT_STRUCT
{
float3 light: TEXCOORD0;
float3 normal: TEXCOORD1;
float3 view: TEXCOORD2;
};
float4 main(PS_INPUT_STRUCT In):COLOR0
{
float power =16;
float3 reflect_vec=reflect(-normalize(In.view),In.normal);
return specular_color*specular_intensity*pow(dot(reflect_vec, In.light),power);
}
|
Бликовая модель освещения:
Модификация бликового освещения по Блинну.
Джим Блинн придумал альтернативный способ вычисления бликового освещения, который устраняет дорогие вычисления над вектором отражения. Он ввел промежуточный вектор, который является средним значением между видовым вектором и вектором позиции источника освещения: H=(L+V)/(|L+V|)
Общая формула имеет вид:
Iblin_specular=kb_s×Ib_s×(N•H)n
Поэтому фрагментный шейдер уже будет такой:
float4 main(PS_INPUT_STRUCT In_struct):COLOR0
{
float3 H=normalize(In.light+In.view);
float n = 16;
return specular_color*specular_intensity*pow(dot(In.normal,H),n);
}
|
Ускорение вычисления яркости свечения.
Шлик предложил замену степени n. Пусть скалярное произведение равно D: D=(N•H)n, тогда по его способу яркость свечения будет вычисляться следующим образом:
Пиксельный шейдер:
float4 main(PS_INPUT_STRUCT In_struct):COLOR0
{
float3 H=normalize(In.light+In.view);
float n = 16;
float D=dot(In.normal,H);
return specular_color*specular_intensity*D/(n-D*n+D);
}
|
Сравнительные графики степенных законов:
На самом деле физический смысл бликового отражения света намного сложнее, чем предполагается в модели освещения Фонга. В более реалистичной модели Is зависит от длины волны l и от угла падения света (N•L). Такая зависимость называется коэффициентом Френеля.
Комбинирование компонентов освещения.
Теперь мы можем сложить три модели освещения (постоянное, диффузное и бликовое), чтобы получить суммарное количество света I, получаемое глазом:
Пиксельный шейдер теперь будет такой:
struct PS_INPUT_STRUCT
{
float3 light: TEXCOORD0;
float3 normal: TEXCOORD1;
float3 view: TEXCOORD2;
};
float4 ambient_color; // Цвет материала.
float ambient_intensity; // Интенсивность цвета.
float diffuse_intensity;
float4 diffuse_color;
float4 specular_color;
float4 specular_intensity;
float n;
float4 main(PS_INPUT_STRUCT In_struct):COLOR0
{
float3 H=normalize(In.light+In.view);
float n = 16;
float D= dot(In.normal,H);
return ambient_color * ambient_intensity +
diffuse_color * diffuse_intensity*dot(In.normal, In.light) +
specular_color*specular_intensity*D/(n-D*n+D);
}
|
Реалистичное освещение на основе Кука-Торренса
В более реальных моделях освещения основное внимание уделяется на распределение энергии падающего света. Часть ее поглощается материалом и превращается в тепло, другая часть рассеивается в виде диффузного света, третья часть задает поверхности бликовую освещенность. Поэтому для различных материалов разделение падающего света происходит по-разному, и зависит оно от:
• Функции распределения нормалей
• Затенения и экранирования
• Коэффициента Френеля
Функция распределение нормалей
Эта функция описывает возможное отклонение нормали к поверхности от идеальной нормали N. Чем более эта функция пологая, тем большие отклонения допустимы и тем большей величины пятно отраженного блика. Необходимые нормали расположены вдоль вектора L+V и видимы в направлении V и находятся под углом (H•N) к зрителю. Кук и Торренс использовали формулу распределения Бекмана:
где d=H•N
m – степень шероховатости объекта. 0.2f – гладкая поверхность, 0.6f – шероховатая. По умолчанию ставят 0.3f.
График распределения Бекмана:
Затенение и экранирование
В модели Кука–Торренса учитываются также и такие эффекты, как затенение (shadowing) и экранирование (masking), которые определяют интенсивность бликовой составляющей.
Неэкранированный свет равен:
Незатененный свет равен:
Тогда финальный множитель G равен:
G=min(1, Gm, Gs)
Коэффициент Френеля
Этот коэффициент определяет долю отраженного света и задается функцией:
где f — угол падения, косинус, которого равен c=(N•H)
n — показатель преломления материала, g=sqrt(n2+c2–1)
В действительности, в шейдере мы будем использовать другую формулу для расчета коэффициента Френеля из-за ограниченности инструкций. Замену предложил Шлик:
F=Rs+(1–Rs)×(1–E•N)n
где Rs — бликовое отражение,
E — вектор наблюдателя,
N — нормаль из карты нормалей.
Такая аппроксимация не учитывает соответственно f и n компоненты, но имеет степень n, с увеличением которой можно добиться не плохих результатов. Шлик использовал n=5.
Комбинирование всех множителей
Торренс и Спэрроу объединили эти множители и вывели формулу для подсчета бликового света:
Знаменатель N•V введен для регулирования интенсивности света.
Общая формула для расчета количества света такая:
Пришлось формулу немного упростить, так как в оригинале каждый компонент освещения (кроме бликового) умножается еще и на F(0, n). Сделано это из-за использования аппроксимации Шлика.
Пиксельный шейдер этого эффекта:
float roughness;
sampler SAMP_COLOR;
sampler SAMP_BUMP;
struct PS_INPUT
{
float2 T : TEXCOORD0;
float3 L : TEXCOORD1;
float3 V : TEXCOORD2;
float3 H : TEXCOORD3;
};
float4 main(PS_INPUT IN): COLOR0
{
float3 N = tex2D(SAMP_BUMP,IN.T);
N = normalize(2.0f * N - 1.0f);
IN.L = normalize(IN.L);
IN.V = normalize(IN.V);
IN.H = normalize(IN.H);
float r2 = roughness * roughness;
float exponent = -(1-dot(N,IN.H) * dot(N,IN.H))/(dot(N,IN.H) * dot(N,IN.H)*r2);
float D = pow(2.71,exponent) / (r2*dot(N,IN.H) * dot(N,IN.H)*dot(N,IN.H) * dot(N,IN.H));
float F = pow(1 - dot(N,IN.V),5);
float G = min(1, min(2.0f * dot(N,IN.H) / dot(IN.V,IN.H) *
dot(N,IN.L), 2.0f * dot(N,IN.H) / dot(IN.V,IN.H) * dot(N,IN.V)));
float4 Spec = max(0.0f,(D*F*G) / (dot(N,IN.V) * 3.14));
float4 Diff = max(0.0f,dot(N,IN.L));
return tex2D(SAMP_COLOR,IN.T) * (Diff + Spec);
}
|
И вершинный шейдер:
float4x4 view_proj_matrix: register(c0);
float4x4 view_matrix;
float4 light_vec;
float4x4 inv_view_matrix;
float4 view_position;
struct VS_INPUT
{
float4 mPosition : POSITION0;
float3 mNormal : NORMAL;
float2 mCoord : TEXCOORD0;
float3 mTangent : TANGENT;
float3 mBinormal : BINORMAL;
};
struct VS_OUTPUT
{
float4 P: POSITION0;
float2 T : TEXCOORD0;
float3 L : TEXCOORD1;
float3 V : TEXCOORD2;
float3 H : TEXCOORD3;
};
VS_OUTPUT main(const VS_INPUT IN)
{
VS_OUTPUT OUT;
OUT.P = mul(view_proj_matrix,IN.mPosition);
OUT.T = IN.mCoord;
OUT.L = float3(dot(light_vec,IN.mTangent),dot(light_vec,IN.mNormal),dot(light_vec,IN.mBinormal));
OUT.V = float3(dot(view_position,IN.mTangent),
dot(view_position,IN.mNormal),dot(view_position,IN.mBinormal));
OUT.H = (OUT.L + OUT.V);
return OUT;
}
|
Specular Bump Mapping.
Ну конечно, без этого никак :), хотелось бы рассказать про этот эффект…
Вот мы и подошли к более серьезным моделям освещения, нам придется рассчитать вектор освещения, создать неортогональную систему координат, и т.д. Для тех кто не знает что такое bump mapping, то выглядит он так:
Для реализации этого эффекта нам понадобиться две текстуры: base map и normal map, base map — это diffuse текстура, а normal map текстуру нам придется делать самим (почти самим). Для этого нужно скачать plug-in к Photoshop (На сайте nVidia есть этот plug-in), или любую другую программу которая конвертирует из height map в dot3. Или же воспользоваться программой normal mapper, которая находиться в Radeon DX SDK. Суть bump mapping’a состоит в том, чтобы сделать иллюзию выпуклости какой либо поверхности. Для bump mapping’а понадобиться tangent space. Вместо normal’и полигона мы будем брать normal map (dot3 текстуру), а tangent и binormal нам понадобятся для расчета освещения. Еще одна трудность правильного bump mapping’a (specular bump mapping’a) состоит в том, что нужно рассчитывать tangent space (в математической литературе привычно называть tangent space касательное пространство). В этой системе координат normal будет осью z, binormal — y, и tangent — x.
Так выглядит tangent space для вершины полигона:
Для расчета нормалей и tangent в D3DX есть следующие функции:
D3DXComputeNormals(…)
D3DXComputeTangent(…) |
Так что давайте сначала сосчитаем normal и tangent:
LPD3DXMESH ResultMesh,pMeshSysMem2,pMeshSysMem = NULL;
// dcl_position
// dcl_normal
// dcl_texcoord
// dcl_tangent
D3DVERTEXELEMENT9 decl[]=
{
//stream, offset, type, method, semantic type
{0,0, D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_POSITION,0},
{0,12,D3DDECLTYPE_FLOAT2,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL, 0},
{0,24,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_TEXCOORD,0},
{0,36,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_TANGENT, 0},
D3DDECL_END()
};
m_pd3dDevice->CreateVertexDeclaration(decl,&m_pDecl);
// Load mesh from .x
d3d__AddMesh(m_pd3dDevice,m_strTorus,&pMeshSysMem,D3DXMESH_SYSTEMMEM);
// Clone SysMesh #1 into SesMesh #2.
pMeshSysMem->CloneMesh(D3DXMESH_MANAGED,decl,m_pd3dDevice,&pMeshSysMem2);
D3DXComputeNormals(pMeshSysMem2,NULL); // compute the normals
D3DXComputeTangent(pMeshSysMem2,0,0,0,TRUE,NULL); // compute tangent(u)
// and later binormal (v)
// New vertex declaration
// dcl_position
// dcl_normal
// dcl_texcoord
// dcl_tangent
D3DVERTEXELEMENT9 decl2[]=
{
{0,0, D3DDECLTYPE_FLOAT4,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_POSITION,0},
{0,12,D3DDECLTYPE_FLOAT2,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL, 0},
{0,24,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_TEXCOORD, 0},
{0,36,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_TANGENT, 0},
D3DDECL_END()
};
// Clone SysMesh into ResultMesh.
pMeshSysMem2->CloneMesh(D3DXMESH_MANAGED,decl2,m_pd3dDevice,&ResultMesh);
SAFE_RELEASE(pMeshSysMem);
SAFE_RELEASE(pMeshSysMem2); |
Шейдеры…
vertex shader
float4x4 m_matMVP; // World * View * Projection
float4x4 m_matWorld; // World transposed
float4 m_vLightPos; // Light position
float4 m_vViewPos; // View position
struct VS_INPUT_STRUCT
{
float4 position: POSITION; // Vertex position
float3 normal: NORMAL0; // Vertex normal
float2 texcoord: TEXCOORD0; // Vertex texcoord
float3 tangent: TANGENT; // Tangent ( u )
float3 binormal: BINORMAL; // Binormal( v )
};
struct VS_OUTPUT_STRUCT
{
float4 position: POSITION; // Vertex position
float2 texcoord: TEXCOORD0; // Texture coordinates
float3 light: TEXCOORD1; // Light vector
float3 view: TEXCOORD2; // View vector
};
VS_OUTPUT_STRUCT main(VS_INPUT_STRUCT In_struct)
{
VS_OUTPUT_STRUCT Out_struct;
Out_struct.position = mul(m_matMVP,In_struct.position);
Out_struct.texcoord = In_struct.texcoord;
float3x3 matTangentSpace;
// Calculate binormal
In_struct.binormal = cross(In_struct.tangent,In_struct.normal);
matTangentSpace[0] = mul(m_matWorld,In_struct.tangent); // x
matTangentSpace[1] = mul(m_matWorld,In_struct.binormal);// y
matTangentSpace[2] = mul(m_matWorld,In_struct.normal); // z
// Calculate light vector and move into tangent space
Out_struct.light.xyz = mul(matTangentSpace,m_vLightPos);
// Calculate view vector and move into tangent space
Out_struct.view = mul(matTangentSpace,m_vViewPos);
return Out_struct;
} |
fragment shader
float4 m_vAmbient;
float4 m_vDiffuse;
float4 m_vSpec;
float4 m_vHelper;
sampler2D m_Base: register(s0); // Base map
sampler2D m_Bump: register(s1); // Dot3 map
float4 main(float2 texcoord: TEXCOORD0;
float3 light: TEXCOORD1;
float3 view: TEXCOORD2): COLOR0;
{
float4 tx_base = tex2D(m_Base,texcoord); // Load base texture
float4 tx_bump = tex2D(m_Bump,texcoord); // Load bump texture
tx_bump = normalize((tx_bump * 2.0) – 1.0f);//Bias normal to range [-1,1]
float3 nrmd_light = normalize(light); // Normalize light
float3 nrmd_view = normalize(view); // Normalize view
// Calculate reflect vector
float4 reflect_vec = reflect(nrmd_view,nrmd_light);
// Normal dot light
float4 n_dot_l = dot(tx_bump,nrmd_light);
// Reflect dot view
float4 r_dot_v = dot(reflect_vec,nrmd_view);
// Calculate ambient model
float4 ambient = tx_base * m_vAmbient * m_vHelper.x;
// Calculate diffuse model
float4 diffuse = tx_base * m_vDiffuse * m_vHelper.y * max(N_dot_L,0.0f);
// Calculate specular model
float4 spec = m_vSpec * m_vHelper.z + pow(r_dot_v, m_vHelper.w);
// Result lighting model
float4 light_m = ambient + diffuse + spec;
return light_m;
} |
Теперь о переменных в шейдерах…
m_matMVP – ModelViewProjection matrix
m_matWorld – World matrix
m_vLightPos – Light position
m_vViewPos – View position
m_vAmbient – Ambient color
m_vDiffuse – Diffuse color
m_vSpec - Specular color
m_vHelper.x - Ambient intensity
m_vHelper.y – Diffuse intensity
m_vHelper.z – Specular intensity
m_vHelper.w – Specular exponent
Для работы примера достаточно установить все константы, например, так:
If(m_hLocation = m_pVertShader->GetConstantByName(NULL,”Нужная константа”))
{
// Здесь ставить константу.
} |
Wood Shader.
Эффект заключается в генерировании древесной поверхности объекта. Вот так выглядит wood shader:
wood shader без освещения и с освещением.
Для такого эффекта нам понадобится текстура, обработанная noise фильтром. Пример такой текстуры:
Древесная поверхность состоит из колец, оси которых постоянно меняются и как бы скачут взад-вперед между полосами. Такой эффект легко можно задать функцией остатка: Rings(R)=frac(R),
где R – радиус колец, который вычисляется между текстурными координатами x и y.
Алгоритм для фрагментного шейдера:
1)Рассчитываем вектор искривления линии (Wooble или Skew), который равен суме текстурных координат и цвета из noise текстуры.
float3 shade = pos + (tex2D(Noise,pos));
Т.е. примерно получится что-то такое:
2)Вычисляем длину вектора искривления линии:
float dist = length (shade);
В эту часть алгоритма также можно добавить некий параметр num, который задает количество линий: float dist = length(shade)*num;
3)Полученное значение dist осталось занести в основную функцию древесного шейдера: float main_color = frac(dist); И для придания натуральности древесной поверхности помножим main_color на коричневый цвет:
return main_color*float4(0.5,0.3,0.01,0);
Все бы хорошо, но без освещения такой эффект не смотрится. Добавим диффузионную модель освещения. Для этого помножим результирующий свет на формулу расчета диффузной освещенности:
return main_color*float4(0.5,0.3,0.01,0)*((diffuse_color * diffuse_intensity) *dot(normal, lt));
Фрагментный шейдер:
float4 ambient_color;
float ambient_intensity;
float4 diffuse_color;
sampler noise_texture;
sampler line_color_texture;
float4 main (float2 coords : TEXCOORD0, float3 lt: TEXCOORD1, float3 normal: TEXCOORD2) : COLOR
{
float3 shade = pos + (tex2D(Noise,pos));
float dist = length (shade);
float main_color = frac(dist);
return main_color*float4(0.5f,0.5f,0.01f,0.0f)*(( diffuse_color *
diffuse_intensity)*dot(normal, lt));
}
|
Алгоритм вершинного шейдера составляет точную копию алгоритма вершинного шейдера dizzy эффекта.
Вершинный шейдер:
float4x4 view_proj_matrix;
float4x4 view_matrix;
float4 light;
struct VS_OUTPUT
{
float4 Pos : POSITION;
float2 TexCoords : TEXCOORD0;
float3 light : TEXCOORD1;
float3 normal : TEXCOORD2;
};
VS_OUTPUT main (float4 pos: POSITION, float3 normal: NORMAL)
{
VS_OUTPUT Out = (VS_OUTPUT) 0;
Out.Pos = mul (view_proj_matrix,pos);
//масштабирование текстурных координат.
Out.TexCoords = pos/25;
Out.light=light;
Out.normal=normalize(mul(view_matrix,normal));
return Out;
}
|
Оптимизация кода.
1. Не изобретайте колесо! Используйте intrinsic функции. Например:
Неоптимизированный код:
float DOT3(float3 v1,float3 v2)
{
return(v1.x * v2.x +v1.y * v2.y +v1.z * v2.z);
}
float v=DOT3(V,R);
на выходе:
mul r0.xy, v0, c0
add r0.w, r0.y, r0.x
mad oPos, c0.z, v0.z, r0.w
Используя dot3 вместо DOT3, на выходе получите такое:
dp3 r0.w, v0, c1
2. Всячески избегайте привидения типов и пытайтесь всегда выбрать необходимый тип. Например:
Неоптимизированный код:
sampler smth;
float4 main(float2 coor: TEXCOORD2) : COLOR
{
float3 col=tex2D(smth, coor);
return float4(col,0);
}
|
на выходе:
ps_2_0
def c0, 0, 0, 0, 0
dcl t2.xy
dcl_2d s0
texld r0, t2, s0
mov r0.w, c0.x
mov oC0, r0
|
Оптимизированный код:
sampler smth;
float4 main(float2 coor: TEXCOORD2) : COLOR
{
float4 col=tex2D(smth, coor);
return col;
}
|
на выходе:
ps_2_0
dcl t2.xy
dcl_2d s0
texld r0, t2, s0
mov oC0, r0
|
3. Объединяйте скаляры в вектора. Таким образом, можно уменьшить число констант в вашем шейдере. Например:
Неоптимизированный код:
float x, y;
float4 main (float4 pos :POSITION) : POSITION
{
return (pos * x+y);
}
|
на выходе:
vs_2_0
dcl_position v0
mul r0, v0, c0.x
add oPos, r0, c1.x
|
Оптимизированный код:
float2 vec;
float4 main (float4 pos :POSITION) : POSITION
{
return (pos * vec.x + vec.y);
}
|
на выходе:
vs_2_0
dcl_position v0
mad oPos, v0, c0.x, c0.y
|
4. Для ветвлений используйте тип bool как static. Например:
Неоптимизированный код:
float4 a;
bool b = true;
float4 main(float4 x: TEXCOORD0, float4 y : TEXCOORD1) : COLOR
{
if(b)
return x+a;
else
return y+a;
}
|
на выходе:
ps_2_0
dcl t0
dcl t1
add r1, t0, c0
add r0, t1, c0
cmp r0, -c1.x, r0, r1
mov oC0, r0
|
Объявление переменной b как static намного оптимизирует выходной код:
ps_2_0
dcl t0
add r0, t0, c0
mov oC0, r0
|
5. Векторизируйте типы. Например:
Неоптимизированный код:
float4 main (float k: COLOR) : COLOR
{
float a, b, c, d;
a = k + 1;
b = k + 2;
c = k + 3;
d = k + 4;
return float4 (a, b, c, d);
}
|
на выходе:
ps 2_0
def c0, 1, 2, 3, 4
dcl v0.x
add r0.x, v0.x, c0.x
add r0.y, v0.x, c0.y
add r0.z, v0.x, c0.z
add r0.w, v0.x, c0.w
mov oC0, r0
|
Оптимизированный код:
float4 main(float k: COLOR) : COLOR
{
float4 v;
v = k + float4(1,2,3,4);
return v;
}
|
на выходе:
ps_2_0
def c0, 1, 2, 3, 4
dcl v0.x
add r0, v0.x, c0
mov oC0, r0
|
Завершение.
Несколько советов:
Подсказка: В помощь программисту шейдеров.
Рекомендуется скачать Render Monkey™ с сайта ATi Technologies®.
Программу Shader Works от Mad Software.
Подобную программу от NVIDIA® — FXComposer™.
Посетить сайт ShaderX
Fragment Level Phong Illumination: http://esprit.campus.luth.se/~humus/.
Еще серия статей: http://www.gamedev.net/columns/hardcore/
Про ppl: http://www.dimensions3.host.sk/web/news.php
А так же: http://www.shadertech.com/ — есть ссылки на разные примеры.
P.S. По поводу API, каждый должен понимать, что все подобные эффекты можно сделать и на OpenGL, применяя при этом или GL_VERTEX/FRAGMENT PROGRAM либо на новом высокоуровневом языке GLSL или Cg.
Особую благодарность авторы выражают IronPeter'у.