Освещение в Quake
Учение - свет, а темнота - друг молодёжы. Так или иначе без освещения не обходится ни один FPS-движок. Даже в Doom уже было посекторное освещение, которое могло динамически изменяться - например моргать, иммитируя сломанную лампу дневного света. В Quake же освещение было проработано еще серъезнее. Тут следует заметить, что подход к освещению бывает трёх типов. Первый тип освещения - статичный. На геометрию уровня накладываются карты освещения, или более привычные нашему уху лайтмапы. Каждая такая карта содержит в себе информацию о яркости и цвете освещения каждого пикселя на карте. Правда, поскольку пикселей на карте очень-очень много, то используется более упрощенный подход: на каждые реальные 240 пикселей текстуры приходится 16 пикселей лайтмапы. Тени и свет при таком подходе оказываются прилично размытыми, что в большинстве случаев даже приветствуется, поскольку чёткие тени от объектов в реальном мире встречаются не очень часто. Как правило это заборы из сетки-рабицы, кусты, деревья. В данном случае наш метод с лайтмапами терпит полное поражение. Второй метод предполагает собой динамическое освещение, реализованное по одной из известных моделей освещения + динамические тени. Тени могут быть выполнены по двум технологиям: Shadow Volume и Shadow Mapping. Есть еще и третий вариант - проекция модели на стену\пол, в терминологии нашего товарища FiEctro - "плющмодель". Но данный вариантв силу своей примитивности годится лишь для игр с одним источником света и абсолютно ровным полом безо всяких стен, по типу какого-нибудь мортал-комбата в современной реинкарнации, поскольку там основной упор делается на грамотную работу с камерой и впечатляющие повторы. Хотя на современных компьютерах подобными вопросами мало кто заморачивается в силу высокой производительности. Shadow Volume или Теневые Объемы, это метод дающий математически правильные тени, лишенные большинства принципиальных недостатков, но к сожалению обладающие двумя
принципиальными ограничениями. Во первых данные тени невозможно сделать мягкими без существенной потери производительности, кроме того даже эти методы выдают не слишком сглаженные тени. А во вторых, и это пожалуй один из главных доводов - теневые объемы не в состоянии отбрасывать правильные тени от текстур с альфа-каналом (пресловутый забор из сетки рабицы делается практически во всех играх именно при помощи такой текстуры, а не модели с тысячми полигонов). К тому же попытка осветить открытую местность при помощи теневых объемов даёт заведомо некрасивый результат. Но, вот что касается мрачных подземелий, то тут с этим полный порядок, теневые объемы справляются как нельзя лучше. Собственно Doom3 и был мрачной игрой с такими вот подземельями. Теневые карты, аналогично световым рассчитываются с позиции источника света. Только в отличие от световых карт, которые обычно считает компилятор, теневые считаются прямо налету во время рендеринга. Этим достигается их динамичность. Результат рендеринга сохраняется в особую текстуру глубины. Буффер глубины разумеется имеет конечную точность, которой в большинстве случаев недостаточно. Отсюда и главный недостаток теневых карт - явно выраженные артефакты, с которыми пытаются бороться различными методами фильтрации. Ну а, собственно освещение считается в вертексном \пиксельном шейдере, точно также беря информацию от ближайших источников света и рассчитывая освещение каждого пикселя\вертекса по какой-либо формуле освещения. Подробнее про модели освещения можно почитать, например у Борескова по этой сцылке. Данный подход обычно практикуется как раз нашими начинающими движкописателями, о которых я упоминал в одной из статей. Ну это когда в одной комнате красиво и реалистично освещенный дракончик и FPS ниже 60 на 8800GTX. Ну и наконец третий, новомодный метод, так называемого Deffered Shading - отложенного освещения. Возможно в комбинации с теневыми картами, поскольку сам метод предполагает только освещение, но не тени. Суть метода заключается в том, что освещается не, собственно, 3D-сцена, а некий буффер, содержащий в себе информацию о всех видимых пикселях сцены. В обычный набор входит нормаль, амбиентный и диффузный свет. Ну а потом мы
пропускаем наш буффер через определенный шейдер и на выходе получаем грубо говоря готовую лайт-карту, наложенную на экран.
В действительности всё, конечно сложнее, но подробное рассмотрение данного метода выходит за рамки этой статьи. Кому интересно - можете почитать у того же Борескова. Из недостатков данного подхода следует выделить проблемы при работе с полупрозрачными объектами и довольно таки большую требовательность к ресурсам. Вообще говоря в современных играх, как правило основные ресурсы потребляют три вещи: Физика, AI монстров и освещение\тени. Но если физику равно как Ai можно всячески оптимизировать и методов для этого дела имеется вагон и маленькая тележка, то вменямых методов оптимизировать рассчет освещения очень мало. А те что есть - дают зачастую не слишком красивый результат. И в этом аспекте старые добрые лайтмапы удивительным образом сочетают в себе и красоту освещения и смехотворную требовательность к железу. Вот собственно про
лайтмапы мы с вами и поговорим в рамках данной статьи. Потому что именно они используются во всех трех квейках, а также в первой и даже во второй халфе. Да чего уж там говорить - даже Сталкер в режиме статического освещения использует именно их, и надо признать, лично мне результат его работы нравится гораздо больше, нежели картинка, выдаваемая рендером с динамическим освещением. Ограничение у лайтмап ровно одно - принципиальная невозможность создания динамических теней\света, поскольку лайтмапы считаются компилятором, а не во время игры. Впрочем, как вы убедитесь по ходу чтения, с этой принципиальной невозможностью можно вполне успешно бороться и достигать в этом деле неплохих результатов.
Итак, углубимся в технические подробности. Выше уже говорилсь, что лайтмапа уникальна для каждой детали уровня. При этом отдельный полигон, размером менее вышеназванных 240х240 занимает лайтмапа соответствующего размера, а если полигон больше, то компилятор его аккуратно разделяет на два и более. Правда тут есть хитрость - размер полигона учитывается не по реальным юнитам карты, а по пикселям текстуры. Легко догадаться, что если маппер растянул текстуру до гигантских размеров, то кол-во пикселей на ней от этого больше не стало. Данный подход не является принципиальным ограничением, непосредственно лайтмап, в принципе никто не мешает заставить компилятор разбить такую большую текстуру еще на несколько полигонов и наложить на каждый из них полноценную лайтмапу, но... На дворе стоял 1996 год и ребята из ID тупо пожалели памяти. А как мы помним в то время дай бог чтобы у среднестатического юзера в компе стояло 8 мегабайт оперативы. Могло быть конечно и меньше, но тут уже квейк начинал сопротивляться и не запускался. Собственно вышеописанный метод с разбиением текстур болього скейла на несколько
независимых полигонов был реализован тов. Ксероксом в HLFX 0.6 под названием "супердискретные лайтмапы" (см. документацию к HLFX 0.6). Однако мы отвлеклись. Размер лайтмапы вычисляется простым делением реального размера полигона на 16. Однако размер блока лайтмапы равен не 16х16 а 18х18 пикселей. Сделано это было из очень простых соображений. Поскольку лайтмапа такого небольшого разрешения будет сильно пикселизированной то её надо сгладить (да-да, даже на софтварном рендерере). А сглаживание с большей долей вероятности размоет края лайтмапы и они выйдут за её реальные границы. Таким образом пара лишних пикселей - это охранный бордюр, сделанный с тем расчетом, чтобы лайтмапы не залазили друг на друга. Поскольку лайтмапы набиваются в текстуры размером 128х128 пикселей, дабы более эффективно использовать видеопамять и избежать лишних переключений текстуры (при аппаратном рендерере). Ну а дальше - дело техники. Поскольку каждая лайтмапа прилинкована к каждому сурфейсу, то и рисуются они одновременно с ними, либо накладываясь с нужной функций смешивания, либо при помощи мультитекстуринга.
Таким нехитрым образом мы получаем правильно освещенный уровень при полном отсутствии торможения. Но - увы в полностью статичном виде. Освещение без возможности хотя бы банально включать\выключать источники света совсем никуда не годится и в ID это понимали. Но как менять уже предрасчитанное освещение? Технически на самом деле проблем нет. Как вы помните лайтмапа смешивается с основной текстурой по определенной формуле. Нам достаточно немного подправить коэффициенты и вот результат будет выглядеть ярче или наоборот темнее. Неплохо, скажете вы, но как узнать для каких именно лайтмап нам необходимо устроить затенение, а какие не трогать? Ведь прямой связи у лайтмапы и источника освещения (т.е. энтити лайт\светящейся текстуры) попросту нет. И тут нам на помощь приходят лайтстили. Что же такое лайтстиль? Это некий уникальный ID поверхности, который расставляется компилятором при построении освещения. Изначально наш ID применяется к выключабельной лампочке (обычно такие лампочки имеют имя, targetname). Пор этому параметру компилятор автоматически выделяет нашей лампочке (а в некоторых версиях ZHLT - даже светящейся текстуре, или особой энтите с ней) персональный номер. Нулевой ID используется по умолчанию для геомтерии мира. Все остальные ID могут быть отданы для наших лампочек. Легко догадаться, что таким образом кол-во выключаемых лампочек не может быть более 255. Это опять-таки не является принципиальным ограничением технологии лайтмап, а просто товарищ Кармак заюзал в структуре сурфейсов переменную unsigned char, в которую больше 255 стилей не влезет Опять таки из соображений экономии памяти. Кроме этого если некоторые лапочки должны выключаться синхронно, то им может быть присвоен одинаковый лайтстиль (компилятор распознает их по одинаковым именам). Так что в целом не всё так уж и страшно. Дальше
освещение считается следующим образом: делается монопольный рассчет освещения для сурфейса с выбранным лайт-стилем. Затем делается рассчет освещения для тех же сурфейсов но уже без учёта нашего источника с номером. Результат сохраняется в raw лайтмапу последовательным образом: то есть сначала пакуется первая лайтмапа (с лайтстилем 0), затем вторая (для нашей лампочки с лайтстилем, ну пусть будет 32). Зная размер лайтмап нам не требуется хранить указатели на их начало для различных лайтстилей. Нам очень легко перемотать этот буффер на нужную величину, используя нехитрую формулу высота Х ширина лайтмапы. В Half-Life дополнительно умножают на 3, поскольку там цветное освещение. Но суть от этого не меняется, как вы понимаете. Ну а непосредственно в переменные styles пишут какому кусочку лайтмапы какой стиль соответствует. Итак у нас есть уже готовый набор данных, которым мы можем управлять непосредственно в игре. При помощи движковой функции SET_LIGHSTYLE. Данная функция принимает два аргумента - собственно номер лайтстиля и его яркость. Технически смешение лайтстилей можно организовать как средствами OpenGL так и на CPU простым сложением значений, а потом делением на кол-во использованных лайт-стилей. В Quake2 например практикуют нормализацию полученного значения. Каждый из методов даёт свою модель смешения несколько не похожую на другие. Затем получившаяся лайтмапа загружается в видеопамять на место того кусочка где у нас была старая лайтмапа. И так - каждый кадр. Конечно операция GlTexImage достаточно медленная, однако, как уже упоминалось выше, никто не запрещает смешать несколько стилей методом мультитекстуринга. Этот метод реализован например в клоне QuakeIII - движке QFusion. К тому же смехотворный размер текстуры (16х16 пикселей и меньше) делает такую операцию достаточно быстрой, чтобы это вообще не ощущалось (а тем более на современных компьютерах). Кроме этого используется механизм оптимизации в виде кэширования текущих лайт-значений (переменная cached_light в структуре msurface_t). Суть её в том, что если высчитанное значение за прошедшее время не изменилось (например если освещение переключаемое - от света к тьме, а не моргающее или пульсирующее), то и загружать текстуру повторно - незачем. Приципиально данный метод лайтстилей даёт очень неплохие результаты, но! Опять таки для статичных источников света, которые не могут двигаться сами. Ну и разумеется - никак не влияет на движущиеся объекты, типа открывающисях дверей и прочих лифтов. Печально, но с этим ограничением уже ничего поделать невозможно. Тут уже принципиальное ограничение оффлайн-расчетов. Второе неприятное ограничение заключается в том, что лампочек с различными лайт-стилями не может быть более 4х, направленных на один и тот же фейс. Даже не четырех, а трех, если поблизости есть источник освещения без назначенного лайт-стиля. В этом случае, как наверное знает любой маппер, компилятор выдаст предупреждение о MAX_LIGHT_STYELS_ON_FACE или о чём-то подобном. А в игре некоторые источники света начнут переключаться странно - возникнут пропадающие участки темноты\света. Данному ограничению мы также
обязаны товарищу Кармаку, который ввел лимит на 4 лайтстиля на фейс. Собственно увеличение количества лайт-стилей на один фейс поможет реализовать очень и очень любопытные эффекты, вроде настоящего солнца, которое реалистично ходит по небу с востока на запад и объекты на уровне при этом отбрасывают мягкие реалистичные тени. Пример такого солнца вы могли наблюдать в том же Сталкере когда в настройках
выбрано статичное освещение. Ну а в Quake, или точнее в Half-Life, где есть источник небесного света light_environment увы повторить подобный трюк не получится в силу вышеописанных причин. Но зато можно хотя бы менять яркость солнца, назначив ему соответствующий лайт-стиль и таким образом изготовив на древнем движке примитивную смену дня и ночи. С лайт-стилями вроде разобрались, теперь поговорим об действительно динамических источниках света. Данные источники являют
собой адский код по выявлению потенциальных поверхностей, могущих быть освещенных динамическим источником света и помеченных соответствующими битами. Поскольку каждый сурфейс хранит переменную dlightbits типа unsigned int то и битов-флагов может быть
всего 32. Отсюда вытекает принципиальное ограничение на видимых кол-во длайтов в одном кадре. Видимых, потому что, прежде чем помечать сурфейсы как потенциально освещенные, они сначала проходят проверку на видимость, попадание во фруструм (см. соответствующую статью). Ну а функция R_MarkLights бегает по BSP дереву и ищет сурфейсы в радиусе нашего длайта. Помеченные таким образом сурфейсы проходят операцию нанесения динамической лайтмапы согласно удалению центра нашего источника света от поверхности и его радиуса. В результате получается примерно круглое пятно. Ну а его сглаженность объясняется включенной
фильтрацией текстуры Данное пятно можно использовать по всякому. Можно опять таки смешать его с остальными текстурами средствами OpenGL, можно смешать на CPU как будто бы это такой особый лайт-стиль и потом загрузить в видеопамять. Последний способ используется в Quake1 и Half-Life и надо признать особой производительностью не блещет. Ну а смешение с лайтмапой путём рендеринга дополнительной текстуры успешно используется, начиная уже со второй кваки и показывает наоборот очень неплохие результаты. Так 32 одновременно включенных источника света практически не роняют FPS. Несмотря на то, что они являются действительно полноценными динамическими источниками освещения. В современной реализации скорость работы можно повысить на порядок и более, если требуемые рассчеты осуществлять непосредственно на GPU. В таком случае даже представляется возможность создания полностью динамических лайтмап. Пример такой реализации можно посмотреть здесь. Теперь, когда мы разобрались с лайтмапами и освещением мира и брашевых моделей давайте поговорим о монстрах, на которых, как известно лайтмапу не наложишь. Но зато у полигональных моделек есть нормали (у брашевых тоже есть, но посольку они освещаются лайтмапами уже на этапе компиляции то в первых двух кваках никак не используются. А вот в третьей используются для вертексного освещения), которые мы можем использовать для построения нехитрого вертексного освещения с определенной позиции модели. Чаще всего - это оригин модели, но никто не мешает для этого дела завести еще одну переменную и как-то её подвинуть относительно оригина в локальном пространстве модели. Рассчет освещения применяется по одной из тех моделей, которые подробно описаны в статье Борескова. В Half-Life для освещения студиомоделей используется модель Ламберта, например. Это всё хорошо, скажете вы, но откуда же мы получим требуемые значения освещения для отдельно взятой модели? Да прямо из лайтмапы! Этим интересным делом в Quake занимается функция LightPoint, которая трейсит мир сверху вниз (в Half-Life есть флаг EF_INVLIGHT, специально для monster_barnacle, прицепленных к потолку и изменяющий порядок трейса на обратный, т.е. снизу вверх). Как только наша траса упрётся в какую либо поверхность нам достаточно будет убедится, что оригин попал в текстурные координаты текущей лайтмапы и просто сложить лайт-стили с этого фейса в конечное значение. Потом оно будет поделено на предельное кол-во всех пикселей в лайтмапе. Доступ к пикселям лайтмапы осуществляется из указателя samples, который находится внутри структуры surface_t. К сожалению в Quake отсутствует какая-либо информация о направлении на источник света, поэтому конечное освещение получается амбиентным - то есть без ярко выраженного направления на свет. Но зато такая модель правильно затеняется при выключении освещения лайтстилем. В Quake3 от этого метода отказались, использовав регулярную сетку освещения. Принцип работы такой сетки заключается в том, что компилятор делит весь уровень на регулярные квадраты в которых производит замеры функцией, похожей на LightPoint, но делает это на этапе компиляции, когда инаформация об освещении несравненно более полная, нежели в уже готовой карте. Каждая ячейка содержит уровень амбиентного и диффузного освещения, а так же направление на свет. Не какое-то конкретное, а общую сумму направлений всех источников, которые потенциально могут влиять на данную ячейку. Поэтому в Quake3 вертексное освещение смотрится гораздо лучше и красвее, особенно это заметно по модели оружия в руках. Минусы такого подхода очевидны: во первых процентов 70 ячеек придется вообще за пределами уровня, но выбросить их нелья, поскольку сетка регулярная. Кроме того у нас будет масса ячеек с очень похожими параметрами, которые опять таки нельзя выбросить. Да и сама сетка имеет точность обусловленную её шагом (по умолчанию 64х64х128). Уменьшение шага приводит к резкому вырастанию конечного размера карты, а его увеличение - к более размытому вертексному освещению. Даже скорее почти к амбиентному. С этим методом боролись путём введения индексации нашей сетки и удалением дублирующих элементов, что здорово уменьшило её конечный размер. Правда уже не в Quake3 а в игре построенной на её базе - Soldiers Of Fortune, например.
На этом пожалуй заканчиваю, если в механизме реализации освещения для вас еще остались "тёмные места" - задавайте вопросы.