본문 바로가기

Game/Graphics

Learn OpenGL - PBR : IBL(Specular IBL)

link : https://learnopengl.com/PBR/IBL/Specular-IBL


Specular IBL


 앞의 튜토리얼에서는 조명의 간접 확산 부분인 일루미네이션 맵을 사전 계산해 이미지 기반 조명과 함께 PBR을 설정했다.


이 튜토리얼에서는 반사율 방정식의 반사 부분에 초점을 맞춘다:



 당신은 Cook-Torrance 반사 부분(kS를 곱한값)이 적분에 대해 일정하지 않고, 들어오는 빛의 방향뿐만 아니라 들어오는 시야 방향에도


의존한다는 것을 알 수 있다. 모든 가능한 뷰 방향을 포함해 모든 들어오는 빛의 방향에 대한 적분을 풀려고하는 것은 조합 과부하이며


실시간으로 계산하기에는 너무 비싸다. 에픽 게임즈(Epic Games)는 분할합 근사로 알려진 몇 가지 타협을 감안할 때 실시간 목적으로


반사 부분을 사전 convolute 할 수 있는 솔루션을 제안했다.



 분할 합계 근사는 반사율 방정식의 반사 부분을 2개의 개별 부분으로 분할해 우리가 개별적으로 컨볼스하고 나중에 반사경 이미지 기반


조명을 위한 PBR 쉐이더에서 결합 할 수 있다. 우리가 방사도 맵을 사전 커버트하는 것과 유사하게, 분할 합계 근사는 컨볼루션 입력으로


HDR 환경 맵을 필요로 한다. 분할 합계 근사를 이해하기 위해 반사율 방정식을 다시 살펴보겠다. 그러나 이번에는 specular 부분에만


초점을 맞춘다. (이전 튜토리얼에서 확산 부분을 추출했다):




 irradiance convolution과 같은 (성능) 이유로, 우리는 실시간으로 integral의 specular 부분을 풀 수 없고, 합리적인 성능을 기대할 수없다.


그래서 우리는 이 적분을 미리 계산해 specular IBL 맵과 같은 것을 얻고, 이 맵을 조각의 정상으로 샘플링 한 다음 이를 수행할 것이다.


그러나 이것은 약간 까다로워진다. 적분은 오직 ωi에 의존하고, 우리는 적분에서 일정한 확산 albedo 항을 움직일 수 있으므로


방사도 맵을 미리 계산할 수 있었다. 이번에, 적분은 BRDF에서 명백한 ωi 이상에 달려있다:



 이번에는 적분 또한 wo에 달려 있으며 두 방향 벡터로 미리 계산된 큐브 맵을 실제로 샘플링 할 수는 없다. 위치 p는 앞의 튜토리얼에서


설명한 것과는 관련이 없다. ωi와 ωo의 모든 가능한 조합에 대해 이 적분을 사전 계산하는 것은 실시간 설정에서는 실용적이지 않다.



 Epic Games의 분할 합 근사법은 사전 계산을 2개의 개별 파트로 나눠서 문제를 해결한다. 나중에 이를 결합해 결과로 얻은 결과를 얻는다.


분할 합 근사는 specular 적분을 두 개의 개별 적분으로 나눈다:



 첫 부분(뒤얽힌경우)은 미리 계산된 환경 convolution 맵(irradiance맵과유사)인 사전 필터링된 환경 맵으로 알려져 있지만,


이번에는 거칠기를 고려해야한다. 거칠기 수준이 증가하면 환경 맵에 흩어져있는 샘플 벡터가 뒤얽혀 반사가 더 흐려진다.


우리가 convolute 각 조도 수준에 대해, 우리는 미리 필터링된 지도의 밉맵 수준에서 순차적으로 blurrier 결과를 저장한다.


예를 들어, 5개의 밉맵 레벨에서 5개의 상이한 조도 값의 pre-convoluted 결과를 저장하는 사전 필터링된 환경 맵은 다음과 같다:


Pre-convoluted environment map over 5 roughness levels for PBR


 우리는 표준 및 시야 방향 모두를 입력으로 사용하는 Cook-Torrance BRDF의 정규 분포 함수(NDF)를 사용해 샘플 벡터와 산란 강도를 생성한다.


Epic Games는 환경 맵을 회선할 때 뷰 방향을 미리 알지 못하기 때문에 뷰 방향이 출력 샘플 방향 ωo와 항상 같다고 가정해 더 근사치를 만든다.


이것은 다음 코드로 변환된다:

vec3 N = normalize(w_o);
vec3 R = N;
vec3 V = R;

 이러한 방식으로 사전 필터링된 환경 회선은 뷰 방향을 인식 할 필요가 없다. 이것은 아래 이미지에서 볼 수 있는 각도에서 반사된 표면 반사를


볼 때 멋진 방목 반사(specular reflections)를 얻지 못한다는 것을 의미한다. 그러나 이것은 일반적으로 적절한 절충안으로 간주된다:


Removing grazing specular reflections with the split sum approximation of V = R = N.


 방정식의 두 번째 부분은 거울 정수의 BRDF 부분과 동일하다. 들어오는 빛이 모든 방향에 대해 완전히 흰색인 것으로 가정하면, 입력 거칠기와


