Компания
1 078,74
рейтинг
29 января 2015 в 12:42

Разработка → Skyforge: технологии рендеринга



Всем привет! Меня зовут Сергей Макеев, и я технический директор в проекте Skyforge в команде Allods Team, игровой студии Mail.Ru Group. Мне хотелось бы рассказать про технологии рендеринга, которые мы используем для создания графики в Skyforge. Расскажу немного о задачах, которые стояли перед нами при разработке Skyforge с точки зрения программиста. У нас свой собственный движок. Разрабатывать свою технологию дорого и сложно, но дело в том, что на момент запуска игры (три года назад) не было технологии, которая могла бы удовлетворить всем нашим запросам. И нам пришлось самим создать движок с нуля.

Основной арт-стиль игры — это смесь фентези с Sci-Fi. Чтобы реализовать задумки арт-директора и художников, нам нужно было создать очень сильную, мощную систему материалов. Игрок может видеть проявления магии, технологии и природные явления, и система материалов нужна, чтобы правдоподобно нарисовать все это на экране. Еще одним «столпом» нашего графического стиля является то, что мы создаем стилизованную реальность. То есть объекты узнаваемы и выглядят реалистично, но это не фотореализм из жизни. Хорошим примером, на мой взгляд, является фильм «Аватар». Реальность, но реальность художественно приукрашенная, реальность и в то же время сказка. Следующий столп графического стиля — освещение и материалы — выглядят максимально естественно. А «естественно» с точки зрения программиста — это значит «физично».

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















Далее я расскажу о том, как мы добились такой картинки.

Physically based shading (шейдинг, основанный на законах физики)


Зачем нужен шейдинг, основанный на физике?
  • Это дает нам более реалистичную и сведенную картинку. 3D-модели, сделанные разными художниками, выглядят целостно в разных типах освещения. Картинка меньше разваливается на части, и художникам по освещению не нужно сводить все варианты освещения со всеми 3D-моделями.
  • Материалы можно настраивать отдельно от света. То есть художники по материалам и художники по свету могут работать параллельно. Корректно настроенные физичные материалы при любом освещении выглядят как положено, не становятся неожиданно черными и пересвеченными, как это часто бывало раньше, при нефизичных материалах.
  • Параметров материала стало меньше, и все они имеют физический смысл. Художникам легче ориентироваться в этих параметрах. Конечно, сначала им приходится переучиваться, но потом работа становится гораздо более предсказуемой.
  • Соблюдение закона сохранения энергии в системе материалов означает, что художникам по свету проще работать. Нельзя, настраивая свет, «рассыпать» картинку на части.
  • Кстати, физическая корректность не обязывает нас к фотореализму. Например, все последние мультфильмы, созданные студиями Pixar и Disney, сделаны с помощью физического рендера. Но при этом там нет фотореализма, а присутствует вполне узнаваемая стилизация.

Физика процесса


Прежде чем программировать что-либо, надо сначала понять физику процесса. Что происходит, когда свет отражается от поверхности? В реальной жизни, в отличие от компьютерной графики, поверхности не гладкие, а на самом деле состоят из множества маленьких неровностей. Эти неровности настолько мелкие, что глазом их не видно. Однако они достаточно большие, чтобы влиять на свет, отраженный от поверхности. Здесь на картинке я назвал это микроповерхностью.



Вот пример из реальной жизни. На картинке лист бумаги А4 под электронным микроскопом. Видно, что лист бумаги на самом деле состоит из множества переплетенных древесных волокон, но глаз их не различает.



Для масштаба я вывел длины волн для разного спектра освещения в нанометрах и выделил на скане бумаги квадрат со стороной в 100000 нанометров. Очевидно, что хотя мы и не видим эти волокна, они оказывают влияние на отражение света.



Рассчитывать освещение с таким уровнем детализации не под силу даже офлайн-рендерам для кино. Это огромное количество вычислений.

Итак, микрогеометрия поверхности. Часть света проникает внутрь и переизлучается после случайных отражений внутри материала или поглощается — превращается в тепло. Часть падающего света отражается от поверхности. Существует разница в том, как разные материалы отражают свет. Две группы, которые ведут себя по-разному — это диэлектрики и проводники (металлы). Внутрь металлов свет не проникает, а практически весь отражается от поверхности. От диэлектриков свет же в основном переизлучается, а отражается малое количество света — около 5%.



Теория — это хорошо, но мы создаем компьютерную игру и оперируем не лучами света, а пикселями на экране. При этом каждый пиксель — это большое количество фотонов света. И весь этот свет может быть поглощен, переизлучен или отражен. Это нам и предстоит рассчитать при разработке шейдеров.

BRDF


