Anti Aliasing
너의 모험적인 렌더링 여행 어딘가에서, 너는 아마도 들쭉 날쭉한(jagged) 톱같은 패턴을 너의 모델들의 edges를 따라 우연히 봤었다. 이러한 jagged edges가 나타나는 이유는 raterizer가 정점 데이터를 그 scene뒤에서 실제 fragments로 바꾸는 방법 때문이다. 이러한 jagged edges가 어떻게 보이는지의 예는 이미 간단한 cube를 그릴 때 보일 수 있다:
즉시 보이지는 않지만, 만약 너가 그 큐브의 edges를 가까이서 본다면, 너는 jagged pattern을 볼 것이다. 만약 우리가 확대한다면, 너는 다음의 것을 볼 것이다:
이것은 명백히 한 프로그램의 최종 버전에서 우리가 원하는 것이 아니다. 명백히 한 edge가 구성되는 픽셀들이 보이는 이 효과는 aliasing이라고 불린다. 좀 더 부드러운 edges들을 만들기 위해 이 aliasing behavior와 싸우는 anti-aliasing 기법이라고 불리는 몇 가지 기법들이 있다.
처음에 우리는 super sample anti-aliasing(SSAA)라고 불리는 기법을 가졌다. 이것은 일시적으로 (super sampling)으로 scene을 렌더링하기 위해 좀 더 높은 resolution(해상도)를 사용했다. visual output이 framebuffer에 업데이트 될 때, 그 해상도는 정상 해상도로 downsampled 된다. 이 extra solution은 이러한 jagged edges를 방지하기위해 사용되었다. 그것이 우리에게 aliasing problem에 대해 한 해결책을 제시하지만, 그것은 중대한 성능 저하가 있다. 왜냐하면 우리는 보통보다 더 많은 fragmetns를 그려야하기 때문이다. 이 기법을 그러므로 오직 짧은 영광의 순간만을 가졌따.
이 기법은 multisample anti-aliasing 또는 MSAA라는 좀 더 현대적인 기법을 낳게 했다. 이것은 좀 더 효율적인 접근법을 구현하는 동안 SSAA의 뒤에 있는 개념으로부터 온 것이다. 이 튜토리얼에서 우리는 광범위하게 OpenGL에 내장된 MSAA 기법을 다룰 것이다.
Multisampling
multisampling이 무엇인지 이해하고, 그것이 aliasing problem을 어떻게 해결하는지를 이해하기 위해서, 우리는 처음에 OpenGL raterizer의 내부 작동을 조금 파헤쳐 볼 필요가 있다.
그 rasterizer는 모든 알고리즘들의 결합이고 너의 최종 처리된 정점들과 fragment shader사이에 있는 프로세스들이다. 그 rasterizer는 single primitive에 속한 모든 정점을 취해서, 이것을 fragments의 한 집합으로 바꾼다. 정점 좌표들은 이론적으로 어떤 좌표를 갖지만, fragments들은 그럴수 없다. 왜냐하면 그것들은 너의 윈도우의 해상도에 의해 제한되기 때문이다. 거의 vertex coordinates과 fragments사이에 일대일 매핑은 있을 수 없을 것이다. 그래서 그 rasterizer는 각 특정한 정점이 무슨 fragment/screen-coordinate에 있는지를 어떤 방식으로 결정해야만 한다.
여기에서 우리는 screen pixels들의 격자를 보는데, 각 픽셀의 중심은 한 픽셀이 삼각형에 덮여있는지를 결정하기 위해 사용되는 sample point를 포함한다. 그 빨간색 sample points는 삼각형에 의해 덮이고, 한 fragment는 그 덮여진 픽셀을 위해 생성될 것이다. 비록 그 삼각형 edges의 어떤 부분들이 여전히 어떤 screen pixels들에 들어갈지라도, 그 pixel의 sample point는 삼각형의 내부에 덮여지지 않는다. 그래서 이 픽셀은 어떤 fragment shader에 의해서 영향받지 않을 것이다.
너는 아마도 이미 지금 당장 aliasing의 기원을 알 수 있다. 그 삼각형의 완전히 렌더링된 버전은 너의 스크린에서 이것처럼 보일 것이다:
스크린 픽셀들의 제한된 양 때문에, 몇 가지 픽셀들은 한 edge를 따라 렌더링 될 것이고, 몇몇은 안될 것이다. 그 결과는 우리가 이전에 보았던 jagged edges를 만드는 부드럽지 않은 edges를 가진 primitives를 렌더링하는 것이다.
multisampling이 하는 것은 삼각형의 범위를 결정하기 위해 단일의 sampling point를 사용하는 것이 아니라, 다양한 sample points를 사용하는 것이다. (그것의 이름으로부터 추측해보아라). 각 픽셀의 중심에 있는 한 개의 sample point 대신에, 우리는 일반적인 패턴으로 4개의 subsamples를 배치할 것이고, pixel coverage를 결정하기 위해 그러한 것들을 사용할 것이다. 이것은 그 color buffer의 크기가 또한 우리가 픽셀마다 사용할 subsamples의 개수에 의해 증가된다는 것을 의미한다.
그 이미지의 왼쪽은 어떻게 우리가 일반적으로 한 삼각형의 범위를 결정하는지를 보여준다. 이 특정한 pixel은 fragment shader에서 작동하지 않을 것이다. (따라서 비어있게 된다) 그것의 sample point가 삼각형에 의해 덮여지지 않았기 때문이다. 오른쪽 이미지는 multisampled version을 보여주는데, 거기에서 각 픽셀은 4개의 샘플 포인트를 포함한다. 여기에서 우리는 샘플포인트의 2개가 삼각형을 덮는 것을 볼 수 있다.
Green Box
샘플 포인트의 양은 우리가 원하면 더 많은 샘플을 가진 갯수가 될 수 있다. 이것은 우리에게 더 좋은 coverage precision을 준다.
이것이 multisampling이 흥미로워지는 부분이다. 우리는 2개의 subsamples가 삼각형에 덮여지는 것을 결정했다. 그래서 그 다음 단계는 이 특정한 픽셀에 대해 컬러를 결정하는 것이다. 우리의 초기의 추측은 우리가 각 덮여진 subsample에 대해 fragment shader를 동작시키고 나중에 픽셀마다 각 subsample의 컬러들을 평균화 하는 것이다. 이 경우에 우리는 각 subsample에 보간된 정점 데이터에 두 번 fragment shader를 작동시킬 것이고, 최종 결과를 그러한 sample points에 저장할 것이다. 이것은 (운좋게도) 그것이 작동하는 방식이 아니다. 왜냐하면 이것은 기본적으로 우리가 multisampling 없이보다 더 많은 fragment shader를 작동시킬 필요가 있기 때문이다. 이것은 급격히 성능을 떨어뜨린다.
MSAA가 작동하는 방법은 fragment shader가 삼각형이 얼마나 많은 subsamples를 덮는지와 상관없이 픽셀마다(각 prmitive에 대해) 오직 한 번만 작동하도록 하는 것이다. 그 fragment shader는 픽셀의 중간에 보간된 vertex data로 작동하고, 그 최종결과는 그러고나서 그 덮여진 subsamples의 각각 안에 저장된다. 그 color buffer의 subsamples가 우리가 렌더링한 primitives의 컬러들로 채워지고나면, 모든 이러한 컬러들은 픽셀마다 단일의 컬러로 만들어서 평균화 된다. 4개의 샘플들 중에 2개만이 이전 이미지에서 덮였기 때문에, 그 픽셀의 컬러는 삼각형의 컬러와 다른 2개의 샘플포인트 (이 경우에 clear color)에 저장된 color로 평균화된다. 이것은 얕은 파란 컬러를 만들어낸다.
그 결과는 컬러 버퍼인데, 거기에서 모든 primitive edges는 이제 좀 더 부드러운 패턴을 만들어 낸다. 멀티샘플링이 우리가 다시 이전 삼각형의 범위를 결정할 때 어떻게 보이는지 봐보자:
여기에서 각 픽셀은 4개의 subsamples를 포함한다. (관련없는 samples들은 숨겨진다) 여기에서 그 파란색 subsamples들은 삼각형으로 덮여져있고, gray sample points는 그러지 않다. 삼각형의 내부 영역 내에서, 모든 픽셀들은 한 번만 fragment shader를 작동시킬 것이다. 거기에서 그것의 color output이 subsamples에서 저장된다. 그 삼각형의 edges에서 모든 subsamples들이 덮여지있는 것이 아니다. 그래서 fragment shader의 결과는 오직 몇몇의 subsamples들에만 저장된다. 덮여진 subsamples들의 양을 기반으로 하여, 최종 pixel color는 삼각형 color와 다른 subsample들에 저장된 컬러들로 결정된다.
기본적으로, sample points가 삼각형으로 더 많이 덮여있을 수록, 그 최종 pixel color를 삼각형의 컬러와 더 같아진다. 만약 우리가 그러고나서 우리가 이전에 non-multisample triangle로 했던 것 처럼 픽셀 컬러들을 채운다면, 우리는 다음의 이미지를 얻는다:
각 픽셀에 대해, 더 적은 subsamples들이 삼각형의 일부분이라면, 그것은 너가 이미지에서도 보듯이 삼각형의 컬러를 덜 취한다. 그 삼각형의 hard edges는 이제 실제 edge color보다는 다소 얕은 색으로 둘러싸여있다. 이것은 거리가 있는 곳으로부터 봐질 때 edge가 부드럽게 나타나도록 한다.
color 값들은 multisampling에 의해서 영향받을 뿐만 아니라, 또한 depth and stencil test는 이제 multiple sample points를 이용한다. depth testing에 대해, vertex의 depth value는 depth test를 작동시키기전에 각 subsample에 보간된다. stencil testing에 대해, 우리는 pixel마다 대신에, subsample마다 stencil values를 저장한다. 이것은 depth and stencil buffer의 크기가 또한 픽셀마다 subsamples의 양에 의해 증가되도록 한다.
우리가 지금까지 이야기 한 것은 어떻게 multisampled anti-aliasing이 scenes뒤에서 작동하는지에 대한 기본적인 개관이다. rasterizer뒤에 있는 실제 로직은 우리가 여기에서 이야기 한 것 보다 좀 더 복잡하다. 그러나 이제 너는 multisampled anti-aliasing 뒤에 있는 개념과 로직을 이해할 수 있을 것이다.
MSAA in OpenGL
만약 우리가 OpenGL에서 MSAA를 사용하고 싶다면, 우리는 픽셀마다 한 컬러 값 이상을 저장할 수 있는 color buffer를 사용할 필요가 있다 (multisampling이 우리에게 sample point마다 컬러를 저장하기 요구하기 때문이다). 우리는 따라서 주어진 양의 multisamples을 저장할 수 있는 새로운 타입의 버퍼가 필요하고 이것은 multisample buffer라고 불려진다.
대부분의 windowing systems은 우리에게 기본 color buffer 대신에 multisample buffer를 제공할 수 있따. GLFW은 또한 우리에게 이 기능을 제공하고, 우리가 할 필요가 있는 모든 것은 GLFW에게 우리가 normal color buffer 대신에, 윈도우를 만들기전에 glfwWindowHint를 호출하여 N개의 samples들을 가진 multisample buffer를 사용하고 싶다고 힌트 주는 것이다:
glfwWindowHint(GLFW_SAMPLES, 4);
우리가 이제 glfwCreateWindow를 호출할 때, 그 렌더링 윈도우가 만들어지고, 이번에는 color buffer가 스크린 좌표마다 4개의 subsamples들을 포함한다. GLFW는 또한 자동적으로 픽셀마다 4개의 subsamples들로 depth and stencil buffer를 만든다. 이것은 모든 버퍼들의 사이즈가 4만큼 증가한 것을 의미한다.
우리가 GLFW에게 multisampled buffers를 요청했으니, 우리는 glEnable를 호출하고 GL_MULTISAMPLE를 활성화하여 multisampling을 활성화할 필요가 있다. 대부분의 OpenGL drivers에서, multisampling은 기본적으로 활성화되어있다. 그래서 이 호출은 조금 반복적인 것이지만, 그것을 어쨋든 활성화시키는 것은 보통 좋은 아이디어 이다. 이 방식으로 모든 OpenGL implementations들이 multisampling을 활성화 시킨다.
glEnable(GL_MULTISAMPLE);
기본 프레임버퍼가 버퍼 attachments를 multisampled 했으니, 우리가 multisampling을 활성화하기 위해 할 필요가 있는 것은 그냥 glEnable를 호출하는 것이다. 그리고 끝났다. 실제 multisampling algorithms은 너의 OpenGL 드라이버에서 raterizer에 구현되어있기 때문에, 우리가 할 필요가 있는 것은 없다. 만약 우리가 이 튜토리얼의 시작에 있는 초록 큐브를 렌더링 한다면, 우리는 더 부드러운 edges를 볼 것이다:
이 컨테이너는 정말로 더 부드러워 보이고, 그 같은 것이 너의 scene에서 그리고 있는 다른 오브젝트에도 적용될 것이다. 너는 이 간단한 예제에 대한 소스코드를 여기에서 볼 수 있다.
off-screen MSAA
GLFW가 multisampled buffers를 만드는 것을 신경쓰기 때문에, MSAA는 꽤 쉽다. 그러나 만약 우리가 우리 자신의 framebuffers를 사용하길 원하다면, 몇가지 off-screen rendering에 대해, 우리는 우리 스스로 multisampled buffers를 만들어야만 한다; 우리는 이제 multisampled buffers를 만드는 것에 신경쓸 필요가 있다.
framebuffers에 대해 attachments로서 작동하는 multisampled buffers를 만들 수 있는 두 가지 방법이 있다: texture attachments와 renderbuffer attachments, 이것은 우리가 framebuffers tutorial에서 다룬 것과 같은 보통의 attachments와 같다.
Multisampled texture attachments
수 많은 sample points의 storage를 지원하는 텍스쳐를 만들기 위해서, 우리는 glTexImage2D 대신에, 그것의 texture target으로서 GL_TEXTURE_2D_MULTISAMPLE을 받는 glTexImage2DMultisample를 사용한다:
glBindTexture(GL_TEXTURE_2D, texColorBuffer); glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 4, GL_RGB, SCREEN_WIDTH, SCREEN_HEGIHT, GL_TRUE); glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
두 번째 인자는 이제 우리가 텍스쳐가 가지길 원하는 샘플들의 개수를 설정한다. 만약 마지막 인자가 GL_TRUE와 같다면, 그 이미지는 동일한 sample locations을 사용할 것이고, 각 texel에 대해 같은 수의 subsamples들을 사용할 것이다.
multisampled texture를 framebuffer에 attach하기 위해, 우리는 glFramebufferTexture2D를 사용하는데, 이번에는 GL_tEXTURE_2D_MULTISAMPLE을 텍스쳐 타입으로서 사용한다:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, texColorBuffer, 0);
현재 바인드된 framebuffer는 이제 texture image의 형태로 multisampled color buffer를 가진다.
Multisampled renderbuffer objects
텍스쳐처럼, multisampled renderbuffer object를 만드는 것은 어렵지 않다. 그것은 심지어 꽤 쉽다. 왜냐하면 우리가 바꿀 필요가 있는 것은 glRenderbufferStorage에 대한 호출이기 때문이다. 그것은 이제 우리가 (현재 바인드된) renderbuffer의 memory storage를 명시할 때 glRenderbufferStorageMultisample로 바꾼다:
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, SCREEN_WIDTH, SCREEN_HEGIHT);
여기에서 바뀐것은 renderbuffer target 이후의 추가 파라미터이다. 거기에서 울니ㅡㄴ 우리가 가지고 싶은 samples의 양을 설정한다. 이 경우에는 4이다.
Render to multisampled framebuffer
multisampled framebuffer object에 렌더링하는 것은 자동으로 된다. 우리가 framebuffer object가 바인드되어있을 동안 어떤 것을 그릴 때 마다, rasterizer는 모든 multisample operations를 신경쓴다. 우리는 그러고나서 multisampled color buffer and/or depth and stecil buffer가 남게된다. multisampled buffer는 조금 특별하기 때문에, 우리는 직접적으로 쉐이더에서 그것들을 샘플링하는 것같은 다른 연산들을 위해 그것들의 버퍼 이미지를 사용하지 않는다.
multisampled image는 보통 이미지보다 더 많은 정보를 포함한다. 그래서 우리가 할 필요가 있는 것은 그 이미지를 downscale or resolve 하는 것이다. 한 multisampled framebuffer를 resolving하는 것은 일반적으로 glBlitFramebuffer로 처리된다. 이것은 한 프레임버퍼의 영역을 다른 곳으로 복사하는 것이다. 또한 어떤 multisampled buffers를 resolving 하면서.
glBlitFramebuffer는 4개의 스크린 공간 좌표에 의해 정의된 주어진 source region을 또한 4개의 스크린 공간 좌표에 의해 정의된 주어진 target region으로 옮긴다. 너는 framebuffers tutorial로부터 만약 우리가 GL_FRAMEBUFFER를 바인드 시킨다면, 우리는 read and draw framebuffer object 둘 다에 바인드 시키는 것이다. 우리는 또한 그러한 타겟들을 개별적으로 framebuffer를 GL_READ_FRAMEBUFFER and GL_DRAW_FRAMEBUFFER에 바인드 시켜서 바인드 시킬 수 있다. 그 glBlitFramebuffer 함수는 어떤 것이 source이고 어떤 것이 target framebuffer인지 결정하기 위해 그러한 두 타겟으로부터 읽어들인다. 우리는 그러고나서 그 이미지를 default framebuffer에 blit하여 multisampled framebuffer output을 실제 스크린에 옮긴다:
glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
만약 그러고나서 우리가 프로그램을 렌더링 한다면, 우리는 framebuffer가 없을 때 처럼 같은 결과를 얻을 것이다 : MSAA를 사용하여 보여지고 따라서 덜 jagged edges를 보여주는 lime-green cube:
너는 여기에서 코드를 볼 수 있다.
그러나 만약 우리가 후처리와 같은 stuff를 하기위해 multisampled framebuffer의 텍스쳐 결과를 사용하길 원한다면 어떨까? 우리는 곧바로 fragment shader에서 multisampled texture를 사용할 수 없다. 우리가 할 수 있는 것은, 그 multisampled buffer(s)를 non-multisampled texture attchment를가진 한 다른 FBO에 blit하는 것이다. 우리는 그러고나서 이 평범한 color attachment texture를 후처리를 위해서 사용한다. 이것은 multisampling을 통해 렌더링된 한 이미지를 효과적으로 후처리 한다. 이것은 우리가 MULTISAMPLED BUFFER를 우리가 fragment shader에서 사용할 수 있는 보통의 2D texture로 resolve하기위해 intermediate framebuffer object로서 작동하는 새로운 FBO를 생성해야 하는 것을 의미한다. 이 프로세스는 슈도코드에서 이것처럼 보인다:
// Draw Scene to my own framebuffer glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO); glBlitFramebuffer(0, 0, SCREEN_WIDTH, SCREEN_HEGIHT, 0, 0, SCREEN_WIDTH, SCREEN_HEGIHT, GL_COLOR_BUFFER_BIT, GL_NEAREST); // second color Texture buffer (post processing) glBindFramebuffer(GL_FRAMEBUFFER, 0); glClearColor(1.f, 1.f, 1.f, 1.f); glClear(GL_COLOR_BUFFER_BIT); screenShader.use(); glBindVertexArray(quadVAO); glDisable(GL_DEPTH_TEST); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, screenTexture); glDrawArrays(GL_TRIANGLES, 0, 6);
만약 우리가 이것을 framebuffers의 튜토리얼의 후처리 코드로 구현한다면, 우리는 jagged edges가 (거의) 없는 scene의 텍스쳐에 모든 종류의 멋진 후처리 효과를 만들 수 있다. blur kerner filter가 적용된다면, 그것은 이것처럼 보일 것이다:
Green Box
screen texture는 single sample point가 있는 normal texture이기 때문에, edge-detection 같은 몇몇 post-processing filters는 또 다시 jagged edges를 도입할 것이다. 이것을 조절하기 위해 너는 텍스쳐를 나중에 blur할 수 있고, 또는 너 자신의 anti-aliasing algorithm을 만들 수 있다.
너는 우리가 multisampling과 off-screen rendering을 결합하려고 할 때, 우리가 몇 가지 추가 세부사항을 신경써야 한다는 것을 볼 수 있다. 모든 세부사항은 추가 노력을 할 가치가 있다. 비록 multisampling이 크게 너의 scene의 visual quality를 높일지라도. multisampling을 활성화하는 것은 눈에 띄게 너의 프로그램의 성능을 줄일 수 있다는 것에 유의해라. 너가 더 많은 samples를 사용할 수록. 이 글에서, 4개의 samples를 가진 MSAA를 사용하는 것이 흔히 선호된다.
Custom Anti-Aliasing algorithm
또한 multisampled texture image를 쉐이더에 직접적으로 보내는 것이 가능하다. 그것들을 처음에 resolve하는 것 대신에. GLSL는 그러고나서 우리에게 subsample마다 texture images를 샘플링할 옵션을 우리에게 준다. 그래서 우리는 우리 자신의 anti-aliasing algorithms을 만들 수 있다. 그리고 이것은 큰 graphics applications에서 흔히 되어진다.
subsample마다 color value를 가져오기 위해, 너는 sampler2DMS로서 texture uniform sampler를 정의해야만 할 것이다. 보통의 sampler2D 대신에.,
uniform sampler2DMS screenTextureMS;
texelFetch 함수를 사용하여, 그러고나서 sample마다 color value를 가져오는 것이 가능하다.
vec4 colorSample = texelFetch(screenTextureMS, TexCoords, 3); // 4th subsample
우리는 커스텀 anti-aliasing 기법의 세부사항으로 가지 않을 것이지만, 단순히 너에게 어떻게 이것과 같은 특징을 구현할지에 대해 지침을 제공했다.
댓글 없음:
댓글 쓰기