정상 n과 빛의 방향 ωi 또는 n⋅ωi 와 n 사이의 입력 각을 고려해 BRDF의 응답을 미리 계산할 수 있다.


Epic Games는 BRDF 통합 맵이라고 하는 2D 룩업 텍스처(LUT)의 다양한 조도 값에 대한 각 정상 및 조명 방향 조합에 대한 사전 계산된 BRDF의


응답을 저장한다. 2D 룩업 텍스처는 표면의 Fresnel 응답에 스케일과 바이어스 값을 출력해 분할된 specular 적분의 두 번째 부분을 제공한다:


Visualization of the 2D BRDF LUT according to the split sum approximation for PBR in OpenGL.


 우리는 평면의 수평 텍스처 좌표를 BRDF의 입력 n⋅ωi 및 해당 입력 텍스처의 수직 텍스처 좌표를 입력 조도 값으로 처리해 검색 텍스처를 생성한다.


이 BRDF 통합 맵과 사전 필터링된 환경 맵을 사용해 모두를 결합해 반사 적분의 결과를 얻을 수 있다:

float lod             = getMipLevelFromRoughness(roughness);
vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod);
vec2 envBRDF          = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy;
vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y) 

 Epic Games의 분할 합 근사가 반사 방정식의 간접적인 반사 부분에 대략 어떻게 접근하는지에 대한 약간의 개요를 제공해야한다.


이제 미리 복잡한 구성 부분을 직접 만들어 보겠다.


 

Pre-filtering an HDR environment map


 환경 맵을 사전 필터링하면 방사 맵을 복잡하게 만드는 방법과 매우 유사하다. 다른 점은 거칠기를 고려하고 사전 필터링된 지도의 밉 레벨에서


순조로운 반사를 순차적으로 저장한다는 것이다.



 먼저 사전 필터링된 환경 맵 데이터를 보관할 새 큐브 맵을 생성해야한다. 밉 레벨에 충분한 메모리를 할당하기 위해 필요한 양의 메모리를


할당하는 쉬운 방법으로 glGenerateMipmap을 호출한다.

unsigned int prefilterMap;
glGenTextures(1, &prefilterMap);
glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap);
for (unsigned int i = 0; i < 6; ++i)
{
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 128, 128, 0, GL_RGB, GL_FLOAT, nullptr);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); 
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

 prefilter를 샘플링 할 계획이기 때문에 밉맵을 맵핑하기 위해서는 축소 필터가 GL_LINEAR_MIPMAP_LINEAR로 설정되어 있는지 확인해 삼선형


필터링을 사용 가능하게 해야한다. 사전 필터링된 정반사를 기본 밉 레벨에서 128x128 픽셀의 면당 해상도로 저장한다.


대부분의 반사에는 충분할 것으로 보이지만 매끄러운 소재가 많으면 해상도를 높이는 것이 좋다.



 이전 튜토리얼에서 구형 좌표를 사용해 반구 Ω에 균일하게 분포된 샘플 벡터를 생성해 환경 맵을 복잡하게 만들었다. 방사 조도에 대해서는


정상적으로 작동하지만 정반사에 대해서는 효율적이지 않다. 표면 거칠기를 기반으로 한 정반사에 관해서는, 빛의 반사 벡터 주위에서


법선 n을 통해 밀접하게 또는 대략적으로 반사되지만 그럼에도 불구하고 반사 벡터 주위에서 반사한다:


Specular lobe according to the PBR microfacet surface model.


 간읗나 출광 광 반사의 일반적인 모양은 반사 로브(specular lobe)로 알려져있다. 거칠기가 증가함에 따라, specular lobe의 크기가 증가한다.


들어오는 빛 방향의 변화에 따라 반사 로브(specular lobe)의 모양이 변한다. 거울 모양 로브의 모양은 재료에 크게 의존한다.



 마이크로 표면 모델에 관해서는, 들어오는 빛의 방향이 주어지면 미세면 중도 벡터에 대한 반사 방향으로 거울상 로브를 상상할 수 있다.


대부분의 광선이 미세 도파로 중간 벡터 주위로 반사된 반사 로브에서 끝나는 것을 보는 것은 비슷한 방식으로 샘플 벡터를 생성하는 것이


가장 효과적이다. 이 프로세스를 중요도 샘플링이라고한다.



Monte Carlo integration and importance sampling


 중요도 샘플링에 대한 이해를 완전히 얻으려면 우선 몬테카를로 통합으로 알려진 수학적 구조를 탐구하는 것이 중요하다.


콘테 카를로 통합은 주로 통계와 확률 이론의 결합을 중심으로 이루어진다. Monte Carlo는 모집단 전체를 고려하지 않고, 인구의 통계 또는


가치를 파악하는 문제를 이산적으로 해결하는데 도움이 된다.



 예를 들어, 한 국가의 모든 시민의 평균 신장을 계산하려고 한다고 가정해보자. 결과를 얻으려면 모든 시민을 측정하고 높이를 평균해 원하는


답을 얻을 수 있다. 그러나 대부분의 국가는 인구가 많기 때문에 현실적인 접근 방식이 아니다. 너무 많은 노력과 시간이 필요하다.



 다른 접근법은 이 집단의 훨씬 더 작은 무작위 부분 집합을 선택하고(편향되지 않은), 높이를 측정해서 그 결과를 평균하는 것이다.