С физикой процесса в общих чертах определились, перейдем к математической модели. Основная функция, которая позволит нам определить, какой процент света был отражен, а какой переизлучен или поглощен, называется BRDF (Bidirectional Reflection Distribution Function) или по-русски ДФОС (двухлучевая функция отражательной способности). Цель данной функции — рассчитать количество энергии, излучаемой в сторону наблюдателя при заданном входящем излучении. В теории это многомерная функция, которая может зависеть от большого количества параметров 3D, 4D, 6D.

Мы же на практике будет рассматривать функцию от двух параметров F(l, v), где l — направление от точки поверхности на источник света, а v — направление взгляда.

BRDF для рассеянного света (Diffuse)


Для модели переизлученного света мы делаем несколько допущений:
  • можем пренебречь точкой входа и выхода луча, т.к. это очень незначительная в нашем случае величина;
  • считаем, что все переизлученные лучи равномерно распределены внутри полусферы.

Поведение фотона внутри материала очень сложное, и для текущего развития компьютерной графики это нормальное приближение, к тому же оно в достаточной мере соответствует реальным физическим замерам.



Получаем следующую функцию для расчета переизлученного (рассеянного) света.



l — направление на источник света, v — направление взгляда, оно никак не используется в данной упрощенной модели, т.к. все энергия переизлучается равномерно по полусфере.

albedo (rgb) — определяет, сколько энергии поглощает поверхность, а сколько переизлучает. Так, к примеру, поверхность с абсолютно черным albedo всю энергию поглощает (преобразует в тепло). На самом деле это известный всем графическим программистам dot(n, l), за исключением деления на PI. Деление на PI нужно для соблюдения закона сохранения энергии. Т.к. свет рассеивается по полусфере, то при n, равном l, мы отразим падающий свет без изменения интенсивности во все стороны по полусфере, что физически невозможно. Но обычно интенсивность источника света, переданная в шейдер, уже учитывает деление на PI, поэтому в шейдере остается только dot(n,l).

Мы знаем, что скалярное произведение векторов (dot) — это косинус между этими векторами. Возникает вопрос: как угол падения света влияет на количество переизлученного света? Ответ прост: площадь проекции зависит от угла падения луча на поверхность и равна косинусу угла падения. Соответственно, чем острее угол падения, тем меньше энергии попадает на поверхность.


Свет падает под «тупым» углом


Свет падает под острым углом, площадь проекции стала больше

BRDF для отраженного света (Specular)


Перейдем к модели отраженного света. Всем известно, что угол падения равен углу отражения, но на приведенной картинке отраженных лучей стало много. Так происходит из-за микронеровностей поверхности. Благодаря этим неровностям каждый отдельный фотон отражается немного в разную сторону. Если поверхность достаточно гладкая, то большинство лучей отражаются в одну сторону и мы видим четкое отражение предметов, вроде как в зеркале.

Если поверхность более шероховатая, то гораздо большее количество лучей отражаются в разные стороны, и тогда мы видим очень матовое отражение. Для сильно неровной поверхности отраженный свет может быть распределен равномерно внутри полусферы и выглядеть для внешнего наблюдателя как рассеянный свет.

Microfacet Theory


Для моделирования эффекта отражения света от поверхности с учетом микрогеометрии поверхности была разработана Microfacet Theory. Это математическая модель, которая представляет поверхность в виде множества микрограней, ориентированных в разные стороны.При этом каждая из микрограней является идеальным зеркалом и отражает свет по тому же простому закону: угол падения равен углу отражения.

Для того чтобы рассчитать освещение в точке, нам нужно рассчитать сумму вклада отраженного света каждой из микрограней. Т.е. нам нужен интеграл. Микрограней в точке очень много, и мы не можем просто взять и численно проинтегрировать. Будем находить решение интеграла в аналитическом виде (приближенно). Вот как в общем виде выглядит функция для расчета отраженного света.



Это функция отраженного света Кука-Торренса.
l — направление света
v — направление взгляда наблюдателя
n — нормаль к поверхности
h — вектор между векторами l и v (half vector)

D(h) — функция распределения микро граней
F(v,h) — функция Френеля
G(l, v, h) — функция затенения микро граней

Все параметры данной функции достаточно простые и имеют физический смысл. Но какой физический смысл имеет half-vector? Half-vector нужен, чтобы отфильтровать те микрограни, которые вносят свой вклад в отражение света для наблюдателя. Если нормаль микрограни равна half-vector, значит данная микрогрань вносит вклад в освещение при направлении взгляда V.



Расcмотрим подробнее члены нашей BRDF.

Функция распределения микро граней D(h)


