Поняв, как работают эти алгоритмы, я решил реализовать их на практике — написать небольшой редактор уровней и визуализатор к нему. Проект назвал Jurassic 3D Engine.
У каждой игры есть свой редактор, где создается геометрия, накладываются текстуры, импортируются модели из 3D Studio Max и т. д. За основу как раз и был взят 3D Studio Max — три проекции и перспектива.
Редактор умел делать базовые вещи:
• создавать примитивы (многоугольники, кубы, сферы, цилиндры);
• накладывать на них текстуры;
• превращать двухмерные многоугольники в трехмерные примитивы вытяжением;
• импортировать сложные модели из 3D Studio Max;
• расставлять источники света.
Весь интерфейс был сделан полностью на OpenGL. На этом редакторе я сделал две небольшие карты: одна с текстурами[10] из Max Payne, вторая — квартира родителей из поселка под Кингисеппом. Под вторую локацию часть текстур пришлось искать в интернете, а другую часть я отснял на цифровую мыльницу. Проблема с отснятым была в том, что при заполнении такой текстурой какой-то поверхности ее противоположные края должны соединяться на стыках без каких-либо графических артефактов.
Завершающим этапом создания карты в редакторе был режим предварительной обработки: построение BSP-дерева, расчет видимости его листьев PVS.
Текстура Наложение текстуры
Rad
Следующий шаг — рассчитать карты освещенности (Lightmaps). Напомню, Lightmap — метод освещения пространства в 3D-приложениях, заключающийся в том, что создается текстура, содержащая информацию об освещенности трехмерных моделей.
Для расчета освещенности я сделал отдельную утилиту Rad. Как следует из сокращенного названия, она рассчитывала также и отраженный свет по алгоритму Radiosity.
Radiosity — это алгоритм расчета освещения, учитывающий отраженный свет. Посмотрите на картинку, на ней комната с синим полом, красным потолком и белыми стенами. На белых стенах появятся красный и синий оттенки.
В Radiosity нет точечных источников света, здесь каждая точка поверхности излучает энергию (свет). Каждая точка поверхности посылает свою энергию всем остальным видимым ей точкам. Часть энергии они поглощают, а часть — отражают. И так бесконечное число раз.
В программных реализациях Radiosity имеют дело не с бесконечным количеством точек, а с конечным количеством маленьких частей поверхности — патчей. Cамый простой алгоритм построения Radiosity: последовательно для кажого патча передавать его энергию всем видимым для него патчам. Тут сразу же возникает проблема в определении видимости одного патча из другого. Придется делать трассировку луча и искать пересечение этого луча со всеми остальными объектами сцены, что очень дорого по вычислениям!
Я поступил несколько иначе. Что если смотреть из центра патча по нормали к поверхности? Мы увидим все то, что видит патч, иначе говоря, мы видим другие патчи, излучающие энергию. Складывая вместе энергию этих патчей, мы можем посчитать количество энергии и света из сцены, достигающей наш патч. Назовем это количество входящим светом (incident light).
В Radiosity нет различия между источниками света и патчами (частями поверхности), а в реальном мире некоторые поверхности излучают свет. Значит, некоторые патчи должны уметь не только поглощать и отражать свет, но и излучать его. Свойство патча, отвечающее за его способность излучать свет, назовем emission. Как раз эмиссию патчей и задавал редактор.
Свойство патча, характеризующее его видимость, это количество излучаемой им в сумме энергии — исходящий свет (excident
light).incident
= сумма всего света, который видит патчexcident
= (incident * reflectance) + emissionТогда для расчета освещения нужно выполнить эти две операции для всех патчей в сцене заданное число раз.
Основная проблема, которую предстояло решить, — визуализация сцены из центра каждого патча. Причем обзор должен охватывать полностью то полупространство, в которое направлена нормаль поверхности. Можно взять полусферу и установить на патч так, чтобы центр полусферы совпадал с центром патча, и спроецировать сцену на полусферу. Но ни одна графическая библиотека не позволяет это сделать.
Вместо полусферы используют полукуб (hemicube). Каждая грань куба — перспективная проекция с углом обзора 90 градусов.
Такой алгоритм очень хорошо оптимизируется с помощью железа — графического ускорителя. Нужно визуализировать лицевую грань куба и половинки левой, правой, верхней, нижней частей, соответственно поворачивая камеру. А поскольку в цвете мы ограничены значением от 0 до 1 (0–255), то вместо реальных цветов патчей мы будем использовать указатели на патчи — будем кодировать указатель из четырех байт в четыре компоненты цвета — RGBA. Тогда при рендеринге нужно отключить любое фильтрование, мы же не хотим интерполировать указатели.