본문 바로가기

Game/Graphics

Learn OpenGL - Advanced Lighting : SSAO

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


SSAO(Screen Space Ambient Occlusion)

* 사물에 의해 생기는 빛의 감쇠를 리얼타임으로 나타내는 3D 쉐이딩기법


 우리는 기본 조명 튜토리얼에 대해 간략히 설명했다. 주변 조명은 빛의 산란을 시뮬레이션하기 위해 장면의 전반적인 조명에 추가하는


고정된 광원 상수이다. 실제로 빛은 다양한 강도의 방향으로 모든 종류의 산란을 일으키므로 장면의 간접 조명 부분은 일정한


주변 구성 요소 대신 다양한 강도를 가져야한다. 한 가지 유형의 간접 조명 근사법은 서로 가깝게 있는 주름, 구멍 및 표면을 어둡게해


간접 조명을 근사하려고하는 Ambient occlusion이라고 한다. 이 영역은 주변 geometry에 의해 대부분 가려지므로 광선은 탈출할


곳이 적으므로 영역이 더 어둡게 나타난다. 빛이 조금 더 어둡게 보일 수 있도록 방의 구석과 주름을 살펴보아라.



 아래는 SSAO가 있거나 없는 장면의 이미지 예제이다. 예를 들어 (주위) 빛이 더 많이 가려지는 주름 사이에서 특히 주의해라:


Example image of SSAO with and without


 엄청나게 명백한 효과는 아니지만 SSAO가 적용된 이미지는 이러한 작은 occlusion(폐색)과 같은 세부 정보로 인해 훨씬 더 사실적으로


느껴지므로 전체 장면에 더 깊은 느낌을 준다.



 Ambient Occlusion 기법은 주변 geometry를 고려해야하므로 많은 비용이 든다. 공간의 각 점에 대해 많은 수의 광선을 쏘아 교합량을


결정할 수 있지만, 이는 실시간 솔루션에 대해 컴퓨터로는 실행 불가능하다. 2007년 Crytek은 Crysis에 사용하기 위해 SSAO를 발표했다.


이 기술은 화면 공간에서 장면의 깊이를 사용해 실제 기하학적 데이터 대신 오클루전 양을 결정한다.


이 방식은 real ambient occlusion과 비교해 믿을 수 없을만큼 빠르며 그럴듯한 결과를 제공하므로 실시간 ambient occlusion을 근사화하는


사실상의 표준이 된다.



 screen-space ambient occlusion의 기본 원리는 간단하다. screen-filled된 쿼드의 각 부분에 대해 조각의 주변 깊이 값을 기반으로 하는


폐색 요인을 계산한다. 그런 다음 오클루전 인수는 조각의 주변 조명 구성 요소를 줄이거나 무효화하는데 사용된다.


occlusion factor는 조각 위치를 둘러싸는 구형 샘플 커널에서 여러 깊이 표본을 취해 각 표본을 현재 단편의 깊이 값과 비교해 얻어진다.


조각의 깊이보다 깊이 값이 큰 샘플의 수는 교합 요인을 나타낸다.


Image of circle based SSAO technique as done by Crysis


 geometry 내부에 있는 회색 깊이 샘플 각각은 총 폐색 요인에 영향을 준다. geometry 내부에서 더 많은 샘플을 찾을수록 조각이 최종적으로


받아야 할 주변 조명이 적다.



 효과의 품질과 정밀도는 우리가 취하는 주변 샘플의 수와 직접적으로 관련이 있음을 분명히 알 수 있다. 샘플 수가 너무 적으면 정밀도가


크게 감소하고 banding이라는 인공물이 생긴다. 너무 높으면 성능이 떨어진다. 샘플 커널에 임의성을 도입해 테스트해야하는 샘플의 양을


줄일 수 있다. 각 조각을 무작위로 샘플 커널을 회전시킴으로써 훨씬 적은 양의 샘플로 고품질의 결과를 얻을 수 있다. 임의성은 눈에 띄는


노이즈 패턴을 유발해 결과를 흐리게 처리해야만 가격이 측정된다. 아래는 밴딩 효과와 그 결과에 대한 무작위성을 보여주는 이미지이다.