В качестве функции распределения отраженного от микрограней света мы используем степень косинуса, с нормированием для соблюдения закона сохранения энергии. Для начала мы берем коэффициент шероховатости поверхности, который лежит в диапазоне 0..1, и раcчитываем из него степень альфа, которая лежит в диапазоне 0.25 — 65536. Далее мы берем скалярное произведение N и H векторов и возводим их в степень альфа. И чтобы получившийся результат не нарушал закон сохранения энергии, мы применяем константу нормализации NDF.

Без нормализации от поверхности будет отражаться больше энергии, чем пришло. Таким образом мы задаем объем, в котором происходит отражение света и распределение энергии в этом объеме. И этот объем зависит от того, насколько гладкая или шероховатая поверхность. Теперь рассмотрим следующий член BRDF.

Функция Френеля F(v,h)


Сила отражения света, зависит от угла падения. Это поведение описывается формулами Френеля. Формулы Френеля определяют амплитуду и интенсивность отраженной и преломленной электромагнитной волны при прохождении через границу двух сред. Этот эффект очень заметен на воде, если смотреть на воду под острым углом, то вода отражает большинство света и мы видим отражение. Если же смотреть на воду сверху-вниз, то отражения мы практически не видим, а видим то, что находится на дне.



Вот так, к примеру, выглядит график отраженного света в зависимости от угла падения для различных материалов. Табличные данные я взял с сайта http://refractiveindex.info/



На графике видно, что пластик практически весь свет рассеивает, но не отражает, пока угол падения не станет 60-70 градусов. После чего количество отраженного света резко увеличивается. Для большинства диэлектриков график будет схожий.

Гораздо интереснее ситуация с металлами. Металлы отражают очень много света под любым углом, но количество отраженного света разное для разной длины волны. Пунктирные графики показывают отражение света для меди. Длины волн соответствуют красному, зеленому и синему цветам. Как видно, красного цвета медь отражает гораздо больше, именно поэтому металлы окрашивают отраженный свет в цвет своей поверхности. А диэлектрики отражают свет, не окрашивая его.

В качестве функции для расчета Френеля мы используем аппроксимацию Шлика, т.к. оригинальные уравнения Френеля слишком тяжелые для вычислений в реальном времени.



Как видно, в функции Шлика участвуют H и V вектора, с помощью которых определяют угол падения, и F0, с помощью которого практически задается тип материала. Коэфицент F0 возможно рассчитать, зная Index of refraction (IOR) материала, который мы моделируем. Фактически его можно найти в справочниках в интернете. Т.к. мы знаем, что IOR воздуха 1, то, зная табличный IOR материала, мы рассчитываем F0 по формуле



Физический смысл F0 следующий — это процент отраженного под прямым углом света. Итак: френель задает, сколько света будет поглощено, а сколько отражено в зависимости от угла падения.

F0 используется в аппроксимации Шлика и задает, сколько света отражается под прямым углом к поверхности. Обычное значение F0 для диэлектриков 2% — 5%, т.е. диэлектрики мало отражают и много рассеивают.

Металлы практически весь свет отражают, при этом для разных длин волн это количество разное. Отражения на металлах окрашиваются в цвет поверхности. Теперь рассмотрим следующий член BRDF.

Функция затенения микро граней G(l, v, h)


На самом деле, не каждая микрогрань, нормаль которой соответствует half-vector'у, вносит свой вклад в освещение. Луч, отраженный микрогранью, может не достичь наблюдателя.



Отраженному лучу может помешать микрогеометрия поверхности. Видно, что часть отраженных лучей не достигнет наблюдателя. Таким образом, не все микрограни будут участвовать в отражении света. Мы используем простейшую функцию видимости. Фактически мы считаем, что все микрограни отражают свет. Это допустимо с нашей функцией распределения отраженного света.

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

Так выглядит наша финальная функция для расчета отраженного света:



Это соответствует нормированной модели Блина-Фонга с представлением микроповерхности в виде карты высот. Вот несколько картинок-примеров, как параметры материала, шероховатость и IOR влияют на внешний вид материала.



Сохранение энергии


Я уже неоднократно упоминал про сохранение энергии. Сохранение энергии означает простую вещь: сумма отраженного и рассеянного света должна быть меньше или равна единице. Из этого следует важное для художников свойство: яркость и форма/площадь блика отраженного света связаны. Такой связи раньше не было в нефизичном рендере, т.к. там возможно нарушать закон сохранения энергии. Для примера — серия изображений. Источник света на всех картинках одинаковый, я буду изменять только шероховатость поверхности.











Видно, что чем на меньшей площади распределен блик, тем он ярче.

Интенсивность источника света


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



