Особенность этого метода заключается в том, что эти источники света не ограничиваются ни количеством ни формой.
Условно алгоритм можно разделить на две составляющие: освещение 2D объектов и форма представления источников света.
Освещение
Отчасти об освещении написано в этом посте.
Для того что бы определить интенсивность освещения каждого пикселя, необходимо знать нормаль этого пикселя и вектор направления к источнику света. Собственно здесь и происходит разделение моего поста на две части: откуда брать нормаль пикселя (текущего объекта) и как вычислять вектор направления освещения.
Как правило нормаль пикселя текущего объекта берется из карты нормалей.
Получить карту нормалей можно разными способами (один из них описан в приведенном выше посте), я создаю ее так:
рисуется спрайт:
Далее для него рисуется карта высот. В моем случае сам по себе спрайт можно интерпретировать как карту высот. О том что такое карта высот и вообще о бамп маппинге в целом можно почитать тут.
По карте высот уже можно построить карту нормалей. Существует несколько утилит, которые умеют это делать. Я использовал плагин для GIMP’a (вот сорцы, но вроде есть в стандартных репозиториях убунты).
Итак, у нас есть оба спрайта для создания эффекта объемного объекта. Рассмотрим шейдер, который используя эти два спрайта и направление источника света определяет интенсивность пикселя, на данном этапе он точно такой же, как и в моем предыдущем посте.
//вершинный varying vec4 texCoord; void main(){ gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex; texCoord = gl_MultiTexCoord0; } //фрагментный uniform sampler2D colorMap; uniform sampler2D normalMap; varying vec4 texCoord; uniform vec2 light; uniform vec2 screen; uniform float dist; void main() { vec3 normal = texture2D(normalMap, texCoord.st).rgb; normal = 2.0*normal-1.0; vec3 n = normalize(normal); vec3 l = normalize(vec3((gl_FragCoord.xy-light.xy)/screen, dist)); float a = dot(n, l); gl_FragColor = a*texture2D(colorMap, texCoord.st); }
Источники света
Эта технология отдаленно напоминает Deferred Shading.
Основная идея заключается в создании отдельного буфера для освещения, где каждый пиксель хранит значение интенсивности освещения для соответствующего пикселя в кадре. Другими словами — это обычный лайтмап для 2D сцены.
Для того, что бы сделать лайтмап, нужно просто отрендерить в него все источники света. Преимущества такого подхода:
- количество источников света ограничена только железом. К примеру 1000 источников света — это 1000 спрайтов. Отрендерить 1000 спрайтов не составит труда даже для мобильного гпу, да и нужно ли в 2D сцене 1000 источников?
- источники света могут быть разного цвета и разной степени прозрачности — ведь это обычная текстура
- форма источников света может быть любой
Вот, к примеру, лайтмап сцены с лавой:
Это не новая техника освещения и у нее есть минус — отсутствие вектора направления света. Однако можно придумать такой алгоритм, который бы определял этот вектор.
Для начала определим что из себя представляет источник света и какие у него есть свойства. Я не буду приводить сложные формулы и цитаты из учебника по физике — все это скучно и не интересно. Попробую объяснить так, как объяснил бы маме.
Итак чем дальше исходят лучи света — тем слабее их интенсивность. Это наблюдение можно использовать для определения вектора направления лучей света. То есть, если у нас есть два соседних пикселя и в первом из них значение света равно 0.5, а во втором 0.25, то можно сделать вывод, что вектор луча света направлен из первого пикселя во второй.
В данном случае простая формула вычисления вектора освещенности выглядит так:
v[cx][cy].x = p[cx][cy].x — p[cx+1][cy].x
v[cx][cy].y = p[cx][cy].y — p[cx][cy+1].y
где cx, cy — координаты рассматриваемого пикселя
Однако разница между двумя соседними пикселями может быть крайне мала, соответственно длина вектора так же может быть маленькой и не точной, поэтому в данном случае освещение может показаться «плоским». Я нашел два варианта решения этой проблемы: домножать результат на некоторый коэффициент или брать пиксели отстоящие друг от друга на 1 или более пикселя. Во втором случае мы жертвуем детализацией освещения. В итоге я скомбинировал оба этих метода и итоговая формула выглядит так:
v[cx][cy].x = (p[cx-d/2][cy].x — p[cx+d/2][cy].x) * k
v[cx][cy].y = (p[cx][cy-d/2].y — p[cx][cy+d/2].y) * k
где k — коэффициент усиления вектора направления света, d — расстояние между пикселями в выбрке.
Эти новые значения можно либо записывать в отдельную карту нормалей освещения либо вычислять «на лету» во время рендера результирующего кадра просто используя лайтмап. Я выбрал второй вариант.
//вершинный varying vec4 texCoord; varying vec4 nmTexCoord; varying vec2 lightMapTexCoord; //координаты среднего пикселя лайтмапа varying vec2 lightMapTexCoordX1; //координаты левого пикселя лайтмапа varying vec2 lightMapTexCoordX2; //координаты правого пикселя лайтмапа varying vec2 lightMapTexCoordY1; //координаты верхнего пикселя лайтмапа varying vec2 lightMapTexCoordY2; //координаты нижнего пикселя лайтмапа //да, я знаю, что можно было использовать массив. Но так нагляднее uniform vec2 fieldSize; // размер игровой карты const float spriteSize = 16.0; //размер зазора между соседними пикселями лайтмапа void main() { gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex; texCoord = gl_MultiTexCoord0; nmTexCoord = gl_MultiTexCoord1; //вычисляем текстурные координаты выборочных пикселей лайтмапа. lightMapTexCoordX1 = vec2(gl_Vertex.x/(fieldSize.x-1.0/spriteSize), gl_Vertex.y/fieldSize.y); lightMapTexCoordX2 = vec2(gl_Vertex.x/(fieldSize.x+1.0/spriteSize), gl_Vertex.y/fieldSize.y); lightMapTexCoordY1 = vec2(gl_Vertex.x/fieldSize.x, gl_Vertex.y/(fieldSize.y-1.0/spriteSize)); lightMapTexCoordY2 = vec2(gl_Vertex.x/fieldSize.x, gl_Vertex.y/(fieldSize.y+1.0/spriteSize)); lightMapTexCoord = vec2(gl_Vertex.x/fieldSize.x, gl_Vertex.y/fieldSize.y); } //--------------------------------------------------------------------------------------------------------------- //фрагментный varying vec4 texCoord; varying vec4 nmTexCoord; varying vec2 lightMapTexCoord; varying vec2 lightMapTexCoordX1; varying vec2 lightMapTexCoordX2; varying vec2 lightMapTexCoordY1; varying vec2 lightMapTexCoordY2; uniform sampler2D colorMap; //в этом атласе и диффузная карта и карта нормалей uniform sampler2D lightMap; uniform float ambientIntensity; //рассеянное освещение uniform float lightIntensity; //коэффициент усиление интенсивности света const float shadowIntensity = 8.0; //коэффициент усиления вектора направления света const vec3 av = vec3(0.33333); //константа для вычисления среднего арифмитического void main() { vec4 lmc = texture2D(lightMap, lightMapTexCoord)*2,0; //текущий пиксель из лайтмапа. Он умножается на два, потому что в проекте максимальное значение компоненты цвета равно 0.5, а не 1.0 (условно). В таком случае цвет можно разбить на две части, обработать, а потом сложить их. Это нужно для того, что бы сверхяркий свет в итоге переходил в белый. // x и y - разница между соседними пикселями лайтмапа float x = (dot(texture2D(lightMap, lightMapTexCoordX1).rgb, av)- dot(texture2D(lightMap, lightMapTexCoordX2).rgb, av))*shadowIntensity; float y = (dot(texture2D(lightMap, lightMapTexCoordY2).rgb, av)- dot(texture2D(lightMap, lightMapTexCoordY1).rgb, av))*shadowIntensity; float br = dot(lmc.rgb, av); //среднее арифмитическое всех трех компонент лайтмапа - яркость пикселя vec3 l = vec3(x, y, br); //создаем вектор из полученых значений, по z позиции устанавливаем яркость пикселя, для того что бы при нормализации получить вектор, характеризующий не только направление, но и яркость пикселя l = normalize(l)*br; //нормализуем и еще дополнительно умножаем на яркость vec3 normal = 2.0*texture2D(colorMap, nmTexCoord.st).rgb-1.0; float a = dot(normal, l)*lightIntensity; a = max(a, 0.0); vec4 c = texture2D(colorMap, texCoord.st); c = a*min(c, lmc)+ambientIntensity*c; //вычисляем цвет пикселя на основе рассеянного и направленного света float m = 0.0; //теперь находим максимальное значение из трех компонент результирующего пикселя, это нужно для того, что бы сверхяркий свет в итоге переходил в белый (см. на видео или gif в шапке). Назовем его избыточным цветом. m = max(m, c.r); m = max(m, c.g); m = max(m, c.b); gl_FragColor = c+max(0.0, m-1.0); //складываем результирующий и избыточный цвета. }
Видео с демонстрацией эффекта: источник света — спрайт произвольной формы, каждая частица лавы — источник света.
ссылка на оригинал статьи http://habrahabr.ru/post/183534/
Добавить комментарий