Post Lists

2018년 8월 18일 토요일

Advanced Lighting - SSAO (10)

https://learnopengl.com/Advanced-Lighting/SSAO

SSAO
우리는 basic lighting tutorial에서 그것을 간단히 다루었다: ambient lighting. Ambient lighting은 Ambient lighting은 우리가 빛의 scattering을 재현하기 위해 scene의 전체 lighting에 우리가 더한 고정된 light constant이다. 현실에서, 빛은 다양한 강도로 모든 종류의 방향으로 흝뿌려진다. 그래서 한 scene의 간접적으로 비춰지는 부분들은 또한 다양한 강도를 가져야만 한다, 상수 ambient component 대신에. 간접적인 lighting의 유형은 서로에게 가까이 있는 주름, 구멍, 그리고 표면들을 어둡게하여 indirect lihgting을 근사하려고 한 ambient occlusion라고 불린다. 이러한 지역들은 크게 주변 geometry에 의해 가려지고 따라서 광선들은 달아날 공간이 더 적다. 그러므로 그 지역은 더 어둡게 된다. 빛이 좀 더 어두운 것처럼 보이는지 보기 위해 너의 방의 모퉁이와 주름들을 보아라.

아래는 SSAO가 있고 없는 한 scene의 예시 이미지이다. 주름들 사이에 (ambient) light가 좀 더 가려지는 방식을 눈치채라:

놀랍게 명백한 효과가 아닐지라도, SSAO가 들어간 이미지는 이러한 작은 occlusion-like 디테일 때문에 좀 더 현실감을 느낄 수 있다. 이것은 전체 scene은 더욱 큰 깊이감을 준다.

Ambient occlusion 기법은 비싸다. 그것들이 주변 geometry를 고려해야하기 때문이다. 어떤 이는 occlusion의 양을 정하기 위해 많은 수의 광선을 공간에 있는 각 점에 쏠 수 있다. 그러나 그것은 빠르게 real-time 해결책으로는 컴퓨터 연산적으로는 그럴듯 하지 않다. 2007년에 Crytek은 screen-space ambient occlusion (SSAO)를 그들의 타이틀 Crysis에 사용하기 위해 만들어 냈다. 그 기법은 실제 기하 정보 대신에 occlusion 양을 결정하기 위해 screen-space의 scene의 depth를 사용한다. 이 접근법은 놀랍게도 real ambient occlusion과 비교해서 빠르고 그럴듯한 결과를 준다. 그리고 이것은 real-time ambient occlusion을 근사하는데 사실 상 표준으로 되었다.

screen-space ambient occlusion 뒤에 있는 기본기는 간단하다: 스크린을 채우는 quad에서 각 fragment에 대해, 우리는 fragment의 주변 depth values를 기반으로 occlusion factor를 계산한다. 그 occlusion factor는 그러고나서 그 fragment의 ambient lighting component를 줄이거나 또는 무효화하기 위해 사용된다. 그 occlusion factor는 fragment position을 둘러싸는 sphere sample kernel에서 많은 depth samples들을 취하여 얻어지고, 현재 fragment의 depth value와 샘플들 각각을 비교한다. 그 fragment의 depth보다 더 높은 depth value를 가진 샘플들의 개수는 occlusion factor를 나타낸다.

geometry안에 있는 회색의 depth samples들 각각은 전체 occlusion factor에 기여한다; 우리가 geometry안에 있는 더 많은 샘플들을 찾을수록, 그 fragment가 결국 받는 ambient lighting은 더 줄어든다.

