본문 바로가기

Game/Graphics

Learn OpenGL - PBR : IBL(Diffuse irradiance)

link : https://learnopengl.com/PBR/IBL/Diffuse-irradiance


Diffuse irradiance


 IBL 또는 Image Based lighting은 이전 튜토리얼에서와 같이 직접적인 분석 조명이 아닌 객체를 조명하는 기술 모음이다.


주변 환경을 하나의 큰 광원으로 간주해 조명한다. 이것은 일반적으로 우리의 조명 방정식에서 직접 사용할 수 있는 큐브맵-환경맵을


조작해 조명 방정식에서 직접 사용할 수 있도록해 수행된다: 각 큐브맵 픽셀을 light emitter(빛 방출?)로 처리한다.


이렇게하면 환경의 전역 조명과 일반적인 느낌을 효과적으로 포착해 객체에 주변 환경을 더 잘 속박할 수 있다.



 IBL(Image based lighting)은 일부 (global)환경의 조명을 캡처하기 때문에 입력은 주변 조명의 더 정밀한 형태로 간주되며 심지어는


전체 조명의 조잡한 근사치로 간주된다. 환경의 조명을 고려할 때 객체가 훨씬 더 물리적으로 정확해 보이기 때문에 PBL에 IBL이 유용하다.



 PBL 시스템에 IBL을 소개하기 위해 반사율 방정식을 다시 한 번 살펴보겠다:



 앞에서 설명한 것처럼 우리의 주된 목표는 반구 Ω에서 모든 들어오는 빛의 방향을 적분하는 것이다. 이전 튜토리얼에서 적분을 해결하는 것은


적분에 기여한 정확한 빛 방향 wi를 미리 알기 때문에 쉽다. 그러나 이번에는 주변 환경으로부터 들어오는 모든 빛의 방향에 따라 약간의 광도가


생겨서 적분을 푸는 것이 쉽지 않을 수 있다. 이렇게하면 적분 문제를 푸는데 필요한 두 가지 주요 요구사항이 생긴다.



- 임의의 방향 벡터가 주어진 경우 장면의 밝기를 검색 할 수 있는 방법이 필요하다.


- 통합을 해결하는 것은 빠르고 실시간이어야한다.



 이제 첫 번째 요구 사항은 상대적으로 쉽습니다. 우리는 이미 암시했지만, 환경이나 장면의 방사 조도를 표현하는 한 가지 방법은 (처리된) 환경


큐브 맵의 형태이다. 이러한 큐브 맵을 감안할 때 큐브 맵의 모든 텍셀을 하나의 단일 광원으로 시각화 할 수 있다. 이러한 큐브 맵을 감안할 때


큐브 맵의 모든 텍셀을 하나의 단일 광원으로 시각화 할 수 있다. 이 큐브 맵을 임의의 방향 벡터 wi로 샘플링함으로써 우리는 그 방향에서


장면의 광도를 검색한다.



 어떤 방향으로 주어지면 장면의 밝기를 얻는 것은 다음과 같이 간단하다:

vec3 radiance = texture(_cubemapEnvironment, w_i).rgb;  

  그럼에도 불구하고 integral을 해결하려면 한 방향뿐만 아니라 모든 조각 쉐이더 호출에 너무 많은 비용이 드는 반구 Ω에 대한 가능한


모든 방향을 샘플링해야한다. 더 효율적인 방식으로 통합을 해결하기 위해 대부분의 계산을 사전처리하거나 사전 계산해야한다.


이를 위해 반사율 방정식에 대해 좀 더 깊이 파고 들어야한다:



 반사 방정식을 잘 살펴보면 BRDF의 확산 kd 및 반사 ks 항이 서로 독립적이며 두 항으로 적분을 나눌 수 있음을 알 수 있다.



 적분을 두 부분으로 나눔으로써 diffuse와 specular라는 용어에 개별적으로 집중할 수 있다. 이 튜토리얼의 초점은 확산 통합에 있다.



 확산 적분을 면밀히 살펴보면 확산 lambert 항은 상수 항이며 정수 변수에 의존하지 않는다는 것을 알 수 있다. 이것을 감안할 때,