The SSAO image quality with multiple samples and a blur added


 보시다시피, 샘플 수가 적기 때문에 SSAO 결과에 눈에 띄는 줄무늬가 생기더라도 무작위성을 도입해 밴딩 효과가 완전히 사라졌다.



 Crytek에서 개발한 SSAO 방법은 특정 시각적 스타일을 가진다. 사용된 샘플 커널은 구형이었기 때문에 커널 샘플의 절반이


주변 geometry로 끝나면 평면 벽이 회색으로 보인다. 아래는 이 회색 느낌을 명확히 묘사한 Crysis의 screen-space ambient occlusion의


이미지이다.


Screen space ambient occlusion in the Crysis game by Crytek showing a gray feel due to them using a sphere kernel instead of a normal oriented hemisphere sample kernel in OpenGL


 이런 이유로 우리는 구체 샘플 커널을 사용하지 않고, 표면의 법선 벡터를 따라 배열된 반구 샘플 커널을 사용할 것이다.


Image of normal oriented hemisphere sample kernel for SSAO in OpenGL


 이 정상 지향적인 반구 주위를 샘플링함으로써 우리는 조각의 기본 기하학을 교합 요인에 대한 기여로 간주하지 않는다. 이렇게하면


ambient occlusion의 회색 느낌이 제거되고 일반적으로 보다 사실적인 결과가 생성된다. 


이 SSAO 튜토리얼에서는 normal-oriented hemisphere method(일반 지향 반구법)과 John Chapman의 훌륭한 SSAO 튜토리얼을


약간 수정한 버전을 기반으로 한다.




Sample buffers


 SSAO는 조각의 폐색 요인을 결정하는 방법이 필요하기 때문에 기하학적 정보가 필요하다. 각 조각마다 다음과 같은 데이터가 필요하다.



- 조각당 위치 벡터


- 조각당 법선 벡터


- 조각당 albedo color


- sample kernel


- sample kernel을 회전시키는데 사용되는 조각당 랜덤 순환 벡터


 fragment-view-space 위치를 사용하면 조각의 view-space 표면 법선을 중심으로 샘플 반구형 커널의 방향을 맞출 수 있고,


이 커널을 사용해 다양한 오프셋에서 위치 버퍼 텍스처를 샘플링 할 수 있다. 각 조각별 커널 샘플에 대한 위치 버퍼의 깊이들을 비교해


폐색 양을 결정한다. 최종적인 주변 조명 구성 요소를 제한하기 위해 마지막 폐색 요인이 사용된다. 또한, 조각당 회전 벡터를 포함시킴으로써


곧 볼 수 있는 샘플 수를 크게 줄일 수 있다.


An overview of the SSAO screen-space OpenGL technique


 SSAO는 화면 공간 기술이므로 screen-filled된 2D 쿼드에서 각 조각에 미치는 영향을 계산하지만 이는 장면의 기하학적 정보가 없음을


의미한다. 우리가 할 수 있는 것은 기하학적인 조각 데이터를 screen-space 텍스처로 렌더링 한 다음 나중에 SSAO 쉐이더에 전송하는 것이다.


그래서 우리는 조각당 기하학적 데이터에 접근 할 수 있다. 이전 튜토리얼을 따라했다면 deferred 렌더링과 거의 유사하게 보이므로 SSAO는


G-buffer에 위치 및 법선 벡터가 이미 있어서 deferred 렌더링과 함께 완벽하게 적합하다.

이 튜토리얼에서는 deferred shading 튜토리얼의 지연 렌더링의 약간 단순화된 버전 위에 SSAO를 구현할 예정이다. 따라서 deferred 쉐이딩을 처음 읽는 것이 확실하지 않는 경우에는 deferred shading 튜토리얼을 참조해라.

 이미 G-buffer에서 사용할 수 있는 조각당 위치와 일반 데이터가 있으므로 geometry 스테이지의 조각 쉐이더는 매우 간단하다:

#version 330 core
layout (location = 0) out vec4 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

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

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 = vec3(0.95);
}  

 SSAO는 가시적인 보기를 기반으로 폐색을 계산하는 screen-space 기술이므로 view-space에서 알고리즘을 구현하는 것이 좋다.


따라서 기하 스테이지의 정점 쉐이더가 제공하는 FragPos는 뷰 공간으로 변환된다. 추가 계산은 모두 뷰 공간에서 수행되므로 G 버퍼의 위치와