그 효과의 퀄리티와 정확도는 우리가 취하는 주변 샘플들의 개수와 관련있는 것은 명백하다. 만약 그 샘플의 개수가 너무 낮다면, 그 정확도는 급격하게 감소하고, 우리는 banding이라는 artifact를 얻게된다; 만약 그것이 너무 높다면, 우리는 성능을 잃게 된다. 우리는 sample kernel에 몇 가지 randomness를 도입하여 검증하기 위해 우리가 가지는 샘플들의 양을 줄일 수 있다. 무작위로 각 fragment마다 그 샘플 커널을 회전시켜, 우리는 좀 더 작은 양의 샘플로 고 퀄리티의 결과를 얻을 수 있다. banding effect와 randomness가 결과에 갖는 영향을 보여주는 (John Chapman 제공) 한 이미지가 아래에 있다:

너가 볼 수 있듯이, 비록 우리가 낮은 샘플의 개수로 인해 SSAO 결과에 banding을 가질지라도, 몇 가지 randomness를 도입하여, 그 banding effects는 완전히 사라진다.

Crytek에 의해 개발된 SSAO 방식은 어떤 시각적 스타일을 가진다. 사용된 sample kernel이 구이기 때문에, 그것은 평평한 벽이 회색으로 보이게 만든다. 커널 샘플들의 절반이 주변 환경에 있기 때문이다. 아래는 이 회색 느낌을 명백히 묘사하는 크라이시스의 screen-space ambient occlusiong의 이미지이다.

그 이유 때문에, 우리는 sphere sample kernel를 사용하지 않을 것이지만, 오히려 표면의 법선 벡터를 향하는 반구 샘플 커널을 사용할 것이다.

이 normal-oriented hemisphere 주변을 샘플링하여, 우리는 fragment의 밑에 있는 geometry를 occlusion factor에의 기여로 고려하지 않게 된다. 이것은 ambient occlusion의 회색 느낌을 제거하고, 일반적으로 좀 더 현실적인 결과를 만들어낸다. 이 SSAO 튜토리얼은 normal-oriented hemisphere 방식에 기반을 두고, John Chapman의 멋진 SSAO tutorial의 다소 변형된 버전이다.

Sample buffers
SSAO는 기하 정보를 요구한다. 우리가 한 fragment의 occlusion factor를 결정할 방법이 필요하기 때문이다. 각 fragment에 대해 우리는 다음의 데이터가 필요할 것이다:

  • fragment마다의 position vector
  • fragment마다의 normal vector
  • fragment마다의 albedo color
  • sample kernel
  • sample kernel를 회전시키기 위해 사용된 fragment 마다의 random rotation vector
fragment마다의 view-space position을 사용하여, 우리는 fragment의 view-space surface normal 주위로 샘플 반구 커널을 향하도록 할 수 있고, 다양한 오프셋에서 포지션 버퍼 텍스쳐를 샘플링하기위해 이 커널을 사용한다. 각 fragment마다의 kernel sample에 대해, 우리는 occlusion의 양을 비교하기 위해 그것의 깊이와 position buffer에 있는 깊이를 비교한다. 그 최종 occlusion factor는 그러고나서 최종 ambient lighting component를 제한하기 위해 상요된다. 또한 fragment마다의 rotation vector를 포함하여, 우리는 상당히 우리가 취할 필요가 있는 샘플들의 개수를 줄이다. 우리는 이것을 곧 볼 것이다.

SSAO는 우리가 그것의 효과를 스크린을 채우는 2D quad에서 각 fragmet에서 계산하는 screen-space technique이지만, 이것은 우리가 scene의 기하 정보를 갖지 않는다는 것을 의미한다. 우리가 할 수 있는 것은, fragment 마다의 기하정보를 우리가 나중에 SSAO shader에 보내는 screen space texture로 렌더링하는 것이다. 그래서 우리는 fragment마다의 기하 데이터에 접근한다. 만약 너가 이전 튜토리얼을 따라왔다면, 너는 이것이 크게 deferred rendering과 비슷하다는 것을 깨닫게 될 것이고, 그 이유 때문에, SSAO는 완벽히 deferred rendering과 조합이 어울리다. 우리는 이미 G-buffer에 position and normal vectors를 가지고 있기 때문이다.

  Green Box
  이 튜토리얼에서 우리는 deferred shading 튜토리얼의 deferred renderer의 다소 간단화된 버전 위에서 SSAO를 구현할 것이다. 그래서 만약 너가 deferred shading이 무엇인지 확신하지 못한다면, 처음에 그것을 읽도록 해라.