우리는 확산 적분으로부터 상수 항을 움직일 수 있다:


 이것은 우리에게 wi에만 의존하는 정수를 제공한다. (environment map의 중심에 p가 있다고 가정)


이 지식으로 우리는 곱셈에 의한 diffuse 적분 결과를 각 샘플 방향(또는 텍셀)에 저장하는 새로운 큐브 맵을 계산하거나 사전 계산할 수 있다.



 convolution은 데이터 집합의 다른 모든 항목을 고려해 데이터 집합의 각 항목에 계산을 적용한다. 데이터 세트는 장면의 광도 또는 환경 맵이다.


따라서 큐브 맵의 모든 샘플 방향에 대해 반구 Ω에 대한 다른 모든 샘플 방향을 고려한다.



 environment 맵을 convolute하기 위해 우리는 반구 Ω에 걸쳐 다수의 방향 wi를 이산적으로 샘플링하고 그 밝기를 평균화해 각 출력에 대한


적분 값을 샘플 방향으로 해결한다. 우리가 샘플 방향 wi를 만드는 반구는 우리가 뒤얽히는 출력 wo 샘플 방향으로 향하게된다.


Convoluting a cubemap on a hemisphere for a PBR irradiance map.


 모든 샘플 방향에 대해 적분 결과를 저장하는 이 사전 계산된 큐브 맵은 방향에 따라 정렬된 일부 표면에 부딪히는 장면의 모든 간접


확산 광의 사전 계산된 합계로 생각할 수 있다. 이러한 큐브 맵은 복잡한 큐브 맵이 효과적으로 어떤 방향에서든 장면의 (사전계산된) 방사량을


직접 샘플링 할 수 있도록 보는 조사 맵으로 알려져 있다.

radiance(복사) 방정식은 방사 조도(복사도) 맵의 중심에 있다고 가정한 위치 p에 따라 달라진다. 이는 모든 diffuse indirect light가 현실의 환상을 깨뜨릴 수 있는 단일 환경지도에서 비롯되어야 함을 의미한다. 렌더 엔진은 각 반사 탐침이 주변의 자체 방사 조도 지도를 계산하는 장면 전체에 반사 탐침을 배치해 이를 해결한다. 이 방법으로, 위치 p에서의 복사 조도(및 복사)는 가장 가까운 반사 탐침 사이의 보간된 복사 조도이다. 지금은 항상 센터에서 환경 맵을 샘플링하고 추후 튜토리얼에서 반사 프로브에 대해 논의한다고 가정한다.

 아래는 cubemap 환경 맵과 그 결과인 irradiance map(웨이브 엔진의 호의)의 예이다. 모든 방향에 대한 장면의 광도를 평균화 한 것이다.


The effect of convoluting a cubemap environment map.


 각 큐브 맵 텍셀에 회전 결과를 저장함으로써, 조도 맵은 환경의 평균 컬러 또는 조명 디스플레이와 같이 다소 표시된다.


이 환경 맵에서 어떤 방향으로 샘플링하면 해당 방향에서 장면의 방사 조도가 생긴다.



PBR and HDR


 조명 튜토리얼에서 간단히 살펴보았다. PBR 파이프 라인에서 장면의 조명의 높은 동적 범위를 고려하는 것이 매우 중요하다. PBR은 실제 물성치와


측정치에 대한 대부분의 입력 값을 기반으로하므로 유입되는 빛 값을 물리적으로 동일한 값으로 정확하게 일치시키는 것이 좋다.


우리가 각 조명의 빛의 유출에 대해 교육적인 추측을하는지 또는 직접적인 물리적 등가물을 사용하는지에 관계없이 간단한 전구 또는 태양의 차이는


어느 것이든 중요하다. HDR 렌더링 환경에서 작업하지 않으면 각 조명의 상대 강도를 올바르게 지정할 수 없다.



 따라서 PBR과 HDR은 서로 밀접한 관련이 있다. 그러나 이 모든 것이 Image based lighting과 어떤 관련이 있나?


이전 튜토리얼에서는 HDR에서 PBR을 작동시키는 것이 상대적으로 쉽다는 것을 알았다. 그러나 Image based lighting의 경우 환경의 간접 조명


