Динамическая система освещения двумерной графики

tubes — копия

Свет является неотъемлемой частью любой картины. Без необходимого затенения зрителю будет довольно сложно представить формы объекта или даже атмосферу композиции, если конечно так не задумано. Но возможно ли добавить этой технике движения, изменчивости?

Реализм

Многочисленные способы наложения теней, бликов в двумерной графике направлены — естественным образом — на достижения сходства с реальным миром. Уберите весь свет и тени — вместе с ними исчезнут и все соответствующие оттенки.

machine&hu_b

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

tumblr_musjptgsp81shus8eo1_r2_400 4beffa59f01c654725378a451b3570c7_original

Это прекрасно, однако, заметьте, что художник заранее определил положение источника света. В случае, если персонаж перемещается, — это будет работать не совсем верно. А что, если бы мы могли двигать источник света? — Наше освещение обрело бы динамическую форму. Разбивая картину на составляющие, мы попробуем воссоздать статичное освещение программными методами.

Цвета и формы поверхностей

В качестве сырья нам необходимы карты цвета и поверхностей.

taketwo

Первое изображение называют диффузной картой, которая задает только цвета текстуры. Это несколько сложно: воссоздать диффузную карту из уже затененного изображения, так как возникнет необходимость вручную убирать все тени.


Куда правильнее — рисовать изначально будущую текстуру «плоскими» цветами, избегая всяческих форм и объема.

Вторая текстуракарта нормалей. Насколько мы знаем, пиксель составляют три компоненты: RGB, а почему бы нам не использовать их как координаты векторов: XYZ?

prev_0

Каждый пиксель будет задавать вектор поверхности в этой точке, что составит форму/рельеф. Для примера мы можем ограничиться так называемой картой высот, обделив вниманием первые два компонента XY.

height_j

Можете заметить, что векторы знакового типа, по этой причине проявляется желтоватый оттенок — XY = 0. Карты нормалей можно задавать вручную, используя специальные палитры, однако, лучше снимать с полигональных моделей.

Абсолютное затенение и освещение

Абсолютным оно названо не случайно, мы представим себе идеальный источник света без рассеивания в виде вектора, направленного под углом α к плоскости поверхности.

angleДля начала рассмотрим случай только с одной плоскостью, заданной одним из вектором карты нормалей. По закону отражения Ламберта (1760) освещенность плоскости имеет зависимость от угла α по закону косинуса — фактически — обратную, вы также можете это проверить сами с помощью лампы и небольшой матовой пластины.

В таком случае для двух векторов: задающий нормаль и свет — вычисление коэффициента освещенности выходит из скалярного произведения последних.

formula_1

Однако Ламберт предполагал, что свет — единичный вектор, т.е. его длина равна единице; для абсолютного затенения нам нужно лишь его направление.

formula_2

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

formula_3

С одной плоскостью все ясно, то же самое проводим с остальными. Нам необходимо проводить эти операции с каждым пикселем, что ж, это ресурсоемкая задача. :-)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void process(image* texture, image* diffuse, image* normal, lightsource light) {
  for (int y=0; y < height; ++y) 
    for (int x=0; x < width; ++x) {
	//Получаем необходимые данные
	col color = getRealPix(d, x, y); //Пиксель диффузной карты [0; 1]
	vec normal = getNormalPix(n, x, y); //Вектор карты нормалей [-1; 1]
 
	//Нормируем вектор источника света
	vec nlight = normalize(light.dir);	
 
	//Вычисляем коэффициент освещенности пикселя
	float k = dot(&normal, &nlight); //Скалярное произведение векторов
	if (k < 0) k = 0; //Защита от отрицательных чисел
 
	//Применяем полученный коэффициент 
	col result = apply(color, k); //Умножение каждого компонента RGB на коэффициент
	setScreenPix(t, x,y, result); //Записываем результирующий цвет
    }
}

Стоит отметить, что мы работаем с разными диапазонами значений. Вектор-пиксель карты поверхности должен быть от -1 до 1, а цвет соответственно от 0 до 1.

1
2
3
4
5
6
..
float normal.x = (pixel.r / 255.0f) * 2.0f - 1.0f;
float normal.y = (pixel.g / 255.0f) * 2.0f - 1.0f;
float normal.z = (pixel.b / 255.0f) * 2.0f - 1.0f;
...
float color.rgb = pixel.rgb / 255.0f;

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

not_full

Как вы можете заметить, никаких «пучков» и следов распространения света не наблюдается.

Модель освещения

Существует довольно много алгоритмов описывающих освещение объектов. Я предлагаю остановиться на довольно простой модели освещения Ламберта.