Эта функция хорошо подходит для описания, как затухает энергия, но имеет несколько недостатков:
  • мы хотим учитывать затухание энергии не от точки в пространстве, а от объема, т.к в реальной жизни не существует источников света, не имеющих собственного размера;
  • интенсивность излучения, описанная этой функцией, стремится к нулю, но никогда его не достигает. Мы же хотим, чтобы на определенном расстоянии интенсивность стала равна нулю. Это простая оптимизация вычислительных ресурсов, нам не нужно рассчитывать то, что не влияет на финальный результат.

Для новой функции затухания света нам нужны будут два новых параметра:
Rinner — размер источника света.
Router — дистанция, на которой нам больше не важен вклад источника в освещение.

Наша функция затухания:



Обладает следующими свойствами:
  • Константна внутри Rinner.
  • На дистанции Router равна 0.
  • Соответствует квадратичному закону затухания.
  • Достаточно дешева для расчета в шейдере.

//функция расчета затухания для источника света
float GetAttenuation(float distance, float lightInnerR, float invLightOuterR)
{
    float d = max(distance, lightInnerR);
    return saturate(1.0 - pow(d * invLightOuterR, 4.0)) / (d * d + 1.0);
}

Вот сравнение двух графиков. Квадратичное затухание и наше. Видно, что большую часть наша функция совпадает с квадратичной.



Модель материала


С физикой и математикой процессов разобрались. Теперь определимся, на что же влияют художники. Какие именно параметры они настраивают? Наши художники задают параметры материалов через текстуры. Вот какие текстуры они создают:
для диэлектриков для металлов
Base color задает значение albedo задает векторную часть F0
Normal нормаль к поверхности (макро уровень) нормаль к поверхности (макро уровень)
Roughness (Gloss) шероховатость поверхности (микро уровень) шероховатость поверхности (микро уровень)
Fresnel F0 тип материала (IOR) для диэлектриков практически всегда константа для металлов скалярная часть F0
Metal всегда 0 всегда 1

Вот пример свойств поверхности такого механического пегаса:




albedo


normal


gloss


FO (IOR)


metal

Для упрощения работы наши художники составили достаточно большую библиотеку материалов. Вот некоторые примеры.













Deferred Shading


При разработке Skyforge мы используем модель отложенного шейдинга. Это широко распространенный в настоящее время метод. Метод называется отложенным, т.к. во время основного прохода рендеринга происходит только заполнение буфера, содержащего параметры, необходимые для расчета финального шейдинга. Такой буфер параметров называют G-Buffer, сокращение от geometry buffer.

Кратко опишу плюсы и минусы отложенного шейдинга:

Плюсы:
  • Шейдеры геометрии и освещения разделены.
  • Легко сделать большое количество источников света.
  • Отсутствие комбинаторного взрыва в шейдерах.

Минусы:
  • Bandwidth. Нужна большая пропускная способность памяти, т.к. буфер параметров поверхности достаточно толстый.
  • Источники света с тенями по-прежнему дорогие. Источники света без теней имеют достаточно ограниченное применение.
  • Сложность встраивания различных BRDF. Тяжело сделать разную модель освещения для разных поверхностей. Например, BRDF для волос или анизотропного металла.
  • Прозрачность. Практически не поддерживается, прозрачность нужно рисовать после того как основная картинка нарисована и освещена.

Основной минус технологии — нужна большая пропускная способность памяти. Мы стараемся максимально упаковать параметры поверхности, необходимые для освещения. В результате мы пришли к формату, который укладывается в 128 бит на пиксель — 96, если не учитывать информацию о глубине.

Как мы храним свойства поверхности (128 бит на пиксель).

Skyforge G-Buffer


Tips & Tricks



Реконструкция позиции


При использовании Deferred shading мы часто сталкиваемся с необходимостью реконструкции позиции пикселя в различных пространствах. Например, world space, view space, shadow space и т.д. В нашем GBuffer'e же мы храним только глубину пикселя, используя аппаратный depth buffer. Нам нужно уметь решать задачу: как быстро получить позицию пикселя в пространстве, имея только аппаратную глубину, которая к тому же имеет гиперболическое распределение, а не линейное. Наш алгоритм делает такое преобразование в два этапа.

Преобразование в линейную глубину


После того как мы заполнили Gbuffer, мы преобразуем depth buffer с гиперболическим распределением в линейный. Для этого мы используем полноэкранный шаг, «выпрямляющий» глубину. Преобразование происходит с помощью такого шейдера:

// Функция для преобразования глубины с гиперболическим распределением в линейную
float ConvertHyperbolicDepthToLinear(float hyperbolicDepth)
{
   return ((zNear / (zNear-zFar)) * zFar) / (hyperbolicDepth - (zFar / (zFar-zNear)));
}