법선이 뷰 공간에 있는지 확인해라 (뷰 행렬도 곱함).

Matt Pettineo가 블로그에서 설명한 것처럼 영리한 트릭을 사용해 깊이 값만으로 실제 위치 벡터를 재구성 할 수 있다. 쉐이더에서 약간의 추가 계산이 필요하지만 G-buffer에 위치 데이터를 저장하지 않아도 되므로 메모리가 많이 소모된다. 간단한 예제를 위해 이러한 최적화를 튜토리얼에서 제외할 것이다.

 gPosition colorbuffer 텍스처는 다음과 같이 구성된다:

glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 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);  

 이것은 각 커널 샘플에 대한 깊이 값을 얻는데 사용할 수 있는 위치 텍스처를 제공한다. 위치는 부동 소수점 데이터 형식으로 저장한다.


이 방법으로 위치 값은 [0.0, 1.0]으로 고정되지 않는다. 또한, GL_CLAMP_TO_EDGE의 텍스처 랩핑 방법에 유의해라.


이렇게하면 텍스처의 기본 좌표 영역 외부의 화면 공간에서 우연히 position/depth 값을 오버 샘플링하지 않게 된다.



 다음으로 실제 반구 샘플 커널과 임의로 회전시키는 방법이 필요하다.



Normal-oriented hemisphere


 우리는 표면의 법선을 따라 많은 수의 샘플을 생성해야한다. 이 튜토리얼의 시작 부분에서 간략하게 논의했듯이 반구를 형성하는 샘플을


생성하려고 한다. 각 표면 법선 방향에 대한 샘플 커널을 생성하는 것이 어렵거나 그럴 가능성이 없으므로, 법선 벡터가 양의 z 방향을


가리키는 탄젠트 공간에서 샘플 커널을 생성할 것이다.


Image of normal oriented hemisphere sample kernel for use in SSAO in OpenGL


 단위 반구가 있다고 가정하면 다음과 같이 최대 64개의 샘플 값을 갖는 샘플 커널을 얻을 수 있다:

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

 우리는 접선 공간에서 x와 y 방향을 -1.0과 1.0 사이에서 변화시키고, 샘플의 z 방향을 0.0과 1.0 사이에서 변화시킨다.


(z 방향을 -1.0과 1.0 사이에서 변화시키면 구형 샘플 커널을 가질 것이다)


샘플 커널이 표면 법선을 따라 배향될 때, 결과 샘플 벡터는 모두 반구에서 끝난다.



 현재 모든 샘플은 샘플 커널에 무작위로 배포되지만 커널 샘플을 원본에 더 가깝게 배포하기 위해 실제 조각에 가까운 폐색에 더 큰 가중치를


두는 편이 좋다. 우리는 가속 보간 함수로 이것을 할 수 있다:

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

 이렇게하면 대부분의 샘플을 원래 위치에 가깝게 배치하는 커널 배포가 가능하다.


SSAO Sample kernels (normal oriented hemisphere) with samples more closer aligned to the fragment's center position in OpenGL


 각 커널 샘플은 뷰 공간 조각 위치를 샘플 주변 geometry로 오프셋하는데 사용된다. 성능에 너무 무거울 수 있는 사실적인 결과를


얻으려면 뷰 공간에서 많은 샘플이 필요하다. 그러나 조각 단위로 semi-random rotation/noise를 도입 할 수 있다면 필요한 샘플 수를 줄일 수 있다.




Random kernel rotations


 좋은 결과를 얻기 위해 필요한 샘플 수를 줄여라. 우리는 장면의 각 조각에 대해 임의의 회전 벡터를 만들 수 있지만 메모리를 빠르게 먹는다.


우리가 화면 위에 타일링하는 임의의 회전 벡터의 작은 질감을 만드는 것이 더 합리적이다.



 우리는 접선 공간 표면 법선을 중심으로 4x4 랜덤 회전 벡터 배열을 만든다:

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.0f); 
    ssaoNoise.push_back(noise);
}  

 샘플 커널은 접선 공간에서 양의 z 방향을 따라 배향되므로 z 성분을 0.0으로 두어 z 축 주위를 회전한다.



 그런 다음 임의의 회전 벡터를 보유하는 4x4 텍스처를 만든다. 그것의 포장 방법을 GL_REPEAT로 설정해 화면 위에 제대로 바둑판 식으로 배열하도록