formula_4

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

light
Если прибегать к линейным выражениям, отняв от единицы длину вектора света, мы оставим реализм далеко за гранью. Гораздо лучше усложнить нашу функцию до квадратичной:

formula_5

Стоит заметить, что находить длину вектора необходимо до его нормирования. Остальные коэффициенты можно подобрать. Например такие:  {0.4; 3; 20}.

Переменные ЦветRGB и Iяркость должны быть в единичном диапазоне, ко всему прочему, мы можем их объединить.

Параметры среды добавляют фоновую составляющую, лишенную теней.

am6b

Из формулы ясно, что она может только подсветить все изображение.

Модель почти завершена, теперь объявим все необходимые константы.

1
2
3
4
5
6
7
8
9
lightsource light;
  //Коэффициенты распространения света (I затухания)
  light.falloff = {0.4f, 3.0f, 20.0f};
  //Цвет источника света
  light.color = {1.0f, 1.0f, 0.4f};
 
col ambient;
  //Фон окружающей среды
  ambient = {0.19f, 0.19f, 0.19f};

Алгоритм обработчика текстуры заметно усложняется, перейдем к программе.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
float attenuation(vec* falloff, float dist) { //Ослабление света
  return(1.0f / (falloff->x + (falloff->y * dist) + (falloff->z * dist*dist)));
}
 
void process(image* texture, image* diffuse, image* normal, lightsource light, col* ambient) {
  for (int y=0; y < height; ++y) 
    for (int x=0; x < width; ++x) {
	//Получаем необходимые данные
	col color = getRealPix(d, x, y); 
	vec normal = getNormalPix(n, x, y); 
 
	col result; //Будущий пиксель
	float brightness = 0; //Освещенность
 
	//Преобразуем вектор источника света
	light.dir.x -= (float)x / screen->w;
	light.dir.y -= (float)y / screen->h;
	//Коррекция соотношения сторон
	light.dir.x *= (float)screen->w / (float)screen->h;
 
	//Сохраним длину вектора освещения
	float distance = length(&light.dir);
 
	//Нормируем вектор
	light.dir = normalize(&light.dir);	
 
	//Вычисление силы источника света
	brightness = dot(&normal, &light.dir) * attenuation(&light.falloff, distance); 
	if (brightness < 0) brightness = 0;
 
	//Вычисляем результирующий свет + добавляем окружающий фон
	result.r = (color.r * brightness * light.color.r) + (color.r * ambient->r); 
	result.g = (color.g * brightness * light.color.g) + (color.g * ambient->g);
	result.b = (color.b * brightness * light.color.b) + (color.b * ambient->b);
 
	//Ограничиваем цвета и записываем результат
	if (result.r > 1.0f) result.r = 1;
	if (result.g > 1.0f) result.g = 1;
	if (result.b > 1.0f) result.b = 1;
 
	setScreenPix(t, x,y, result);
    }
}

Обратите внимание на изменения вектора света внутри цикла — это нововведение необходимо для корректного расчета рассеянного света. Оно вычисляет направление.

devcpp-20150727-1113559

Я записал анимацию с тремя разными значениями Z координаты источника света.

Результат на лицо

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

t1 t2

У вас есть огромный диапазон в 256 значений для каждой компоненты вектора!

devcpp-20150727-1308498 t33

Однако не обязательно ориентироваться на модели высокого разрешения, система работает и на пиксель-арт объектах точно так же.

Заметки

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

Несколько источников?
Действительно, в реальном мире у нас неисчислимое количество вещей, способных излучать свет. t7Для чего нам ограничиваться здесь?

Каждый источник света добавит свою интенсивность (см. формулу), необходимо сложить силы всех источников, а затем добавить параметр окружающей среды и умножить на цвет диффузной карты.

formula_6

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

Рисование карты поверхностей
Несомненно, это трудно, нарисовать вручную трехмерную поверхность. Использование пипетки и специальной палитры значительно облегчит вашу работу. Но не забывайте, что нулевой вектор имеет цвет: {128; 128; 128}!

Составление карты поверхности с реального предмета
Составить карту нормалей по фотографиям действительно возможно. Я широко использовал метод четырех фотографий, описанный здесь \-.

fan stone

Особенно детальной получилась окаменелость, изображенная справа.

Файлы проекта

Post scriptum 
Технология подобного освещения могла бы замечательно реализована на базе FPGA с использование конвейерной обработки, но это тема уже для другой статьи.

Вы можите оставить комментарий, или поставить трэкбек со своего сайта.

Написать комментарий

XHTML: Вы можете использовать эти теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Bug Report
Локализовано: шаблоны Wordpress