Линейную глубину мы сохраняем в формате R32F и потом на всех этапах рендера используем только линейную глубину. Второй этап — это реконструкция позиции, используя линейную глубину.

Реконструкция позиции с помощью линейной глубины


Для быстрой реконструкции позиции мы используем следующее свойство подобных треугольников. Отношение периметров и длин (либо биссектрис, либо медиан, либо высот, либо серединных перпендикуляров) равно коэффициенту подобия, т.е. в подобных треугольниках соответствующие линии (высоты, медианы, биссектрисы и т. п.) пропорциональны. Рассмотрим два треугольника: треугольник (P1,P2,P3) и треугольник (P1, P4, P5).



Треугольник (P1,P2,P3) подобен треугольнику (P1, P4, P5).





Таким образом, мы, зная дистанцию (P1-P4) (наша линейная глубина) и гипотенузу (P1,P3), пользуясь подобием треугольников, можем рассчитать дистанцию пикселя до камеры (P1,P5). А зная дистанцию до камеры, позицию камеры и направление взгляда, мы с легкостью можем рассчитать позицию в пространстве камеры. Сама же камера в свою очередь может быть задана в любом пространстве: world space, view space, shadow space и т.д., что дает нам реконструированную позицию в любом нужном нам пространстве.

Итак, еще раз алгоритм по шагам:
  1. Преобразование гиперболической глубины в линейную.
  2. В вершинном шейдере рассчитываем треугольник (P1, P2, P3).
  3. Передаем отрезок (P1,P3) в пиксельный шейдер, через интерполятор.
  4. Получаем интерполированный вектор RayDir (P1,P3).
  5. Считываем линейную глубину в данной точке.
  6. Position = CameraPosition + RayDir * LinearDepth.

Алгоритм очень быстрый: один интерполятор, одна ALU инструкция MAD и одно чтение глубины. Можно реконструировать позицию в любом удобном однородном пространстве. HLSL код для реконструкции в конце статьи.

Reversed Depth Buffer


При разработке Skyforge перед нами стояла задача: уметь рисовать локации с очень большой дальностью видимости, порядка 40 км. Вот несколько картинок, иллюстрирующих дистанцию отрисовки.









Для того чтобы избежать Z-fighting при больших значениях Far Plane, мы используем технику reversed depth buffer. Смысл этой техники очень прост: при расчете матрицы проекции необходимо поменять местами Far Plane и Near Plane и инвертировать функцию сравнения глубины на больше или равно (D3DCMP_GREATEREQUAL). Этот трюк работает, только если менять местами FarPlane и NearPlane в матрице проекции. Трюк не работает, если менять местами параметры вьюпорта или разворачивать глубину в шейдере.

Сейчас я объясню, почему так происходит, и где мы выигрываем в точности расчетов. Чтобы понять, где теряется точность, разберемся, как же работает матрица проекции.



Итак, стандартная матрица проекции. Нас интересует та часть матрицы, которая выделена серым. Z и W компоненты позиции. Как рассчитывается глубина?

// умножение позиции на матрицу проекции
float4 postProjectivePosition = mul( float4(pos, 1.0), mtxProjection );

// перспективное деление
float depth = postProjectivePosition.z / postProjectivePosition.w;

После умножения позиции на матрицу проекции мы получаем позицию в пост-проективном пространстве. После перспективного деления на W мы получаем позицию в clip space, это пространство задано единичным кубом. Таким образом, получается следующее преобразование.



Для примера рассмотрим Znear и Zfar, дистанция между которыми очень большая, порядка 50 км.

Znear = 0.5
Zfar = 50000.0

Получим следующие две матрицы проекции:


Стандартная матрица проекции


Развернутая матрица проекции

Глубина после умножения на стандартную матрицу проекции будет равна следующему:



Соответственно, после умножения на развернутую матрицу проекции:



Как мы видим, в случае стандартной матрицы проекции при расчете глубины будет происходить сложение чисел очень разного порядка — десятки тысяч и 0.5. Для сложения чисел разного порядка FPU должен сначала привести их экспоненты к единому значению (большей экспоненте), после чего сложить и нормировать полученную сложением мантису. Фактически на больших значениях z это просто добавляет белый шум в младшие биты мантисы. В случае использования развернутой матрицы проекции такое поведение происходит только вблизи камеры, где из-за гиперболического распределения глубины и так избыточная точность. Вот пример, что получится при значении z = 20 км:



И для развернутой матрицы проекции:



Итого:
  • Стандартный 24-битный буфер глубины D24 легко покрывает дальность в 50 км без Z-fighting.
  • Reverse depth подходит для любого движка, я бы рекомендовал его использовать во всех проектах.
  • Лучше всего закладывать поддержку с начала разработки, т.к. существует много мест, которые, возможно, придется переделать: извлечение плоскостей фрустума из матрицы проекции, bias у теней и т.п.
  • Если на целевой платформе доступен float depth буфер, то лучше его использовать. Это еще увеличит точность, т.к. значения будут хранится с большей точностью.

