steps3D - Tutorials - Physically Based Rendering

Physically Based Rendering (PBR)

В большинстве современных игр (да и не только игр) уже давно перешли к использованию так называемого рендеринга, основанного на физике или PBR (Physically Based Rendering). И в этой статье я хочу рассказать о том, что такое PBR и в чем его основные преимущества. Кроме того мы рассмотрим и его реализацию на GLSL.

Как уже ясно аббревиатура PBR означает Physically Based Rendering, т.е. рендеринг основанный на достаточно точном соблюдении основных физических законов. Но при этом с другой стороны PBR подразумевает определенную унификацию процесса подготовки материалов для моделей, дающую гарантированные и ожидаемые результаты.

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

Обратите внимание что из этой формулы не следует, что fr<=1 так как имеет место умножение на косинус угла падения. Кроме того, для ряда таких распространенных моделей как Блинна-Фонга и Фонга закон сохранения энергии не выполнен - требуется специальная нормализация.

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

Кроме этих двух физических законов PBR использует так называемую микрофасетную модель поверхности. Она позволяет аккуратно описать почему луч, падающий на поверхность, дает при отражении целый пучок лучей (см. рис. 1 ).

Рис 1. Неровная поверхность.

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

Рис 2. Микрорельеф поверхности.

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

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

Со статистической точки зрения и вектор h и угол между векторами h и n являются случайными величинами с некоторым законом распределения. Обычно задается закон (точнее плотность распределения) для угла между этими векторами. Существует несколько моделей для плотности распределения D, некоторые из которых мы далее рассмотрим.

Данная функция D обычно зависит также и от некоторого параметра m, называемого неровностью поверхности. Чем больше значение этого параметра (обычно он принимает значения на отрезке [0,1]) тем более неровной является поверхность (рис. 3).

Рис 3. Неровность поверхности.

Поскольку D это плотность распределения, то для нее должно быть выполнено условия нормирорвки:

Обычно PBR-модели освещения строятся на основе модели Кука-Торранса (Cook-Torrance). Согласно этой модели поверхность состоит из микрограней, являющихся идеальными микрозеркалами. Это значит, что луч, падающий на такую микрогрань, претерпевает идеальное отражение. Но так как имеет место вероятностное распределение ориентации микрограней, то вместо одного отраженного луча мы получаем целый набор лучей, как показано на рис. 1

Давайте теперь посмотрим от чего будет зависеть видимая из направления вектора v освещенность точки P. В первую очередь от функции плотности распределения D, отвечающая за вероятность того, что микрогрань будет ориентирована таким образом, что свет отразится точно в направлении вектора v. Кроме того при идеальном отражении доля отраженного света (световой энергии) задается коэффициентом Френеля (Fresnel) F (соответственно доля преломленного света будет равно 1-F).

Но на самом деле коэффициент Френеля зависит не только от угла падения, но и от длины волны. Обычно для задания коэффициента Френеля используется приближение Шлика (Schlick), которое разделяет зависимость от угла падения от зависимости от длины волны. В этом приближении коэффициент Френеля F выражается через угол падения и коэффициент при нулевом угле падения (F(0)=F0, случай перпендикулярного падения).

Рис 4. F0 для различных материалов.

Рис 5. Коэффициент Френеля для сферы (через приближение Шлика)

На самом деле коэффициент F0 для неметаллов можно выразить через коэффициент преломления по следующей формуле

На рис. 4 представлена зависимость F0 от длины волны для различных распространенных материалов.

Обратите внимание, что по формуле Шлика . Для металлов F0 обычно достаточно велик, а вот для диэлектриков F0 <<1. Обычно считают для диэлектриков F0 равен 0.04.

Кроме F и D есть еще одна функция, оказывающая заметное влияние на итоговое освещение в точке P. Дело в том, что когда наша поверхность имеет некоторый микрорельеф, он он начинает оказывать затеняющее значение на свет. Причем это затенение может выступать в двух видах - микрорельфе затеняет точку от падающего на нее света (shadowing, рис 6 справа) и когда микрорельеф затеняет отраженный поверхностью свет от наблюдателя (masking, рис 6 слева).

Рис 6. Затенение света микрорельефом поверхности.

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

Итоговая формула для освещения по модели Кука-Торранса описывается следующей формулой (обратите внимание, что она описывает лишь бликовую компоненту освещения):