우리는 이미 G-buffer로부터 이용가능한 per-fragment position과 normal data를 가지고 있기 때문에, 그 geometry stage의 fragment shader는 꽤 간단하다:


#version 330 core

layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

struct Material
{
    sampler2D diffuse;
    sampler2D specular;
};

uniform Material material;

void main()
{
    // store the fragment position vector in the first gbuffer texture
    gPosition = FragPos;
    // also store the per-fragment normals into the gbuffer
    gNormal = normalize(Normal);
    // and the diffuse per-fragment color
    gAlbedoSpec.rgb = texture(material.diffuse, TexCoords).rgb;
    // store specular intensity in gAlbedoSpec's alpha component
    gAlbedoSpec.a = texture(material.specular, TexCoords).r;
}

SSAO는 occlusion이 visible view를 기반으로 계산되는 screen-space 기법이기 때문에, view-space에서 알고리즘을 구현하는 것이 맞다. 그러므로, geometry stage의 vertex shader에 의해 공급되는 FragPos는 view space로 변환된다. 모든 나중의 계산들은 또한 view-space가 된다. 그래서 G-buffer의 positions과 normals들이 view-space에 있도록 해라 (view matrix에도 또한 곱해지도록).

  Green Box
  Matt Pettineo가 그의 블로그에서 묘사한 것처럼 몇 가지 똑똑한 트릭을 사용하여 depth values로부터 실제 위치 벡터들을 재구성하는 것이 가능하다. 이것은 쉐이더들에서 몇 가지 추가 계산을 요구하지만, 많은 메모리를 소비하는 G-buffer의 position data를 저장하는 것을 아끼게 해준다. 간단한 예제의 목적으로, 우리는 이 최적화를 튜토리얼에서 제외할 것이다.

gPosition colorbuffer texture는 다음과 같이 설정된다:


glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCREEN_WIDTH, SCREEN_HEGIHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

이것은 우리에게 커널 샘플들의 각각을 위해 depth values를 얻도록 하기위해 우리가 사용할 수 있는 position texture이다. 우리가 positions들을 floating point data format으로 저장하는것에 주목해라; 이 방식으로 position 값들은 [0.0, 1.0]의 범위로 clamped되지 않는다. 또한 GL_CLMAP_TO_EDGE의 texture wrapping 방식을 주목해라. 이것은 텍스쳐의 기본 좌표 영역 밖의 screen-space에서 position/depth 값들을 우연히 oversample하지 않게 한다.

다음으로 우리는 실제 hemisphere sample kernel과 그것을 랜덤하게 회전시킬 방법이 ㅣㄹ요하다.

Normal-oriented hemisphere
우리는 표면의 normal를 따라서 향하는 많은 샘플들을 생성할 필요가 있다. 우리가 간단히 그의 튜토리얼의 시작에서 이야기 했듯이, 우리는 반구를 형성하는 샘플들을 생성하기를 원한다. 각 surface normal direction에 대해 sample kernel를 생성하는 것은 어렵고 그럴듯 하지 않기 때문에, 우리는 tangent space에서 sample kernel를 생성할 것이다. 양의 z방향을 가리키는 normal vector와 함께.

우리가 단위 반구를 가지고 있다고 가정하여, 우리는 다음과 같이 최대 64개의 샘플 값을 가진 sample kernel를 얻을 수 있다:


std::uniform_real_distribution<float> randomFloats(0.0, 1.0); // random floats between 0.0 - 1.0
std::default_random_engine generator;
std::vector<glm::vec3> ssaoKernel;
for (unsigned int i = 0; i < 64; ++i)
{
 glm::vec3 sample(randomFloats(generator) * 2.0 - 1.0,
  randomFloats(generator) * 2.0 - 1.0,
  randomFloats(generator));
 sample = glm::normalize(sample);
 sample *= randomFloats(generator);
 float scale = (float)i / 64.0;
 ssaoKernel.push_back(sample);
}

우리는 tangent space에서 -1.0 ~ 1.0 사이의 x와 y방향을 다양하게 하고, 0.0 ~ 1.0 사이의 샘플의 z방향을 다양하게 한다. (만약 우리가 z방향을 -1.0 ~ 1.0으롷 ㅏㄴ다면 또한, 우리는 sphere sample kernel를 갖게 될 것이다). sample kernel이 표면의 normal을 따라 향하게 될 것이기 때문에, 최종 sample vectors는 모두 hemisphere로 될 것이다.

현재, 모든 샘플들은 랜덤하게 샘플 커널에서 분산되어있지만, 우리는 원점에 더 가까운 kernel samples들을 분산시키기 위해 더욱 큰 가중치를 실제 fragment에 가까운 occlusions에 두고 싶다. 우리는 이것을 accelerating interpolation function으로 할 수 있다:


 scale = lerp(0.1f, 1.0f, scale * scale);
 sample *= scale;
 ssaoKernel.push_back(sample);
}

거기에서 lerp는 다음고 같이 정의된다:


float lerp(float a, float b, float f)
{
 return a + f * (b - a);
}

이것은 우리에게 대부분의 샘플들을 그것의 원점에 가깝게 배치하는 kerenl distribution을 준다.

커널 샘플들 각각은 주번 geometry를 샘플링하는 view-space fragment position을 offset하기 위해 사용될 것이다. 우리는 현실적인 겨로가를 얻기위해 view-space에서 꽤 많은 샘플들이 필요하다. 그리고 그것은 성능에 너무 무거울 것이다. 그러나, 만약 우리가 몇 가지 semi-random rotation/noise를 per-fragment basis에 도입한다면, 우리는 크게 요구되는 샘플들의 개수를 줄일 수 있다.

Random kernel rotations
몇 가지 randomness를 샘플 커널들에 도입하여, 우리는 좋은 결과를 얻는데 필요한 샘플들의 개수를 크게 줄인다. 우리는 한 scene의 각 fragment에 대해 random rotation vector를 만들 수 있지만, 그것은 빠르게 메모리를 잡아먹는다. 우리가 screen에 대해 타일링하는 random rotation vectors의 작은 텍스쳐를 만드는게 좀 더 좋다.

우리는 tangent-space surfcae normal를 향하는 random rotation vectors의 4x4 array를 만든다:


std::vector<glm::vec3> ssaoNoise;
for (unsigned int i = 0; i < 16; ++i)
{
 glm::vec3 noise(randomFloats(generator) * 2.0 - 1.0,
  randomFloats(generator) * 2.0 - 1.0,
  0.f);
 ssaoNoise.push_back(noise);
}

sample kernel이 tangent space에서 양의 z방향을 향하기 때문에, 우리는 z component를 0.0으로 둔다. 그래서 우리는 z축에 대해 회전시킨다.

우리는 그러고나서 random rotation vectors를 가지는 4x4 texture를 생성한다; 그것의 wrapping method가 GL_REPEAT가 되도록 설정해라. 그래서 그것은 적절히 screen에 대해 타일링한다.


unsigned int noiseTexture;
glGenTextures(1, &noiseTexture);
glBindTexture(GL_TEXTURE_2D, noiseTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

우리는 이제 SSAO를 구현하는데 필요한 모든 관련된 input data를 가진다.

The SSAO shader
SSAO shader는 생성된 fragments 각각의 occlusion value를 계산하는 2D 스크린을 채우는 quad에 대해 작동한다. (최종 lighting shader에서 사용을 위해). 우리가 SSAO stage의 결과를 저장할 필요가 있기 때문에, 우리는 아직 다른 framebuffer object를 만든다:


unsigned int ssaoFBO;
glGenFramebuffers(1, &ssaoFBO);
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);

