LINUX.ORG.RU

Описание алгоритма генерации 2d теней на GLSL

 


1

1

может кому интересно будет, коротко опишу

описываю то что использовалось в этой демке GLSL демка/игра (играть онлайн)

Что в итоге:
1. генерация теней для «любого количества объектов» от 1 источника света имеет одинаковую нагрузку (неважно тени для 1 или 10000 объектов строим)
2. количество источников света ограничено высотой экрана(читай ниже про ограничения) (чем больше источников света те медленнее)

Реальное применение и нагрузка на GPU:
при 1-20 источниках света нагрузка шейдера 10-20% от мощности GPU(к примеру нагрузка блума+FXAA 30%)
такая нагрузка практически не заметна в любой игре и никак не влияет на производительность прочих шейдеров
на gt750 я создавал 200 источников света и ФПС падал до 30 со 100% нагрузкой

Сразу проблемы и ограничения
1. точность расчетов фреймбуферов, баги/ограничения/оптимизации драйверов, по прежнему в Nvidia видеокартах существует баг «неполной генерации фреймбуфера» из за чего многие шейдеры работают неправильно (он есть и в этом шейдере но не критичен(из за особенностей алгоритма))
два года назад этот алгоритм построения теней не работал на половине видеокарт
2. из за цикла отрисовки света, чем больше источников света тем медленнее
3. так как используется карта объектов в виде «кадра»(определенного размера), то и тени можно построить только для объектов на этом кадре(к примеру тени генерируемые на CPU темже box2d будут видны тени объектов за экраном, в случае с шейдером будет только кусок градиента освещения лампы вне экрана но теней не будет)
4. тени от мелких объектов(меньше шага) будут неправильными, для мелких объектов надо строить полноценную SDF карту, при sdf равном размеру объекта не выйдет ничего

SDF- это simple distance field, если экран весь черного цвета(0) то объекты на экране будут градиентом от 0 до 1(белого) градиент по форме объекта вот так https://www.shadertoy.com/view/lsKfDh (нажмите левую кнопку мыши увидите SDF)

Описание алгоритма:
код для примера https://www.shadertoy.com/view/lsyBzw
0. Кто не знает как работают шейдеры- код «шейдера» запускается на каждом пикселе одновременно для всех пикселей на экране(для размера фреймфуфера), входные данные для каждого пикселя это значения из uniform(передаваемые вашей программой(там разреение экрана/текстуры и что угодно что пошлете)) и номер текущего пикселя (x,y)(fragcoord на shadertoy), результат работы шейдера- цвет(4 цифры RGBA) текущего пикселя для которого он был запущен
1. объекты рисуются в BufB, по дефолту размер объекта чуть больше его размера, если уменьшить чтоб 1:1 было то скорость поворота теней от не круглых объектов будет не равномерной(но это можно если нужно)
2. в BufA в цикле(функция MarchShadow) составляется «карта теней» для каждого источника света, номер источника света=номеру пикселя y в bufA
все «шейдеры» на первом по высоте пикселе запустят построение карты для первого источника света, на втором для второго и тд
каждый шейдер по ширине запустит «лучь» в направление от своего пикселя

опишу на примере
размер буфера 1920*1080(экрана)
запуск шейдеров для y=0, x=(с)0(по)1920 это все шейдеры по 1 строке пикселей на экране
Важно понимать-Каждый пиксель(программа шейдер на пикселе) запускает ОДИН лучь в одну сторону он не кастует лучи во все стороны по кругу(не запутайтесь)
по кругу лучи идут суммарно со всех пикселей(шейдеров) на строке y=0 x=0->1920

1. берется угол текущего пикселя vec2 dir = vec2(cos(a), sin(a));
2. функции MarchShadow(light.origin, dir); передается light.origin- положение источника света(на экране 100,100 к примеру), dir-угол
3. MarchShadow в цикле берет float ds = Scene(dir * d - orig); Scene() возврящает значение красного канала буфера BufB для координаты (dir * d - orig)
в начале цикла d=0, тоесть первая точка «луча света»=позиции источника света light.origin
4. следующая координата d+=значению прошлого шага
и пока шаг>0(не уперлись в объект, шагать некуда) продолжаем цикл