Кроме бликовой части освещения, даваемой этой формулой, есть еще и дуффузная компонента. Давайте рассмотрим как она возникает.

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

При падении часть света (равная F) отражается. А вторая часть (равная 1-F) преломляется и уходит внутрь материала. Там этот свет претерпевает многочисленные переотражения от частиц материала и его часть выходит наружу (рис 7). Понятно что в этом случае направление выходящего на поверхность света никак не зависит от направления падающего. Вот эта выходящая часть света и образует диффузную составляющую.

Рис 7. Преломление света внутрь материала.

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

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

Но есть и материалы, которые являются исключением из этого правила. И в первую очередь это кожа человека. В таких материалах свет может пройти внутри материала значительное расстояние прежде чем выйдет наружу. И в этом случае имеет место так называемое подповерхностное рассеивание (subsurface scattering). И поэтому для правдоподобного рендеринга таких материалов необходимо учитывать подповерхностное рассеивание, но в этой статье мы будем его игнорировать.

Таким образом мы в результате приходим к следующей формуле для освещения с учетом как бликовой так и диффузной компонент.

В этой формуле через C обозначен цвет материала в точке P (как функция от длины волны). Обратите внимание на деление на пи - оно вытекает из условия сохранения энергии.

Давайте теперь рассмотрим различные варианты задания функций D и G. Классическим распределением для модели Кука-Торрасна является распределение Бекмена:

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

Параметр m в этом распределении отвечает за неровность поверхности - чем он больше, тем более неровной является поверхность.

Классическим вариантом для функции затенения G является следующая:

На рисунках 8 и 9 показано как выглядят эти функции на поверхности сферы.

Рис 8. Функция распределения Бекмена DBeckman

Довольно часто бликовый член в модели Кука-Торранса записывает в следующей форме:

Здесь через V обозначена так называемая видимость (visbility), задаваемая следующей формулой:

Рис 9. Классическая функция затенения G

Рис 10. Классический Кук-Торранс

Ниже приводится реализация приближения Шлика для F, распределения Бекмена DBeckman и геометрического затенения G на GLSL.

vec3 fresnel ( in vec3 f0, in float product )
{
    product = clamp ( product, 0.0, 1.0 );      // saturate
    
    return mix ( f0, vec3 (1.0), pow(1.0 - product, 5.0) );
}

float D_beckmann ( in float roughness, in float NdH )
{
    float m    = roughness * roughness;
    float m2   = m * m;
    float NdH2 = NdH * NdH;
    
    return exp( (NdH2 - 1.0) / (m2 * NdH2) ) / (pi * m2 * NdH2 * NdH2);
}

float G_default ( in float nl, in float nh, in float nv, in float vh )
{
    return min ( 1.0, min ( 2.0*nh*nv/vh, 2.0*nh*nl/vh ) );
}

На самом деле есть много других вариантов задания функций D и G и мы сейчас рассмотрим некоторые из них. Очень распространенным в последнее время в различных играх является распределение Троубриджа-Рейтца или GGX:

Рис 11. DGGX для различных значений неровности (0.1, 0.3, 0.5, 0.7, 0.9, 1.0).

Как уже отмечалось классическая формула Фонга не является физически-корректной, но есть распределение Фонга, которое можно использовать в PBR:

Есть формуля, выражающая степень в NDF Фонга через неровность поверхности:

Рис 12. DPhong для разных значений неровности (1, 3, 5, 10, 20).

Для функции затенения также есть несколько вариантов:

В ряде случаев функция затенения представляется как произведение двух функций, одна из которых зависит от l, а другая - от v.

Ниже приводятся реализации этих функций на GLSL.

float D_blinn(in float roughness, in float NdH)
{
    float m = roughness * roughness;
    float m2 = m * m;
    float n = 2.0 / m2 - 2.0;
    return (n + 2.0) / (2.0 * pi) * pow(NdH, n);
}

float D_beckmann ( in float roughness, in float NdH )
{
    float m    = roughness * roughness;
    float m2   = m * m;
    float NdH2 = NdH * NdH;
    
    return exp( (NdH2 - 1.0) / (m2 * NdH2) ) / (pi * m2 * NdH2 * NdH2);
}

float D_GGX ( in float roughness, in float NdH )
{
    float m  = roughness * roughness;
    float m2 = m * m;
    float NdH2 = NdH * NdH;
    float d  = (m2 - 1.0) * NdH2 + 1.0;
    
    return m2 / (pi * d * d);
}

