Программирование шейдеров на HLSL. (4 стр)
Модели освещения.
Наверное, каждый слышал о моделях освещения, они окружают нас повсюду: например, возьмем любой объект, тот же стакан, стоящий на столе. На него падает некий свет, если по точнее, то свет от солнца или любого другого источника освещения, это и называется некоторой моделью освещения. На практике обычно никто не пытаются запрограммировать все физические принципы света, поскольку их вычисление очень дорого обходится. Однако придумано множество моделей дающих реалистичный результат при малых расчетах.
Рассмотрим все подробнее, создадим простую модель освещения — равномерное освещение.
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). Сделано это из-за использования аппроксимации Шлика.
Пиксельный шейдер этого эффекта: