Skyforge: технологии рендеринга
Всем привет! Меня зовут Сергей Макеев, и я технический директор в проекте Skyforge в команде Allods Team, игровой студии Mail.Ru Group. Мне хотелось бы рассказать про технологии рендеринга, которые мы используем для создания графики в Skyforge. Расскажу немного о задачах, которые стояли перед нами при разработке Skyforge с точки зрения программиста. У нас свой собственный движок. Разрабатывать свою технологию дорого и сложно, но дело в том, что на момент запуска игры (три года назад) не было технологии, которая могла бы удовлетворить всем нашим запросам. И нам пришлось самим создать движок с нуля.Основной арт-стиль игры — это смесь фентези с Sci-Fi. Чтобы реализовать задумки арт-директора и художников, нам нужно было создать очень сильную, мощную систему материалов. Игрок может видеть проявления магии, технологии и природные явления, и система материалов нужна, чтобы правдоподобно нарисовать все это на экране. Еще одним «столпом» нашего графического стиля является то, что мы создаем стилизованную реальность. То есть объекты узнаваемы и выглядят реалистично, но это не фотореализм из жизни. Хорошим примером, на мой взгляд, является фильм «Аватар». Реальность, но реальность художественно приукрашенная, реальность и в то же время сказка. Следующий столп графического стиля — освещение и материалы — выглядят максимально естественно. А «естественно» с точки зрения программиста — это значит «физично».
Важный момент для программиста: нам нужны огромные открытые пространства. В игре существуют как небольшие локации-инстансы, так и огромные территории, на которых находится много игроков. На этом видео вы можете посмотреть, чего мы добились на текущий момент. Все, что показано в видео, отрендерено с помощью игры в максимальных настройках
[embedded content]
Далее я расскажу о том, как мы добились такой картинки.
Зачем нужен шейдинг, основанный на физике? Это дает нам более реалистичную и сведенную картинку. 3D-модели, сделанные разными художниками, выглядят целостно в разных типах освещения. Картинка меньше разваливается на части, и художникам по освещению не нужно сводить все варианты освещения со всеми 3D-моделями. Материалы можно настраивать отдельно от света. То есть художники по материалам и художники по свету могут работать параллельно. Корректно настроенные физичные материалы при любом освещении выглядят как положено, не становятся неожиданно черными и пересвеченными, как это часто бывало раньше, при нефизичных материалах. Параметров материала стало меньше, и все они имеют физический смысл. Художникам легче ориентироваться в этих параметрах. Конечно, сначала им приходится переучиваться, но потом работа становится гораздо более предсказуемой. Соблюдение закона сохранения энергии в системе материалов означает, что художникам по свету проще работать. Нельзя, настраивая свет, «рассыпать» картинку на части. Кстати, физическая корректность не обязывает нас к фотореализму. Например, все последние мультфильмы, созданные студиями Pixar и Disney, сделаны с помощью физического рендера. Но при этом там нет фотореализма, а присутствует вполне узнаваемая стилизация. Прежде чем программировать что-либо, надо сначала понять физику процесса. Что происходит, когда свет отражается от поверхности? В реальной жизни, в отличие от компьютерной графики, поверхности не гладкие, а на самом деле состоят из множества маленьких неровностей. Эти неровности настолько мелкие, что глазом их не видно. Однако они достаточно большие, чтобы влиять на свет, отраженный от поверхности. Здесь на картинке я назвал это микроповерхностью.
Вот пример из реальной жизни. На картинке лист бумаги А4 под электронным микроскопом. Видно, что лист бумаги на самом деле состоит из множества переплетенных древесных волокон, но глаз их не различает.
Для масштаба я вывел длины волн для разного спектра освещения в нанометрах и выделил на скане бумаги квадрат со стороной в 100000 нанометров. Очевидно, что хотя мы и не видим эти волокна, они оказывают влияние на отражение света.
Рассчитывать освещение с таким уровнем детализации не под силу даже офлайн-рендерам для кино. Это огромное количество вычислений.
Итак, микрогеометрия поверхности. Часть света проникает внутрь и переизлучается после случайных отражений внутри материала или поглощается — превращается в тепло. Часть падающего света отражается от поверхности. Существует разница в том, как разные материалы отражают свет. Две группы, которые ведут себя по-разному — это диэлектрики и проводники (металлы). Внутрь металлов свет не проникает, а практически весь отражается от поверхности. От диэлектриков свет же в основном переизлучается, а отражается малое количество света — около 5%.
Теория — это хорошо, но мы создаем компьютерную игру и оперируем не лучами света, а пикселями на экране. При этом каждый пиксель — это большое количество фотонов света. И весь этот свет может быть поглощен, переизлучен или отражен. Это нам и предстоит рассчитать при разработке шейдеров.
С физикой процесса в общих чертах определились, перейдем к математической модели. Основная функция, которая позволит нам определить, какой процент света был отражен, а какой переизлучен или поглощен, называется BRDF (Bidirectional Reflection Distribution Function) или по-русски ДФОС (двухлучевая функция отражательной способности). Цель данной функции — рассчитать количество энергии, излучаемой в сторону наблюдателя при заданном входящем излучении. В теории это многомерная функция, которая может зависеть от большого количества параметров 3D, 4D, 6D.Мы же на практике будет рассматривать функцию от двух параметров F (l, v), где l — направление от точки поверхности на источник света, а v — направление взгляда.
Для модели переизлученного света мы делаем несколько допущений: можем пренебречь точкой входа и выхода луча, т.к. это очень незначительная в нашем случае величина; считаем, что все переизлученные лучи равномерно распределены внутри полусферы. Поведение фотона внутри материала очень сложное, и для текущего развития компьютерной графики это нормальное приближение, к тому же оно в достаточной мере соответствует реальным физическим замерам.
Получаем следующую функцию для расчета переизлученного (рассеянного) света.
l — направление на источник света, v — направление взгляда, оно никак не используется в данной упрощенной модели, т.к. все энергия переизлучается равномерно по полусфере.
albedo (rgb) — определяет, сколько энергии поглощает поверхность, а сколько переизлучает. Так, к примеру, поверхность с абсолютно черным albedo всю энергию поглощает (преобразует в тепло). На самом деле это известный всем графическим программистам dot (n, l), за исключением деления на PI. Деление на PI нужно для соблюдения закона сохранения энергии. Т.к. свет рассеивается по полусфере, то при n, равном l, мы отразим падающий свет без изменения интенсивности во все стороны по полусфере, что физически невозможно. Но обычно интенсивность источника света, переданная в шейдер, уже учитывает деление на PI, поэтому в шейдере остается только dot (n, l).
Мы знаем, что скалярное произведение векторов (dot) — это косинус между этими векторами. Возникает вопрос: как угол падения света влияет на количество переизлученного света? Ответ прост: площадь проекции зависит от угла падения луча на поверхность и равна косинусу угла падения. Соответственно, чем острее угол падения, тем меньше энергии попадает на поверхность.
Свет падает под «тупым» углом
Свет падает под острым углом, площадь проекции стала больше
Перейдем к модели отраженного света. Всем известно, что угол падения равен углу отражения, но на приведенной картинке отраженных лучей стало много. Так происходит из-за микронеровностей поверхности. Благодаря этим неровностям каждый отдельный фотон отражается немного в разную сторону. Если поверхность достаточно гладкая, то большинство лучей отражаются в одну сторону и мы видим четкое отражение предметов, вроде как в зеркале.Если поверхность более шероховатая, то гораздо большее количество лучей отражаются в разные стороны, и тогда мы видим очень матовое отражение. Для сильно неровной поверхности отраженный свет может быть распределен равномерно внутри полусферы и выглядеть для внешнего наблюдателя как рассеянный свет.
Для моделирования эффекта отражения света от поверхности с учетом микрогеометрии поверхности была разработана 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.
В качестве функции распределения отраженного от микрограней света мы используем степень косинуса, с нормированием для соблюдения закона сохранения энергии. Для начала мы берем коэффициент шероховатости поверхности, который лежит в диапазоне 0…1, и раcчитываем из него степень альфа, которая лежит в диапазоне 0.25 — 65536. Далее мы берем скалярное произведение N и H векторов и возводим их в степень альфа. И чтобы получившийся результат не нарушал закон сохранения энергии, мы применяем константу нормализации NDF.Без нормализации от поверхности будет отражаться больше энергии, чем пришло. Таким образом мы задаем объем, в котором происходит отражение света и распределение энергии в этом объеме. И этот объем зависит от того, насколько гладкая или шероховатая поверхность. Теперь рассмотрим следующий член BRDF.
Сила отражения света, зависит от угла падения. Это поведение описывается формулами Френеля. Формулы Френеля определяют амплитуду и интенсивность отраженной и преломленной электромагнитной волны при прохождении через границу двух сред. Этот эффект очень заметен на воде, если смотреть на воду под острым углом, то вода отражает большинство света и мы видим отражение. Если же смотреть на воду сверху-вниз, то отражения мы практически не видим, а видим то, что находится на дне.
Вот так, к примеру, выглядит график отраженного света в зависимости от угла падения для различных материалов. Табличные данные я взял с сайта http://refractiveindex.info/
На графике видно, что пластик практически весь свет рассеивает, но не отражает, пока угол падения не станет 60–70 градусов. После чего количество отраженного света резко увеличивается. Для большинства диэлектриков график будет схожий.
Гораздо интереснее ситуация с металлами. Металлы отражают очень много света под любым углом, но количество отраженного света разное для разной длины волны. Пунктирные графики показывают отражение света для меди. Длины волн соответствуют красному, зеленому и синему цветам. Как видно, красного цвета медь отражает гораздо больше, именно поэтому металлы окрашивают отраженный свет в цвет своей поверхности. А диэлектрики отражают свет, не окрашивая его.
В качестве функции для расчета Френеля мы используем аппроксимацию Шлика, т.к. оригинальные уравнения Френеля слишком тяжелые для вычислений в реальном времени.
Как видно, в функции Шлика участвуют H и V вектора, с помощью которых определяют угол падения, и F0, с помощью которого практически задается тип материала. Коэфицент F0 возможно рассчитать, зная Index of refraction (IOR) материала, который мы моделируем. Фактически его можно найти в справочниках в интернете. Т.к. мы знаем, что IOR воздуха 1, то, зная табличный IOR материала, мы рассчитываем F0 по формуле
Физический смысл F0 следующий — это процент отраженного под прямым углом света. Итак: френель задает, сколько света будет поглощено, а сколько отражено в зависимости от угла падения.
F0 используется в аппроксимации Шлика и задает, сколько света отражается под прямым углом к поверхности. Обычное значение F0 для диэлектриков 2% — 5%, т.е. диэлектрики мало отражают и много рассеивают.
Металлы практически весь свет отражают, при этом для разных длин волн это количество разное. Отражения на металлах окрашиваются в цвет поверхности. Теперь рассмотрим следующий член BRDF.
На самом деле, не каждая микрогрань, нормаль которой соответствует 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
Для упрощения работы наши художники составили достаточно большую библиотеку материалов. Вот некоторые примеры.
При разработке Skyforge мы используем модель отложенного шейдинга. Это широко распространенный в настоящее время метод. Метод называется отложенным, т.к. во время основного прохода рендеринга происходит только заполнение буфера, содержащего параметры, необходимые для расчета финального шейдинга. Такой буфер параметров называют G-Buffer, сокращение от geometry buffer.Кратко опишу плюсы и минусы отложенного шейдинга:
Плюсы:
Шейдеры геометрии и освещения разделены. Легко сделать большое количество источников света. Отсутствие комбинаторного взрыва в шейдерах. Минусы: Bandwidth. Нужна большая пропускная способность памяти, т.к. буфер параметров поверхности достаточно толстый. Источники света с тенями по-прежнему дорогие. Источники света без теней имеют достаточно ограниченное применение. Сложность встраивания различных BRDF. Тяжело сделать разную модель освещения для разных поверхностей. Например, BRDF для волос или анизотропного металла. Прозрачность. Практически не поддерживается, прозрачность нужно рисовать после того как основная картинка нарисована и освещена. Основной минус технологии — нужна большая пропускная способность памяти. Мы стараемся максимально упаковать параметры поверхности, необходимые для освещения. В результате мы пришли к формату, который укладывается в 128 бит на пиксель — 96, если не учитывать информацию о глубине.Как мы храним свойства поверхности (128 бит на пиксель).
Skyforge G-Buffer
При использовании 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 и т.д., что дает нам реконструированную позицию в любом нужном нам пространстве.
Итак, еще раз алгоритм по шагам:
Преобразование гиперболической глубины в линейную. В вершинном шейдере рассчитываем треугольник (P1, P2, P3). Передаем отрезок (P1, P3) в пиксельный шейдер, через интерполятор. Получаем интерполированный вектор RayDir (P1, P3). Считываем линейную глубину в данной точке. Position = CameraPosition + RayDir * LinearDepth. Алгоритм очень быстрый: один интерполятор, одна ALU инструкция MAD и одно чтение глубины. Можно реконструировать позицию в любом удобном однородном пространстве. HLSL код для реконструкции в конце статьи. При разработке 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.5Zfar = 50000.0
Получим следующие две матрицы проекции:
Стандартная матрица проекции
Развернутая матрица проекции
Глубина после умножения на стандартную матрицу проекции будет равна следующему:
Соответственно, после умножения на развернутую матрицу проекции:
Как мы видим, в случае стандартной матрицы проекции при расчете глубины будет происходить сложение чисел очень разного порядка — десятки тысяч и 0.5. Для сложения чисел разного порядка FPU должен сначала привести их экспоненты к единому значению (бjльшей экспоненте), после чего сложить и нормировать полученную сложением мантису. Фактически на больших значениях 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