Если вы еще не читали первую часть - прочитайте хотя бы вступление.
Итак, вперёд...
Тень, такая, какая она есть
К выходу третьего дума в свет нас долго готовили, обещая нечто, чего
раньше не было. И нас, в общем, не обманули - в этой игре все тени
вычисляются динамически. Хотя и до Doom3 существовали технодемки с
динамическими тенями, именно эта игра стала первой их использовать.
Тут я вынужден сделать небольшое лирическое отступление. Во многих
играх (GL Quake, Serious Sam, Quake 2, Quake 3, Unreal, и всё, что с
ними связано) были псевдо-динамические тени. Эти тени отбрасывали
некоторые подвижные объекты. Но механизм работал довольно плохо,
особенно если тень падала на сложный рельеф. К сожалению у меня нет
возможности сделать скриншоты таких теней, но если кто-нибудь их
пришлёт, буду весьма рад.
Как же создавались эти тени?
Проекционная тень
Рисунок 1. Тень от зомби
На картинке вы можете видеть вполне приемлимую тень от зомби. Эта тень
сделана как раз старым способом. Принцип создания теней такой:
Выбирается плоскость для проекции. (Обычно выбирается то, на чём стоит объект)
Рисуется модель объекта
Чёрным полупрозрачным цветом рисуется та же самая модель, но спроецированая на плоскость.
Если источников света много - операция повторяется для каждого источника света.
Само проецирование на плоскость особой сложности для видеокарты не
составляет. Ведь в любом случае для того, чтобы вывести изображение на
экран, нужно спроецировать все рисуемые объекты на виртуальную
"экранную" плоскость. При создании таких теней проецирование происходит
всего лишь на один раз больше. Сначала на плоскость, а потом с
плоскости на экран. Ну а теперь о плохом. Во первых, такие тени не
могут ложиться на сложный рельеф. Это ограничение можно обойти,
проверяя несколько плоскостей, но чем больше плоскостей проверяем, тем
сложнее. к тому же нужно как-то обрезать тень по краю плоскости (во
всех играх плоскости как раз конечны, хотя правильнее было бы их
называть гранями). Во вторых такие тени неправильно работают в случае,
если источников света много. И в третьих, самозатенение объектов на
таких тенях невозможно.
А теперь, собственно, вкусное.
Стенсил буфер
Довольно давно в видеокартах появился дополнительный текстурный
канал. Собственно, немного о том, что это вообще такое. Картинка в
видеопамяти представлена обычно тремя цветами (красный, синий и
зелёный). При смешении этих цветов получается практически любой цвет.
На самом деле существуют цвета, которые не могут быть отображены таким
способом, но это уже тонкости. Вообще, такому решению способствует то,
что монитор принимает от видеокарты как раз три компоненты - красную,
зелёную и синюю. Кроме этого видеокарта подаёт на монитор синхросигнал,
который определяет, к какой точке на экране относятся конкретные
данные. Но кроме этих каналов в 3d ускорителях есть ещё и служебные
каналы. Например, альфа, z-буфер и, собственно, стенсил буфер, о
котором сейчас пойдёт речь.
Про альфа канал вы уже наверняка слышали - он определяет
прозрачность
участка картинки. На монитор он, естественно, не выводится, но во
внутренних операциях с изображением может участвовать. Стенсил буфер -
фактически ещё один альфа канал, но видеокарта работает с ним по
другому. А именно, значение в этом буффере можно уменьшать и
увеличивать. Фактически в него можно рисовать так же, как и на экран -
только вместо цвета к закрашеному участку будет приьавляться некоторое
число, или же вычитаться. Кроме этого, с помощью него можно фильтровать
вывод изображения. Например, не рисовать те точки, у которых
стенсил-"цвет" больше нуля.
Вот такая картинка для примера:
Рисунок 2. Что-то подобное мы бы увидели, если бы нарисовали
тень зомби из первого примера не на экран, а в stencil.
Чёрный ящик - Shadow volume
Ну ладно, со стенсилом познакомились. Но какое он вообще отношение
имеет к теням? Об этом чуть позже, а сейчас ещё об одном важном
элементе этой кухни. А именно - о теневых объёмах (shadow volumes).
Собственно, по факту, теневой объём - это такой объём, который чем-то
закрыт от света. Для удобства вот вам ещё одна картинка:
Рисунок 3. Рыжими линиями обведён теневой объём для этого кубика.
На самом деле теневой объём устроен чуть иначе, чем
на картинке. А именно - он занимает в пространстве место от задней
стороны кубика (та, что в тени) и расширяется в бесконечность. Но если
вы рисовали карты к третьему думу, то вы видели там такой параметр
источника света, как радиус. В игре теневой объём никогда не выходит за
этот радиус. Но это всё была лишь присказка - теперь начинается сама
сказка.
Как дум рисует уровень - для каждого источника света он рисует всю
геометрию, которая попадает в радиус самого источника. При этом он
вырезает из этого то, что попадает в какой-либо теневой объём. Сами
объёмы кэшируются. (Кстати, для многих лайтов эти объёмы можно найти в
proc файле, лежащем рядом с картой). Возникает только один вопрос - как
же вырезать из этого рисунка тени? Как вариант, конечно, подходит CSG
substract. Но на самом деле это очень долго. Но у нас в арсенале есть
стенсил. Вот только как правильно его использовать???
Ну вот и настало время главного фокуса. Посмотрите на третий рисунок -
на нём, собственно, объём. У этого объёма есть грани, повёрнутые к нам
лицом - те, которые было бы видно, даже если бы объём был непрозрачным.
Так же у него есть задние грани - те, которые видно только благодаря
прозрачности объёма. А теперь время фокуса. Закрасьте (мысленно, но
если не доверяете - в фотошопе) передние грани чёрным. И сотрите этот
чёрный с тех мест, где были задние грани. Что получилось? Да,
получилась именно она - тень. Игра поступает примерно так же, но каждая
закраска передних граней увеличивает значение stencil на единичку, а
каждая закраска задней грани - уменьшает. Если после отрисовки всех
теневых объёмов в данной точке экрана stencil > 0 - данная точка
попала в тень, и мы её не рисуем. Благодаря этому игра может рисовать
все теневые объёмы сразу, а потом просто нарисовать геометрию, из
которой автоматически будут вырезаны затенённые участки. При отрисовке
теневой объём рисуется так же, как и любой другой объект, поэтому те
объекты, которые ближе к камере, его закрывают. Остаётся только одна
тонкость. А именно, если игрок находится внутри тени, то при таком
алгоритме тени могут серьёзно пострадать. В игре это учтено: если игрок
внутри тени - то все края тени считаются задними сторонами, а stencil
изначально равен 1, а не 0.
Небольшое дополнение или Shadowmaps
Сейчас многие разработчики переходят к использованию shadowmaps вместо
stencil shadows. По сути stencil успел поучаствовать только в движке
третьего дума (Может и ещё в каких играх, только я о них не знаю).
Причиной этому, скорее всего, стало появление достаточно развитых
способов управлять отрисовкой картинки - пиксельных и вертексных
шейдеров новых версий. В принципе в думе тоже могли быть использованы карты теней,
но у них есть недостаток. Если для stencil требуется только 1 render
проход на источник света, то для shadowmaps на каждый источник света
требуется ещё и текстура. С другой стороны у shadowmaps есть и плюс -
тень содержит все детали, как они есть. То есть если в думе была
полупрозрачная текстура сетки, то тени она не отбрасывала вообще.
shadowmaps (sm) такую тень нарисует без особых проблем.
Теперь сам техпроцесс. Парадокс в том, что он описан ещё в книгах 1989
года. В общем для начала рисуем всё с позиции каждого источника света.
Естественно на картинке будет видно только то, что не попало в тень.
(Ещё один забавный факт - к HL2:EP2 разработчики таки прикрутили
динамическую тень... Единственным источником света, отбрасывающим такие
тени, был... ...фонарик. Учитывая тот факт, что игрок смотрит из
точки, очень близкой к фонарику, эти тени нужно было ещё постараться
заметить. Мультиплеера это, конечно, не касается - там чужой фонарик
виден сразу. Кстати, в думе фонарик тоже даёт тень, и её тоже никто не
видел.) Так вот, имеется очень полезная картинка. Кроме всего прочего,
при создании изображения видеокарта создаёт карту глубины изображения (z-буфер) В эту карту попадает z-координата всех нарисованых на экране точек. Собственно, эту информацию и нужно перетащить в текстуру shadowmap-а.
Теперь картинка стала ещё более забавной - этакая рельефная отливка пространства. Ну и самое главное. Когда все каты теней будут готовы, можно рисовать сцену с позиции игрока. Хотя к этому вопросу существует несколько подходов. Первый вариант - как в думе, получили один shadowmap, рисуем с ним картинку. Получили следующий - рисуем картинку поверх предыдущей. Плюсы очевидны, но будут и минусы, так называемый "пересвет". Вариант второй - в одну текстуру пакуется три или четыре источника света (текстура имеет четыре канала цветности, z влазит в один канал), потом с ними рисуется сцена. Потом опять пакуется четыре источника света... И третий вариант - создаются все shadowmaps для всех доступных источников света, потом рисуется сцена.
При отрисовке самой сцены производится простой, но хитрый трюк. Берётся координата рисуемой точки (в 3D) и преобразуется в пространство источника света (фактически выясняем положение этой точки на экране, если бы мы её рисовали с позиции источника света). Получив координаты, берём из shadowmap'а z и сравниваем с нынешним (тоже преобразованым в пространство источника света). Если тот z, который рисуется сейчас равен (для устранения артефактов допускается некоторый разброс) тому, что в текстуре - точка рисуется без тени. Если же больше - точка в тени. Ну а если меньше - это очень странно.
В общем пример работы техники:
Получается вполне адекватная тень. Но у этой техники всё же есть два критических недостатка:
Вот первый - по сравнению с чёткой stencil - тенью просто ужас. Хорошо, что проявляется только на больших расстояниях. Чем ближе к модели - тем чётче тень. А второй недостаток - проблемы с omni-источниками света. А именно - поле зрения камеры, к сожалению, не превышает 180 градусов. Значит для таких источников нужно по крайней мере два shadowmap-а, чтобы перекрыть все 360 обзоора.