이 인구는 100명 정도 될 수 있다. 정확한 답변만큰 정확하지는 않지만, 진실과 비교적 가까운 답변을 얻을 수 있다. 이것은 많은 수의 법칙으로


알려져 있다. 전체 인구에서 무작위로 추출한 샘플 중 작은 크기의 N세트를 측정하면 결과는 실제 답변에 상대적으로 가까워지고


샘플 N의 수가 증가함에 따라 가까워진다.



 몬테 카를로 통합은 많은 수의 법칙을 토대로 하며 통합을 푸는 데에도 동일한 접근법을 취한다. 가능한 모든 샘플 값 x에 대한 적분 값을 푸는


것보다는 단순히 전체 인구 및 평균에서 무작위로 추출한 N개의 샘플 값을 생성해라. N이 증가함에 따라 우리는 적분의 정확한 답에 더 가깝게


결과를 얻을 수 있다:



 

 정수를 풀기 위해 우리는 모집단 a에서 b까지 N개의 무작위 표본을 취해 이를 합산하여 총 평균 표본수로 나눈다.


pdf는 특정 샘플이 전체 샘플 세트에서 발생할 확률을 알려주는 확률 밀도 함수를 나타낸다. 예를 들어, 인구 높이의 pdf는 다음과 같이 보인다:


Example PDF (probability distribution function).


 이 그래프에서 모집단의 무작위 표본을 취하면 표본의 높이가 1.5일 가능성이 낮은 것과 비교해 높이가 1.7인 표본을 선택할 확률이


높아지는 것을 알 수 있다.



 몬테 카를로 통합과 관련해 일부 샘플은 다른 샘플보다 생성될 확률이 더 높을 수 있다. 모든 일반 몬테카를로 추정에 대해 샘플 값을


pdf에 따라 샘플 확률로 나누거나 곱한다. 지금까지 적분을 산정한 각각의 사례에서 우리가 생성한 샘플은 똑같은 기회를 가지면서


균일했다. 지금까지의 견적은 편파적이었다. 즉, 점점 더 많은 양의 시료가 주어진다면 결국에는 적분의 정확한 해답으로 수렴할 것이다.



 그러나 일부 몬테카를로 추정량은 편향되어있어 생성된 샘플이 완전히 무작위는 아니지만 특정 값이나 방향으로 집중된다.


이렇나 편향된 몬테카를로 추정치는 수렴 속도가 빠르므로 정확한 솔루션으로 훨씬 더 빠른 속도로 수렴할 수 있지만 편향된 특성으로 인해


정확한 솔루션으로 수렴하지는 않을 것이다. 결과가 시각적으로 수용될 수 있는한 정확한 해결책이 중요하지 않기 때문에 일반적으로


컴퓨터 그래픽에서 특히 그렇다. 우리는 곧 중요도 샘플링을 통해 볼 수 있듯이 생성된 샘플은 특정 방향으로 편향된다.


이 경우 각 샘플을 해당 pdf로 곱하거나 나눠서 계산한다.



 몬테 카를로 통합은 연속적인 적분을 이산적이고 효율적인 방식으로 근사하는 매우 직관적인 방식이므로 컴퓨터 그래픽에서 널리 보급된다:


(반구 Ω처럼) 샘플링할 영역/볼륨을 취하고, 그 영역 내에 N개의 무작위 샘플을 생성하고 합계해 최종 결과에 대한 모든 샘플 기여도를 측정한다.



 몬테 카를로 통합은 방대한 수학적 주제이며 자세한 내용을 다루지는 않겠지만 무작위 샘플을 생성하는 여러 가지 방법이 있음을 언급할 것이다.


기본적으로 각 샘플은 이전처럼 오나전히 무작위이지만 semi-random sequence의 특정 속성을 사용해 여전히 임의이지만 흥미로운 속성을


갖는 샘플 벡터를 생성할 수 있따. 예를 들어, 무작위 샘플을 생성하는 낮은 불일치 시퀀스라고 불리는 것에 몬테 카를로 통합을 할 수 있지만


각 샘플은 보다 균등하게 분산된다:


Low discrepancy sequence.


 몬테 카를로 샘플 벡터를 생성하기 위해 낮은 불일치 시퀀스를 사용할 때 프로세스는 준 몬테 카를로 통합으로 알려져있다.


Quasi-Monte Carlo 방법은 빠른 속도로 수렴 속도가 높아 성능이 중요한 응용 프로그램에 유용하다.



 Monte Carlo와 Quasi-Monte Carlo 통합에 대해 새로이 습득한 지식을 감안할 때 중요도 샘플리으로 알려진 보다 빠른 수렴 속도에


사용할 수 있는 흥미로운 속성이 있다. 이 튜토리얼에서 앞서 언급했지만 빛의 경면 반사에 관해서는 반사광 벡터가 표면의 거칠기에 의해


결정되는 크기를 갖는 반사 로브에 구속된다. 반사 로브 외부의 임의의 무작위로 생성된 샘플이 specular integral과 관련이 없단든 것을


알게되면 Monte Carlo 추정기가 편향된 대가로 샘플 생성을 반사 로브에 집중시키는 것이 이치에 맞다.



 이것은 본질적으로 중요도 샘플링이 무엇에 관한 것인가이다:


미세 영역의 중도 벡터 주위에 거친 방향으로 구속된 일부 영역에서 샘플 벡터를 생성한다. Quasi-Monte Carlo 샘플링과 낮은 불일치 시퀀스를


결합하고, 중요도 샘플링을 사용해 샘플 벡터를 바이어싱함으로써 우리는 높은 수렴 속도를 얻는다. 우리가 더 빠른 속도로 솔루션에 도달하기


때문에, 우리는 충분한 근사치에 도달하기 위해 샘플이 더 적게 필요하다. 이 때문에 이 조합을 사용하면 그래픽 응용 프로그램이 결과를


사전 계산하는 것보다 훨씬 느려지긴 하지만 실시간으로 반사 적분을 해결할 수도 있다.



A low-discrepancy sequence


 이 튜토리얼에서는 Quasi-Monte Carlo 방법을 기반으로 하는 임의의 낮을 불일치 시퀀스를 고려한 중요도 샘플링을 사용해 간접 반사율


방정식의 반사 부분을 미리 계산한다. 우리가 사용한 시퀀스는 holger Dammertz가 자세히 설명한 Hammersley Sequence로 알려져 있다.


Hammersley 시퀀스는 소수점 주위의 10진수 2진 표현을 반영하는 Van Der Corpus 시퀀스를 기반으로 한다.



 몇 가지 깔끔한 비트 트릭을 감안할 때 N 총 샘플보다 Hammersley 시퀀스 샘플을 얻는데 사용할 쉐이더 프로그램에서 Van Der Corpus 시퀀스를


매우 효율적으로 생성할 수 있다:

float RadicalInverse_VdC(uint bits) 
{
    bits = (bits << 16u) | (bits >> 16u);
    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
    return float(bits) * 2.3283064365386963e-10; // / 0x100000000
}
// ----------------------------------------------------------------------------
vec2 Hammersley(uint i, uint N)
{
    return vec2(float(i)/float(N), RadicalInverse_VdC(i));
}  

 GLSL Hammersley 함수는 크기 N의 전체 샘플 세트 중 낮은 불일치 샘플 i를 제공한다.

Hammersley sequence without bit operator support

 모든 OpenGL 관련 드라이버가 비트 연산자를 지원하는 것은 아니다. 이 경우 비트 연산자에 의존하지 않는 Van Der Corpus Sequence의 다른 버전을 사용할 수 있다.


float VanDerCorpus(uint n, uint base)
{
    float invBase = 1.0 / float(base);
    float denom   = 1.0;
    float result  = 0.0;

    for(uint i = 0u; i < 32u; ++i)
    {
        if(n > 0u)
        {
            denom   = mod(float(n), 2.0);
            result += denom * invBase;
            invBase = invBase / 2.0;
            n       = uint(float(n) / 2.0);
        }
    }

    return result;
}
// ----------------------------------------------------------------------------
vec2 HammersleyNoBitOps(uint i, uint N)
{
    return vec2(float(i)/float(N), VanDerCorpus(i, 2u));
}

구형 하드웨어의 GLSL 루프 제한으로 인해 시퀀스는 가능한 모든 32비트를 반복한다. 이 버전은 성능이 떨어지지만 비트 운영자가 없어도 모든 하드웨어에서 작동한다.



GGX Importance sampling


 정수의 반구 Ω에 대해 샘플 벡터를 생성하는 균일하게 또는 무작위로 대신에 우리는 표면의 거칠기를 기반으로 한 마이크로 표면 중간 벡터의


일반 반사 방향으로 편향된 샘플 벡터를 생성한다. 샘플링 과정은 이전에 보았던 것과 유사하다. 큰 루프를 시작하고, 랜덤 시퀀스 값을 생성하고,


시퀀스 값을 가져서 접선 공간에서 샘플 벡터를 생성하고, 월드 공간으로 변환하고 장면의 광도를 샘플링한다. 다른 점은 샘플 벡터를 생성하기


위해 적은 불일치 시퀀스 값을 입력으로 사용한다는 것이다:

const uint SAMPLE_COUNT = 4096u;
for(uint i = 0u; i < SAMPLE_COUNT; ++i)
{
    vec2 Xi = Hammersley(i, SAMPLE_COUNT);  

 또한, 샘플 벡터를 만들기 위해서는 샘플 벡터를 표면 거칠기의 반사 로브 방향으로 편향시키는 몇 가지 방법이 필요하다. Theory 튜토리얼에서


설명한대로 NDF를 가져와서 Epic Games에서 설명한 구형 샘플 벡터 프로세스에서 GGX NDF를 결합 할 수 있다:

vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness)
{
    float a = roughness*roughness;
	
    float phi = 2.0 * PI * Xi.x;
    float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
    float sinTheta = sqrt(1.0 - cosTheta*cosTheta);
	
    // from spherical coordinates to cartesian coordinates
    vec3 H;
    H.x = cos(phi) * sinTheta;
    H.y = sin(phi) * sinTheta;
    H.z = cosTheta;
	
    // from tangent-space vector to world-space sample vector
    vec3 up        = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
    vec3 tangent   = normalize(cross(up, N));
    vec3 bitangent = cross(N, tangent);
	
    vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
    return normalize(sampleVec);
}  

 이것은 우리에게 약간의 입력 거칠기와 낮은 불일치 시퀀스 값 Xi를 기반으로 예상된 마이크로 표면의 중간 벡터를 중심으로 다소 향한 샘플