강도를 환경 큐브 맵의 색상 값에 기초해 조명의 높은 동적 범위를 환경 맵에 저장하는 방법이 필요하다.



 큐브맵이 LDR(low dynamic range)에 있을 때 한 우리가 사용해본 환경 맵이다. 우리는 0.0에서 1.0 사이 범위의 개별 face image에서 직접


색상 값을 사용해 그대로 처리했다. 이것이 시각적인 결과물에 대해서는 잘 작동할 수 있지만 물리적인 입력 매개 변수로 가져가면 효과가


없을 것이다.




The radiance HDR file format


 Radiance 파일 형식을 입력해라. Radiance(발광) 파일 형싱근 모든 6면이 부동 소수점 데이터로 가득찬 전체 큐브 맵을 저장해 누구나


0.0에서 1.0 범위 밖의 색상 값을 지정해 조명에 올바른 색상 강도를 제공할 수 있도록 한다. 파일 형식은 또한 각 부동 소수점 값을 채널당


32비트 값이 아니라 색상의 알파 채널로 지수로 사용해 채널당 8비트를 저장한다. 이것은 꽤 잘 작동하지만 parsing 프로그램은


각 색상을 부동 소수점으로 다시 변환해야한다.



 아래 예제에서 볼 수 있는 slBL아카이브와 같은 소스에서 무료로 사용할 수 있는 꽤 많은 발광 HDR 환경 맵이 있다:


Example of an equirectangular map


 이미지가 왜곡되어 나타나기 전에 예상했던 것과 정확히 일치하지 않을 수 있으며 이전에 본 환경 맵의 6개의 개별 큐브 맵 면을


표시하지 않는다. 이 환경 맵은 구면에서 평면으로 투영되어 환경을 직사각형 맵이라고 하는 단일 이미지에 보다 쉽게 저장할 수 있다.


대부분의 시각적 해상도는 가로 보기 방향으로 저장되는 반면, 아래쪽 및 위쪽 방향은 보존되지 않으므로 작은 주의가 필요하다.


대부분의 경우 이 옵션은 거의 모든 렌더러에서 가로 보기 방향으로 재미있는 조명과 주변 환경을 찾을 수 있는 것처럼 타협하지 않는다.



HDR and stb_image.h


 복사 발광 HDR 이미지를 직접로드하는 것은 너무 어렵지는 않지만 번거로운 파일 형식에 대한 지식이 필요하다. 운 좋게도 인기있는 하나의


헤더 라이브러리인 stb_image.h는 우리의 요구에 완벽하게 맞는 부동 소수점 값의 배열로서 직접 복사 발광 HDR 이미지를 로드하는 것을 지원한다.


프로젝트에 stb_image를 추가하면 HDR 이미지로드가 다음과 같이 간단해진다:

#include "stb_image.h"
[...]

stbi_set_flip_vertically_on_load(true);
int width, height, nrComponents;
float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0);
unsigned int hdrTexture;
if (data)
{
    glGenTextures(1, &hdrTexture);
    glBindTexture(GL_TEXTURE_2D, hdrTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); 

    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);

    stbi_image_free(data);
}
else
{
    std::cout << "Failed to load HDR image." << std::endl;
}  

 stb_image.h는 HDR 값을 기본적으로 부동 소수점 값 목록에 맵핑한다. (채널당 32비트 및 색상당 3채널)


이것이 등변 삼각형 HDR 환경 맵을 2D 부동 소수점 텍스처로 저장하는데 필요한 모든 것이다.



From Equirectangular to Cubemap


 환경 lookup을 위해 등변 삼각형 맵을 직접 사용할 수도 있지만, 이러한 작업은 상대적으로 비용이 많이 들며 direct 큐브 맵 샘플이 더 효율적이다.


따라서 이 튜토리얼에서는 먼저 정사각형 이미지를 추가 처리를 위한 큐브 맵으로 변환한다. 이 과정에서 우리는 3D 환경 맵인 것처럼 대각선 맵을


샘플링하는 방법을 보여준다. 이 경우 원하는 솔루션을 자유롭게 선택할 수 있다.



 등각 투영 이미지를 큐브 맵으로 변환하려면 (단위) 큐브를 렌더링하고 내부에서 모든 큐브의 면에 대한 등각성 맵을 투영하고 각 큐브의 각면의


