Стенсильные тени изнутри.
Автор: Семен Козлов
Intro. Сейчас уже практически всем понятно, что в играх без теней — никуда, а тем более динамических. Особенно в таких играх, которые подразумевают эффект присутствия. Ну там, 3D Action, всяческие гоночки, RPG от первого лица и так далее. В принципе, то что без теней никуда понимали и раньше, но раньше (раньше — это первые Voodoo и иже с ними) ресурсов для отображения правильных теней в мало-мальски приличной сцене попросту не хватало. Сейчас стало чуть лучше, но тем не менее, тени — один из самых «тяжелых» эффектов в плане производительности. Одно можно сказать точно: проблема теней еще далеко не решена в полном объеме, и надо думать, статей, начинающихся с «Как известно, отображение теней — одна из важных задач современной трехмерной графики в реальном времени», еще выйдет немало.
Введение в Shadow Volumes и Стенсил
Основная идея Shadow Volumes. Стенсил.
Подробности реализации
1. Построение волюма.
2. Отрисовка волюма.
Шаг 1. Отрисовка сцены.
Шаг 2. Отрисовка волюмов в стенсил.
Шаг 3. Отобразить тень на экране.
Проблемы на near plane
1) Честное построение near cap.
2) Использование расширения NV_DEPTH_CLAMP.
3) ZFail approach или Carmack's Reverse.
Оптимизации
1) Оптимизации построения волюма.
2) Оптимизации отображения волюма.
Всяческий stuff на тему стенсильных теней
1. Мягкие тени.
1.1. Рендер в разные разрешения.
1.2. Размытие и трехмерная текстура.
1.3. Блур между тенями разных размеров.
2. Гладкие волюмы.
3. LOD'ы для теней.
Заключение
Преимущество стенсильных теней
Недостатки стенсильных теней
Введение в Shadow Volumes и Стенсил
Алгоритмов создания теней не то чтобы много, но есть. Для динамических теней наиболее популярны два алгоритма: Shadow Volumes или стенсельные тени, и Shadow Mapping. У каждого из них свои достоинства и недостатки, углубляться в детальное сравнение я здесь не буду.
В этой статье я постараюсь рассказать о стенсельных тенях.
Основная идея Shadow Volumes. Стенсил.
С самого начала чуть-чуть общепринятой терминологии. Объект, отбрасывающий тень, называется Shadow Caster, объект, на которого отбрасывается тень — Shadow Receiver.
В идеале вся сцена является и Shadow Caster'ом, и Shadow Receiver'ом, но часто можно указать Shadow Caster'ы и Shadow Receiver'ы более детально, и получить на этом неслабую экономию ресурсов. Например, если у нас есть более-менее плоский ландшафт с объектами на нем, то логично не делать ландшафт Shadow Caster'ом.
При самозатенении Shadow Caster является еще и Shadow Receiver'ом своей же тени.
Теперь о главном.
Как ни удивительно, Shadow Volumes не зря так называются. Вся идея в том чтобы построить такой объект, что бы все что внутрь него попадает считалось затененным. Отсюда и Shadow Volume — теневой объем.
То есть для каждого объекта в сцене мы строим Shadow Volume — особый невидимый объект, такой чтобы все что попадало в тень от этого объекта находилось внутри него. Понятно, что этот объем — это сам Shadow Caster, вытянутый по направлению от источника.
После этого возникает два вопроса: как строить Shadow Volume для данного объекта, и как проверять, какая часть сцены находится внутри этого объекта.
Надо сказать, что эти вопросы связаны друг с другом, то есть от того как строить Shadow Volume будет зависеть то, как мы будем находить часть сцены внутри него, и наоборот.
В самом простом случае ответ на первый вопрос звучит так — Shadow Volume это силуэт объекта с точки зрения источника света, вытянутый до бесконечности.
Опять же картинки должны помочь. То есть метод построения такой: находим все силуэтные ребра объекта (с позиции источника света), и каждое такое ребро превращаем в квад из двух треугольников, вытянутый по направлению света. Понятно, что до бесконечности мы вытянуть его не можем, поэтому вытягиваем на какую-то величину, заведомо превыщающую размеры сцены.
Теперь самое интересное, а именно как находить часть сцены внутри волюма. Именно здесь на сцену вступает стенсил буфер (в дальнейшем просто стенсил).
Сначала вкратце о том, что такое стенсил-буфер. Это дополнительный буфер размера экрана, то есть каждому пикселю экрана соответствует свое значение в стенсил-буфере. Каждый раз когда точка рисуется на экран, то кроме тестов вроде сравнения с глубиной в Z-буфере она проходит еще и стенсил тест. То есть, например, можно сказать — точка рисуется, только если в стенсиле значение больше единицы. С другой стороны, можно сказать, как изменить значение стенсила после того как пиксель в этом месте отрисуется.
Это, например, полезно для разного рода отсечений. Характерный пример — портальное отсечение. Вот, есть у нас портал в виде какого-то набора треугольников, и нам не хочется, чтобы та часть сцены, что за порталом, вылезала за его границы. Если портал произвольной формы, то гарантировать это при простой отрисовке сложновато. Но если есть стенсил, то все становится просто — сначала рисуем портал в стенсил определенным значением (запись на экран или в Z-буфер отключаем), а потом при отрисовке сцены за порталом включаем на это значение стенсил тест. Надеюсь, идея понятна.
На запись в стенсил есть следующие команды — что делать со стенсилом и при каких условиях. Возможности изменения, которые нам понадобятся такие: «увеличить значение в стенсиле», «уменьшить значение в стенсиле», «записать в стенсил данное значение». Условия на запись: «точка прошла Z-Test», «точка не прошла Z-Test», «точка прошла стенсил тест», «точка не прошла стенсил тест».
То есть можно установить, например, что при пройденном Z-Test'e значение в стенсиле увеличивается, а при пройденном стенсил тесте, уменьшается и так далее, в любых комбинациях.
Конечно же, есть свои тонкости, и набор возможных изменений стенсила зависит от конкретной карты, но те команды, которые я написал есть практически на всех картах, на которых вообще есть стенсил.
Итак, как же наличие стенсил-буфера помогает узнать какая часть сцены находится внутри волюма?
Изначально мы рисуем всю сцену без теней. Основная задача — чтобы Z-буфер был правильно заполнен. Теперь мы два раза рисуем волюм (оба раза запись в Frame buffer отключена, то есть на экран мы ничего не рисуем).
Первый раз рисуем те грани, которые повернуты лицевой стороной к камере (front-cull), и при этом увеличиваем стенсил там, где пройден Z-test. В результате в стенсиле мы получим значения больше нуля там, где передняя часть волюма находится к нам ближе чем к сцене:
За второй проход рисуем грани, которые повернуты лицевой стороной от камеры (back-cull), и уменьшаем значения стенсила там, где пройден Z-test. На картинке - те точки, где мы уменьшим значения стенсила:
В результате после обоих проходов мы получим ненулевые значения там, где передняя часть волюма прошла тест, а задняя не прошла. На самом деле, это и есть та часть сцены на экране, которая находится внутри волюма, а значит — в тени от того объекта, от которого строили волюм. Те области, которые мы не вычеркнули вторым проходом и есть то, что лежит в тени.
Естественно, во время отрисовки никакие такие цвета не рисуются, все эти значения — только в стенсиле.
Еще одна важная деталь: во время отрисовки волюма мы выключаем запись в Z-буфер. То есть сам Z-буфер по-прежнему работает, но если точка волюма прошла Z-test, то ее координата не записывается в Z-буфер.
После того как мы получили в стенсиле ненулевые значения только там где есть тень, то наложить эту тень уже несложно. Самый простой метод — нарисовать черный полупрозрачный квад во весь экран и выводить его с блендингом там где стенсил тест пройден. Тогда там, где есть тень, освещение станет меньше.
Для более сложных объектов все эти картинки становятся гораздо запутанней из-за того что по одному месту может проходить больше граней волюмов, но прицип остается тем же.
Более подробно о конкретной реализации алгоритма — в следующей части.
5 апреля 2003 (Обновление: 27 сен 2009)