unsigned int ssaoColorBuffer;
glGenTextures(1, &ssaoColorBuffer);
glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCREEN_WIDTH, SCREEN_HEGIHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBuffer, 0);

ambient occlusion 결과는 single grayscale value이기 때문에, 우리는 오직 텍스쳐의 red component만 필요하다. 따라서 우리는 그 colorbuffer의 internal format을 GL_RED로 설정한다.

SSAO를 렌더링하는 완전한 프로세스는 그러고나서 이것처럼 보인다:


그 shaderSSAO 쉐이더는 입력으로서 관련된 G-buffer 텍스쳐, noise texture 그리고 normal-oriented hemisphere kernel samples를 받는다.


#version 330 core
out float FragColor;

in vec2 TexCoords;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D AlbedoSpecular;
uniform sampler2D texNoise;

uniform vec3 samples[64];
uniform mat4 projection;

// tile noise texture over screen based on screen dimensions divided by noise size
const vec2 noiseScale = vec2(800.0/4.0, 600.0/4.0);

void main()
{

}

여기에서 주목해야할 것은 noiseScale 변수이다. 우리는 스크린에 대해 모두 noise texture를 타일링하기를 원하지만, TexCoords는 0.0 ~ 1.0 사이이기 때문에, texNoise 텍스쳐는 모두 타일링하지 못할 것이다. 그래서 우리는 screen의 크기를 noise texture size로 나누어서 우리가 얼마나 많이 TexCoords 좌표를 스케일링 해야하는지를 계산할 것이다:

vec3 fragPos = texture(gPosition, TexCoords).xyz;
vec3 normal = texture(gNormal, TexCoords).rgb;
vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz;

우리가 texNoise의 tiling 파라미터로 GL_REPEAT를 설정했기 때문에, 그 랜덤 값들은 스크린에 대해 모두 반복될 것이다. fragPos와 normal vector와 함께, 우리는 그러고나서 어떤 벡터를 tangent-space에서 view-space로 변환하는 TBN matrix를 만들기에 충분한 데이터를 갖는다:


vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
vec3 bitangent = cross(normal, tangent);
mat3 TBN = mat3(tangent, bitangent, normal);

Gramm-Schmidt process라고 불리는 프로세스를 사용하여, 우리는 orthogonal basis를 만든다. 그리고 그것은 매번 randomVec의 값을 기반으로 다소 휘어진다. 우리가 tangent vector를 구성하기 위해 random vector를 사용하기 때문에, geometry의 surface와 정확히 정렬된 TBN matrix를 가질 필요가 없다는 것에 주목해라. 따라서 per-vertex tangent (and bitangent) vectors가 필요없다.

다음으로 우리는 kernel samples들 각각에 대해 반복하고, 샘플들을 tangent to view-space로 변환하고, 그것들을 현재 fragment position에 더해고, fragment position의 depth와 view-space position buffer에 저장된 sample depth와 비교한다. 이것을 차근차근 이야기해보자.


float occlusion = 0.0;
for(int i = 0; i < kernelSize; ++i)
{
 // get sample position
 vec3 f_sample = TBN * samples[i]; // From tangent to view-space;
 f_sample = fragPos + f_sample * radius;

}

여기에서 kernerSize와 radius는 그 효과를 조절하기위해 우리가 사용할 수 있는 변수들이다; 이 경우에 각각 64와 0.5이다. 각 반복마다, 우리는 처음에 개별 sample을 view-space로 변환한다. 그러고나서 우리는 view-space kernel offset sample을 view-space fragment position에 더한다. 우리는 그러고나서 SSAO의 효과적인 샘플 반경을 증가시키기 위해 (또는 줄이기 위해) offset sample에 radius를 곱한다.

