SSLR: Screen Space Local Reflections в AAA-играх

  • Tutorial
image

Привет, друг! В этот раз я опять подниму вопрос о графике в ААА-играх. Я уже разобрал методику HDRR (не путать с HDRI) тут и чуть-чуть поговорил о коррекции цвета. Сегодня я расскажу, что такое SSLR (так же известная как SSPR, SSR): Screen Space Local Reflections. Кому интересно — под кат.

Введение в Deferred Rendering


Для начала введу такое понятие как Deferred Rendering (не путать с Deferred Shading, т.к. последнее относится к освещению). В чем суть Deferred Rendering? Дело в том, что все эффекты (такие как освещение, глобальное затенение, отражения, DOF) можно отделить от геометрии и реализовать эти эффекты как особый вид постпроцессинга. К примеру, что нужно, чтобы применить DOF (Depth Of Field, размытие на дальних расстояниях) к нашей сцене? Иметь саму сцену (Color Map) и иметь информацию о позиции текселя (другими словами на сколько пиксель далеко от камеры). Далее — все просто. Применяем Blur к Color Map, где радиус размытия будет зависеть от глубины пикселя (из Depth Map). И если взглянуть на результат — чем дальше объект, тем сильнее он будет размыт. Так что же делает методика Deferred Rendering? Она строит так называемый GBuffer, который, обычно, в себя включает три текстуры (RenderTarget):

  • Color map (информация о диффузной составляющий или просто цвет пикселя)
    image
  • Normal map (информация о нормали “пикселя”)
    image
  • Depth map (информация о позиции “пикселя”, тут храним только глубину)
    image


В случае с Color map, Normal map вроде все понятно, это обычные Surface.Color текстуры: пожалуй, за исключением того, что вектор нормали может лежать в пределах [-1, 1] (используется простая упаковка вектора в формат [0, 1]).

А вот ситуация с Depth map становится непонятной. Как же Depth map хранит в себе информацию о позиции пикселя, да еще и одним числом? Если говорить сильно упрощенно, трансформация примитива:

float4 vertexWVP = mul(vertex, World*View*Projection);


Дает нам экранные координаты:

float2 UV = vertexWVP.xy;


И некоторую информацию о том, насколько “далеко” от камеры пиксель:

float depth = vertexWVP.z / vertexWVP.w;


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

В дальнейшем мы сможем реконструировать позицию пикселя очень простым способом:

float3 GetPosition(float2 UV, float depth)
{
	float4 position = 1.0f; 
 
	position.x = UV.x * 2.0f - 1.0f; 
	position.y = -(UV.y * 2.0f - 1.0f); 

	position.z = depth; 
 
	//Transform Position from Homogenous Space to World Space 
	position = mul(position, InverseViewProjection); 
 
	position /= position.w;

	return position.xyz;
}


Напомню, что для построения GBuffer необходима такая методика как MRT (Multiple Render Targets), которая рисует модель сразу в несколько Render Target (причем в каждом RT содержится разная информация). Одно из правил MRT — размерность всех Render Target должна быть одинаковой. В случае Color Map, Normal MapSurface.Color: 32-ух битная RT, где на каждый канал ARGB приходится по 8 бит, т.е. 256 градаций от 0 до 1.

Благодаря такому подходу мы можем применять сложные эффекты к любой геометрии, например самый популярный Screen Space эффект: SSAO (Screen Space Ambient Occlusion). Этот алгоритм анализирует буферы глубины и нормали, считая уровень затенения. Весь алгоритм я описывать не буду, он уже описывался на хабре, скажу лишь то, что задача алгоритма сводится к трассировки карты глубины: у нас есть набор случайных векторов, направленных из считаемого “пикселя” и нам нужно найти кол-во пересечений с геометрией.

Пример эффекта (слева без SSAO, справа с SSAO):
image


Так же Deferred Shading является Screen Space эффектом. Т.е. для каждого источника света на экране (без всяких оптимизаций) мы рисуем квад в режиме Additive в так называемый RenderTarget: Light Map. И зная мировую позицию “пикселя”, его нормаль, позицию источника света — мы можем посчитать освещенность этого пикселя.

Пример Deferred Shading (освещение выполнено отложено, после отрисовки геометрии):

image


Достоинства и проблемы Screen Space эффектов