벡터를 제공한다. Epic Games는 Disney의 원래 PBR 조사를 기반으로 더 나은 시각적 결과를 위해 제곱된 조도를 사용한다.



 low-discrepancy(낮은-불일치) Hammersley 시퀀스와 샘플 생성이 정의 됨으로써 pre-filter convolution 쉐이더를 완성 할 수 있다:

#version 330 core
out vec4 FragColor;
in vec3 localPos;

uniform samplerCube environmentMap;
uniform float roughness;

const float PI = 3.14159265359;

float RadicalInverse_VdC(uint bits);
vec2 Hammersley(uint i, uint N);
vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness);
  
void main()
{		
    vec3 N = normalize(localPos);    
    vec3 R = N;
    vec3 V = R;

    const uint SAMPLE_COUNT = 1024u;
    float totalWeight = 0.0;   
    vec3 prefilteredColor = vec3(0.0);     
    for(uint i = 0u; i < SAMPLE_COUNT; ++i)
    {
        vec2 Xi = Hammersley(i, SAMPLE_COUNT);
        vec3 H  = ImportanceSampleGGX(Xi, N, roughness);
        vec3 L  = normalize(2.0 * dot(V, H) * H - V);

        float NdotL = max(dot(N, L), 0.0);
        if(NdotL > 0.0)
        {
            prefilteredColor += texture(environmentMap, L).rgb * NdotL;
            totalWeight      += NdotL;
        }
    }
    prefilteredColor = prefilteredColor / totalWeight;

    FragColor = vec4(prefilteredColor, 1.0);
}  

 pre-filter 큐브 맵의 각 밉맵 레벨에 따라 달라지는 입력 거칠기를 기반으로 환경을 사전 필터링하고 prefilteredColor에 결과를 저장한다.


최종 prefilteredColor는 전체 샘플 가중치로 나누어진다. 여기서 최종 결과에 영향이 적은 샘플이 최종 가중치에 덜 기여한다.



Capturing pre-filter mipmap levels


 이제 남은 일은 OpenGL에서 여러 밉맵 레벨에 대해 서로 다른 거칠기 값으로 환경 맵을 사전 필터링하도록 하는 것이다.


이것은 실제로 방사 조명사의 원래 설정으로 수행하는 것이 매우 쉽다:

prefilterShader.use();
prefilterShader.setInt("environmentMap", 0);
prefilterShader.setMat4("projection", captureProjection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);

glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
unsigned int maxMipLevels = 5;
for (unsigned int mip = 0; mip < maxMipLevels; ++mip)
{
    // reisze framebuffer according to mip-level size.
    unsigned int mipWidth  = 128 * std::pow(0.5, mip);
    unsigned int mipHeight = 128 * std::pow(0.5, mip);
    glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight);
    glViewport(0, 0, mipWidth, mipHeight);

    float roughness = (float)mip / (float)(maxMipLevels - 1);
    prefilterShader.setFloat("roughness", roughness);
    for (unsigned int i = 0; i < 6; ++i)
    {
        prefilterShader.setMat4("view", captureViews[i]);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
                               GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, prefilterMap, mip);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        renderCube();
    }
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);   

 이 과정은 방사 조도맵 회선과 비슷하지만, 이번에는 프레임 버퍼의 크기를 적절한 밉맵 스케일로 확장한다. 각 밉 레벨은 크기를 2로 줄인다.


또한, glFramebuffereTexture2D의 마지막 매개 변수에서 렌더링 할 밉 레벨을 지정하고, 사전 필터링된 거칠기를 사전 필터 쉐이더에 전달한다.



 이렇게하면 우리가 적절하게 사전 필터링된 환경 맵을 얻을 수 있게 되어, 우리가 액세스하는 더 높은 밉 레벨에서 흐릿한 반사를 반환한다.


skybox shader에 사전 필터링된 환경 큐브 맵을 표시하고, 쉐이더의 첫 번째 밉 레벨보다 약간 앞선 샘플을 다음과 같이 정확하게 샘플링하면:

vec3 envColor = textureLod(environmentMap, WorldPos, 1.2).rgb; 

 실제로 원래 환경의 흐릿한 버전처럼 보이는 결과를 얻는다:


Visualizing a LOD mip level of the pre-filtered environment map in the skybox.


 비슷한 경우 HDR 환경 맵을 미리 사전 필터링했다. 다양한 밉맵 레벨을 가지고 놀면서 프리 필터 맵이 밉 레벨 증가에 따라 서서히 흐려지는


반사에서 점진적으로 변하는 것을 보아라.




Pre-filter convolution artifacts


 현재 사전 필터 맵은 대부분의 경우 잘 작동하지만 조만간 pre-filter convolution과 직접 관련이 있는 여러 렌더링 아티팩트를 접하게 될 것이다.



Cubemap seams at high roughness


 표면이 거친 표면에서 사전 필터 맵을 샘플링한다는 것은 낮은 밉 레벨 중 일부에서 사전 필터 맵을 샘플링한다는 것을 의미한다.


큐브 샘플을 샘플링 할 때 기본적으로 OpenGL은 큐브 맵면을 선형으로 보간하지 않는다. 낮은 밉 레벨은 낮은 해상도와 사전 필터 맵이