На этом все, спасибо за внимание!

Литература

Naty Hoffman. Crafting Physically Motivated Shading Models for Game Development

Yoshiharu Gotanda. Physically Based Shading Models in Film and Game Production — Practical implementation at tri-Ace

Emil Persson. Creating Vast Game Worlds

Nickolay Kasyan, Nicolas Schulz, Tiago Sousa. Secrets of CryENGINE 3 Graphics Technology

Eric Heitz. Understanding the Masking-Shadowing Function

Brian Karis. Real Shading in Unreal Engine 4

HLSL код реконструкции (вершинный шейдер)

// Часть матрицы проекции 
float tanHalfVerticalFov; // invProjection.11; 
float tanHalfHorizontalFov; // invProjection.00; 
// Базис камеры в пространстве реконструкции
float3 camBasisUp;
float3 camBasisSide;
float3 camBasisFront;
// postProjectiveSpacePosition в homogeneous projection space 
float3 CreateRay(float4 postProjectiveSpacePosition)
{
   float3 leftRight = camBasisSide * -postProjectiveSpacePosition.x * tanHalfHorizontalFov;
   float3 upDown = camBasisUp * postProjectiveSpacePosition.y * tanHalfVerticalFov;
   float3 forward = camBasisFront;
   return (forward + leftRight + upDown);
}
void VertexShader(float4 inPos, out float4 outPos : POSITION, out float3 rayDir : TEXCOORD0)
{
   outPos = inPos;
   rayDir = CreateRay(inPos);
}

HLSL код реконструкции (пиксельный шейдер)

// Позиция камеры в пространстве реконструкции 
float3 camPosition;
float4 PixelShader(float3 rayDir : TEXCOORD0) : COLOR0 
{
   ... 
   float linearDepth = tex2D(linearDepthSampler, uv).r;
   float3 position = camPosition + rayDir * linearDepth;
   ... 
}
// Функция для преобразования глубины с гиперболическим распределением в линейную 
float ConvertHyperbolicDepthToLinear(float hyperbolicDepth)
{
   return ((zNear / (zNear-zFar)) * zFar) / (hyperbolicDepth - (zFar / (zFar-zNear)));
}

Слайды оригинального доклада

www.slideshare.net/makeevsergey/skyforge-rendering-techkri14finalv21
Автор: @SergeyMakeev

