главная страница статьи файлы о сайте ссылки |
Георгий Мошкин Скачать исходники по этой теме можно здесь (Delphi 6). Поговорим о двух методах, которые позволяют получать высокореалистичные изображения. Здесь я затрону лишь те вопросы, с которыми столкнулся при написании собственных программ: генератор лайтмапов по Radiosity и Photon Mapper. Не исключено, что в моём описании есть ошибочные сведения. Если вы знаете английский язык, то рекомендую вам поискать публикации в интернете, так как тема алгоритма Radiosity освящена очень хорошо, да и по Photon Mapping-у материалов не так уж и мало. Оба метода имеют свои достоинства и недостатки. В методе Photon Mapping для отрисовки сцен используют алгоритм RayTrace совместно с предварительно расчитанными "фотонными картами" (photon maps). Считается, что с повышением быстродействия ПК, и если оптимизировать данный метод и ввести различные ухищрения, то можно добиться производительности Realtime. Производительность Photon Mapping падает с ростом числа "фотонов" как на этапе создания фотонных карт, так и на этапе RayTrace. Существуют различные модификации и названия этих методов, но я думаю, что не стоит особо зацикливаться на терминах. Сначала обратимся к уже хорошо известному многим методу Radiosity, чтобы пояснить некоторые детали. Для тех, кто не знаком с этим методом, это может послужить отправной точкой в понимании. Но обратите внимание, что "хорошая" реализация метода Radiosity во многом отличается от той, что я даю ниже. Даже можно сказать, что в некотором смысле я опишу некий multipass lightmap generation, или "упрощённое Radiosity", или "как бы Radiosity". Photon Mapping будет обсуждаться ниже, так как я его ещё не реализовал в своей программе. По этой же причине в тексте присутствуют скриншоты только из генератора лайтмапов по Radiosity. RadiosityНа сегодняшний день широко распространён метод Radiosity, в котором все поверхности сцены делятся на маленькие элементы, называемые патчами. В качестве патча (patch) может выступать пиксель lightmap-а. Чем больше патчей, тем выше будет качество освещения. Одна из упрощённых реализаций этого метода такова: все полигоны в сцене получают дополнительное разбиение, в результате чего у нас имеется сцена с большим количеством маленьких полигонов (каждый треугольник делится на несколько маленьких). Патчами в данном случае будут треугольники. Чем больше patch, тем больше света он будет воспринимать. Но я опишу упрощенный метод, в котором не учитывается площадь, а все patch-и расположены в вершинах треугольников. Так как у нас имеется высокополигональная сцена, то вместо LightMap-ов будет использоваться glColor3f перед отправкой в OpenGL координат вершин треугольников:
Основная идея алгоритма - помещать OpenGL-евскую камеру в каждую из этих вершин, рендерить всю сцену, и по полученной картинке ("то, что видит вершина") определять цвет освещения этих вершин. Вместо разбиения на треугольники можно использовать LightMap: тогда камеру нужно будет перемещать по поверхности треугольников, последовательно проходя все точки в текстуре LightMap. Для определения цвета достаточно уменьшенного ViewPort-а.
У каждой вершины полигона имеется цвет, хранящийся в виде RGB. Перед началом расчёта освещения все данные о цвете приравниваются нулю: color.r:=0, color.g:=0, color.b:=0. А в местах, где присутствуют источники света, данные значения приравниваются некоторым заданным значениям: color.r:=r; color.g:=g; colorb:=b. Далее мы устанавливаем размер ViewPort маленьким, чтобы повысить скорость расчёта (мы рассмотрим реализацию, использующую мощность видеокарты). Угол FOV приравнивается 90, чтобы обеспечить больший охват. И начинается итеративный процесс, в котором выполняются следующие действия:
Останавливать этот цикл (пункты 1-11) приходиться вручную. Если сделать слишком много итераций, то все точки сцены будут иметь цвет ярко белый цвет, а если слишком мало - то получится неестественно тёмная комната с яркой лампой на потолке. Но на то оно и "упрощённое", "как бы Radiosity". Хотя, я делал так: у меня был общий счётчик цветовых компонент, которые видели точки в сцене, и когда значение переваливало за некоторое (было определено методом тыка), то итерации прекращались. Но, к сожалению, для других сцен приходилось опять искать это значение вручную. Такая же ситуация возникла и лайтмап-версии моего Radiosity генератора. Хочу лишь отметить, что можно реализовать достаточно корректное решение для решения этой проблемы (есть площадь источников света, можно задать яркость, и т.д.) и написать соответствующий алгоритм. Тот же алгоритм, но уже на низкополигональной сцене с применением LightMap-ов. Перед началом расчёта освещения у одной из стен в текстуру LightMap-а помещается яркая текстура (у всех других стен текстура LightMap-а чёрная, и заполняется по мере расчёта освещения). Плюсом в данном случае является то, что сцена не разбивается на большое число полигонов. Картинка сделана на написанном мною Radiosity Render. Время генерации lightmap менее 2 минут. Программа откомпилированна в TMT Pascal 4.01. Мне с успехом удалось реализовать данную идею multipass lightmap generation как с помощью задания цвета процедурой glColor3f, так и с использованием генерации карт освещённости (lightmap), наложенных поверх текстур сцены. При написании рендерера использовался TMT Pascal. Знатоки компьютерной графики могут возразить, что это не Radiosity. Да, это так. Но зато это весьма простой алгоритм, демонстрирующий сущность Radiosity: с каждой итерацией всё больше вершин в сцене приобретают цвет, отличный от нуля. Полигоны становятся светлее (OpenGL обеспечивает плавное перетекание между цветами вершин). И , таким образом, сами становятся источниками света. После небольшой доработки и регулировки полученных цветовых значений, можно получить весьма интересные и красивые результаты.
Представьте себе трёхмерную сцену "cornell box". На полу комнаты лежит две коробки белого цвета, немного повёрнутые относительно друг друга. В комнате потолок, пол и стены тоже белые, за исключением двух стен: одна красная, другая зелёная. На потолке находится полигон, являющийся источником света (лампа). Теперь посмотрим, как будет работать этот алгоритм. Сначала мы видим абсолютно чёрный экран, так как значения color у всех полигонов равны нулю: color.r:=0; color.g:=0; color.b:=0. Потом мы устанавливаем, что полигон на потолке является источником света, и для всех его вершин polygon[i].vertex[j].color компоненты r, g и b приравниваем 1. После того, как начался главный цикл (пункты 1-10), мы "смотрим" камерой из каждого полигона на сцену. И что же мы видим? Ведь у всех точек сцены будет glColor3f(color.r, color.g, color.b), а мы приравняли их нулю! Нет, всё же не увсех. Есть один полигон на потолке, у которого r,g и b отличны от нуля. Рассмотрим что будет видно в маленьком ViewPort-е из разных точек:
Например, пусть у нас ViewPort имеет размер 32x32. Для некоторой точки polygon[i].vertex[j].position мы имеем координаты x,y,z (position.x, position.y position.z) и нормаль polygon[i].vertex[j].normal (normal.x, normal.y, normal.z). Используя процедуру gluLookAt мы помещаем камеру в точку position (x,y,z) и устанавливаем направление normal (x,y,z). Рендерим сцену (пункты 2-4). Вызываем glReadPixels и получаем на выходе двухмерный массив screenshot (32x32), в некотором смысле скриншот того, что видно из точки j полигона i. Теперь необходимо рассчитать среднее значение цвета из скриншота, в котором 32x32=1024 точек. Можно было бы просто сделать так:
И посчитать среднее значение:
Но чтобы получить более корректное освещение, следует использовать так называемый FormFactor. Для этого предварительно заполним массив FormFactor, в котором значения будут представлять собой белое пятно. Сделать это можно с помощью функции sin:
Подсчёт среднего значения не изменится, а вот в суммировании учтём FormFactor:
После отработки алгоритма на одной из вершин, к её цвету будет прибавлен tempcolor. Смысл такой: как уже было сказано выше, одна стена красная, а другая - зелёная. Всё остальное белого цвета. На потолке висит лампа, а на полу стоят две коробки. Как вы понимаете, свет от лампы может попасть на пол не только по прямой, но и многократно отразившись от различных предметов. Поэтому из-за цветных стен пол тоже немного окрашивается. На рисунке, показанном выше, источник света был помещён на полу, а не на потолке. Можно заметить, что большая коробка отбрасывает мягкую тень. Это произошло благодаря тому, что точки на затенённой части стены не могли видеть источник света напрямую. Вместо этого они видели освещённые стены, которые сами стали источниками света. Таким образом, можно заключить, что в методе Radiosity с каждой итерацией всё больше точек в сцене сами становятся источниками света. Сделаю оговорку, что существует и более "правильная", приближенная к физике, реализация метода Radiosity. Источники света в "правильном" Radiosity имеют яркость, а в "простом" Radiosity есть только цвет. В "правильном" Radiosity мощность видеокарты (GPU) если и используется, то совсем по-другому: при отключенном Dither патчам присваиваются уникальные цвета, а полученный "screenshot" служит для определения того, какие патчи видит текущий патч. Всё остальное рассчитывается по формулам. Помимо этого более корректным будет использование не одной камеры, а целых пяти!! Одна из них по прежнему будет смотреть по нормали, а другие - перпендикулярно. Для каждой камеры должен быть предварительно просчитанный соответствующий FormFactor (для направленных перпендикулярно нормали камер FormFactor будет "половинчатым").
Также хочу отметить, что несмотря на использование GPU, скорость моего метода достаточно низкая. Во-первых, это связано с низкой производительностью процедуры glReadPixels. Во-вторых, для каждой точки всех полигонов приходится рендерить всю сцену два раза. Этого можно избежать, если не делать 3-его и 4-го пунктов, но тогда точеки пола под коробками будут освещены так, будто на них нет никаких коробок. В-третьих, после рассчёта освещения делается post processing для цветовой коррекции полученных значений. Этот Radiosity Render подходит для предварительного расчёта карт освещённости, называемых lightmap-ами (RGB цвета в текстуре). Или для карт освещённости по RGB цветам вершин. Photon MappingДанный метод очень интересен, так как результаты его работы впечатляют. Это относится и к скорости рендеринга, и к качеству получаемой картинки. В отличие от Radiosity, в котором напролом идёт цикл по диффузному переотражению света, здесь дело обстоит несколько иначе. Можно сказать, что Photon Mapping "круче", чем Radiosity. Есть правда одно "но", о чём будет сказано ниже. Хочу сразу сказать, что, возможно, я опять-таки опишу вам нечто похожее на Photon Mapping, а не оригинальный метод. Возможно, что в моёом описании присутствуют весьма серьёзные ошибки и недочёты. Итак, в начале мы создаём массив (photons: array of photon) из переменных типа "фотон". Тип "фотон" - это обычные записи (photon = record ... end;), описывающие параметры фотона. Потом мы устанавливаем, какие поверхности в нашей сцене являются источниками света, и их параметры (яркость, цвет, зеркальная отражательность, диффузная отражательность, прозрачность и т.п.). Как и в методе Radiosity здесь любые поверхности могут служить источниками света. Но вот и первое отличие: появилась зеркальная отражательность и прозрачность. Любая поверхность может стать источником света, достаточно сделать ambient составляющую материала поверхности отличную от нуля: ambient.r:=0.5; ambient.g:=0.0; ambient.r:=0.0; - это даст красный источник света. В тоже время он может быть зеркалом или сферическим стеклянным шариком. Photon Mapping делается в два этапа. На первом этапе мы "облепливаем" все источники света фотонами: расставляем их равномерно на поверхностях источников света (photon[i].position.x:=...; photon[i].position.y:=...; photon[i].position.z:=...). Чем ярче источник света, тем больше концентрация фотонов на его поверхности. Яркость лампы (material.light.power), которую облепливают фотоны, делится между ними поровну:
Общее число фотонов в сцене можно выбрать подбором, но их должно быть достаточно много. Одновременно с расстановкой фотонов на поверхностях ламп мы задаём их стартовое направление. Так как лампа "светим фотонами" во вне, то начальное направление полёта фотона необходимо брать по нормали к поверхности в той точке, где мы размещаем фотон. После этого мы выстреливаем всеми фотонами. Если фотон попадает в какой-нибудь полигон, то, в зависимости от свойств материала, производим сфотоном следующие манипуляции:
Обратите внимание, что хотя поверхность может иметь зеркальную составляющую, мы не заменяем фотон несколькими более слабыми фотонами. Вместо этого мы выбираем одно из двух действий. Например, если поверхность имеет значение material.reflect.r:=0.3, то мы будем из десяти фотонов отражать только три, из 100 - 30, из 1000 - 300 и т.д.. Этого можно достигнуть чередованием или использовать random. После завершения данного этапа производится оптимизация перед вторым этапом: мы разделяем всю сцену на кубы и сортируем фотоны в массиве так, чтобы подряд шли фотоны, находящиеся в одном кубе. В каждом кубе указываем индекс начала фотонов и их количество (cube[x,y,z].index:=..., cube[x,y,z].photoncount:=...); Второй этап - это непосредственно сам процесс создания картинки. Здесь то и откроется минус данного метода: сцена рендерится из определённого положения камеры, а если нужно из другого - идёт повторение второго этапа. На втором этапе мы будем пользоваться подготовленными массивами: photon[i] и cube[x,y,z]. Отметим, что у каждого фотона имеется параметр photon.direction, оставшийся с предыдущего этапа. Как и в случае с методом Radiosity, где окружающий мир воспринимала полусфера, расположенная на месте патча (вместо долгих вычислений с полусферой использовалось 5 камер и formfactor, или 1 камера в "как бы Radiosity"), здесь присутствует аналогичная вещь. Но для Photon Mapping-а используется ухищрение: мы не смотрим из точек поверхности полусферы, а определяем какие фотоны находятся около точек в сцене. Поэтому на первом этапе так важно задать достаточное количество фотонов. По ним мы будем подсчитывать некое усреднённое значение освещённости около точки на поверхности сцены. Чем больше фотонов мы зададим на первом этапе, тем выше качество будет получено на втором. Чтобы достичь переотражения потребуется создать дополнительные фотоны. Иными словами, при попадании фотонов на диффузные поверхности мы должны сгенерировать дополнительные фотоны, чтобы сделать эти поверхности светящимися. При этом необходимо основываться на данных material.light.power той поверхности, в которую попал "основной" фотон. Общее число фотонов при этом сильно увеличится, поэтому упадёт скорость расчётов и на втором этапе. При разработке второго этапа метода Photon Mapping не будет лишним наличие своего готового несложного RayTracer-а. К сожалению, "вкусности" фотон маппинга во многом и основаны на применении рейтрейсинга. Возможно, будет найден способ обойтись без RayTracing-а или использовать GPU для ускорения работы алгоритма. Но реализовать зеркальные поверхности и преломляющие материалы при этом будет сложнее. Фотонные карты совместно с RayTracing дают потрясающие реалистичные результаты. И ещё несколько слов по поводу реализации алгоритма Photon Mapping. Чтобы получить заметную скорость рендеринга сцен с помощью photon maps, необходимо уделить больше внимания оптимизации:
Дело в том, что с ростом числа фотонов на этапе RayTracing-а будет всё медленнее и медленнее работать поиск по кубам, где идёт RayTracing. Вторая же оптимизация связана с вопросом экономии памяти. При реализации Photon Mapping я столкнулся
с некоторыми трудностями: во-первых, я ещё не написал рейтрейсера, а по
виду фотонных карт сложно понять какой будет итоговая картинка. Во-вторых,
исследование полученных "фотонных карт" (photon maps), у меня
возникают подозрения насчёт того, всё ли верно в описанных мною выше действиях.
Насколько я понял, основоположником метода Photon Mapping является Henrik
Jensen, чьи работы можно найти в Google. Там же можно найти достаточно
большое количество увесистых исходников на c++ и pdf публикаций различных
авторов на эту тему. В связи с нехваткой времени я не знаю насколько быстро
я смогу реализовать Photon Mapping и внести коррективы в сделанное мною
выше описание. |