6개 이미지를 큐브 맵면으로 가져와야한다. 이 큐브의 정점 쉐이더는 큐브를 그대로 렌더링하고 로컬 위치를 3D 샘플 벡터로 조각 쉐이더에 전달한다.

#version 330 core
layout (location = 0) in vec3 aPos;

out vec3 localPos;

uniform mat4 projection;
uniform mat4 view;

void main()
{
    localPos = aPos;  
    gl_Position =  projection * view * vec4(localPos, 1.0);
}

 조각 쉐이더의 경우 큐브의 각면에 등변 방형지도를 깔끔하게 접은 것처럼 큐브의 각 부분을 색칠한다. 이것을 달성하기 위해 조각의


샘플 방향을 큐브의 로컬 위치에서 보간된 것으로 취한 다음 이 방향 벡터와 일부 삼각법 마법을 사용해 등방성 맵을 큐브 맵 자체처럼


샘플링한다. 결과를 큐브-면의 조각에 직접 저장한다. 이 조각은 우리가 해야 하는 모든 것이다:

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

uniform sampler2D equirectangularMap;

const vec2 invAtan = vec2(0.1591, 0.3183);
vec2 SampleSphericalMap(vec3 v)
{
    vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
    uv *= invAtan;
    uv += 0.5;
    return uv;
}

void main()
{		
    vec2 uv = SampleSphericalMap(normalize(localPos)); // make sure to normalize localPos
    vec3 color = texture(equirectangularMap, uv).rgb;
    
    FragColor = vec4(color, 1.0);
}

 장면의 중앙에 큐브를 렌더링하면 HDR 등변 투영 맵이 주어지며 다음과 같은 모양이 된다:


OpenGL render of an equirectangular map converted to a cubemap.


 이는 입방형 이미지를 효과적으로 입방형으로 맵핑했지만 소스 HDR 이미지를 큐브 맵 텍스처로 변환하는데 아직 도움이 되지 않음을 보여준다.


이것을 달성하기 위해 우리는 큐브의 각 개별면을 보면서 동일한 큐브를 6번 렌더링해야하며 프레임 버퍼 객체로 시각적 결과를 기록해야한다:

unsigned int captureFBO, captureRBO;
glGenFramebuffers(1, &captureFBO);
glGenRenderbuffers(1, &captureRBO);

glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);  

 물론, 우리는 또한 해당 큐브 맵을 생성해 6개의 면 각각에 대해 메모리를 미리 할당한다:

unsigned int envCubemap;
glGenTextures(1, &envCubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
for (unsigned int i = 0; i < 6; ++i)
{
    // note that we store each face with 16 bit floating point values
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 
                 512, 512, 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);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

 그런 다음 투영된 2D 텍스처를 큐브 맵면에 캡처하는 것이다.


 앞에서 프레임 버퍼 및 포인트 쉐도우 튜토리얼에서 논의한 코드 세부 주제와 관련해 자세한 내용을 다루지는 않겠지만,


큐브의 각면을 향한 6개의 서로 다른 뷰 매트릭스를 설정하는 것이 효과적이다. 90도를 사용해 전체 면을 캡처하고 결과를 부동 소수점 프레임


버퍼에 6번 큐브 렌더링:

glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
glm::mat4 captureViews[] = 
{
   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f,  0.0f,  0.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f,  1.0f,  0.0f), glm::vec3(0.0f,  0.0f,  1.0f)),
   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f,  0.0f), glm::vec3(0.0f,  0.0f, -1.0f)),
   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f,  0.0f,  1.0f), glm::vec3(0.0f, -1.0f,  0.0f)),
   glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f,  0.0f, -1.0f), glm::vec3(0.0f, -1.0f,  0.0f))
};

// convert HDR equirectangular environment map to cubemap equivalent
equirectangularToCubemapShader.use();
equirectangularToCubemapShader.setInt("equirectangularMap", 0);
equirectangularToCubemapShader.setMat4("projection", captureProjection);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrTexture);