Самый главный плюс Screen Space эффектов — независимость сложности эффекта от геометрии.

Самый главный минус — локальность всех эффектов. Дело в том, что мы постоянно будем сталкиваться с Information Lost, во многих случаях это сильно зависит обзора, поскольку SSE зависит от смежных глубин текселей, которые могут быть сгенерированы любой геометрией.

Ну и стоит отменить, что Screen Space эффекты выполняются полностью на GPU и являются пост-процессингом.

Наконец SSLR


После всей теории мы подошли к такому эффекту, как Screen Space Local Reflections: локальные отражения в экранном пространстве.

Для начала разберемся с перспективной проекцией:

image


Горизонтальный и вертикальный угол зрения задается FOV (обычно 45 градусов, я предпочитаю 60 градусов), в виртуальной камере они разные т.к. учитывается еще и Aspect Ratio (соотношение сторон).

Окно проекции (там, где мы оперируем UV-space данными) — это, что мы видим, на то мы проецируем нашу сцену.
Передняя и задняя плоскости отсечения это соответственно Near Plane, Far Plane, задаются так же в проекцию как параметры. Делать в случае Deferred Rendering слишком большим значением Far Plane стоит, т.к. точность Depth Buffer сильно упадет: все зависит от сцены.

Теперь, зная матрицу проекции и позицию на окне проекции (а так же глубину) для каждого пикселя мы вычисляем его позицию следующим образом:

float3 GetPosition(float2 UV, float depth)
{
	float4 position = 1.0f; 
 
	position.x = UV.x * 2.0f - 1.0f; 
	position.y = -(UV.y * 2.0f - 1.0f); 

	position.z = depth; 
 
	position = mul(position, InverseViewProjection); 
 
	position /= position.w;

	return position.xyz;
}


После нам нужно найти вектор взгляда на этот пиксель:

float3 viewDir = normalize(texelPosition - CameraPosition);

В качестве CameraPosition выступает позиция камеры.
И найти отражение этого вектора от нормали в текущем пикселе:

float3 reflectDir = normalize(reflect(viewDir, texelNormal));

Далее задача сводится к трассировке карты глубины. Т.е. нам нужно найти пересечение отраженного вектора с какой-либо геометрией. Понятное дело, что любая трассировка производится через итерации. И мы в них сильно ограниченны. Т.к. каждая выборка из Depth Map стоит времени. В моем варианте мы берем некоторое начальное приближение L и динамически меняем его исходя из расстояния между нашим текселем и позицией, которую мы “восстановили”:

float3 currentRay = 0;

float3 nuv = 0;
float L = LFactor;

for(int i = 0; i < 10; i++)
{
    currentRay = texelPosition + reflectDir * L;

    nuv = GetUV(currentRay); // проецирование позиции на экран
    float n = GetDepth(nuv.xy); // чтение глубины из DepthMap по UV

    float3 newPosition = GetPosition2(nuv.xy, n);
    L = length(texelPosition - newPosition);
}


Вспомогательные функции, перевод мировой точки на экранное пространство:

float3 GetUV(float3 position)
{
	 float4 pVP = mul(float4(position, 1.0f), ViewProjection);
	 pVP.xy = float2(0.5f, 0.5f) + float2(0.5f, -0.5f) * pVP.xy / pVP.w;
	 return float3(pVP.xy, pVP.z / pVP.w);
}


После завершения итераций мы имеет позицию “пересечения с отраженной геометрией”. А наше значение nuv будет проекцией этого пересечения на экран, т.е. nuv.xy – это UV координаты в экранном нашем пространстве, а nuv.z это восстановленная глубина (т.е. abs(GetDepth(nuv.xy)-nuv.z) должен быть очень маленьким).

В конце итераций L будет показывать расстояние отраженного пикселя. Последний этап — собственно добавление отражения к Color Map:

float3 cnuv = GetColor(nuv.xy).rgb;
return float4(cnuv, 1);


Разбавим теорию иллюстрациями, исходное изображение (содержание Color Map из GBuffer):


После компиляции шейдера (отражения) мы получим следующую картину (Color Map из GBuffer + результат шейдера SSLR):



Не густо. И тут стоит еще раз напомнить, что Space-Screen эффекты это сплошной Information Lost (примеры выделены в красные рамки).

Дело в том, что если вектор отражения выходит за пределы Space-Screen – информация о Color-карте становится недоступной и мы видим Clamping нашего UV.

Чтобы частично исправить эту проблему, можно ввести дополнительный коэффициент, который будет отражать “дальность” отражения. И далее по этому коэффициенту мы будем затенять отражение, проблема частично решается:

L = saturate(L * LDelmiter);

float error *= (1 - L);


Результат, отражение умноженное на error (попытка убрать артефакт SSLR — information lost):



Уже лучше, но мы замечаем еще одну проблему, что будет, если вектор отразится в направлении камеры? Clamping’а UV происходить не будет, однако, несмотря на актуальность UV (x > 0, y > 0, x < 1, y < 1) он будет неверным:



Эту проблему так же можно частично решить, если как-нибудь ограничить углы допустимых отражений. Для этого идеально подходит фишка с углами от эффекта Френеля:

float fresnel = dot(viewDir, texelNormal);

Чуть-чуть модифицируем формулу:

float fresnel = 0.0 + 2.8 * pow(1+dot(viewDir, texelNormal), 2);

Значения Френеля, с учетом Normal-маппинга (значения fresnel-переменной для SSLR-алгоритма):



Те области, которые отражаются в “камеру” будут черными, и их мы не учитываем (взамен можно сделать fade в кубическую текстуру).

Отражение, умноженное на error и fresnel (попытка удалить большую часть артефактов SSLR):



Кстати, значение Fresnel стоит лимитировать по какому-либо параметру, т.к. из-за “шероховатости” нормалей значение будет на порядок больше единицы (или другого числа-лимитера).

И завершающий этап сегодняшний статьи — это размытие отражений, т.к. идеальное отражение только у зеркала. Степень размытия можно считать как 1-error (чем дальше отраженный пиксель — тем сильнее размыт). Это будет своеобразный вес размытия и хранить его можно в альфа-канале RT-отражений.

Результат (финальное изображение с убранными артефактами и с размытыми отражениями):



Заключение


Так же, стоит добавить некоторую информацию об отражающей способности: насколько четкое отражение, насколько поверхность вообще способна отражать, в те места где SSLR не работает — добавить статическое отражение кубической текстуры.

Конечно, Space-Screen эффекты не являются честными, и разработчики стараются скрыть артефакты, но сейчас в реалтайме подобное (при сложной геометрии) сделать невозможно. А без подобных эффектов игра начинает выглядеть как-то не так. Я описал общую методику SSLR: основные моменты из шейдера я привел. Код, к сожалению, прикрепить не могу, т.к. в проекте слишком много зависимостей.

Удачных разработок! ;)
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 47

    +1
    А «space screen» — это принятая формулировка? Как-то режет глаз, все время думал что «screen-space» (ssao — screen-space ambient occlusion/obscurance).
      0
      Да, не сразу заметил. Все верно, Screen Space Local Reflections.
      +2
      Мне многие вещи в статье непонятны, а понять хочется. Особенно мне непонятно, когда вы говорите, что вот, мол, пример, показываете картинку, но не обьясняете на что смотреть. Был бы признателен, если бы вы дописали к картинкам комментарии, что надо искать на них, и как это что-то относится к обсуждаемой теме.
        0
        Жду не дождусь raytracing в realtime. Судя по Brigade 3, осталось совсем недолго. Причем сразу со вторичкой, или по крайней мере с AO.
          –13
          Играю в GTA 5 и не парюсь. Впрочем как 4,3,2,1

          А не парюсь благодаря таким умным людям, которые постоянно приближают графику к реальности.
            0
            <#de#l>
            +1
            По чему-то ни слова не сказано, что взрослые дяди в место
            GetColor(nuv.xy).rgb;
            берут цвет из прошлого HDR кадра с репроекцией текстурных координат.
            При этом прошлый HDR кадр блюрится по мипам и выбор мипа зависит от шерохофватости отражающей поверхности (ну или просто можно сделать контрейсинг).
              +1
              HDR в этой статье не описан и даже не затронут, репроекция координат это есть nuv. GetColor(UV).rgb — релеватно tex2D(GBufferColorMap, UV).rgb.
              В классическом варианте SSLR берется текущий кадр, а не прошлый, блюр происходит после SSLR-алгоритма.
                0
                Текущий брать нельзя, ведь он не дорендерен. Блюрить после применения тоже нельзя — замажем то что находится под отражениями (например мостовая под лужей).
                  0
                  Я ведь писал в статье, что любой Screen Space эффект является пост-процессингом? А пост-процессинг выполняется после рендера основной сцены. В этом и прелесть SSLR, что можно создать локальные отражения основываясь на текущем Scene RT. А размывается не вся картинка, а только SSLR RT, которая содержит в себе информацию только об отражении.
                    0
                    Этот Screen Spac эффект влияет на освещение сцены, его нельзя рассматривать в вакуме. По крайне мере для честного true Physically Based Render. По этому в серьезных движках и берут прошлый HDR кадр (обязательно делая репроекцию) и заменяют им текущий Environment Specular (спекуляр от окружения, если очень упрощенно — от кубмапы).
                      0
                      Так ведь в статье вообще не описано освещение как таковое. Насчет Environment-отражения — где есть эффект Френеля используют RLR (SSLR), где происходит весомый Information Lost — lerp'ят отражения от статической кубмапы.

                      Я не совсем понимаю, причем тут прошлый HDR (и причем тут вообще идеология HDRR) кадр, если эта методика есть Screen Space post-process и завязанная на информации в уже отрендерином GBuffer (а если еще с DS, то вообще на готовой сцене с Normal и Depth картами). Можно какие-нибудь статьи про то, что вы говорите? Я правда Вас не понимаю.
                        0
                        Вот пример — есть не освещенная комната, на полу которой мы хотим отрендерить SSLR. В итоге получим сияющий в темноте пол. А если бы мы взяли информацию с прошлого кадра, то все освещение склеилось бы.

                        То что описано в статье было бы приемлемо лет пять назад. Сейчас расчет освещения шагнул значительно в перед. Аппроксимации усложнились. BRDF и Physically Based Render уже несколько лет в любом «ААА» проекте.

                        Повторю ссылки:
                        www.neogaf.com/forum/showthread.php?t=557670&page=8
                        docs.unrealengine.com/latest/INT/Resources/Showcases/Reflections/index.html
                          +1
                          Вот пример — есть не освещенная комната, на полу которой мы хотим отрендерить SSLR. В итоге получим сияющий в темноте пол. А если бы мы взяли информацию с прошлого кадра, то все освещение склеилось бы.


                          Мы очевидно говорим о разном, либо вы не понимаете. Как мы можем получить сияющий пол, если Scene RT — не содержит света вообще?

                          Ладно, попробую объяснить в перспективе алгоритм:

                          1) рисуем геометрию, строим GBuffer, получаем Color, Normal, Depth
                          2) строим Light map, используя Deferred Shading.
                          3) объеденяем Light map с Color map и получаем Scene RT

                          Теперь все бы ничего, у нас есть красиво освещенная комната, но хочется отражений.

                          4) запускаем наш SSLR, передавая туда только Scene RT, Normal, Depth и получаем SSLR map
                          5) размываем SSLR map с учетом веса в альфе канале.
                          6) объеденяем Scene RT и SSLR map с учетом свойств материала.

                          И спрашивается, причем тут вообще прошлый кадр? Что не отрендерино? GBuffer на стадии четыре у нас не отрендерен? Причем тут освещение вообще (про HDRR зачем-то написали)? Я правда не понимаю, о чем вы говорите. Я написал статью с рассказом о том, как происходит аппроксимации путем Deferred Rendering — отражений (а конкретно реализация SSLR-алгоритма). Заголовок статьи же не: «делаем AAA-игру за 2 минуты».

                          Сейчас расчет освещения шагнул значительно в перед.

                          Основной метод остался тем же, как и пять лет назад. Появились методики Global Illumination и Ambient Occlusion (тот же SSAO, но для удовлетворения вашего аппетита возьмем HBAO).

                          И да, BRDF это не один шейдер, а целый chain из пост-процессинга.
                            0
                            Вот со Scene RT уже понятнее, а то по статье получается что берется просто альбедо из G-буфера.
                            Однако в этом Scene RT нет отраженного света, который уже посчитан в прошлом кадре. По этому и берут прошлый кадр с полным освещением.
                              0
                              Если вы имеете ввиду учет отражения отражения с освещением, тогда я вас понял.
                0
                Так расскажите нам отдельной статьей :-)
              +1
              Мне кажется, после отражения изображение должно быть более блеклым и тёмным. То есть не совсем нормально, когда яркость отражённой коробки больше, чем яркость оригинальной.

              В жизни такое может быть, но это очень редкий эффект, который лучше избегать.
                0
                Я написал как получить это отражение (методику), а для того, что-бы показать результат — вывел: сцена + отражение.
                P.S. освещение на сцене максимальное (ambient=1), можно сказать, что оно отсутствует вообще.
                  0
                  В этих условиях отражение банальным образом неправильное. Если освещение нейтральное рассеяное, то не может отражение быть светлее оригинала.
                    0
                    Окей. Каюсь. Отражение действительно было помножено на два.
                      0
                      Обновите картинку (хотя бы последнюю) для оценки результата.
                –5
                Мне вот интересно, когда все-же игровая графика сделает заметный революционный, а не размытый в годах эволюционный скачок? Half-Life 2, который вышел 10 лет назад выглядит ничуть не лучше современных игр, с их проблемами и артефактами.
                  +1
                    +1
                    Вы серьезно упоминаете half life 2 в контексте графики? Он уже вскоре после выхода выглядел устаревшим. Сейчас это динозавр, так же как source engine. Сейчас пришли времена physically-based rendering и какого никакого рилтаймового global illumination. Если брать half life 2, то революционный скачок на лицо. А так, все плавно и постепенно улучшается, отчего выглядит медленно и незаметно. Хороший пример недавний call of duty, который как никто другой смог приблизиться к фотореализму. Заодно это, по моему, первый коммерческий опыт использования нашумевшей технологии рендера лиц www.iryoku.com/next-generation-life
                      +1
                      Жду The Order: 1886 — читал их статьи, ресерч у них был очень мощный и потенциально должны опередить фростбайт.
                      Играл в демку на выставке — смотрится очень круто :)
                        0
                        Хех, у них заодно и используется упомянутый forward+.
                          0
                          Кстати да. Но на мой вкус тайловый деферед лучше :) (BF3, BF4)
                    +1
                    Для начала введу такое понятие как Deferred Rendering (не путать с Deferred Shading, т.к. последнее относится к освещению)

                    Странная фраза какая-то. Сколько не читал на эту тему, всю жизнь под Deferred Rendering подразумевался скорее общий подход к разделению рендера на стадии рендера геометрии и собственно шейдинга. Deferred Shading уже является классической реализацией в 2 прохода, которую описали вы. Альтернативная реализация, которая можно сказать уже вытеснила с рынка классическую, это light pre-pass или deferred lighting уже с 3 проходами.
                    Собственно, CryEngine 3, который впервые показал миру SSLR (возможно даже сами crytek являются авторами, не знаю), использует deferred lighting.
                      0
                      Deferred Rendering — построение GBuffer'a.
                      Deferred Shading — расчет освещения на основе данных GBuffer'a.
                      Их часто путают.

                      DR может включать DS, а может и не включать.
                      А вот DS включает в себя стадию DR всегда.
                        0
                        Я просто к тому, что во многочисленных статьях такого деления не видел. Есть Deferred Shading, есть Deferred Lighting. Две реализации одного подхода к рендерингу, отложенного, который является альтернативой forward'у.
                        0
                        Что-то какая-то путаница.
                        Deferred Shading не требует предварительного препаса геометрии, по этому он будет быстрее. И как раз в CryEngine 3 перешли на него.
                        Вот тут хорошо расписано blog.gamedeff.com/?p=642
                          0
                          Наоборот и по вашей ссылке так и написано. В CryEngine 3 использует 3-проходный light prepass. Несмотря на дополнительный проход он как раз считается более эффективным подходом нежели deferred shading. В основном из-за куда более гуманных требований к шине. Прошлое поколение консолей доминировало на рынке игр, а там это было очень важно, отчего все современные движки использовали именно deferred lighting.

                          Сейчас вот forward+ от AMD набирается обороты и уже засветился в парочке крупных проектов. Не знаю, что с ним там на самом деле, но из крупиц информации выглядит очень вкусно.
                            0
                            Я дико извиняюсь, но из статьи:
                            В Крайзис 2 пришли к схеме deferred lighting
                            aka light pre-pass rendering

                            Значит, новый релиз крайзиса – новый тек! Два прохода геометрии жаба душит, хочется один проход
                            Толстый gbuffer не хочется, хочется тонкий
                            Итого получился deferred shading!

                            Отложенное освещение требует обязательного препаса глубины. В деферед шейдинге геометрия рендерится лишь один раз. Что более гуманно к шине?
                              0
                              Я серьезно не понимаю о чем речь.
                              Что тут не так написано:
                              Deferred Rendering — один проход по геометрии с созданием GBuffer (Color+Normal+Depth).
                              Deferred Shading — один проход (рисование квада в самом простейшем случае) на один источник света с реконструированием позиции (исходя из Depth-буфера), с последующим посчетом освещения в Light Map
                              И собственно микс Light map с Albedo (Color). Где тут ошибка? Три прохода. На каждый DS — light pre-pass.
                                0
                                Deferred Shading и Deferred Lighting это две разновидности Deferred Rendering.
                                В Deferred Shading заполняем г-буффер, рендерим все освещение (квадом).
                                В Deferred Lighting делаем препроцес всей геометрии с заполнением в г-буффере карты нормалей и спекуляра. Рендерим освещение (квадом). Еще раз рендерим всю геометрию применяя к ней рассчитанное в прошлом шаге освещение.
                                0
                                Я хоть и интересуюсь темой всех этих технологий, но ответить на ваш вопрос прямо со всеми подробностями вряд ли смогу. Вот ссылка gamedevcoder.wordpress.com/2011/04/11/light-pre-pass-vs-deferred-renderer-part-1/. Ну и сам факт, что именно на консолях именно light pre-pass стал, по сути, единственным подходом к шейдингу. Подавляющее большинство игр его использовало и именно он использует ся в cryengine 3. Хотя вот эта ссылка вносит путаницу во все это gameangst.com/?p=141 Но реальные коммерческие проекты говорят совсем не в пользу deferred shading — даже эксклюзивы той же пс3. Uncharted как раз на light pre-pass.
                                  0
                                  А в не давнем тру NextGen «Ryse: Son of Rome» на PS4 и PC от крайтеков как раз таки деферед шейдинг.
                                  В CryEngyne 3 изначально был Deferred Lighting, но они заменили его на Deferred Shading
                                    0
                                    Понятно, я как раз не слышал, что они сменили. Интересно знать, почему они так решили.
                          0
                          >Поэтому стоит хранить в карте глубины не позицию пикселя, а только глубину.
                          Нужно использовать аппаратную карту глубины, а не делать свой велосипед.

                          >Одно из правил MRT — размерность всех Render Target должна быть одинаковой.
                          Во-первых, не размерность, а битовая глубина (можно забиндить вместе R8G8B8A8 и RGB10A2). Во-вторых, это верно только для DX 9-level API, в DX 11 таких ограничений нет.

                          >position.x = UV.x * 2.0f — 1.0f;
                          >position.y = -(UV.y * 2.0f — 1.0f);
                          Такие вещи стоит зашивать в матричку

                          Предложенное размытие в реальной игре работать будет плохо, т. к. разные материалы будут блидить друг на друга.

                          Ну и главная проблема SSR — вовсе не локальность. Во-первых, SSR довольно дорого, особенно фулрез хорошего качества. Во-вторых, артефакты из-за объектов, которые не касаются пола (появляются хало и прочее) — как следствие, видимые стыки между SSR и кубмапом.

                            –2
                            Взаимоисключающие параграфы:
                            Нужно использовать аппаратную карту глубины, а не делать свой велосипед.
                            и Deferred Rendering, не находите?
                              0
                              Нет, конечно.
                              В деферреде используется ровно такой же depth buffer, как и в форварде. Нафига увеличивать bandwidth и биндить еще один рендер таргет, если эта информация уже и так есть?
                            0
                            Я не очень хорошо разбираюсь в программировании и реал-тайм движках, но похожий эффект очень пригодился бы в некоторых ситуациях на посте… Например в Нюке… Такую штуку можно как плагин продавать… Нюк поддерживает пайтон (питон)… )))
                              0
                              Прошу прощения, если здесь это неуместно, но хотел бы поблагодарить за отличные статьи!

                                0
                                Тот случай, когда читаешь статью про технологии, вроде даже не старую статью, которая заканчивается словами " сейчас в реалтайме подобное (при сложной геометрии) сделать невозможно." и понимаешь, что уже возможно…

                                Only users with full accounts can post comments. Log in, please.