다음으로 우리는 sample을 screen-space로 변환하길 원한다. 그래서 우리는 마치 우리가 그것의 위치를 직접적으로 스크린에 렌더링하는 것처럼 sample의 position/depth value를 샘플링한다. 그 벡터는 현재 view-space에 있기 때문에, 우리는 projection matrix uniform을 상요하여 그것을 clip-space로 변환할 것이다:


vec4 offset = vec4(f_sample, 1.0);
offset = projection * offset;  // from view to clip-space
offset.xyz /= offset.w; // perspective divide
offset.xyz = offset.xyz * 0.5 + 0.5; // transform to range 0.0 - 1.0

그 변수가 clip-space로 변환된 후에, 우리는 그것의 xyz 컴포넌트들을 w 컴포넌트로 나누어서 perspective divide step을 수행한다. 최종 normalized device 좌표는 그러고나서 [0.0, 1.0]의 범위로 변환된다. 그래서 우리는 그것들을 position texture에 샘플링하기 위해 사용할 수 있다:

  float sampleDepth = texture(gPosition, offset.xy).z;

우리는 그 offset vector의 x와 y 컴포넌트를 viewer의 관점으로부터 보여지는 샘플의 z값 또는 depth를 얻기 위해 position texture를 샘플링하려고 사용한다 (첫 번째 가려지지 않은 보이는 fragment이다). 우리는 그러고나서 샘플의 현재 depth value가 저장된 depth value보다 더 큰지를 비교하고, 만약 그렇다면 우리는 최종 contribution factor에 기여한다:

  occlusion += (sampleDepth >= sample.z + bias ? 1.0 : 0.0);

우리가 여기에서 원래의 fragment의 depth value에 작은 bias를 더한 것을 주목해라. (그 예제에서는 0.025로 설정되어있다). 한 bias는 항상 필수적이지는 않지만, 그것은 시작적으로 SSAO effect를 조절하는데 도움을 준다. 그리고 scene의 복잡성을 기반으로 발생할지도 모르는 acne effects를 해결한다.

우리는 아직 완전히 끝난 것이 아니다. 우리가 고려해야할 작은 문제가 여전히 있기 때문이다. 한 fragment가 한 표면의 edge에 가까이 정렬되는 ambient occlusion에 대해 검증될 때 마다, 그것은 또한 그 test surface 멀리 뒤에 있는 surfaces들의 depth values들을 고려할 것이다. 우리는 이것을 다음의 이미지 (John Chapman 제공)가 보여주듯이 range check를 도입하여 해결할 수 있다:

우리는 그것의 fragment의 depth valueㄴ가 sample의 radius안에 있다면, 한 fragment가 그 occlusion factor에 기여하도록 하는 range check를 도입한다. 우리는 마지막 줄을 이렇게 바꾼다:


float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));
occlusion += (sampleDepth >= f_sample.z + bias ? 1.0 : 0.0) * rangeCheck;

여기에서 우리는 GLSL의 smoothstep function을 사용했다. 그것은 부드럽게 그것의 세 번째 파라미터를 첫 번째와 두 번째 파라미터의 범위로 보간한다. 만약 그것의 첫 번째 파라미터 이하라면 0.0을 반환하고, 두 번째 파라미터 이상이라면 1.0을 반환한다. 만약 그 depth 차이가 radius 사이에 있다면, 그것의 값은 다음의 곡선에 의해 0.0과 1.0 사이로 부드럽게 보간된다:

만약 우리가 만약 depth values가 radius 밖에 있다면 occlusion contributions을 급작스럽게 제거하는 hard cut-off range check를 사용하려 한다면, 우리는 명백한 (매력적이지 않은) 경계선을 볼 것이다. 거기에서 range check가 적용된다.

마지막 단계로서, 우리는 kernel의 사이즈로 occlusion contribution을 normalize하고, 그 결과를 만들어낸다. 우리가 occlusion factor를 1.0에서 뺀 것에 주목해라. 그래서 우리는 직접적으로 ambient lighting component를 스케일하기 위해 occlusion factor를 쓸 수 있다.

  occlusion = 1.0 - (occlusion / kernelSize);
  FragColor = occlusion;

만약 우리가 우리가 가장 좋아하는 nanosuit model을 잠깐 낮잠자게 하는 scene을 상상한다면, 그 ambient occlusion shader는 다음의 텍스쳐를 만들어낸다:


우리가 볼 수 있듯이, ambient occlusion은 훌륭한 깊이감을 만들어 낸다. ambient occlusion texture로, 우리는 벌써 명백히 그 모델이 바닥 위에서 떠다니는 대신에 정말 바닥에 누워있는 것을 알 수 있다.

그것은 여전히 완벽해 보이지 않는다. noise texture의 반복되는 패턴이 명백히 보이기 때문이다. 부드러운 ambient occlusion 결과를 만들기위해, 우리는 ambient occlusion texture를 blur처리 할 필요가 있다.

Ambient occlusion blur
SSAO pass와 lighting pass 사이에, 우리는 처음에 SSAO texture를 블러하길 원한다. 그래서 blur 결과를 저장하기 위한 다른 framebuffer를 만들어보자:


unsigned int ssaoBlurFBO;
glGenFramebuffers(1, &ssaoBlurFBO);
glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO);

unsigned int ssaoBlurTexture;
glGenTextures(1, &ssaoBlurTexture);
glBindTexture(GL_TEXTURE_2D, ssaoBlurTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCREEN_WIDTH, SCREEN_HEGIHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoBlurTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "SSAO Framebuffer not complete!" << std::endl;

타일링된 랜덤 벡터 텍스쳐가 우리에게 일관된 randomness를 주기 때문에, 우리는 매우 간단한 blur shader를 만드는 우리의 이점에 대해 이 특징을 사용할 수 있다:


#version 330 core
out float FragColor;

in vec2 TexCoords;

uniform sampler2D ssaoInput;

void main()
{
 vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0));
 float result = 0.0;
 for(int x = -2; x < 2; ++x)
 {
  for(int y = -2; y < 2; ++y)
  {
   vec2 offset = vec2(float(x), float(y)) * texelSize;
   result += texture(ssaoInput, TexCoords + offset).r;
  }
 }
 FragColor = result / (4.0 * 4.0);
}

여기에서 우리는 noise texture의 크기와 동일한 양으로 SSAO texture를 샘플링하면서 -2.0 2.0 사이의 주변 SSAO texels들을 가로지른다. 우리는 주어진 텍스쳐의 크기를 vec2로 반환하는 textureSize를 사용하여 단일 텍셀의 정확한 크기로 각 텍스쳐 좌표를 지정한다. 우리는 간단하지만 효과적인 블러를 얻기위해 얻어진 결과를 평균화한다:




그리고 다왔다. per-fragment occlusion data가 있는 텍스쳐이다. lighting pass에서 사용할 준비를 하자.

Applying ambient occlusion
lighting 식에 대해 occlusion factors를 적용하는 것은 매우 쉽다: 우리가 해야할 모든 것은 per-fragment ambient occlusion factor를 lighting의 ambient component에 곱하는 것이고 끝났다. 만약 우리가 이전 튜토리얼의 Blinn-Phong deferred lighting을 가지고와서 그것을 조금 수정한다면, 우리는 다음의 fragment shader를 얻는다:


#version 330 core

out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedoSpec;
uniform sampler2D SSAO;

struct Light
{
    vec3 Position;
    vec3 Color;

    float Linear;
    float Quadratic;
    float Radius;
};

uniform Light light;
uniform vec3 viewPos;

// view-space lighting
void main()
{

 // retrieve data from G-buffer
    vec3 FragPos = texture(gPosition, TexCoords).rgb;
    vec3 Normal = texture(gNormal, TexCoords).rgb;
    vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb;
    float Specular = texture(gAlbedoSpec,TexCoords).a;
 float AmbientOcclusion = texture(SSAO, TexCoords).r;

 // ambient
 vec3 ambient = 0.3 * Albedo * AmbientOcclusion; ;

 // diffuse
 vec3 viewDir = normalize(-FragPos);
 vec3 lightDir = normalize(light.Position - FragPos);
 vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Albedo * light.Color;

 // specular
 vec3 halfwayDir = normalize(viewDir + lightDir);
 float spec = pow(max(dot(Normal, halfwayDir), 0.0), 64.0);
 vec3 specular = spec * light.Color;
 
 float dist = length(light.Position - FragPos);
 float attenuation = (1.0) / (1.0 + light.Linear * dist + light.Quadratic * dist *dist);
 diffuse *= attenuation;
 specular *= attenuation;
 
 vec3 result = ambient + diffuse + specular;

 const float gamma = 2.2;
 const float exposure = 1.0;
 vec3 hdrColor = result;
 
 // Reinhard tone mapping
 // vec3 mapped = hdrColor / (vec3(1.0) + hdrColor);

 // Exposure tone mapping
 vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
 
 // gamma correction
 mapped = pow(mapped, vec3(1.0 / gamma));

 FragColor = vec4(vec3(hdrColor), 1.0);
}

(view-space로의 변화 말고) 이전의 lighting 구현과 비교해서 우리가 정말 바꿔야할 유일한 것은 scene의 ambient component에 AmbientOcclusion value를 곱하는 것이다. scene에서 하나의 파란 빛깔의 point light로 우리는 다음의 결과를 얻는다: (나는 빛의 색깔을 바꾸었다)



너는 여기에서 데모 scene의 풀 소스코드를 볼 수 있다.

SSAO는 scene의 유형을 기반으로 그것의 파라미터들을 조절하는 것에 크게 의존하는 매우 customizable한 효과이다. 모든 scene의 타입에 대해 parameters들의 완벽한 조합은 없다; 몇 scene들은 오직 작은 radius로만 작동된다. 반면에 몇몇 scene들은 더 큰 radius를 요구하고 현실적으로 보이기 위해 더 많은 sample count를 요구한다. 현재 데모는 조금 많은 64개의 샘플들을 사용한다. 더 작은 커널 사이즈로 놀아보아라, 그리고 좋은 결과를 얻으려고 노력해라.

너가 조절할 수 있는 몇가지 파라미터들 (예륻를어 uniforms을 사용하여) : kernel size, radius, bias and/or the size of the noise kernel. 너는 또한 최종 occlusion value를 사용자가 정의한 지수승할 수 있다. 그것의 강도를 높이기 위해:

SSAO의 customizability를 이해하기 위해 다른 scene들과 parameters들을 가지고 놀아보아라.

비록 SSAO가 너무 명백히 눈에 띄지않는 미묘한 효과일지라도, 그것은 적절히 lighted된 scenes에 많은 양의 현실성을 더하고 명백히 너가 너의 toolkit에 가져야할 기법이다.

Additional resources

  • SSAO Tutorial : John Chapman에 의한 우수한 SSAO tutorial; 이 튜토리얼의 코드와 기법들의 많은 부분이 그의 아티클을 기반으로 한다.
  • Know your SSAO artifacts : SSAO specific artifacts를 개선시키는 훌륭한 자료
  • SSAO With Depth Reconstruction : depth로만 position vectors를 재구축하는 것에 대한 OGLDev의 SSAO의 확장 튜토리얼. 이것은 우리에게 G-buffer의 비싼 position vectors를 저장하는 것을 아껴준다.










댓글 없음:

댓글 쓰기