float   D_Phong ( in float nh, in float n )
{
    return (n+2.0) * pow ( max ( 0, nh ), n ) / (2.0 * pi);
}

float G_schlick ( in float roughness, in float nv, in float nl )
{
    float k = roughness * roughness * 0.5;
    float V = nv * (1.0 - k) + k;
    float L = nl * (1.0 - k) + k;
    
    return 0.25 / (V * L);
}

float G_neumann ( in float nl, in float nv )
{
    return nl * nv / max ( nl, nv );
}

float G_klemen ( in float nl, in float nv, in float vh )
{
    return nl * nv / (vh * vh );
}

float G_default ( in float nl, in float nh, in float nv, in float vh )
{
    return min ( 1.0, min ( 2.0*nh*nv/vh, 2.0*nh*nl/vh ) );
}

    // Kelem-Szirmay-Kalos visibility
float   KelemVis ( in float nl, in float nv, in float a )
{
    return 1.0 / ((nl*(1.0-a) + a)*nv*(1.0-a) + a);
}

Рассмотренные выше модели рассматривают только изотропные материалы. Хотя большинство встречающихся в нашей жизни материалов и являются изотропными, но есть еще и анизотропные материалы. К ним в частности относятся полированный метал и волосы/мех.

Для рендеринга анизотропной поверхности нам уже не достаточно вектора нормали n, на будет нужен и касательный вектор t - тангент. Вектора нормали и касательный (вместе с вектором бинормали) образуют касательный базис TBN. Чтобы адаптировать PBR для рендеринга анизотропных материалов нам достаточно просто использовать функцию распределения D с поддержкой анизотропности. Часто используется анизотропный вариант функции DGGX, обратите внимание, что у нас неровность поверхности задается сразу двумя коэффициентами (а не одним как раньше).

На следующем рисунке приведен результат использования этой функции.

Рис. 13. Пример анизотропной поверхности.

Иногда для задания анизотропной поверхности вместе параметров ax и ay используется другой подход. В ней также используются два параметра - общая неровность a и анизотропия. Тогда коэффициенты ax и ay рассчитываются по следующим формулам:

Рис. 14. Примеры сферы с неровностью 0.25 и изменяющейся анизотропностью - 0, 0.3, 0.6 и 0.9.

Кроме собственно расчета освещенности обычно PBR подразумевает и определенный набор свойств, описывающий материал в каждой точке поверхности. Эти свойства обычно задаются в виде текстур и в PBR обычно используют один из двух подходов для задания свойства материала - metal-roughness и specular-glossiness.

Мы далее будем рассматривать только первый из них, при котором мы для каждой точки поверхности задаем неровность a, цвет C и металличность m. Принимающий значения на отрезке [0,1] параметр a задает неровность поверхности. Параметр m также принимает значения от 0 до 1. Значению 0 соответствует диэлектрик, а значению 1 соответствует металл. Промежуточные значения для этого параметра допустимы и могут применяться например для случая диэлектрика, содержащего металлическую пыль.

Интерпретация цвета C зависит от металличности. Есть материал это металл, то у него нет диффузной составляющей и тогда C задает F0. В противном случае (материал является диэлектриком) мы принимаем F0=0.04. В общем случае используется следующая формула:

К текстурам, задающим цвет C, неровность a и металличность m обычно добавляют текстуру с картой нормалей (значения нормали в касательном пространстве). Иногда также используется еще одна дополнительная текстура, задающая так называемую фоновую затененность (ambient occlusion). Она задает как неровности, заданные картой нормалей, затеняют грань (т.е. не на уровне микрограней).

Рис. 15. Набор текстур для PBR

Ниже приводится код фрагментного шейдера на GLSL для PBR.

#version 330 core

in  vec2 tx;
in  vec3 l;
in  vec3 h;
in  vec3 v;
in  vec3 t;
in  vec3 b;
in  vec3 n;
out vec4 color;     // output value

uniform sampler2D albedoMap;
uniform sampler2D metallnessMap;
uniform sampler2D normalMap;
uniform sampler2D roughnessMap;

const vec3  lightColor = vec3 ( 1.0 );
const float gamma = 2.2;
const float pi    = 3.1415926;
const float FDiel = 0.04;       // Fresnel for dielectrics

vec3 fresnel ( in vec3 f0, in float product )
{
    product = clamp ( product, 0.0, 1.0 );      // saturate
    
    return mix ( f0, vec3 (1.0), pow(1.0 - product, 5.0) );
}