해라.

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를 구현하는데 필요한 모든 관련 입력 데이터가 있다.



The SSAO shader


 SSAO 쉐이더는 생성된 조각(final lighting shader에서 사용하기 위한)의 폐색 값을 계산하는 2D screen-filled 쿼드에서 실행된다.


SSAO 단계의 결과를 저장할 필요가 있을 때 우리는 또 다른 프레임 버퍼 객체를 만든다.

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, SCR_WIDTH, SCR_HEIGHT, 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 결과가 단일 그레이 스케일 값이기 때문에 텍스처의 빨강 컴포넌트만 필요하므로 색상 버퍼의 내부 형식을 GL_RED로 설정한다.



 SSAO를 렌더링하기 위한 전체 과정은 다음과 같이 보인다:equest

// geometry pass: render stuff into G-buffer
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    [...]
glBindFramebuffer(GL_FRAMEBUFFER, 0);  
  
// use G-buffer to render SSAO texture
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
    glClear(GL_COLOR_BUFFER_BIT);    
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, gPosition);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, gNormal);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, noiseTexture);
    shaderSSAO.use();
    SendKernelSamplesToShader();
    shaderSSAO.setMat4("projection", projection);
    RenderQuad();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
  
// lighting pass: render scene lighting
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shaderLightingPass.use();
[...]
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
[...]
RenderQuad();  

 shaderSSAO 쉐이더는 관련 G-buffer 텍스처, noise 텍스처 및 normal-oriented hemisphere kernel 샘플을 입력으로 사용한다.

#version 330 core
out float FragColor;
  
in vec2 TexCoords;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
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(1280.0/4.0, 720.0/4.0); // screen = 1280x720

void main()
{
    [...]
}

 여기서 주목해야 할 것은 noiseScale 변수이다. 우리는 화면 전체에 noise texture를 바로그 싶지만, TexCoords가 0.0과 1.0 사이에서 다양하기


때문에 texNoise 텍스처는 전혀 타일링되지 않는다. 그래서 우리는 스크린의 크기를 noise texture 크기로 나눔으로써 TexCoords 좌표를 스케일링


해야하는 정도를 계산할 것이다:

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

 texNoise의 타일링 매개 변수를 GL_REPEAT로 설정하면 무작위 값이 화면 전체에 반복된다. fragPos와 normal vector와 함께 우리는


tangent-space에서 view-space로 모든 벡터를 변환하기 위한 TBN 행렬을 생성하기에 충분한 데이터를 가지고 있다.

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

 Gramm-Schmidt 프로세스라고 불리는 프로세스를 사용해 우리는 randomVec의 값에 따라 약간 기울어질 때마다 직교 기초를 만든다.


접선 벡터를 생성하기 위해 무작위 벡터를 사용하기 때문에 TBN 행렬을 geometry 표면에 정확히 정렬 할 필요가 없어서


정점당 접선 벡터가 필요하지 않다.



 다음으로 각 커널 샘플을 반복하고, 샘플을 접선에서 뷰 공간으로 변환하고, 현재의 단편 위치에 추가하고, 단편 위치의 깊이와 뷰 공간


위치 버퍼에 저장된 샘플 깊이를 비교한다. 이를 단계적으로 논의해보겠다.

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

 여기 kernelSize와 radius는 효과를 조정할 때 사용 할 수 있는 변수이다. 이 경우 각각 64와 0.5이다. 반복 할 때마다 먼저 각 샘플을 뷰 공간으로


변환한다. 그런 다음 뷰 공간 커널 오프셋 샘플을 뷰 공간 조각 위치에 추가한다. 그런 다음 오프셋 샘플에 반지름을 곱해 SSAO의 유효 샘플 반경을


늘리거나 줄인다.



 다음으로 샘플을 화면 공간으로 변환해 화면의 위치를 화면에 직접 렌더링하는 것처럼 샘플의 position/depth 값을 샘플링 할 수 있다. 벡터가 현재


뷰 공간에 있기 때문에 먼저 투영 행렬 유니폼을 사용해 클립 공간으로 변환한다.