glViewport(0, 0, 512, 512); // don't forget to configure the viewport to the capture dimensions.
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
for (unsigned int i = 0; i < 6; ++i)
{
    equirectangularToCubemapShader.setMat4("view", captureViews[i]);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
                           GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    renderCube(); // renders a 1x1 cube
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

 우리는 프레임 버퍼의 색상 첨부를 가져와서 큐브 맵의 모든 면에 텍스처 타겟을 전환해 장면을 큐브 맵의 면 중 하나로 직접 렌더링한다.


일단 이 루틴이 끝나면 큐브 맵 envCubemap은 원래의 HDR 이미지의 큐브 맵 환경 버전이어야한다.



 매우 간단한 skybox shader를 작성해 큐브 맵을 테스트해보자:

#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 projection;
uniform mat4 view;

out vec3 localPos;

void main()
{
    localPos = aPos;

    mat4 rotView = mat4(mat3(view)); // remove translation from the view matrix
    vec4 clipPos = projection * rotView * vec4(localPos, 1.0);

    gl_Position = clipPos.xyww;
}

 큐브 맵 튜토리얼에 설명된대로 렌더링된 큐브 조각의 깊이 값이 항상 최대 1.0인 1.0으로 끝나도록 하는 xyww 트릭을 여기에 유의해라.


깊이 비교 함수를 GL_LEQUAL로 변경해야한다는 점에 유의해라:

glDepthFunc(GL_LEQUAL); 

 조각 쉐이더는 큐브의 로컬 조각 위치를 사용해 큐브 맵 환경 맵을 직접 샘플링한다:

#version 330 core
out vec4 FragColor;

in vec3 localPos;
  
uniform samplerCube environmentMap;
  
void main()
{
    vec3 envColor = texture(environmentMap, localPos).rgb;
    
    envColor = envColor / (envColor + vec3(1.0));
    envColor = pow(envColor, vec3(1.0/2.2)); 
  
    FragColor = vec4(envColor, 1.0);
}

 샘플에 올바른 방향 벡터에 직접 해당하는 보간된 정점 큐브 위치를 사용해 환경 맵을 샘플링한다. 카메라의 translate 구성 요소가 무시되는 것을


보면서 이 쉐이더를 큐브 위에 렌더링하면 환경 맵이 움직이지 않는 배경으로 제공된다. 또한, 환경 맵의 HDR 값을 기본 LDR 프레임 버퍼에 직접


출력 할 때 색상 값을 적절하게 톤 맵핑하려고한다. 또한, 거의 모든 HDR 맵은 기본적으로 선형 색상 공간에 있으므로 기본 프레임 버퍼에 쓰기


전에 감마 보정을 적용해야한다.



 이제 이전에 렌더링 된 구체 위로 샘플된 환경 맵을 렌더링하면 다음과 같이 보일 것이다:


Render the converted cubemap as a skybox.


 여기에 도착하는데는 다소 시간이 걸렸지만, HDR 환경 맵을 읽고 모자이크 맵에서 큐브 맵으로 변환하고 HDR 큐브 맵을 스카이 박스로


렌더링하는데 성공했다. 또한, 큐브 맵의 6개면 모두에 렌더링 할 수 있는 작은 시스템을 설정했다. 이 맵은 환경 맵을 convolution(회선) 할 때


다시 필요할 것이다.



Cubemap convolution


 튜토리얼의 시작 부분에서 설명했듯이 우리의 주된 목표는 큐브 맵 환경 맵의 형태로 장면의 조도를 고려해 모든 diffuse indirect lighting에


대한 적분을 해결하는 것이다. 우리는 방향 wi에서 HDR 환경 맵핑을 샘플링해 특정 방향으로 장면 L(p,wi)의 광도를 얻을 수 있다는 것을 안다.


이 적분을 풀기 위해 우리는 각 조각에 대해 반구 Ω 내의 가능한 모든 방향에서 장면의 광도를 샘플링해야한다.



 그러나 Ω의 가능한 모든 방향에서 환경 조명을 샘플링하는 것은 계산상 불가능하다. 가능한 방향의 수는 이론적으로 무한하다. 그러나 유한한


수의 방향이나 표본읠 취해 균일하게 또는 반구 내에서 무작위로 취해 방사열을 상당히 정확하게 근사해 방향 수를 근사적으로 구할 수 있다.



 그러나 아직 샘플의 수가 괜찮은 결과를 위해 상당히 클 필요가 있으므로 실시간으로 모든 조각에 대해 이렇게 하기에는 너무 비싸므로 


미리 계산할 수 있다. 반구의 방향이 우리가 복사 조도를 포착하는 위치를 결정하기 때문에 모든 반경 방향에 대해 복사 조도를 미리 계산할 수 있다.



 임의의 방향 벡터 wi가 주어지면, 미리 계산된 방사도 맵을 샘플링해 방향 wi로부터 전체 diffuse 방사를 추출할 수 있다.


조각 표면에서의 간접 확산 광의 양을 결정하기 위해, 우리는 표면의 법선을 중심으로 한 반구로부터 전체 복사도를 구한다.


장면의 방사 조도를 얻는 것은 다음과 같이 간단하다:

vec3 irradiance = texture(irradianceMap, N);

 이제는 방사 조도 맵을 생성하기 위해 큐브 맵으로 변환된 환경 조명을 회선 할 필요가 있다. 각 단편에 대해 표면의 반구가 법선 벡터 N을


따라 배향된 경우, 큐브 맵을 뒤얽히는 것은 N을 따라 반구 Ω에서 각 방향 wi의 전체 평균 광도를 계산하는 것과 같다.


 Convoluting a cubemap on a hemisphere (oriented around the normal) for a PBR irradiance map.


 고맙게도 이 튜토리얼의 모든 성가신 설정은 변환된 큐브 맵을 직접 가져와서 조각 쉐이더에서 회선하고 6개의 모든 면 방향으로 렌더링하는


프레임 버퍼를 사용해 새로운 큐브 맵에 그 결과를 캡처할 수 있기 때문에 전혀 도움이 되지 않는다. 앞에서 equirectangular 환경 맵을


큐브 맵으로 변환하도록 설정했으므로 완전히 동일한 접근 방식을 취할 수 있지만 다른 조각 쉐이더를 사용할 수 있다.

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

uniform samplerCube environmentMap;

const float PI = 3.14159265359;

void main()
{		
    // the sample direction equals the hemisphere's orientation 
    vec3 normal = normalize(localPos);
  
    vec3 irradiance = vec3(0.0);
  
    [...] // convolution code
  
    FragColor = vec4(irradiance, 1.0);
}

 environmentMap은 투명한 HDR 환경 맵에서 변환된 HDR 큐브 맵이다.



 환경 맵을 회선하는 방법은 많이 있지만 이 튜토리얼에서는 샘플 방향을 중심으로 한 반구 Ω을 따라 큐브 맵 텍셀당 고정된 양의 샘플 벡터를


생성하고 결과를 평균화한다. 고정된 양의 샘플 벡터는 반구 내에서 균일하게 퍼진다. 적분은 연속 함수이며 고정된 양의 샘플 벡터가 주어지면


그 함수를 이산적으로 샘플링하는 것은 근사값이다. 우리가 사용하는 샘플 벡터가 많을수록 더 근사해진다.



 반사율 방정식의 적분 ∫은 작업하기가 다소 어려운 입체각 dw를 중심으로 회전한다. 입체각 dw에 대해 통합하는 대신에 등가의 구 좌표인 θ와


φ에 대해 통합할 것이다.


Converting the solid angle over the equivalent polar azimuth and inclination angle for PBR


 우리는 극지방 방위 φ를 사용해 반구의 고리 주위를 0과 2π 사이에서 표본 추출하고, 천구 각 θ를 0과 12π 사이에서 사용해 반구의 증가하는


고리를 샘플링한다. 이것은 우리에게 업데이트된 반사율 적분을 줄 것이다:



적분을 해결하려면 반구 Ω 내에서 고정된 수의 개별 샘플을 취해 그 결과를 평균화해야한다. 이것은 각 구 좌표의 n1 및 n2 이산 샘플을


각각 Riemann sum에 기초한 다음과 같은 이산 버전으로 통합한다:



 우리가 구형 값을 이산적으로 샘플링 할 때, 위의 이미지가 보여주듯이 각 샘플은 반구 위의 영역을 근사하거나 평균화한다.


구형의 일반적인 속성으로 인해 반구의 개별 샘플 영역을 샘플 영역이 중심 상단으로 수렴함에 따라 천정각 θ가 높을수록 작아진다.


더 작은 영역을 보완하기 위해 추가된 죄를 명확히하는 sinθ로 영역 크기를 조정해 기여도를 평가한다.



 각 조각 호출에 대한 적분의 구 좌표를 제공하면 반구를 개별적으로 샘플링해 다음 코드로 변환한다:

vec3 irradiance = vec3(0.0);  

vec3 up    = vec3(0.0, 1.0, 0.0);
vec3 right = cross(up, normal);
up         = cross(normal, right);

float sampleDelta = 0.025;
float nrSamples = 0.0; 
for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
    for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
    {
        // spherical to cartesian (in tangent space)
        vec3 tangentSample = vec3(sin(theta) * cos(phi),  sin(theta) * sin(phi), cos(theta));
        // tangent space to world
        vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; 

        irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
        nrSamples++;
    }
}
irradiance = PI * irradiance * (1.0 / float(nrSamples));

 우리는 반구를 통과하기 위해 고정된 sampleDelta 값을 지정한다. 샘플 델타를 줄이거나 늘리면 정확도가 각각 증가하거나 감소한다.



 두 루프 내에서 구형 좌표를 가져와서 3D Cartesian 샘플 벡터로 변환하고 샘플을 접선에서 월드 공간으로 변환한 다음 이 샘플 벡터를


사용해 HDR 환경 맵을 직접 샘플링한다. 우리는 각 샘플 결과를 복사 조도에 더한다. 복사 조도는 최종적으로 총 샘플 수로 나누어


평균 표본 복사 조도를 제공한다. 우리는 더 큰 각에서는 약한 빛 때문에 더 큰 반구 영역에서 더 작은 샘플 영역을 설명하기 위해


sin(theta)로 인해 샘플링 된 색상 값을 cos(theta)로 스케일한다.



 이제 남은 일은 OpenGL 렌더링 코드를 설정해 이전에 캡쳐한 envCubemap을 convolute(둘둘말린) 할 수 있다. 우선 우리는 irradiance 큐브 맵을


만든다. (다시 한 번 말하지만, 이것은 렌더링 루프 전에 한 번만 해야한다):

unsigned int irradianceMap;
glGenTextures(1, &irradianceMap);
glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap);
for (unsigned int i = 0; i < 6; ++i)
{
    glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 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);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

 방사 조도 맵은 모든 주위의 광도를 평균화하므로 고주파 세부 사항이 많지 않아서 맵을 저해상도(32x32)로 저장할 수 있고,


OpenGL의 선형 필터링을 통해 대부분의 작업을 수행 할 수 있다. 다음으로 캡처 프레임 버퍼를 새 해상도로 다시 조정한다:

glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
glBindRenderbuffer(GL_RENDERBUFFER, captureRBO);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32); 

 회선 쉐이더를 사용해 우리는 환경 큐브 맵을 캡처한 것과 비슷한 방식으로 환경맵을 convolute(돌돌만다)한다:

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

glViewport(0, 0, 32, 32); // don't forget to configure the viewport to the capture dimensions.
glBindFramebuffer(GL_FRAMEBUFFER, captureFBO);
for (unsigned int i = 0; i < 6; ++i)
{
    irradianceShader.setMat4("view", captureViews[i]);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
                           GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    renderCube();
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

 이제 이 루틴을 수행 한 후 우리는 확산된 이미지 기반 조명에 직접 사용할 수 있는 사전 계산된 방사도 맵을 가져야한다.


우리가 성공적으로 환경 맵을 복잡하게하는지 보려면 skybox의 호나경 샘플러로 irradiance 맵을 환경 맵으로 대체해라:


Displaying a PBR irradiance map as the skybox background.


 환경 맵이 크게 흐린 버전인 경우 환경 맵을 성공적으로 뒤집어 놓았다.



PBR and indirect irradiance lighting


 방사 조도 맵은 모든 주변 간접 광에서 축적된 반사율 적분의 확산 부분을 나타낸다. 빛이 어떤 direct 광원에서도 나오는 것은 아니지만


주변 환경에서 우리는 확산 조명과 반사 간접 조명을 주변 조명으로 취급해 이전에 설정된 상수를 대체한다.



 먼저 사전 계산된 방사도 맵을 큐브 샘플러로 추가해야한다:

uniform samplerCube irradianceMap;

 장면의 간접 확산 광을 모두 포함하는 방사 조도 맵을 제공하면, 조각에 영향을 주는 방사 조도량을 검색하는 것은 표면의 법선이 주어지는


단일 텍스처 샘플만큼 간단하다:

// vec3 ambient = vec3(0.03);
vec3 ambient = texture(irradianceMap, N).rgb;

 그러나 간접 조명에는 반사 방정식의 분할 버전에서 보았듯이 확산 및 반사 부분이 모두 포함되어 있으므로 확산 부분의 무게를 적절히 조정해야한다.


이전 튜토리얼에서 했던 것과 마찬가지로 Fresnel 방정식을 사용해 굴절 또는 확산 비율을 유도하는 표면의 간접 반사 비율을 결정한다:

vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0);
vec3 kD = 1.0 - kS;
vec3 irradiance = texture(irradianceMap, N).rgb;
vec3 diffuse    = irradiance * albedo;
vec3 ambient    = (kD * diffuse) * ao; 

 주변 광은 반구 내의 모든 방향에서 정상 N 주위로 향하기 때문에 fresnel 응답을 결정하는 단일 중간 벡터는 없다.