float D_blinn(in float roughness, in float NdH)
{
    float m = roughness * roughness;
    float m2 = m * m;
    float n = 2.0 / m2 - 2.0;
    return (n + 2.0) / (2.0 * pi) * pow(NdH, n);
}

float D_beckmann ( in float roughness, in float NdH )
{
    float m    = roughness * roughness;
    float m2   = m * m;
    float NdH2 = NdH * NdH;
    
    return exp( (NdH2 - 1.0) / (m2 * NdH2) ) / (pi * m2 * NdH2 * NdH2);
}

float D_GGX ( in float roughness, in float NdH )
{
    float m  = roughness * roughness;
    float m2 = m * m;
    float NdH2 = NdH * NdH;
    float d  = (m2 - 1.0) * NdH2 + 1.0;
    
    return m2 / (pi * d * d);
}

float G_schlick ( in float roughness, in float nv, in float nl )
{
    float k = roughness * roughness * 0.5;
    float V = nv * (1.0 - k) + k;
    float L = nl * (1.0 - k) + k;
    
     return 0.25 / (V * L);
}

float G_neumann ( in float nl, in float nv )
{
    return nl * nv / max ( nl, nv );
}

float G_klemen ( in float nl, in float nv, in float vh )
{
    return nl * nv / (vh * vh );
}

float G_default ( in float nl, in float nh, in float nv, in float vh )
{
    return min ( 1.0, min ( 2.0*nh*nv/vh, 2.0*nh*nl/vh ) );
}

vec3 cookTorrance ( in float nl, in float nv, in float nh, in float vh, in vec3 f0, in float roughness )
{
    float D = D_GGX     ( roughness, nh );
    float G = G_neumann ( nl, nv );

    return f0 * D * G;
}

void main ()
{
    vec3 base        = texture ( albedoMap,     tx).xyz;
    vec3 n           = texture ( normalMap,     tx).xyz * 2.0 - vec3 ( 1.0 );
    float roughness  = texture ( roughnessMap,  tx).x;
    float metallness = texture ( metallnessMap, tx ).x;

    base = pow ( base, vec3 ( gamma ) );
    
    vec3  n2   = normalize ( n );
    vec3  l2   = normalize ( l );
    vec3  h2   = normalize ( h );
    vec3  v2   = normalize ( v ); 
    float nv   = max ( 0.0, dot ( n2, v2 ));
    float nl   = max ( 0.0, dot ( n2, l2 ));
    float nh   = max ( 0.0, dot ( n2, h2 ));
    float hl   = max ( 0.0, dot ( h2, l2 ));
    float vh   = max ( 0.0, dot ( h2, v2 ));
    
    vec3 F0          = mix ( vec3(FDiel), base, metallness );
    vec3 specFresnel = fresnel ( F0, nv );  // hv
    vec3 spec        = cookTorrance ( nl, nv, nh, vh, specFresnel, roughness ) * nl / max ( 0.001, 4.0 * nl * nv );
    vec3 diff        = (vec3(1.0) - specFresnel) * nl / pi;

    color = pow ( vec4 ( ( diff * mix ( base, vec3(0.0), metallness) + spec ) * lightColor, 1.0 ), vec4 ( 1.0 / gamma ) );
}

Обратите внимание, что для получения корректного изображения очень важно при использовании PBR правильно применять гамма-коррекцию. Это значит, что текстура с цветом C считается содержащей значения в гамма-пространстве. А весь расчет освещенности мы должны всегда проводить в линейном пространстве. Поэтому мы прочитанный из текстуры цвет необходимо преобразовать в линейное пространство перед использованием его в расчете освещения. Точно также перед выводом рассчитанного освещения необходимо преобразовать его гамма-пространство.

Рис. 16. Пример изображения построенного без учета гамма-коррекции

Рис. 16. Пример изображения построенного с учетом гамма-коррекции

Рис 17. Изменение параметров неровности и металличности

Ссылки по PBR:

Learn OpenGL. Урок 6.1. PBR или Физически-корректный рендеринг. Теория

Готовим Physically Based Rendering + Image-based Lighting. Теория+практика. Шаг за шагом

Physically Based Rendering in Filament

The PBR Guide - Part 1

The PBR Guide - Part 2

Specular BRDF Reference

Хорошие PBR-текстуры можно найти на ambientcg.com.

Пример PBR-материала с ambientcg.com