www.demoscene.ruenglish version
новостимузыкадемографикаинформацияпрограммыфорум‘подкаст’
 
авторы    статьи    интервью   

Music for games, video game music
Enlight project

Хоровод историй

Лаборатория альтернативной истории

статьи
Radiosity. Алгоритм глобального освещения
 

Radiosity и Photon mapping с использованием OpenGL. Часть 2

От редактора: цикл статей по теме Radiosity создан из статей tmtlib (Georgy Moshkin), но последовательность статей переработана. Изучение исходных кодов программ рекомендуется из всего цикла.

Поговорим о двух методах, которые позволяют получать высокореалистичные изображения. Здесь я затрону лишь те вопросы, с которыми столкнулся при написании собственных программ: генератор лайтмапов по 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 координат вершин треугольников:

  • glColor3f(r[0],g[0],b[0])
  • glVertex3f(x[0],y[0],z[0])
  • glColor3f(r[1],g[1],b[1])
  • glVertex3f(x[1],y[1],z[1])
  • glColor3f(r[2],g[2],b[2])
  • glVertex3f(x[2],y[2],z[2])


Сцена поделена на большое количество треугольников. У соседних треугольников
имеются общие точки. Для повышения скорости алгоритма камеру в эти точки следует помещать только один раз.

Основная идея алгоритма - помещать OpenGL-евскую камеру в каждую из этих вершин, рендерить всю сцену, и по полученной картинке ("то, что видит вершина") определять цвет освещения этих вершин. Вместо разбиения на треугольники можно использовать LightMap: тогда камеру нужно будет перемещать по поверхности треугольников, последовательно проходя все точки в текстуре LightMap. Для определения цвета достаточно уменьшенного ViewPort-а.


создаём маленький ViewPort
в левом нижнем углу экрана

У каждой вершины полигона имеется цвет, хранящийся в виде RGB. Перед началом расчёта освещения все данные о цвете приравниваются нулю: color.r:=0, color.g:=0, color.b:=0. А в местах, где присутствуют источники света, данные значения приравниваются некоторым заданным значениям: color.r:=r; color.g:=g; colorb:=b. Далее мы устанавливаем размер ViewPort маленьким, чтобы повысить скорость расчёта (мы рассмотрим реализацию, использующую мощность видеокарты). Угол FOV приравнивается 90, чтобы обеспечить больший охват. И начинается итеративный процесс, в котором выполняются следующие действия:

  1. Камера помещается в вершину j полигона i. Направление взгляда берётся по нормали полигона.
  2. Рендерится вся сцена, с использованием with polygon[i].vertex[j] do glColor3f(color.r,color.g,color.b).
  3. Изменяется режим Culling, чтобы видимыми стали полигоны с точками "по часовой стрелке".
  4. Рендерится вся сцена, с использованием with polygon[i].vertex[j] do glColor3f(0,0,0).
  5. С помощью glReadPixels считывается кусочек экрана, на котором умещается ViewPort (например, 16x16).
  6. Рассчитывается среднее значение цвета из данных, полученных glReadPixels.
  7. Цвет заносится в polygon[i].vertex[j].tempcolor
  8. Установка Culling обратно в режим "против часовой стрелки".
  9. Повторяются пункты 1-8 для всех вершин всех полигонов.
  10. Данные из polygon[i].vertex[j].tempcolor прибавляются к polygon[i].vertex[j].color
  11. Всё повторяется, начиная с первого пункта (до достижения красивой картинки).


Красными точками показаны вершины, на которых камера уже побывала.