vec4 offset = vec4(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  

 변수가 클립 공간으로 변환된 후 xyz 구성 요소를 w 구성 요소로 나누어 perspective divide 단계를 수행한다. 결과의 정규화된 장치 좌표는 [0.0, 1.0] 범위로


변환되어 위치 텍스처를 샘플링하는데 사용할 수 있다.

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

 우리는 오프셋 벡터의 x와 y 성분을 사용해 위치 텍스처를 심플링해 뷰어의 시점(최초로 보이지 않는 보이는 부분)에서 보여지는 샘플 위치의 깊이


또는 z 값을 검색한다. 그런 다음 샘플의 현재 깊이 값이 저장된 깊이 값보다 큰지 확인한 후 최종 깊이 요소 값에 추가한다:

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

 기존 조각 깊이 값에 작은 기울기를 추가한다. 기울기는 항상 필요한 것은 아니지만 SSAO 효과를 시각적으로 조정 할 수 있으며 장면의 복잡성을 기반으로


발생할 수 있는 여드름 효과를 해결한다.



 우리는 아직 고려해야 할 작은 문제가 있기 때문에 아직 완전히 완성되지 않았다. 조각이 표면의 가장자리에 가깝게 정렬된 주변 폐색에 대해


테스트 될 때마다 시험면 뒤쪽의 표면 깊이 값도 고려한다. 이 값들은 폐색 요인에 (부정확하게) 기여한다. 우리는 다음 이미지와 같이


범위 검사를 도입해 이를 해결할 수 있다:


Image with and without range check of SSAO surface in OpenGL


깊이 값이 표본의 반경 내에 있으면 조각이 폐색에 기여하는지 확인하는 범위 검사를 도입한다. 마지막 줄을 다음과 같이 변경한다:

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

 여기서는 GLSL의 smoothstep 함수를 사용해 첫번째 매개 변수의 범위와 두 번째 매개 변수의 범위 사이에서 세번째 매개 변수를 부드럽게 보간하고,


첫번째 매개 변수보다 작거나 같은 경우 0.0을, 두번째 매개 변수와 같거나 높은 경우 1.0을 반환한다. 깊이 차이가 반지름 사이에서 끝나면 그 값은


다음 곡선에 의해 0.0과 1.0 사이에서 부드럽게 보간된다:


Image of smoothstep function in OpenGL used for rangecheck in SSAO in OpenGL


 깊이 값이 반경 밖에 있는 경우 갑작스러운 폐색 기여를 제거하는 하드 컷오프 범위 검사를 사용하면 범위 검사가 적용되는 명백한 경계가 표시된다.



 마지막 단계로 커널의 크기에 따라 폐색 기여도를 표준화하고 결과를 출력한다. 우리는 occlusion factor를 1.0에서 빼서 주변 조명 구성 요소의 크기를 조절하기


위해 occlusion factor를 직접 사용할 수 있다.

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

 우리가 선호하는 나노 수트 모델이 약간 낮잠을 자고 있는 장면을 상상해보면 ambient occlusion shader는 다음 텍스처를 생성한다:


Image of SSAO shader result in OpenGL


 우리가 볼 수 있듯이 ambient occlusion은 깊이감을 준다. ambient occlusion 텍스처만 있으면 모델이 실제로는 약간 위에 위치하지 않고 바닥에 실제로


놓여 있음을 분명히 볼 수 있다.



 noise texture의 반복되는 패턴이 선명하게 보이기 때문에 여전히 완벽하지는 않다. 부드러운 ambient occlusion 결과를 생성하려면 ambient occlusion 텍스처를


흐리게 처리해야한다.



Ambient occlusion blur


 SSAO 패스와 라이팅 패스 사이에서 우리는 먼저 SSAO 텍스처를 블러하고 싶다. 그래서 블러 결과를 저장하기 위한 또 다른 프레임 버퍼 오브젝트를 만든다:

unsigned int ssaoBlurFBO, ssaoColorBufferBlur;
glGenFramebuffers(1, &ssaoBlurFBO);
glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO);
glGenTextures(1, &ssaoColorBufferBlur);
glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 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, ssaoColorBufferBlur, 0);

 바둑판식 랜덤 벡터 텍스처가 일관성 있는 무작위성을 제공하기 때문에 이 속성을 사용해 매우 간단한 블러 쉐이더를 만들 수 있다:

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

 여기서 -2.0과 2.0 사이의 주변 SSAO 텍셀을 가로지르며 SSAO 텍스처를 noise 텍스처의 차원과 동일한 양으로 샘플링한다. 주어진 텍스처 크기의 vec2를 반환하는