Комментарии (49)

  • +32
    Блин, пора менять работу.
    Хочу такие же интересные штуки делать.
    • +3
      Чтобы окончательно разочароваться в своей работе, рекомендую посмотреть ещё дневники (Development Updates) человека, который в одиночку под краудфандинг пилит космосим: www.youtube.com/user/LimitTheory/videos?shelf_id=0&view=0&sort=dd
      • 0
        Судя по FAQ на сайте игры ltheory.com/faq.html их там больше одного.
        Текст повествуется от группы людей во множественном числе.

        Multiplayer is not planned for the first release. We would love to...
        • +4
          Не всегда. У меня есть знакомый, который пилит на Ruby свой движок для блогов в одиночку, но во всех письмах говорит «Мы» и подписываться «Команда {ProjectName}»

          Говорят это придает проекту оттенок зрелости и тп.
        • 0
          Я так понял, что он один, но иногда нанимает подрядчиков (например, композитора для музыки и т. п.).
      • +1
        Справедливости ради упомяну, что есть еще один такой умелец www.youtube.com/watch?x-yt-ts=1422503916&x-yt-cl=85027636&v=rswRCT3097g
    • +3
      Что мешает делать интересные штуки не на работе?
      • +8
        Необходимость иногда спать.
        • 0
          Если после работы у вас остаётся время только на сон, то такую работу явно необходимо менять.
          • +3
            продавать бизнес вместе с командой пока не планирую
            • 0
              Так в вашем случае работа же интересная, иначе бы вы этим не занимались. Поэтому, не понимаю, почему про сон речь зашла. Ведь в вашем случае вы и на работе интересностями занимаетесь, следовательно на сон время есть.

              • –3
                Интересной работы остается процентов 10-15%, с начала этого года я занимался «интересными задачами»:
                1. «выбиванием» денег с клиентов-должников на з/п сотрудников и аренду
                2. реанимацией 3-х некросайтов
                3. выполнением нескольких нетривиальных требований контролирующих организаций для клиентских сайтов
                4. решением кадровых проблем
                5. решением налоговых и организационных проблем
                6. подготовкой к арбитражу
                7. решением проблем с МТБ
                8. резким поиском фрилансеров на задачи по которым подходит дедлайн и в рабочем порядке их делать не успеваем
                9. бестолковой перепиской с объяснением почему вода мокрая, огонь горячий а воздух прозрачный
                10. миграцией нескольких проектов, чувствительных к географии хостинга на отечественные сервера после соответствующих предписаний

                Интересной работой последний раз занимался сегодня с часу ночи до четырех утра, и то по причине бессонницы чтобы немного перенаправить нервяк.

                А вот занимался бы или нет — тут кроме интересности есть еще определенные существующие обязательства, которые просто так не разорвать, в том числе ипотечные.
                • +4
                  К чему все эти подробности?
                  • +1
                    А к чему были уточнения на вполне очевидные фразы?
                    Просто выводы сделанные на основании одной фразы не всегда близки к реальности. И в жизни не всегда удается заниматься только интересной работой. Это действительно так.
  • 0
    Воу-воу, полегче. Столько интересностей сразу. Глаза разбегаются.
  • 0
    Картинка в шапке — тоже рендер игровым движком?
    • +4
      Картинка в шапке это арт из игры. Что бы получить этот арт персонаж был выставлен в специальную позу в движке и картинка была отрендерена в движке, а после дорисована художниками.

      Остальные картинки и видео это рендер движка. В некоторых картинках рендер на ultra high настройках, в некоторых просто на high.
  • +1
    Отличная статья!
    Шероховатость (Roughness) — как вы его подбираете? Какие-то данные есть в «справочниках»?

    Кстати, почему бы не хранить нормали в GBuffer XY в ViewSpace? А значение Z потом восстанавливать?
    • 0
      И очень хотелось бы увидеть — как у вас организованно реализация PMREM (Prefiltered Mipmaped Radiance Environment map) и последующий просчет сферической гармоники.
      • +3
        В настоящий момент мы используем Modified AMD Cubemapgen и в нем Cosine filter и наша аналитическая модель освещения полностью совпадает с IBL частью освещения.

        У него только один недостаток — очень долгое время работы. Есть несколько альтернатив и презентация от tri-Ace как это делать быстрее.
    • +3
      Если хранить нормали во ViewSpace то точность упаковки нормалей будет изменяться если вращать камеру. Для больших плоскостей (например пол) это выглядит бажно — вращаешь камеру, чуть плавает освещение.

      Roughness для типичных материалов у нас указан в гайдлайне для художников. Насколько мне известно в «справочниках» таких данных нет, но можно попробовать использовать MERL BRDF Database и Disney's BRDF explorer что бы подобрать физичные значения.
    • +1
      Есть еще проблема в том, что иногда нормаль может иметь отрицательный Z во ViewSpace — например, из-за интерполяции вертексных нормалей или нормал мапа. Тогда придется, как минимум, еще где-то битик хранить и его распаковывать.
  • 0
    Не очень понял зачем вы делаете отдельный проход с восстановлением глубины? Можно ведь сразу писать глубину в R32F или восстанавливать уже в финальных шейдерах.
    • 0
      Разве не для того, что-бы потом в каком-нибудь SSAO не восстанавливать по сто раз эту глубину? :)
      • 0
        Не совсем очевидно какой из подходов быстрее. Я и сам склоняюсь к меньшему количеству проходов — это делает пайплайн рендеринга проще и универсальнее. Но на практике — все зависит от проекта. Возможно в данном случае это оправдано. Может быть автор прокоментирует?
        • 0
          На самом деле в дополнительном проходе ничего страшного нет. Но да, тут верно подмечено, что неизвестно — как по скорости будет дополнительный проход с последующим его использованием, либо каждый раз в финальных шейдерах восстанавливать.

          И еще тут подумал, как написал sergey_reznik — почему бы в MRT не добавить еще один R32F, где мы уже будем писать восстановленную глубину (в хардварный по ряду причин писать не правильно).

          Да, хотелось бы автора услышать :)
          • 0
            почему бы в MRT не добавить еще один R32F

            Думаю потому, что некоторые видеокарты поддерживают максимум 4 рендер-таргета. Выходить за эти рамки — значит либо уменьшать целевую аудиторию, либо поддерживать две версии
            • 0
              В таком раскладе у нас не будет 4 РТ:

              Вариант для MRT (который сейчас):
              1) Albedo RT
              2) Normal RT
              3) Depth DSV (hardware)

              Два RT, глубина не относится к MRT.

              Вариант с еще одной R32F:
              1) Albedo RT
              2) Normal RT
              3) LDepth RT
              4) Depth DSV (hardware)

              Три RT, глубина опять же не относится к MRT.

              Если MRT вообще поддерживается — то их кол-во точно уж не будет меньше четырех. Поэтому, не совсем вас понял :)
              • 0
                Судя по статье в текущем варианте еще есть таргет для HDR (или я что то упустил?)
                image
                Но да, совсем забыл, что хардварная глубина не считается как дополнительный таргет. В итоге максимум 4, мой косяк
          • +5
            Да, возможно использовать еще один R32F RT и рисовать в него линейную глубину.
            Проблема в overdraw при заполнении G-Buffer. Во время заполнения G-Buffer, overdraw обычно больше 1, иногда значительно (например: деревья, трава и т.п.)

            Это значит, что информация о глубине будет записана для каждого пикселя больше одного раза, а это неэффективное использование bandwidth.

            В случае же «линеаризации» отдельным проходом, мы получаем гарантии, что запись в R32F текстуру будет выполнена один раз для каждого пикселя.
            • 0
              Да, все сходится, не подумал. Спасибо за развернутые ответы!
            • 0
              Ага, логично.
              По поводу G-буфера — у вас не объяснено что такое HDR RGB в третьем таргете, поэтому складывается ощущение, что он лишний. Могу предположить, что это что-то вроде интенсивности освещения, тогда может стоит подумать о RGBM/RGBE?
              И еще хорошо бы нормали упаковать в две компоненты, а на освободившееся место засунуть остальную информацию. По идее, можно было бы втиснуть в 2 таргета все.
              Хотя, конечно, все зависит от железа, на которое вы целитесь.
              • 0
                С двумя компонентами будут проблемы.
              • +1
                HDR это текущее значение буфера освещения. Туда например записывается информация для само-светящихся объектов и для global illumination.

                RGBE/RGBM там к сожалению использовать нельзя, т.к. нужен аддитивный блендинг в этот таргет.

                Мы сейчас пробуем еще ужать G-Buffer, например используя технику сжатия base color в два канала (YCoCg) как описано в статье
                The Compact YCoCg Frame Buffer. В данный момент я уверен — наша текущая раскладка G-Buffer не финальная и мы будем её еще ужимать.
                • 0
                  Ужимание тратит инструкции на запаковку/распаковку. В одном малоизвестном проекте при переходе на ужимание потеряли 3% перфоманса. Пришлось делать без него, так как ни кто такой просадки не позволил :(
                  • +1
                    The AMD GCN Architecture — A Crash Course слайд #70

                    Export cost can be more costly than PS execution!
                    Each fast export is equivalent to 64 ALU ops on R9 290X

                    Обычно память (особенно на запись, где кеш не помогает) более ограниченный ресурс чем ALU, в том числе и на встроенных/мобильных GPU.

                    • 0
                      Еще от организации рендера зависит. Например нужно распаковывать нормаль для HBAO…
  • +1
    Круто, внушает.
  • +1
    Reversed Depth Buffer — гениально)
  • 0
    Очень круто
  • +2
    Вот бы ещё вся эта красота не тормозила — было бы совсем замечательно.
    • +5
      Мы сейчас практически все усилия направляем на оптимизацию, мы снимаем очень много метрик с ЗБТ который идет в данный момент и используем эти данные для оптимизации игры.

      На мобильных видеокартах поведение к примеру оказалось не таким как мы расчитывали. Это для нас отдельный и очень важный фронт работ сейчас.
      • 0
        Желаю вам скорейших успехов на этом фронте, так очень хочу насладиться игрой в полной мере.
  • +1
    Спасибо за статью!
    Очень классно все написано.

    Буду надеяться, что это не последняя статья такого плана.
  • +1
    Круто! Если бы ещё понять всё это до конца!
  • 0
    На первых нескольких картинках очень резкие не естественные тени. Почему? Ведь судя по рендерам в конце статьи, движок умеет реалистичные мягкие тени
    • +1
      Мягкая тень или резкая это зависит от настроек которые сделали художники и от настроек качества графики — мягкие тени более дорогие для производительности.

      Я на самом деле точно не помню какие именно настройки в первых картинках влияют на тени. Это либо настройки источников света либо настройки качества при снятии скриншота.
  • +1
    Спасибо за шикарное описание PBR! Скрины красивые, особенно с далёким обзором. Проникаюсь уважением к Mail.Ru :)
  • +1
    Reverse depth подходит для любого движка, я бы рекомендовал его использовать во всех проектах.

    Хорошо вам с Direct X. У меня проект на OpenGL, и там реверс z-буфер идёт лесом на Intel HD. Интелы до сих пор не добавили в драйвер расширение, приводящее z к диапазону [0, 1] вместо стандартного для GL [-1, 1]. По крайней мере на старых картах типа Intel HD 4000 его нет, и скорее всего не будет.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка