Physically-based rendering. Ray marching (часть 2)
Привет, с вами снова Герман и продолжение моей статьи про рендеринг. В первой части, которую вы можете найти по ссылке (link), мы поговорили о трассировке лучей и маршевом методе, а в этой части мы с вами получим изображение мыльного пузыря.
Интерференция света в мыльной пленке
Для того, чтобы мыльный пузырь был реалистичен, мы должны понимать, что радужные пятна на поверхности мыльного пузыря возникают из-за интерференции света. Часть света проходит внутрь пленки, остальная часть отражается, так как толщина пленки непостоянна: при каждом проходе свет смещается на определенное значение, пропорциональное длине волны и зависящее от толщины этой мыльной пленки. Чтобы понять, на какое значение смещается длина волны испускаемого света, рассмотрим небольшую часть мыльной пленки, как показано на рисунке 1.
Луч света из направления
Давайте предположим, что свет, поступающий вдоль
Теперь мы можем говорить о двух параллельных лучах света - один проходит от
Вы скорее всего помните, что свет распространяется медленнее в более плотной, чем воздух, среде, например, в мыльной воде. Поскольку свет движется медленнее, волна проходит большую часть своего периода, проходя через воду, нежели через эквивалентное расстояние по воздуху. Мы моделируем этот эффект, умножая пройденное расстояние на показатель преломления, увеличивающий длину до эквивалентного расстояния, которое волна преодолела бы в воздухе за то же время. Таким образом, эта часть разности фаз составляет
Упростим это выражение:
Отлично, мы почти закончили со скучными вычислениями и разобрались с первым членом разности фаз. Второй член - это оптический сдвиг фазы. Оказывается, когда свет переходит из одной среды в среду с более высоким показателем преломления (например, из воздуха в мыльную воду), он претерпевает фазовый сдвиг, равный половине длины его волны (интересно, что этого не происходит на обратном пути). Нам нужно добавить
Теперь у нас есть понимание, как должен вычисляться интерференционный свет в мыльной пленке. Для того, чтобы понять, как преобразовывать длину волны в RGB цвета, я советую ознакомиться с этой статьей.
Если вы все сделали правильно, должно получиться что-то подобное (рисунок 3).
Возможно кому-то раздел об интерференции показался скучным, и возникло желание получить похожий результат без особых усилий и размышлений, поэтому попробуем другой способ получить реалистичный результат с помощью случайности и броуновского движения.
Деформация или искажение цвета
Деформация или искажение цвета — очень распространенный метод в компьютерной графике для создания процедурных текстур и геометрии. Он часто используется, чтобы сжать объект, растянуть его, скрутить, согнуть, сделать его толще или применить любую деформацию, которую захотите . Это работает до тех пор, пока базовый цветовой узор или геометрия определяются как функция пространства. Мы с вами разберем очень частный случай деформации - искажение на основе шума или функцию шума. Такой способ используется с 1984 года, когда сам Кен Перлин создал свою первую процедурную текстуру мрамора.
Предположим, что у нас есть некоторая геометрия или изображение, определенное как функция расстояния (SDF). Для поверхности, как это было показано ранее, это будет функция вида
Эта техника действительно хорошая и позволяет формировать яблоки, здания, животных или любые другие вещи, которые вы можете себе представить. Далее будем работать с паттернами на основе
Само по себе дробное броуновское движение представляет собой простую сумму волн с увеличивающимися частотами и уменьшающимися амплитудами. Так как в GLSL нет функции для генерации случайных чисел, мы напишем свою:
const mat2 m = mat2( 0.80, 0.60, -0.60, 0.80 );
float noise( in vec2 p )
{
return sin(p.x)*sin(p.y);
}
float fbm( vec2 p )
{
float f = 0.0;
f += 0.500000*(0.5+0.5*noise( p )); p = m*p*2.02;
f += 0.250000*(0.5+0.5*noise( p )); p = m*p*2.03;
f += 0.125000*(0.5+0.5*noise( p )); p = m*p*2.01;
f += 0.062500*(0.5+0.5*noise( p )); p = m*p*2.04;
f += 0.031250*(0.5+0.5*noise( p )); p = m*p*2.01;
f += 0.015625*(0.5+0.5*noise( p ));
return f/0.96875;
}
Мне, например, понадобилось 15 минут на то, чтобы подобрать паттерн, искажающий цвета таким образом, чтобы это было похоже на интерференцию в мыльной пленке. Вы можете поиграть с параметрами так, чтобы результат был лучше:
float pattern( in vec2 p, out vec2 q, out vec2 r )
{
q.x = fbm( p + vec2(0.0,0.1 * iTime) );
q.y = fbm( p + vec2(5.2,1.3) );
r.x = fbm( p + 4.0*q + vec2(100.7 + 0.1 * iTime,91.2) );
r.y = fbm( p + 4.0*q + vec2(90.3,2.8 + 0.1 * iTime) );
return fbm(vec2(fbm( p * p + 10.0*r + fbm(5.0 * p + q)), fbm(100.0 * p * p * q)));
}
В итоге мы с вами получили достаточно реалистичный рендер мыльного пузыря без серьезных размышлений (рисунок 5).
Antialiasing
В трассировке лучей достаточно часто возникает такая проблема, как aliasing. Заключается она в эффекте ступенчатости, который проявляется на границах поверхностей. Эффект возникает из-за недостаточной точности расчета цвета пикселя по одному лучу. Если присмотреться к границе пузыря на рисунке 6, то мы увидим лесенку:
Чтобы избавиться от этой проблемы, мы будем выпускать по 4 луча на каждый пиксель с небольшим смещением и для каждого луча получать цвет объекта. После этого мы усредним цвет в пикселе.
<...>
vec3 rayDir1 = getRayDirection(uv, cameraPos, lookAt);
vec3 rayDir2 = getRayDirection(uv, cameraPos, lookAt + vec3(0.01, 0.0, 0.0));
vec3 rayDir3 = getRayDirection(uv, cameraPos, lookAt + vec3(0.0, 0.01, 0.0));
vec3 rayDir4 = getRayDirection(uv, cameraPos, lookAt + vec3(0.0, 0.0, 0.01));
vec4 col1 = render(cameraPos, rayDir1);
vec4 col2 = render(cameraPos, rayDir2);
vec4 col3 = render(cameraPos, rayDir3);
vec4 col4 = render(cameraPos, rayDir4);
vec4 col = 0.25 * (col1 + col2 + col3 + col4);
// Output to screen
fragColor = col;
<...>
На рисунке 6 можно заметить, что граница стала плавнее, но FPS упал в 4 раза, т.к. сейчас нам приходится рендерить сцену 4 раза для получения одного кадра. Метод достаточно затратный, но эта красота того стоит!
На этом вторая часть статьи про pbr rendering подошла к концу. Благодарю всех, кто остался со мной до конца, оставляйте свои вопросы в комментариях, буду рад ответить.
Исходный код шейдера доступен по ссылке.