как это работает на практике
в BufB значение красного канала по умолчанию 0.45 значения объектов это угасание канала до 0(градиент)
для 0 итерации цикла в MarchShadow
допустим у нас light.origin в позиции где нет объектов
функция Scene возвращает return texture(iChannel1,1.-uv).r/30.;
это 0,45/30=.01500
d += .01500;(d было 0)
для 1 итерации
d определяет величину следующего шага для взятия значения с текстуры объектов
у нас 1920 пикселей ширина экрана, шаг .01500, это 1920*.01500=28.80000 пикселей
это фактически минимальный размер объекта в текущем варианте когда SDF чуть больше или равен размеру объекта
также так как у меня MAX_STEPS=68 то это .01500*68=1.02000
1920*1.02000=1958.40000 это больше чем размер экрана, такого шага хватает чтоб охватить весь экран
можно сделать размер шага меньше поставив в строке 30 BufB вместо /30. деление на 50
размер шага станет 0,5/50=.01000 это 1920*.01000=19.20000 умножить на 68 щагов =1305.60000 этого не хватит на веь экран и нарисуется темный круг(где лучь закончится) в таком радиусе от источника света, поставьте сами убедитесь

если первый шаг был в точке (100,100) пикселей(координата orig), то этот будет в точке (100+28.80000*(уголX),100+28.80000*(уголY)) (угол=dir)
и функция Scene() вернет значение пикселя по этой новой координате
если объект есть то значение будет около нуля(так как градиент) и оно сравниться с if(ds < EPS) и выйдет из функции считая что это конец луча(ударились в объект)
для 2 итерации цикла
d будет .01500(с 0 итерации)+.01500(с 1)=.03000 и следущая точка (100+(1920*.03000)*(dirX),100+(1920*.03000)*(dirY))
и так далее

еще более наглядно смотрите эту картинку https://i.imgur.com/RvaJ6Cr.png
ds(зеленым цветом на картинке)- величина следущего шага, это значения пикселя в BufB по координатам dir * d - orig
где dir(фиолетовый)- угол
d(бирюзовый)- значение текущего шага
orig(красный)- координата точки света
желтым направление хода луча

Дальше в буфере Image строятся тени функцией MixLights, тень банально накручивается по кругу с уменьшением яркости тени в зависимости от расстояния
Тень в буфере bufA выглядит так https://i.imgur.com/awoqYyI.png (тут пять источников света и пара объектов, высота фреймбуфера 5 пикселей) https://i.imgur.com/fUYwgNt.png (помоему сотня источников света и куча объектов,высота=количеству источников света)

Зачем же нужен качественный SDF- потому что идет d += ds; и имея качественный(больше размера объекта в разы) градиент-чем ближе к объекту тем величина шага будет уменьшаться что повышает точность тени и формы объекта
если SDF отсутствует(SDF равен форме объекта тоесть сразу 0 без градиента) тоо будет так https://youtu.be/386lNQABEVM для объектов круглой формы проблемы такой не будет(только размер кругов должен быть больше размера шага)
шаг будет всегда одинаковый поэтому он «проскакивает» положение объекта темболее треугольной формы из за этого тени прыгают
если сделаь шаг меньше(начиная с 18 секунды видео) прыгать станет меньше но количества шагов не хватит на весь экран, а увеличивать количество шагов- это значит нагрузка возрастет в разы
у меня размер SDF чуть больше размера объекта вот так выглядит https://i.imgur.com/53pWq3H.png

в чем суть и «фишки»- генерация всех теней от 1 источника света занимает количество шейдеров равное (ширине экрана, 1 пиксель высоты) что в тыщи раз быстрее в сравнении с «неправильными» 2д-тенями шейдерами(где генерируется «тень» в каждый пиксель)

ссылки используемого https://gist.github.com/mattdesl/5286905 описание алгоритма на английском, реализация на shadertoy через SDF https://www.shadertoy.com/view/XsK3RR

Очевидные оптимизации- помимо того что для статических объектов можно строить идеальный SDF,только для динамических нужно маленькие формы
в Opengl3+ есть возможность через блум улучшать карту объектов как тут к примеру написали http://prideout.net/archive/bloom/#Sneaky
в OpenglES и webgl2 все очень ограничено, пишите шейдеры для всего как говориться в оффдоке GLES (без серьезных потерь производительности никак)