Останавливать этот цикл (пункты 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.
(автор этой картинки неизвестен)

Представьте себе трёхмерную сцену "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-е из разных точек:

  • Точки на потолке: смотрят по нормали вниз и видят темноту (на полу r:=0; g:=0; b:=0).
  • Точки на полу: смотрят по нормали вверх и видят лампу или кусочек лампы (загораживают коробки).
  • Точки на стенах около потолка видят лампу, но очень плохо.
  • Точки на стенах около пола видят черноту (точки стен и коробок имеют нулевые r,g,b).

Например, пусть у нас 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 точек. Можно было бы просто сделать так:

  • в polygon[i].vertex[j].tempcolor.r занести сумму всех точек screenshot[u,v].r;
  • в polygon[i].vertex[j].tempcolor.g занести сумму всех точек screenshot[u,v].g;
  • в polygon[i].vertex[j].tempcolor.b занести сумму всех точек screenshot[u,v].b;

И посчитать среднее значение:

  • в polygon[i].vertex[j].tempcolor.r:=polygon[i].vertex[j].tempcolor.r/((32*32)*256);
  • в polygon[i].vertex[j].tempcolor.g:=polygon[i].vertex[j].tempcolor.g/((32*32)*256);
  • в polygon[i].vertex[j].tempcolor.b:=polygon[i].vertex[j].tempcolor.b/((32*32)*256);


примерно так будет выглядеть двухмерный
массив FormFactor (в центре значение элемента FormFactor[15,15]
равно 1, к краям доходит до нуля).

Но чтобы получить более корректное освещение, следует использовать так называемый FormFactor. Для этого предварительно заполним массив FormFactor, в котором значения будут представлять собой белое пятно. Сделать это можно с помощью функции sin:

  • для u = 0..31 и v = 0..31
  • FormFactor[u,v]:=sin( u*3.14/(32-1) )*sin( v*3.14/(32-1) );

Подсчёт среднего значения не изменится, а вот в суммировании учтём FormFactor:

  • в polygon[i].vertex[j].tempcolor.r занести сумму всех точек screenshot[u,v].r*FormFactor[u,v];
  • в polygon[i].vertex[j].tempcolor.g занести сумму всех точек screenshot[u,v].g*FormFactor[u,v];
  • в polygon[i].vertex[j].tempcolor.b занести сумму всех точек screenshot[u,v].b*FormFactor[u,v];


Пример работы моего Radiosity маппера. На белых коробках заметен характерный для Radiosity эффект. Так же на задней стене произошло смешение отраженного зелёного и красного цвета. Источник света был расположен на полу, около ног трёхмерной модельки. Время расчёта освещения около 1-2 минут. Программа откомпилированна в TMT Pascal 4.01.

После отработки алгоритма на одной из вершин, к её цвету будет прибавлен tempcolor. Смысл такой: как уже было сказано выше, одна стена красная, а другая - зелёная. Всё остальное белого цвета. На потолке висит лампа, а на полу стоят две коробки. Как вы понимаете, свет от лампы может попасть на пол не только по прямой, но и многократно отразившись от различных предметов. Поэтому из-за цветных стен пол тоже немного окрашивается. На рисунке, показанном выше, источник света был помещён на полу, а не на потолке. Можно заметить, что большая коробка отбрасывает мягкую тень. Это произошло благодаря тому, что точки на затенённой части стены не могли видеть источник света напрямую. Вместо этого они видели освещённые стены, которые сами стали источниками света. Таким образом, можно заключить, что в методе Radiosity с каждой итерацией всё больше точек в сцене сами становятся источниками света.

Сделаю оговорку, что существует и более "правильная", приближенная к физике, реализация метода Radiosity. Источники света в "правильном" Radiosity имеют яркость, а в "простом" Radiosity есть только цвет. В "правильном" Radiosity мощность видеокарты (GPU) если и используется, то совсем по-другому: при отключенном Dither патчам присваиваются уникальные цвета, а полученный "screenshot" служит для определения того, какие патчи видит текущий патч. Всё остальное рассчитывается по формулам. Помимо этого более корректным будет использование не одной камеры, а целых пяти!! Одна из них по прежнему будет смотреть по нормали, а другие - перпендикулярно. Для каждой камеры должен быть предварительно просчитанный соответствующий FormFactor (для направленных перпендикулярно нормали камер FormFactor будет "половинчатым").


Мой Radiosity Render, написанный на TMT Pascal-е. Лампа находится на потолке. Все объекты статичные, включая модель человека. Время расчёта освещения около 1-2 минут.

Также хочу отметить, что несмотря на использование 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), которую облепливают фотоны, делится между ними поровну:

  • photon[i].power.r:=material.light.power.r/lamp[k].photons
  • photon[i].power.g:=material.light.power.g/lamp[k].photons
  • photon[i].power.b:=material.light.power.b/lamp[k].photons

Общее число фотонов в сцене можно выбрать подбором, но их должно быть достаточно много. Одновременно с расстановкой фотонов на поверхностях ламп мы задаём их стартовое направление. Так как лампа "светим фотонами" во вне, то начальное направление полёта фотона необходимо брать по нормали к поверхности в той точке, где мы размещаем фотон. После этого мы выстреливаем всеми фотонами. Если фотон попадает в какой-нибудь полигон, то, в зависимости от свойств материала, производим сфотоном следующие манипуляции:

  • заменить фотон более слабым фотоном
  • отразить фотон диффузно
  • отразить фотон зеркально
  • преломить направление фотона (прозрачная среда)
  • убрать фотон

Обратите внимание, что хотя поверхность может иметь зеркальную составляющую, мы не заменяем фотон несколькими более слабыми фотонами. Вместо этого мы выбираем одно из двух действий. Например, если поверхность имеет значение 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, необходимо уделить больше внимания оптимизации:

  • на первом этапе вместо разбиения на кубы использовать другой, более совершенный метод
  • хранить направление движения фотонов не в векторе (x,y,z:single), а в (alpha,beta:integer)

Дело в том, что с ростом числа фотонов на этапе RayTracing-а будет всё медленнее и медленнее работать поиск по кубам, где идёт RayTracing. Вторая же оптимизация связана с вопросом экономии памяти.

При реализации Photon Mapping я столкнулся с некоторыми трудностями: во-первых, я ещё не написал рейтрейсера, а по виду фотонных карт сложно понять какой будет итоговая картинка. Во-вторых, исследование полученных "фотонных карт" (photon maps), у меня возникают подозрения насчёт того, всё ли верно в описанных мною выше действиях. Насколько я понял, основоположником метода Photon Mapping является Henrik Jensen, чьи работы можно найти в Google. Там же можно найти достаточно большое количество увесистых исходников на c++ и pdf публикаций различных авторов на эту тему. В связи с нехваткой времени я не знаю насколько быстро я смогу реализовать Photon Mapping и внести коррективы в сделанное мною выше описание.


 страница 1
страница 3 


интро   демо   flash   анимация   3D-графика   арт   синглы   альбомы   статьи   трекеры

Дизайн и программирование: Александр Ильин aka Real/SandS     
Шеф-редактор: Антон Уткин aka Frown/Rcd     
Креатив и интерфейс: Александр Мачуговский aka Manwe/SandS