textureSize를 사용해 각각의 텍스처 좌표를 단일 텍셀의 정확한 크기만큼 오프셋한다. 얻은 결과를 평균해 단순하지만 효과적인 흐림 효과를 얻는다:


Image of SSAO texture with blur applied in OpenGL


 그리고 per-fragment ambient occlusion을 가진 텍스처가 있다. lighting pass에 사용할 준비가 되었다.



Applying ambient occlusion


 조명 방정식에 occlusion factor를 적용하는 것은 매우 쉽다. 조각 주변 occlusion 요인을 조명 주변 구성 요소에 곱하면된다. 


이전 튜토리얼의 Blinn-Phong deferred lighting shader를 사용해 약간 조정하면 다음과 같은 조각 쉐이더가 생성된다:

#version 330 core
out vec4 FragColor;
  
in vec2 TexCoords;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedo;
uniform sampler2D ssao;

struct Light {
    vec3 Position;
    vec3 Color;
    
    float Linear;
    float Quadratic;
    float Radius;
};
uniform Light light;

void main()
{             
    // retrieve data from gbuffer
    vec3 FragPos = texture(gPosition, TexCoords).rgb;
    vec3 Normal = texture(gNormal, TexCoords).rgb;
    vec3 Diffuse = texture(gAlbedo, TexCoords).rgb;
    float AmbientOcclusion = texture(ssao, TexCoords).r;
    
    // blinn-phong (in view-space)
    vec3 ambient = vec3(0.3 * Diffuse * AmbientOcclusion); // here we add occlusion factor
    vec3 lighting  = ambient; 
    vec3 viewDir  = normalize(-FragPos); // viewpos is (0.0.0) in view-space
    // diffuse
    vec3 lightDir = normalize(light.Position - FragPos);
    vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color;
    // specular
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    float spec = pow(max(dot(Normal, halfwayDir), 0.0), 8.0);
    vec3 specular = light.Color * spec;
    // attenuation
    float dist = length(light.Position - FragPos);
    float attenuation = 1.0 / (1.0 + light.Linear * dist + light.Quadratic * dist * dist);
    diffuse  *= attenuation;
    specular *= attenuation;
    lighting += diffuse + specular;

    FragColor = vec4(lighting, 1.0);
}

 이전의 조명 구현과 비교해 실제로 변경된 뷰 공간으로의 변경을 제외하고는 AmbientOcclusion 값으로 장면의 주변 구성 요소를 곱하는 것 뿐이다. 장면에서 단일 지점의


파란색 빛이 표시되면 다음과 같은 결과가 나타난다:


Image of SSAO applied in OpenGL


  Screen-space ambient occlusion은 장면의 유형에 따라 매개 변수를 조정하는데 크게 의존하는 맞춤 가능한 효과이다. 모든 장면 유형에 대한 완벽한 매개 변수


조합은 없다. 일부 장면은 작은 반지름으로만 작동하지만 일부 장면은 더 큰 반경과 더 큰 샘플 수가 현실적으로 보이도록 요구한다. 현재의 데모는 64비트의


샘플을 사용한다. 약간의 커널 크기로 좋은 결과를 얻기 위해 노력한다.



 몇 가지 매개 변수를 조정할 수 있다 (ex:유니폼을 사용해서) : kernel size, radius, bias and/or noise kernel size


최종 폐색 값을 사용자 정의 출력으로 올리면 강도를 높일 수 있다:

occlusion = 1.0 - (occlusion / kernelSize);       
FragColor = pow(occlusion, power);

 다양한 장면과 다른 매개 변수로 SSAO의 사용자 정의 가능성을 이해해보아라.



 SSAO는 너무 뚜렷한 눈에 띄지 않는 미묘한 효과이지만 적절하게 밝게 빛나는 장면에 많은 사실감을 더하고 툴킷에 넣고 싶은 기능이다.