훨씬 더 큰 샘플 로브로 뒤틀어지기 때문에 큐브 사이의 필터링이 거의 필요하지 않다:


Visible cubemap seams in the pre-filter map.


 다행스럽게도 OpenGL은 GL_TEXTURE_CUBE_MAP_SEAMLEE를 활성화해 큐브 맵면을 적절히 필터링하는 옵션을 제공한다:

glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);  

 단순히 이 속성을 응용 프로그램의 시작 부분에서 사용하도록 설정하면 솔기가 사라진다.



Bright dots in the pre-filter convolution


 고주파의 세부 사항과 정반사의 격렬한 광 강도로 인해 정반사를 회선하는 것은 HDR 환경 반사의 거친 특성을 제대로 설명하기 위해


많은 수의 샘플을 필요로 한다. 우리는 이미 많은 수의 샘플을 가지고 있지만, 일부 환경에서는 여전히 거친 밉 레벨 중 일부에서 여전히 충분하지


않을 수 있다. 이 경우 틈이 나는 패턴이 밝은 영역 위주에 나타난다:


Visible dots on high frequency HDR maps in the deeper mip LOD levels of a pre-filter map.


 한 가지 옵션은 샘플 수를 추가로 늘리는 것이지만 모든 환경에 충분하지는 않다. Chetan Jags가 설명했듯이 (pre-filter convolution 중에)


환경 맵을 직접 샘플링하지 않고, 통합 PDR 및 거칠기를 기반으로 환경 맵의 밉 레벨을 샘플링해 이 인공물을 줄일 수 있다:

float D   = DistributionGGX(NdotH, roughness);
float pdf = (D * NdotH / (4.0 * HdotV)) + 0.0001; 

float resolution = 512.0; // resolution of source cubemap (per face)
float saTexel  = 4.0 * PI / (6.0 * resolution * resolution);
float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001);

float mipLevel = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); 

 밉 레벨을 샘플링하고자 하는 환경 맵에서 3선형 필터링을 사용하는 것을 잊지 말아라:

glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); 

 큐브 맵의 기본 텍스처가 설정된 후 OpenGL에서 밉맵을 생성하도록 한다:

// convert HDR equirectangular environment map to cubemap equivalent
[...]
// then generate mipmaps
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

 놀랍게도 효과적이며 거친 표면의 사전 필터 맵에서 모든 점은 아니더라도 대부분을 제거해야한다.



Pre-computing the BRDF


 사전 필터링 된 환경을 가동해 실행하면 BRDF인 분할 합 근사의 두 번째 부분에 집중할 수 있다. 반사 분할 합 근사를 다시 간단히 살펴보겠다:



 사전 필터링 된 맵에서 분할된 근사치의 왼쪽 부분을 다른 거칠기 레벨로 미리 계산했다. 오른쪽은 각도 n⋅ωo, 표면 거칠기 및 Fresnel의


F0에 대해 BRDF 방정식을 뒤섞어야한다. 이는 반사성 BRDF를 단색 흰색 환경 또는 1.0의 일정한 복사 휘도와 통합하는 것과 유사하다.


3변수를 통해 BRDF를 회선하는 것은 다소 어렵지만, 우리는 F0을 specular BRDF 방정식에서 벗어날 수 있다:



 F는 fresnel 방정식이다. 프레넬 분모를 BRDF로 이동하면 다음과 같은 등식을 얻을 수 있다:


 가장 오른쪽의 F를 Fresnel-Schlick 근사로 대체하면 다음과 같이 된다:



 F0에 대해 더 쉽게 풀 수 있도록 (1-ωo⋅h)^5를 α로 바꾼다:



 그런 다음 Fresnel 함수 F를 두 개의 적분에 걸쳐 분할한다:



 이렇게하면, F0는 적분에 대해 일정하고, F0를 적분에서 취할 수 있다. 다음으로, α를 원래의 형태로 되돌려 최종분할 합 BRDF 방정식을 얻는다:



 두 개의 결과 적분은 각각 F0에 대한 스케일 및 바이어스를 나타낸다. f (p, ωi, ωo)가 이미 F에 대한 항을 포함하고 있기 때문에


두 항이 모두 상쇄되어 f에서 F를 제거한다는 점에 유의해라.



 이전의 복잡한 환경 맵과 비슷한 방식으로 BRDF 방정식을 입력에 곱할 수 있다. 즉, n과 ωo 사이의 각도와 거칠기를 말하며 복잡한 결과를


텍스처에 저장한다. 마지막 회선 간접 반사 결과를 얻기 위해 나중에 pbr 조명 쉐이더에서 사용하는 BRDF 통합 맵으로 알려진 2D 조회


텍스처(LUT)에 복잡한 결과를 저장한다.



 BRDF 컨볼루션 쉐이더는 2D 텍스처 좌표를 직접 BRDF 컨볼루션(NdotV 및 거칠기)에 대한 입력으로 사용해 2D 평면에서 작동한다.


회선 코드는 사전 필터 컨볼루션과 거의 비슷하지만, 이제는 BRDF의 기하학 함수 및 Fresnel-Schlick의 근사에 따라 샘플 벡터를 처리한다는 점만


제외하면 다음과 같다:

vec2 IntegrateBRDF(float NdotV, float roughness)
{
    vec3 V;
    V.x = sqrt(1.0 - NdotV*NdotV);
    V.y = 0.0;
    V.z = NdotV;

    float A = 0.0;
    float B = 0.0;

    vec3 N = vec3(0.0, 0.0, 1.0);

    const uint SAMPLE_COUNT = 1024u;
    for(uint i = 0u; i < SAMPLE_COUNT; ++i)
    {
        vec2 Xi = Hammersley(i, SAMPLE_COUNT);
        vec3 H  = ImportanceSampleGGX(Xi, N, roughness);
        vec3 L  = normalize(2.0 * dot(V, H) * H - V);

        float NdotL = max(L.z, 0.0);
        float NdotH = max(H.z, 0.0);
        float VdotH = max(dot(V, H), 0.0);

        if(NdotL > 0.0)
        {
            float G = GeometrySmith(N, V, L, roughness);
            float G_Vis = (G * VdotH) / (NdotH * NdotV);
            float Fc = pow(1.0 - VdotH, 5.0);

            A += (1.0 - Fc) * G_Vis;
            B += Fc * G_Vis;
        }
    }
    A /= float(SAMPLE_COUNT);
    B /= float(SAMPLE_COUNT);
    return vec2(A, B);
}
// ----------------------------------------------------------------------------
void main() 
{
    vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y);
    FragColor = integratedBRDF;
}

 보시다시피 BRDF 컨볼루션은 수학에서 코드로의 직접적인 변환이다. 각도 θ와 조도를 입력으로 사용하고 중요도 샘플링을 사용해 샘플 벡터를


생성한 다음 형상과 BRDF의 유도된 프레넬 조건을 처리하고 각 샘플의 F0에 스케일과 바이어스를 모두 출력해 평균화한다.



 BRDF의 기하학적 용어는 k 변수가 약간 다른 해석을 하기 때문에 IBL과 함께 사용할 때 약간 다른 것으로 나타났다:



 BRDF 컨볼루션은 specular IBL integral의 일부이므로 Schlick-GGX Geometry 함수에 kIBL을 사용한다:

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float a = roughness;
    float k = (a * a) / 2.0;

    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}
// ----------------------------------------------------------------------------
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);

    return ggx1 * ggx2;
}  

 k는 매개 변수로서 a를 취할 때 우리는 a의 다른 해석을 위해 원래했던 것처럼 거칠기를 정사각형으로 만들지 않았다.


이미 여기에서 제곱한 것 같습니다. Epic Games의 일부분이나 디즈니 원고의 불일치인지는 모르겠지만, 거친 것을 직접 번역하면 Epic Games 버전과


동일한 BRDF 통합 맵이 제공된다.



 마지막으로 BRDF 컨볼루션 결과를 저장하기 위해 512x512 해상도의 2D 텍스처를 생성한다.

unsigned int brdfLUTTexture;
glGenTextures(1, &brdfLUTTexture);

// pre-allocate enough memory for the LUT texture.
glBindTexture(GL_TEXTURE_2D, brdfLUTTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 

 Epic Games에서 권장하는대로 16비트 정밀 부동 형식을 사용한다. 가장자리 샘플링 아티팩트를 방지하려면 랩핑 모드를 GL_CLAMP_TO_EDGE로


설정해라.



 그런 다음 동일한 프레임 버퍼 객체를 다시 사용하고, NDC 스크린 공간 쿼드를 통해 이 쉐이더를 실행한다:

glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brdfLUTTexture, 0);

glViewport(0, 0, 512, 512);
brdfShader.use();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
RenderQuad();

glBindFramebuffer(GL_FRAMEBUFFER, 0);  

 분할 합계의 복잡한 BRDF 부분은 다음 결과를 제공한다:


BRDF LUT


 미리 필터링된 환경 맵과 BRDF 2D LUT 모두를 사용해 분할 합 근사에 따라 간접적인 적분을 재구성할 수 있다. 결합된 결과는 간접 또는 주변의


반사광으로 작용한다.



Completing the IBL reflectance


 반사율 방정식의 간접 반사 부분을 얻고 실행하려면 분할 합 근사의 두 부분을 함께 스티칭해야한다. 미리 계산된 조명 데이터를


PBR 쉐이더 상단에 추가하는 것으로 시작해보겠다:

uniform samplerCube prefilterMap;
uniform sampler2D   brdfLUT;  

 먼저 반사 벡터를 사용해 사전 필터링된 환경 맵을 샘플링해 표면의 간접 반사를 얻는다. 표면 거칠기를 기준으로 적절한 밉 레벨을 샘플링해


거친 표면에 거울 반사를 흐리게 처리한다. 

void main()
{
    [...]
    vec3 R = reflect(-V, N);   

    const float MAX_REFLECTION_LOD = 4.0;
    vec3 prefilteredColor = textureLod(prefilterMap, R,  roughness * MAX_REFLECTION_LOD).rgb;    
    [...]
}

 프리 필터 단계에서 우리는 환경 맵을 최대 5 밉 레벨까지만 회귀시켰다. 여기서는 MAX_REFLECTION_LOD로 표시해 관련 데이터가 없는


밉 레벨을 샘플링하지 않도록한다.



 그런 다음 재질의 거칠기와 일반 벡터와 뷰 벡터 사이의 각도를 고려해 BRDF 조회 텍스처를 샘플링한다:

vec3 F        = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);
vec2 envBRDF  = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);

 F0에 스케일과 바이어스가 주어지면 BRDF 룩업 텍스처에서 간접 프레넬 결과 F를 직접 사용한다. 이를 IBL 반사율 방정식의 왼쪽 프리 필터 부분과


결합하고, 근사된 적분 결과를 반사광으로 재구성한다.



 이것은 반사율 방정식의 간접 specular 부분을 제공한다. 이제 이것을 지난 튜토리얼의 반사율 방정식의 확산 부분과 결합하면 완전한 PBR IBL 결과를


얻을 수 있다:

vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness);

vec3 kS = F;
vec3 kD = 1.0 - kS;
kD *= 1.0 - metallic;	  
  
vec3 irradiance = texture(irradianceMap, N).rgb;
vec3 diffuse    = irradiance * albedo;
  
const float MAX_REFLECTION_LOD = 4.0;
vec3 prefilteredColor = textureLod(prefilterMap, R,  roughness * MAX_REFLECTION_LOD).rgb;   
vec2 envBRDF  = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg;
vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y);
  
vec3 ambient = (kD * diffuse + specular) * ao; 

 우리가 이미 거기에 프레넬 곱셈을 가지고 있기 때문에 우리는 kS에 의한 specular를 곱하지 않는다.



 이제 거친 느낌과 금속성이 다른 일련의 구체에서 이 정확한 코드를 실행하면 최종적으로 PBR 렌더러에서 실제 색상을 볼 수 있게 된다:


Render in OpenGL of full PBR with IBL (image based lighting) on spheres with varying roughness and metallic properties.


 우리는 이곳에서 멋진 PBR 소재를 사용할 수 있다:


Render in OpenGL of full PBR with IBL (image based lighting) on textured spheres.


Render in OpenGL of full PBR with IBL (image based lighting) on a 3D PBR model.


 나는 우리의 조명이 훨씬 더 설득력있게 보일 것이라고 모두 동의 할 수 있을 것이라고 확신한다. 더 나은 점은 우리가 사용하는 환경 맵에 관계없이


우리의 조명이 물리적으로 정확하다는 것이다. 아래에는 몇 가지 미리 계산된 HDR 맵이 표시되어 조명의 동역학을 완전히 바꿔 놓았지만 하나의


조명 변수를 변경하지 않고도 물리적으로 정확한 것으로 나타난다!



What's next?


 바라기를, 이 튜토리얼이 끝나기 전에 PBR이 무엇인지 명확하게 이해해야하며 실제 PBR 렌더러를 가동시켜야한다. 이 튜토리얼에서는 응용


프로그램을 시작할 때 렌더 루프 전에 모든 관련 PBR IBL 데이터를 사전 계산했다. 이것은 교육적인 목적으로 좋았지만, PBR의 실제 사용에는


그다지 좋지 않다. 첫째, 사전 계산은 매번 시작할 때가 아니라 한 번만 수행하면 된다. 두 번째, 여러 환경 맵을 사용하는 순간마다 시작될 때마다


각각의 환경 맵을 미리 계산해야한다.



 이러한 이유로 일반적으로 환경 맵을 방사량으로 사전 계산하고 사전 필터 맵을 한 번만 사용한 다음 디스크에 저장한다.


(BRDF 통합 맵은 환경 맵에 종속되지 않으므로 한 번 계산하거나 로드)


즉, 밉 레벨을 비롯해 HDR 큐브 맵을 저장하는 맞춤 이미지 형식이 필요하다. 또는 밉 레벨 저장을 지원하는 .dds와 같이 사용 가능한 형식 중 하나로


저장하고 로드한다.



 또한, 사전 계산된 IBL 이미지를 생성해 PBR 파이프 라인에 대한 이해를 돕는 등이 자습서의 전체 프로세스를 설명했다. 그러나 cmftStudio 또는


IBLBaker와 같은 몇 가지 훌륭한 도구를 사용하면 이러한 미리 계산된 맵을 생성해 사용할 수 있다.



 우리가 건너뛴 한가지 점은 reflection 프로브로 사전 계산된 큐브 맵이다: 큐브 맵 보간 및 시차 보정.


이것은 특정 위치에서 장면의 큐브 스냅 샷을 찍는 장면에 여러 개의 반사 프로브를 배치하는 과정이다. 그러면 장면의 해당 부분에 대한


IBL 데이터로 전환 할 수 있다. 카메라 주변을 기반으로 하는 이러한 프로브 중 몇 가지를 보간하면 우리가 배치하고자 하는 반사 프로브의 양에


의해 제한되는 로컬 고선형 이미지 기반 조명을 얻을 수 있다. 이렇게 하면 장면의 밝은 옥외 섹션에서 어두운 실내 섹션으로 이동할 때


이미지 기반 조명이 올바르게 업데이트 될 수 있다.

'Game > Graphics' 카테고리의 다른 글

OpenGL : Terrain  (0) 2018.11.06
OpenGL Figure draw function  (2) 2018.11.06
Learn OpenGL - PBR : IBL(Diffuse irradiance)  (0) 2018.10.20
Learn OpenGL - PBR : Lighting  (0) 2018.10.20
Learn OpenGL - PBR : Theory  (0) 2018.10.19