Fresnel을 여전히 시뮬레이트하기 위해 normal/view vector 사이의 각도에서 fresnel을 계산한다. 그러나, 우리는 fresnel 방정식의 입력으로서


표면의 거칠기의 영향을 받은 마이크로 표면 중도 벡터를 사용했다. 현재 우리는 거칠기를 고려하지 않았기 때문에 표면의 반사율은 항상


상대적으로 높게 끝날 것이다. 간접 조명은 직사광과 동일한 특성을 따르므로 거친 표면이 표면 가장자리에서 덜 강하게 반사 될 것으로 기대한다.


표면의 거칠기를 고려하지 않았으므로 간접 프레넬 반사 강도는 거친 비금속 표면을 보인다:


The Fresnel equation for IBL without taking roughness into account.


 Sébastien Lagarde에 의해 설명된 것처럼 Fresnel-Schlick 방정식에 거친 용어를 주입해 이 문제를 완화 할 수 있다:

vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness)
{
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}   

 프레넬 응답을 계산할 때 표면의 거칠기를 고려하면 ambient code는 다음과 같이 끝난다:

vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); 
vec3 kD = 1.0 - kS;
vec3 irradiance = texture(irradianceMap, N).rgb;
vec3 diffuse    = irradiance * albedo;
vec3 ambient    = (kD * diffuse) * ao; 

 보시다시피 실제 이미지 기반의 조명 계산은 매우 간단하며 큐브 맵 텍스처 룩업 하나만 필요하다. 대부분의 작업은 사전 계산이나 환경 맵을


방사 조도 맵으로 전환하는데 있다.



 각 구가 수직으로 증가하는 금속과 수평으로 증가하는 조도 값을 갖는 조명 튜토리얼에서 초기 장면을 취하고, 확산 이미지 기반 조명을 추가하면


다음과 같이 보인다:


Result of convoluting an irradiance map in OpenGL used by the PBR shader.


 금속 표면이 점 광원에서만 오는 금속 표면처럼 보이기 시작하기 위해서는 더 많은 금속 구체가 어떤 형태의 반사를 필요로 하기 때문에


여전히 조금 이상하다. 그럼에도 불구하고 표면 반응이 환경의 주변 조명에 따라 반응하기 때문에 환경이 환경 내에서 보다 잘 느껴질 수 있다고


이미 말할 수 있다.



 다음 튜토리얼에서는 반사율 적분의 간접 반사 부분을 추가해 실제로 PBR의 힘을 보게 될 것이다.




 





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

OpenGL Figure draw function  (2) 2018.11.06
Learn OpenGL - PBR : IBL(Specular IBL)  (0) 2018.10.21
Learn OpenGL - PBR : Lighting  (0) 2018.10.20
Learn OpenGL - PBR : Theory  (0) 2018.10.19
Learn OpenGL - Advanced Lighting : SSAO  (0) 2018.10.11