Прошлая статья закончилась на том, что модель освещения не считает тени вообще. max(0, N·L) отвечает на вопрос «повёрнута ли площадка к источнику» — и на этом всё. Между площадкой и лампой может стоять стена, модели это безразлично: она про ориентацию, а не про видимость.
Вопрос «видит ли точка источник» решается отдельным механизмом, и почти вся графика реального времени решает его одним и тем же способом: рендерит сцену со стороны света, запоминает глубину и потом сравнивает. Способ работает, но подменяет исходный вопрос похожим — и все знакомые беды теней растут ровно из этой подмены.
Разберём:
- как карта глубины превращает вопрос о видимости в сравнение двух чисел, и какой именно вопрос при этом оказывается задан;
- откуда берётся shadow acne — и почему минимально достаточный запас считается формулой, а не подбирается ползунком;
- чем за этот запас платят: peter-panning, отрыв тени от основания, и почему на закатном солнце он становится неприличным;
- зубцы по краю: тексель карты против пикселя экрана, и почему увеличение разрешения — самый дорогой из возможных ответов;
- PCF и порядок действий, который нельзя переставлять: сравнить, потом усреднить;
- каскады: схемы разбиения, швы, дрожание края и то, во что это обходится по памяти и по проходам;
- и как всё это собирается в два прохода рендера и одну функцию в шейдере.
Такие разборы — с рабочим кодом — выходят в канале каждую неделю.
Тень — это тест видимости
Итак, задача формулируется в одну строку. Для точки на поверхности нужно узнать, есть ли что-нибудь между ней и источником света. Есть — точка в тени, множитель прямого освещения ноль. Нет — считаем свет по всем формулам прошлой статьи.
Прямой ответ даёт луч: выпустить из отрезок в сторону лампы и проверить пересечения со сценой. Ответ будет точным, потому что это буквально определение тени. Стоить он будет одного луча на пиксель на источник, и в 1978 году, когда задачу решали впервые, это было немыслимо. Сегодня это уже возможно, но и сегодня это дорого — и рядом лежит приём, который справляется одной текстурой.
Переиспользовать буфер глубины
Приём предложил Лэнс Уильямс в работе «Casting curved shadows on curved surfaces» (SIGGRAPH, 1978), и держится он на одном наблюдении. Буфер глубины уже умеет отвечать на вопрос «какая поверхность ближайшая вдоль этого луча». Обычно его считают из камеры. А если посчитать его из источника света, получится ровно карта ближайших поверхностей со стороны лампы — то есть карта того, что освещено.
Отсюда два прохода:
- Рендерим сцену из позиции света. Цвет не нужен, пишется только глубина. Результат — текстура, которую называют картой теней (shadow map).
- Рендерим сцену из камеры. Для каждой затеняемой точки переводим её в систему координат света и сравниваем её глубину с той, что записана в карте.
Сравнение читается так:
Слева — глубина самой точки относительно света. Справа — глубина ближайшей поверхности, которую свет увидел в том же направлении. Если точка глубже, значит по дороге стояло что-то ещё.
Перевод в пространство света — обычная цепочка матриц, та же самая, что переводит вершины в пространство камеры:
vec4 lightPos = uLightViewProj * vec4(worldPos, 1.0);
vec3 proj = lightPos.xyz / lightPos.w * 0.5 + 0.5; // → [0, 1]
float stored = texture(uShadowMap, proj.xy).r;
float visible = proj.z <= stored ? 1.0 : 0.0;Матрица для солнца — ортографическая: у направленного источника лучи параллельны, схождения в точку нет. Для прожектора берут перспективную, для точечной лампы — шесть карт на грани куба, потому что светит она во все стороны сразу.
Почему у солнца именно ортографическая проекцияОсторожно! Математика!
Перспективная проекция сводит лучи в одну точку — позицию наблюдателя. У направленного источника такой точки нет: он бесконечно далеко, лучи параллельны. Ортографическая проекция это и описывает.
Побочная выгода — то, как проекция распределяет точность глубины. Ортографическая пишет в карту линейную функцию — в нормировке на
— каждый метр дальности получает одинаковый кусок диапазона. Перспективная из-за деления на пишет гиперболу:
Спросим, на какой дистанции потрачена половина всех представимых значений глубины:
Половина точности сгорает до удвоенной ближней плоскости. При и это 20 сантиметров: первые 10 сантиметров дальности забирают половину диапазона, оставшиеся 99,8 метра делят вторую. Для камеры такой размен осмыслен — вблизи точность нужнее. Для карты теней прожектора это головная боль: далёкая геометрия квантуется огромными шагами, и запас глубины приходится подбирать иначе, чем для солнца.
Глазами света
Ползунок разрешения тут важнее, чем кажется. Карта — это не «изображение сцены со стороны света», это конечный набор чисел: сколько текселей, столько лучей, столько записанных глубин. Между ними у карты нет данных, и когда позже понадобится глубина в промежутке — её никто не восстановит.
Какой вопрос на самом деле задан
Собственно, вот главное место статьи, и всё остальное — его последствия.
Спрашивали: видна ли точка из источника. Отвечают: глубже ли , чем ближайшая поверхность в том текселе, куда спроецировалась.
Два вопроса совпадают только в идеальном случае — когда тексель бесконечно мал, а глубина хранится без потерь. В реальности не выполняется ни то, ни другое:
- тексель имеет размер, и внутри него все точки получают одно и то же записанное число;
- глубина хранится с конечной точностью — обычно 24 бита, иногда 16;
- карта покрывает конечную область: за её пределами данных нет вообще.
Каждое из трёх ограничений даёт свой класс артефактов. Первое — acne и зубцы, второе — «плавающие» тени на больших дистанциях, третье — тени, обрывающиеся по невидимой линии. Дальше по статье разбираем их по очереди.
Acne: поверхность против самой себя
Первое, что видит человек, включивший тени по инструкции из документации, — полосы. Ровный пол покрывается муаром из чередующихся светлых и тёмных лент, стены идут зеброй, а на округлых поверхностях появляется рябь. Артефакт называется shadow acne.
Разберём, откуда он берётся, потому что тут всё считается на бумаге.
Тексель хранит одну точку, а отвечает за целый кусок
Рендер карты — это растеризация. В тексель попадает глубина того места, куда пришёлся его центр. Одно число.
При шейдинге в этот же тексель проецируется не одна точка, а весь кусок поверхности, накрытый текселем. И у этого куска своя глубина в каждой точке — если поверхность к лучу света хоть немного наклонена.
Возьмём поверхность, на которую свет ложится под углом к самой плоскости ( — свет в лоб, — скользящий). Чем ниже свет, тем длиннее след текселя на поверхности и тем больший разброс глубин прячется внутри одного записанного числа. Разброс растёт как , где — угол между нормалью поверхности и направлением на свет, то есть ровно тот угол, косинус которого стоит в .
Отсюда минимальный запас, при котором acne исчезает полностью: — половина разброса внутри текселя. Меньше — и часть поверхности объявляет себя загороженной. Причём при нулевом запасе тёмной оказывается ровно половина поверхности — та половина каждого текселя, что смотрит от света. Не «примерно много», а половина: именно поэтому acne выглядит как регулярная зебра, а не как случайный шум.
Вывод минимального запаса и доли ложной тениОсторожно! Математика!
Тексель шириной ложится на поверхность следом длиной , и на этой длине глубина набирает на единицу. Вместе:
Дальше арифметика. Записано значение в центре текселя, сравниваемая точка отстоит от центра не больше чем на . Значит ошибка сравнения доходит до половины разброса:
Считается и доля ложной тени: смещение точки от центра текселя равномерно размазано по , ложная тень возникает там, где ошибка превысила запас, отсюда
При — ровно половина.
Acne и цена запаса
Красным на поверхности — ложная тень, оранжевым — обратная беда, свет там, где должна быть настоящая тень. Верхний счётчик меряет долю ложной тени прямо по кадру, второй считает её по формуле из спойлера — расходятся они разве что на округлении, и на нулевом запасе оба приходят к 50%.
Чем платят за запас
Запас — это сдвиг сравниваемой глубины: точка притворяется чуть ближе к свету, чем она есть. Значит и её тень начинается позже — отклеивается от основания предмета, и предмет начинает выглядеть парящим над полом. Артефакт так и называется — peter-panning. Размер отрыва пропорционален запасу: .
Вывод отрыва из запасаОсторожно! Математика!
Точка на полу, отстоящая на от места, где загородивший объект касается пола, находится от него на расстоянии вдоль луча. При запасе она считается освещённой, пока это расстояние меньше запаса:
А теперь подставим минимально достаточный запас в отрыв и посмотрим, что получилось:
Вот это и есть вся боль теней одной формулой. При высоком солнце знаменатель большой, отрыв микроскопический. При закатном — , и отрыв уходит в бесконечность. Карта на область в 20 метров, солнце в 45° — отрыв 7 миллиметров, никто не заметит. То же самое солнце в 10° — 5,5 сантиметра. В 5° — уже 11 сантиметров, и тень заметно отходит от ног.
Отсюда, кстати, практический вывод, который в документации движков обычно не пишут: закат — самое дорогое время суток для теней. Если в игре есть цикл дня и ночи, настроенные на полдень параметры к вечеру перестают работать — это свойство метода.
Три способа дать запас
Константа. Самое простое: вычесть из глубины фиксированное число. Работает ровно до тех пор, пока не вырос — то есть до первого наклонного пола или до первого заката. Подобранная на одной сцене константа переезжает на другую и там ломается.
Slope-scale bias. Раз минимум пропорционален , поправку туда и вписывают:
Именно эта формула стоит за glPolygonOffset(factor, units) и за парой «Depth Bias / Slope Scaled Depth Bias» в настройках движков. Она снимает зависимость от наклона — но за неё же и платит: чем острее угол, тем больше запас и тем сильнее отрыв.
Normal offset. Приём другого рода: двигать не сравниваемое число, а место выборки. Точку перед проецированием сдвигают вдоль нормали поверхности:
vec4 lightPos = uLightViewProj * vec4(worldPos + N * uNormalOffset, 1.0);Сдвиг вдоль нормали уводит точку выборки в соседний тексель, где записана глубина уже не той площадки, с которой мы её сравниваем. Самозатенение пропадает, а вот отрыва тени не появляется: сравниваемая глубина не подкручена, изменилось только место выборки. Платить приходится смещением самого края тени на величину сдвига — на плоских участках это незаметно, а вот на тонкой геометрии тень начинает соскальзывать с предмета. В демке этот режим стоит четвёртым, разница с константой на низком солнце видна сразу.
И четвёртый способ — отрезать проблему геометриейОсторожно! Практика!
В теневом проходе можно рисовать не передние грани, а задние: glCullFace(GL_FRONT). Тогда в карту попадёт дальняя стенка объекта, а не ближняя, и освещённая поверхность окажется заведомо ближе к свету, чем записанное значение. Самозатенение снимается почти целиком, и запас нужен символический.
Цена — требование к геометрии. Приём работает на замкнутых объектах с толщиной. Лист бумаги, забор из одного полигона, любая односторонняя геометрия задней грани не имеют, и в карте для них не окажется вообще ничего: тени пропадут. В сценах, где смешано и то, и другое, приходится держать оба режима и помечать модели вручную — что само по себе тот ещё источник ошибок.
Зубцы: тексель против пикселя
Запас выставлен, полосы ушли, и становится виден следующий слой. Край тени идёт лесенкой. Не гладкой кривой, как у настоящей тени, а ступеньками — причём ступеньки эти живут не в экранной сетке, а в какой-то своей, наклонной и крупной.
Своя сетка — это сетка карты. Тень заканчивается там, где кончился тексель, потому что внутри текселя ответ один на всех.
Насколько крупный тексель на самом деле
Тексель — квадрат в проекции света, со стороной
где — сторона области, которую накрывает карта, — разрешение. Карта на область 100 метров даёт около пяти сантиметров — звучит прилично.
Только на поверхность этот квадрат ложится не квадратом. След текселя на полу растянут делением на синус:
Те же пять сантиметров при солнце в 15° превращаются в 19 сантиметров следа. При 5° — в 56. Ступенька по краю тени будет ровно такой ширины, и уже на среднем плане её видно невооружённым глазом.
Зубцы по краю
В демке сетка текселей нарисована поверх пола, и край тени буквально ложится на её линии. Ползунок высоты света показывает второй множитель: при том же разрешении низкое солнце размазывает тексель на метры.
Почему разрешение — плохой ответ
Первое желание — поднять . Оно работает, но арифметика тут неприятная.
Ступенька обратно пропорциональна , а память карты растёт как . Чтобы уменьшить зубец вдвое, карту надо взять вчетверо большую. При четырёх байтах на тексель — а именно столько занимает привычный формат с 24-битной глубиной — карта 1024² весит 4 мегабайта, 2048² — 16, 4096² — 64. И это одна карта на один источник; в сцене с тремя тенями от прожекторов расход утраивается.
Причём заплатив памятью, вы не получаете пропорционального выигрыша по картинке. Тексель бьётся не с абсолютным размером, а с размером пикселя на экране, а тот меняется с дистанцией. Разница между ними — то, что называют перспективным алиасингом: у камеры под ногами пиксель покрывает миллиметры, а тексель карты, растянутой на весь уровень, — сантиметры.
Полезно посмотреть на отношение:
Единица — идеал: тексель ровно в пиксель, ступеньки не видно, лишней памяти не потрачено. Больше единицы — зубцы. Меньше — вы платите за разрешение, которого глаз не увидит. У одной карты на весь уровень проходит весь диапазон от сотен у ног до долей на горизонте, и никакое одно не годится сразу для всех дистанций.
Три разных алиасинга, которые часто путаютОсторожно! Математика!
Перспективный — тот, что описан выше: пиксель мельчает с приближением к камере, тексель нет. Его можно записать точно. Пиксель на дистанции накрывает в мире полосу
где — высота кадра в пикселях. Тексель карты от не зависит вовсе, и отношение из основного текста разворачивается в гиперболу:
Дистанция в кадре гуляет на три порядка, а сидит в формуле константой — единицей становится на одной-единственной дистанции: ближе неё зубцы, дальше — разрешение, потраченное впустую. Лечится тем, что карту подгоняют под фрустум камеры, а фрустум режут на куски со своим у каждого. Это пятая часть.
Проекционный — след текселя растёт на поверхностях, повёрнутых к свету ребром. Треугольник тот же, что в выводе acne: поперечник текселя — катет напротив угла , след на поверхности — гипотенуза:
Лечится плохо: в знаменателе — угол между поверхностью и светом, то есть геометрия сцены, на которую распределение разрешения не влияет. Отсюда же и половина проблем с запасом глубины из прошлой части.
Временной — при движении камеры сетка текселей едет вместе с картой, и край тени переливается от кадра к кадру. Отдельно неприятен тем, что на статичном скриншоте его нет. Лечится привязкой карты к целым текселям, об этом тоже в пятой части.
Первые два — про то, что тень выглядит грубой. Третий — про то, что она выглядит грязной в движении, и раздражает он обычно сильнее.
Что на самом деле помогает
Заметьте, где именно тратится разрешение в наивной схеме: карта накрывает весь уровень, потому что источник светит на всё сразу. При этом камера в конкретном кадре видит малую его часть.
Отсюда первая настоящая оптимизация: подогнать карту не под сцену, а под то, что видно камере. Область падает с сотен метров до десятков, тексель мельчает во столько же раз, память не растёт вообще. Стоит это пересчёта матрицы света каждый кадр — и появления новых артефактов в движении, потому что теперь карта едет вместе с камерой.
Вторая — заметить, что и внутри видимого куска разрешение нужно неравномерно. Это уже каскады, и до них осталась одна часть.
PCF: край как доля, а не как ответ
Ступеньку по краю хочется сгладить, и первое, что приходит в голову человеку, который уже сглаживал текстуры, — включить фильтрацию. Поставить карте билинейный фильтр вместо ближайшего соседа, или, если этого мало, прогнать её размытием.
Да? Конечно же нет. И разбор того, почему нет, стоит дороже самого приёма.
Что не так с размытием карты
Карта хранит глубины. Среднее двух глубин — это глубина поверхности, которой в сцене нет.
Возьмите тексель на краю стены: слева записана крыша на высоте два метра, справа — пол. Усредните — получится ступенька высотой метр, висящая в воздухе. Сравнив себя с ней, пол объявит себя то освещённым, то затенённым в зависимости от того, по какую сторону этой ступеньки оказалась его глубина, и результат по-прежнему будет бинарным: край никуда не денется, он просто уедет в сторону. Ровно это и происходит на местах контакта: тень отклеивается от предметов там, где никакого запаса глубины не выставляли.
Приём, который работает, придумали Ривз, Салезин и Кук («Rendering antialiased shadows with depth maps», SIGGRAPH, 1987) и назвали percentage-closer filtering. Идея в перестановке двух действий:
- неправильно: усреднить глубины соседних текселей → сравнить один раз;
- правильно: сравнить с каждым текселем по отдельности → усреднить ответы.
Во втором случае усредняются нули и единицы, и результат — доля выборок, оказавшихся на свету. Полбалла означает «половина карты вокруг говорит, что тут светло», и это уже осмысленная величина.
float lit = 0.0;
for (int j = -R; j <= R; ++j)
for (int i = -R; i <= R; ++i) {
float stored = texture(uShadowMap, uv + vec2(i, j) * texel).r;
lit += step(depth - bias, stored); // 1.0, если освещено
}
return lit / float((2 * R + 1) * (2 * R + 1));PCF: край как доля
Переключатель порядка в демке ставит эти два варианта рядом. На «размыть → сравнить» край не смягчается ни на пиксель — зато тени сползают с оснований, и это единственное, что вы получаете за ту же цену в выборках.
Сколько это стоит
Ядро — это выборок из текстуры на каждый пиксель на каждый источник. 3×3 — девять, 5×5 — двадцать пять. Для полноэкранного прохода в 1080p это 18,7 и 51,8 миллиона выборок соответственно, и на мобильном GPU разница между ними хорошо заметна в кадре.
Поэтому в железе есть подпорка. Видеокарты умеют делать сравнение внутри выборки: тип sampler2DShadow в GLSL и SamplerComparisonState в HLSL берут четыре соседних текселя, сравнивают каждый с переданной глубиной и возвращают билинейно взвешенную долю — то есть готовое PCF 2×2 по цене одной выборки. Четыре таких обращения, разнесённых на два текселя, накрывают область 4×4: шестнадцать сравнений по цене четырёх выборок.
Почему точки лучше брать не по сеткеОсторожно! Математика!
У квадратного ядра есть свой характерный дефект: край распадается на ровные ступени вместо одной. Причина в регулярности — все пиксели берут выборки в одной и той же решётке, и ошибка у соседних пикселей одинаковая, то есть складывается в видимый узор.
Лечится двумя приёмами, обычно вместе.
Нерегулярное ядро. Точки раскладывают по кругу так, чтобы они не садились на сетку: исторически — таблицей Пуассона, сейчас чаще спиралью Фибоначчи:
Шаг по углу — золотой угол . Корень отвечает за равномерность: кольцо между соседними радиусами имеет одну и ту же площадь,
— по точке на кольцо, и плотность постоянна по всему кругу. Золотое сечение взято не для красоты: из всех чисел оно хуже всего приближается дробями — его цепная дробь состоит из одних единиц. Шаг, близкий к рациональной доле оборота, за несколько витков собирает точки в лучи; золотой угол не даёт им выстроиться ни в лучи, ни в кольца ни при каком , и ни одна точка не садится в узел решётки.
Свой поворот в каждом пикселе. Ядро крутят на угол, взятый из маленькой текстуры шума. Ошибка остаётся той же по величине, но перестаёт быть согласованной у соседей — и полосы превращаются в зерно, которое глаз ловит хуже. Дальше это зерно обычно догоняет темпоральное сглаживание.
В демке это пятый вариант ядра. Ошибка там не меньше, чем у спирали без поворота — она просто по-другому распределена.
Что PCF не чинит
Ширина размытия у PCF — это ширина ядра в текселях, помноженная на размер текселя. И всё. Она не зависит ни от размера источника, ни от того, как далеко предмет стоит от поверхности, на которую падает тень.
У настоящей полутени зависит и то, и другое: тень от ноги на асфальте почти острая, тень от той же ноги, если поднять её на метр, заметно размыта. Приём, который это воспроизводит, называется PCSS (Fernando, 2005): сначала отдельным проходом ищут, насколько глубоко в карте лежит загородивший объект, из этого считают ширину полутени и уже её берут радиусом ядра. Стоит это ещё одного набора выборок на пиксель, и потому вписывается не в каждый бюджет.
Второе, и более важное. PCF не добавляет карте разрешения. Если тексель кладётся на пол сорокасантиметровым пятном, то ядро 5×5 размажет край на два метра — и получится не мягкая тень, а размытая неправильная. Смягчать имеет смысл край, который уже стоит там, где надо; чинить фильтром грубую карту не выйдет.
Каскады: разное разрешение на разной дистанции
В третьей части мы уткнулись в то, что одного разрешения на всю дальность не бывает. У ног нужны сантиметры, на горизонте хватит метров, а карта одна и тексель у неё везде одинаковый.
Чтож, раз требования к плотности разные на разных дистанциях, разобьём дальность на куски и дадим каждому свою карту. Приём называется каскадными картами теней (cascaded shadow maps), и на сегодня это стандартный способ делать тени от солнца в открытом мире.
Где резать
Пусть камера видит от до , а каскадов . Границы можно ставить по-разному, и разница между схемами — не косметическая.
Равномерно — каждому каскаду одинаковый кусок дальности:
Просто и плохо. При дальности 300 метров и четырёх каскадах первый накрывает всё до 75 метров — то есть ровно та зона, где нужна максимальная плотность, обслуживается одной картой на семьдесят пять метров.
Логарифмически — одинаковый кусок в логарифме дальности:
Эта схема идеальна по плотности: она держит отношение «тексель к пикселю» постоянным по всей дальности, что и требовалось. И она непригодна в чистом виде. При и первая граница встаёт на 1,7 метра: целый каскад тратится на пятно под ногами, а на всё остальное остаётся на один каскад меньше.
Практическая схема — взвешенное среднее двух крайностей:
Её предложили Чжан и соавторы в работе про parallel-split shadow maps (2006, разбор — в десятой главе GPU Gems 3), и с тех пор она стоит почти везде. — та самая ручка, которая в настройках движка называется чем-нибудь вроде «Cascade Distribution»: ноль — равномерно, единица — логарифм, между ними — размен разрешения ближней зоны на разрешение дальней.
Каскады и их швы
График под фрустумом — плотность текселей на метр по дистанции. Переключая схемы, обратите внимание не на среднее, а на самое низкое место: качество тени определяется худшим каскадом, а не лучшим.
Насколько велика карта одного каскада
Тут есть неочевидное место. Каскад — это кусок фрустума, усечённая пирамида, а карта — квадрат. Насколько большим должен быть квадрат?
Соблазнительно взять габариты куска, повёрнутого в систему координат света. Так и делают, и потом ловят дрожание: при повороте камеры проекция куска на плоскость карты меняет размеры, вместе с ней меняется масштаб текселя, и вся сетка едет — край тени начинает переливаться на каждом кадре.
Устойчивое решение — накрывать кусок сферой. Сфера не меняет радиус при повороте, значит и тексель остаётся постоянного размера. Радиус считается точно: у куска между и центр охватывающей сферы лежит на оси взгляда, и найти его можно из равенства расстояний до ближних и дальних углов.
Вывод радиуса охватывающей сферыОсторожно! Математика!
Обозначим через квадрат отношения «расстояние до угла кадра» к «дальности по оси», где — соотношение сторон. Угол кадра на дистанции отстоит от оси на .
Центр сферы ищем из условия, что ближние и дальние углы равноудалены:
Раскрываем и сокращаем на :
Если получилось — а на широком угле обзора так и выходит, — сфера садится на дальнюю плоскость, и её радиус равен расстоянию до дальних углов, . Сторона карты — диаметр, то есть .
Отсюда, кстати, видно, чем платят за устойчивость: сфера накрывает заметно больше, чем сам кусок фрустума, и часть текселей уходит на пустоту. Это осознанный размен — дрожание края в движении раздражает сильнее, чем потеря части разрешения.
Второй половиной лечения дрожания будет привязка карты к целым текселям: центр карты каждый кадр округляют до узла её же сетки. Тогда при движении камеры сетка не ползёт непрерывно, а прыгает ровно на тексель, и край тени остаётся на месте относительно геометрии.
Швы
Плотность внутри каскада постоянна, между каскадами — скачет. На границе это видно как шов: с одной стороны тень чуть острее, с другой чуть грубее, а если запас глубины у каскадов подобран по-разному, то ещё и сдвигается.
Прячут шов полосой смешивания: в окрестности границы считают тень по обоим каскадам и плавно перемешивают по дистанции. Стоит это удвоенного числа выборок в узкой полосе экрана — терпимо. Кроме того, запас глубины обязан пересчитываться на каждый каскад: тексель у них разного размера, а из второй части пропорционален именно ему. Одна константа на все каскады даёт acne в дальнем и отрыв в ближнем.
Чего это стоит
Главная статья расходов — не память, хотя четыре карты это 64 мегабайта. Главное — проходы. Каскад рисуется отдельным проходом по геометрии, и объект, попавший в три каскада, рисуется трижды. Плюс основной проход. Это ровно тот случай, когда узкое место кадра оказывается не в шейдерах, а в количестве команд отрисовки — про них была отдельная статья.
Поэтому в движках каскадов обычно не больше четырёх, дальний каскад часто обновляют не каждый кадр, а в список объектов для дальних каскадов не включают мелочь, тень которой на такой дистанции всё равно меньше текселя.
Оба прохода целиком
Соберём. Ниже — рабочая сцена: два прохода рендера, настоящая карта глубины в кадровом буфере, все переключатели из предыдущих частей.
Оба прохода целиком
Функция, которая считает тень, напечатана целиком. Это тот же исходник, который склеивается в компилируемый шейдер демки, а не его пересказ:
float shadow(vec3 worldPos, vec3 N, vec3 L) {
// Normal-offset: точку двигают вдоль нормали ДО перевода в свет —
// сдвигается место выборки, а не сравниваемое число.
vec4 lightPos = uLightViewProj * vec4(worldPos + N * uNormalOffset, 1.0);
vec3 proj = lightPos.xyz / lightPos.w * 0.5 + 0.5;
// За границами карты ничего не записано. Считаем, что там открыто —
// иначе весь мир за краем карты уйдёт в тень.
if (proj.z > 1.0) return 1.0;
if (any(lessThan(proj.xy, vec2(0.0))) || any(greaterThan(proj.xy, vec2(1.0)))) return 1.0;
// Slope-scale bias: чем острее свет ложится на поверхность, тем сильнее
// гуляет её глубина внутри одного текселя карты.
float cosT = clamp(dot(N, L), 1e-3, 1.0);
float tanT = sqrt(1.0 - cosT * cosT) / cosT;
float bias = uBiasConst + uBiasSlope * tanT;
vec2 texel = 1.0 / vec2(textureSize(uShadowMap, 0));
// PCF: каждая выборка отвечает «в тени / не в тени», и усредняются ОТВЕТЫ.
// Усреднить сначала глубины — значит сравнить себя с поверхностью, которой
// в сцене нет.
float lit = 0.0;
for (int j = -PCF_R; j <= PCF_R; ++j) {
for (int i = -PCF_R; i <= PCF_R; ++i) {
float stored = texture(uShadowMap, proj.xy + vec2(float(i), float(j)) * texel).r;
lit += step(proj.z - bias, stored);
}
}
float taps = float((2 * PCF_R + 1) * (2 * PCF_R + 1));
return lit / taps;
}Пройдёмся по ней сверху вниз.
Сдвиг вдоль нормали идёт первым, до перевода в пространство света. Так и должно быть: normal offset двигает место, откуда берётся выборка, а не сравниваемое число. Если поставить его после проекции, получится обычный сдвиг по глубине с другим коэффициентом.
Деление на w и переход в [0, 1]. У ортографической проекции солнца и деление ничего не меняет, но для прожектора оно обязательно. Дальше координаты сдвигаются из NDC в диапазон текстуры — та самая * 0.5 + 0.5.
Два выхода наружу. Если точка глубже дальней плоскости карты или вышла за её пределы по xy, функция возвращает единицу — «освещено». Это не забота о корректности, а выбор из двух зол: вне карты данных нет, и любой ответ будет выдуманным. «Освещено» выдумывать безопаснее, потому что альтернатива — чёрная стена по невидимой линии. В движках вместо этого чаще делают затухание тени к краю дальнего каскада.
Запас. cosT — это , зажатый снизу, чтобы не делить на ноль. Из него получается и дальше формула из второй части. Обратите внимание: обе константы приходят снаружи в единицах глубины карты, а не в метрах. У ортографической проекции перевод простой — поделить мировой запас на диапазон глубины; у перспективной он зависит от дистанции, и это ещё одна причина, по которой тени от прожекторов настраиваются отдельно.
Цикл PCF. textureSize даёт разрешение карты прямо в шейдере, из него — размер текселя в координатах текстуры. step(proj.z - bias, stored) возвращает единицу, если освещено. Складываются именно ответы, а не глубины.
PCF_R — это #define, а не uniform. Иначе цикл не развернётся, и вместо прямолинейного кода получится ветвление в каждом пикселе. Смена размера ядра означает пересборку шейдера — поэтому в движках варианты качества теней компилируются заранее, отдельными комбинациями ключей.
Где на самом деле тратится кадр
Функция выше стоит выборок из текстуры. Это заметно, но не это главный расход.
Главный — первый проход. Чтобы построить карту, нужно заново прогнать всю геометрию сцены, с отсечением, с выставлением состояний, с командами отрисовки. Один каскад — один такой прогон. Четыре каскада — четыре, плюс основной проход: пять раз за кадр по одному и тому же списку объектов.
Отсюда практика, которая со стороны выглядит странно, а на деле прямо следует из арифметики: в теневой проход отдают упрощённые модели, отключают все шейдеры кроме позиции, режут списки по дистанции и обновляют дальние каскады через кадр. Всё это — про количество команд, а не про пиксели.
Выводы
Если оставить от статьи одну мысль, пусть будет такая: карта теней отвечает не на тот вопрос, который вы задали. Вопрос был про видимость точки из источника. Ответ — про то, глубже ли эта точка, чем ближайшая поверхность в текселе, куда она спроецировалась. Всё, что мы разбирали, — это разница между двумя формулировками, и все приёмы борьбы с артефактами живут по одну сторону от неё.
Отсюда и разбор симптомов:
- полосатая рябь на ровных поверхностях — запаса глубины нет или он меньше ;
- рябь появилась только к вечеру — запас константный, а вырос вместе с заходом солнца;
- предметы парят над полом — запас больше необходимого, и тень отъехала на ;
- и рябь, и парение одновременно — окно между ними закрылось: карта слишком грубая для такого угла света, числом это уже не чинится;
- край тени идёт крупной лесенкой — тексель кладётся на поверхность пятном шире пикселя;
- лесенка мелкая, но переливается в движении — карта не привязана к целым текселям;
- тень отклеилась от предмета, хотя запас нулевой — глубины усреднили до сравнения вместо после;
- тень мягкая, но контакт с полом потерян — ядро фильтра расширили, чтобы спрятать грубость карты;
- полоса на определённой дистанции — шов каскадов без смешивания или с разным запасом у соседей;
- тени обрываются по невидимой линии — кончилась область, которую накрывает карта.
Чего мы не разобрали
Другие способы хранения. Всё в статье построено на сравнении глубин, но глубину можно хранить так, чтобы карту стало можно фильтровать заранее и обычными средствами. В variance shadow maps (Donnelly и Lauritzen, 2006) в текстуру пишут глубину и её квадрат, а видимость оценивают неравенством Чебышёва; развитие идеи — exponential (Annen и соавторы, 2008) и moment shadow maps (Peters и Klein, 2015). Все они меняют один набор артефактов на другой: вместо acne появляется просвет тени в местах, где загораживающих поверхностей несколько.
Тени от точечных ламп. Шесть граней кубической карты, свой набор швов и своя арифметика запаса, потому что проекция там перспективная.
Контактные тени. Короткая трассировка по буферу глубины прямо в экранном пространстве — то, что подбирает мелкие детали, для которых тексель карты слишком крупен. Это уже соседняя тема, ближе к затенению окружением.
Трассировка. Тот самый прямой ответ из первой части, который в 1978 году был немыслим, а сейчас — вариант. Он снимает разом весь список артефактов выше, потому что не подменяет вопрос. Взамен приносит свой: шум, который надо чем-то давить.
И общая закономерность, сквозная для всей серии. Карта теней не врёт — она даёт точный ответ на свой вопрос. Ошибка начинается там, где разница между её вопросом и вашим перестала быть маленькой: скользящий свет, грубая карта, протяжённый источник. Разница между «подкрутил bias, стало лучше» и «знаю, что чиню», — в том, чтобы держать в голове, где именно эти два вопроса расходятся.
Дальше по конвейеру — то, что мы всю статью аккуратно обходили стороной. Тень считалась бинарной, потому что всё в сцене было непрозрачным. Стоит поставить на пути стекло, воду или листву, и вопрос «загорожено ли» перестаёт иметь ответ «да» или «нет» — а вместе с ним перестаёт работать и буфер глубины, на котором всё держалось. Следующий сезон — «Сквозь стекло»: альфа, порядок и то, во что превращается кадр, когда объекты начинают просвечивать друг сквозь друга.
Надеюсь, статья была полезна. Заходите в телеграм-канал, такие разборы выходят там каждую неделю, и буду рад дополнениям в комментариях: у теней нюансов заметно больше, чем влезло в шесть частей.
Разборы графики с кодом — каждую неделю в канале.
