KoreanFoodie's Study

[OpenGL ES] 8강 : Fragment Shader, 텍스쳐링(Texturing), Texture Coordinates, Texture Wrapping, 밉맵(MipMap) 본문

Game Dev/OpenGL ES

[OpenGL ES] 8강 : Fragment Shader, 텍스쳐링(Texturing), Texture Coordinates, Texture Wrapping, 밉맵(MipMap)

GoldGiver 2023. 4. 18. 01:25

이 강의는 유투브에 무료로 공개되어 있는 한정현 교수님의 컴퓨터 그래픽스 강좌를 정리한 글입니다. 자세한 내용은 강의를 직접 들으시거나 을 구입하셔서 확인해 보세요. 강의 자료는 깃헙 링크에 올라와 있습니다.

요약 :

1. Fragment Shader 는 Texturing 과 Lighting 을 수행한다. 이때, 우리는 각 픽셀에 적절한 텍스쳐 값을 입히는 과정에서 Nearest point sampling 과 Bilinear sampling 을 선택하여 적용할 수 있다.

2. 텍스쳐링을 위해, 텍스쳐 공간을 normalize 시키게 되는데, 이때의 결과를 Texture Coordinates 라고 부른다. 1 값을 벗어나는 공간을 처리하는 방식으로는 Clamp-to-Edge, Repeat, Mirrored-Repeat 등이 있다.

3. Minification 에서 발생하는 aliasing 문제를 해결하기 위한 대표적인 anti-aliasing 기법으로 밉맵(Mipmap)이 있다. Fragment Shader 는 텍셀의 갯수를 픽셀의 갯수에 맞게 줄이는 down-sampling 을 통해 축소된 level 을 피라미드를 쌓듯이 생성한다. 이때 만들어지는 level 피라미드를 밉맵이라고 부른다.

텍스쳐링(Texturing)

우리는 이전 강에서 래스터라이저에 대해 배웠다. 래스터라이저는 Screen space 에 fragment 들을 삼각형 안에 저장하는 역할을 한다. 해당 fragment 들은 normal 과 texture coordinate 에 대한 정보를 갖고 있을 것이다.

해당 정보들은 이제 fragment shader 에게 전달되어 Lighting 과 Texturing(texture mapping) 이 진행되는데, 이번 글에서는 Texturing 에 대해 다룰 것이다.

 

픽셀처럼, 텍스쳐의 구성 요소를 부르는 단어가 있다. 바로 텍셀(texel) 이다! 참고로 픽셀은 Picture Element 의 약자이다 😄

텍셀의 위치는 해당 텍셀이 위치한 중앙의 좌표이다.

 

7장에서는 Scan line 을 따라서 보간(interpolation)이 되는 과정을 보여주었다. 텍스쳐링을 위해서는 각 vertex 를 texture coordinates 로 표현해야 하는데, 위의 직각 삼각형의 양 끝점을 각각 (1, 0), (0, 1) 이라고 두자.

그럼 y 축의 경우 높이가 4, x 축의 경우 너비가 4 인 녀석들이 길이가 1인 것처럼 축소되어야 한다. 따라서, 삼각형 왼쪽 아래에 있는 텍셀의 중앙 좌표가 (1/8, 1/8) 이 되는 것이다. 이것만 이해하면 나머지도 같은 방식으로 변환하면 된다. 해당 변환은 래스터라이저가 해 준다.

그런데 위의 식을 보면, interpolated 된 texture coordinates 에 r(x) 와 r(y) 가 곱해지는 것을 볼 수 있다. 각각의 값은 texture space 로 투영(projected)될 때 fragment 의 색상을 구하기 위해 scaling factor 가 곱해지는 것이라고 보면 된다. 위에서는 각각 4 를 곱해주고 있다. 즉, texture space 에서는 (1/8, 1/8) 이 (1/2, 1/2) 가 된다. 이는 실제 해상도를 곱해주어 해당 texture 에 맞는 RGB 값을 꺼내오는 방법을 보여준다.

 

위의 원기둥에 폴리곤 메시 붙어 있고, 각 꼭짓점의 좌표가 붙어 있다. 각 꼭짓점의 좌표는 normalized 되어 있다. 실제로 텍스쳐링이 적용될 때는 삼각형 안의 fragment 들이 해당 좌표값을 이용해 보간된다.

 

parameter space 는 normalized 되어 있다(texture coordinates 도). normalized 된 texture coordinates 는 특정한 텍스쳐 해상도에 의존하지 않으며, 다양한 텍스쳐에 적용될 수 있다. 즉, 텍스쳐를 갈아끼울 수 있다는 뜻이다! 

 

바로 위처럼.. 텍스처를 갈아끼울 수 있다!

 

그렇다면 3D 오브젝트의 텍스쳐 정보를 2D 이미지로 어떻게 저장할까?

위의 원통을 보면... 간단히 생각해서, 원통을 잘라 펼쳤다고 생각하면 된다. 중요한 것은 parameter space 라는 개념인데, 바로 텍스쳐의 가로 세로 범위가 (0, 1) 로 normalize 를 시켰다는 것만 기억해 두자.

 

위의 얼굴 모양은 중앙의 신기한 텍스쳐로 변환되는데, 개념적으로... 잘 무두질을 했다고 생각하자 😅

궁색한 설명이지만, 해당 텍스쳐를 어떻게 펴는지는 해당 글의 수준을 벗어나므로, 그래픽스 분야에서 정립한 알고리즘을 적용하여 잘 펼쳤다고 이해하고 넘어가도록 하자 🤣

 

복잡한 폴리곤 메시는 수많은 'patch' 들로 나누어진다. 패치의 이미지 텍스쳐는 'chart' 라고 불린다. 복수의 chart 는 더 큰 텍스쳐로 묶여 저장되는데, 이를 atlas(아틀라스) 라고 부른다.

펼쳐진 vertex 는 atlas 에서 각각 (s, t) 를 갖고 있으며, 이 좌표들은 폴리곤 메시에 대한 vertex array 에 저장되어 있다.

 

위에서 설명한 텍스쳐링을 요약해서 보여주는 그림이다.

 

위 Texel 을 보면, R, G, B, A(투명도) 로 이루어진 것을 확인할 수 있다.

Vertex Buffer 를 만들 때 Generate -> Bind -> Data 제공의 스텝을 밟은 것처럼, Texturing 도 마찬가지의 흐름을 탄다.

 

위의 그림을 보면, 텍스쳐를 실제로 적용할 때 어떤 식으로 할지 각각의 방법에 따른 결과물을 보여준다. (b) 를 보면, 원본의 길이가 각각 3.5 였다. 이를 1 로 줄이는 과정에서 나머지 영역을 어떻게 처리할지를 결정해야 하는데...

각각을 보면 texture wrapping mode 에 따라 range 밖의 영역이 어떻게 texturing 되는지 알 수 있다 :

  • (c) : Clamp-to-Edge / 범위를 넘어가는 녀석은 edge 에 있는 텍스쳐값을 갖는다. 잘 보면 x, y 좌표가 모두 1을 넘을 경우 보라색이 칠해지고 있다.
  • (d) : Repeat / 텍스쳐가 반복되고 있다.
  • (e), (f) : Mirrored-Repeat / 반복되고 있기는 하나, 경계선 부분을 부드럽게 처리하기 위해 반복이 되는 경우 각각 x, y 축을 기준으로 뒤집혀서 반복 패턴이 적용되고 있다.

 

 

Texture Filtering - 확대(Magnification) 과 축소(Minification)

우리는 위에서 texture coordinates 가 텍셀과 일대일 대응되는 것처럼 이야기 했지만, 실제로는 texture coordinates 가 텍셀의 중앙과 완벽히 일치하지 않을 것이다. 즉, 텍셀과 텍셀 사이 어딘가에 중구난방으로 찍힐 수 있다는 뜻이다. 그럼 각 점에 따라 텍스쳐 값을 어떻게 계산해야 할까? 이를 계산하는 과정을 texture filtering 이라고 한다.

예를 들어, texture coordinates... 이제는 그냥 pixel 이라고 하자. pixel 이 texel 보다 더 많으면 어떻게 될까?

그 경우, 원본 텍스쳐에 비해 사진은 확대될 것이다.

 

위는 텍셀의 갯수가 더 많은 경우를 보여준다. 즉, 텍셀 그리드 하나에 픽셀이 한 개조차 들어오지 않는 경우도 존재하게 된다는 뜻이다.

 

그렇다면... Magnification 에서는 texture 가 어떻게 filtering 될까? (텍셀이 노란색, 픽셀이 초록색이다)

첫번째 방법으로는, 각 픽셀이 가장 가까운 텍셀의 텍스쳐 값을 사용하는 것이다. 이를 Nearest point sampling 이라고 한다. 그런데 이 방법은 여러 개의 픽셀(여기서는 4개)이 같은 텍스쳐 값을 사용하므로, blocking image, 즉 뭉쳐 있는 듯한 이미지가 만들어질 가능성이 크다.

 

두 번째 방법은 Bilinear interpolation 이다. 뭐... 그냥 interpolation 이 2번 이루어진다고 생각하면 된다.

위에서 각 픽셀 마다 픽셀 안에서의 상대적인 위치를 이용하여, 인접한 텍셀들의 texture 값으로 보간을 진행한다.

이 방식을 이용하면, 각 픽셀이 조금씩 다른 텍스쳐 값을 가지게 되므로, nearest point sampling 보다 blocking image 문제를 잘 해결할 수 있게 된다.

 

그렇다면 minification 은 어떻까? 언뜻 보면 nearest point sampling 이나 bilinear interpolation 을 쓰면 될 것 같지만, 사실 상황이 그리 간단하지는 않다.

 

축소의 경우, 픽셀은 텍셀보다 더 넓은 간격으로 배치되어 있다. 위처럼, texture 가 체커보드 이미지를 그려낸다고 해 보자.

nearest  point sampling 이든 bilinear interpolation 이든, (a) 의 경우에는 textred quad 가 회색으로, (b) 의 경우에는 흰색으로 나올 것이다.

이 문제를 aliasing 이라고 한다. 이 문제는 높은 해상도의 텍스쳐를 낮은 해상도의 픽셀에 적용할 때 발생한다. 즉, 우리는 이런 aliasing 문제를 해결하기 위해 anti-aliasing 기법이 필요하다!

 

 

밉맵(Mipmap)

간단히 생각해서, 텍셀이 너무 많은 것이 문제니까... 텍셀을 줄여볼 생각을 할 수 있을 것이다.

텍스쳐 사이즈를 줄이기 위해, down-sampling 을 이용해 보자. 위의 사진처럼, 8 x 8 사이즈 텍셀을 4 개씩 묶은 후, 평균값을 내서 1/4 사이즈의 텍셀을 만든다. 해당 과정을 반복하면, 작은 해상도의(픽셀 해상도와 비슷한) texture 를 만들 수 있다.

이때 down-sampling 한 녀석들을 위의 피라미드처럼 level 을 붙여 표현할 수 있고, 해당 피라미드를 mipmap 이라고 부른다. 필터링할 레벨을 level of detail 이라고 부르는데, 보통 λ 값으로 표현한다.

 

스크린을 생각해 보면, 사실 픽셀은 동그라미 형태가 아니다. 그렇다기 보다는, 개념적으로는 격자 형태에 더 가깝다. 그 격자를 우리는 pixel 의 footprint 라고 부른다. 위의 그림에서는 빨간색 격자이다.

위에서 level 0 을 보면 한 픽셀(빨간색 박스)가 4개의 텍셀을 포함하고 있다. 그리고 level 1 의 경우에는 텍셀이 픽셀과 일대일 대응을 하고 있으며, 해당 레벨이 우리가 원하는 레벨이 될 것이다!

즉, 1 개의 pixel footprint 가 m x m 개의 텍셀을 갖고 있을 때, λ 의 값은 log2(m) 으로 표현할 수 있다.

 

위의 예에서는, level 2 의 경우가 우리가 원하는 mipmap 의 level 임을 알 수 있다. footprint 는 4 x 4 의 텍셀을 갖고 있으므로, log2(4) = 2 값이 되는 것이다. 그리고 downsampling 과정에서 텍셀의 값은 기존 텍셀 뭉치들의 평균값이 될 것이다.

 

그런데... footprint 가 정확히 2 의 제곱수가 될 일은 없을 것이다. 위에서는 footprint 가 9 (3 x 3) 이다. 여기에 로그를 취하면 1.585 가 되는데...

 

1.585 의 경우에는 레벨 1과 2 중 어느 값을 택해야 할까? 답은... 둘다 아니어도 된다! 😆 그냥 Bilinear interpolation 을 취한 값을 적용하면 되기 때문이다.

 

각 레벨에서 픽셀에 텍셀의 값을 적용할 때도, nearest point 를 택할 수도 있고, 아래 그림처럼 Bilinear interpolation 을 적용하여, 적절한 텍셀의 값을 계산할 수 있다.

그렇다면 왜 Bilinear 인가..? 그건 그냥 x, y 각 좌표마다 한 번씩 총 2번 하니까 Bilinear 이라고 이름을 붙인 것이다 😄

레벨을 선택할 때도 bilinear interpolation, 각 텍셀 값도 bilinear interpolation 한 경우를 trilinear interpolation 한 케이스라고 부른다!

 

이제 코드를 보자. 변수만 봐도... 각 함수가 어떤 역할을 하는지 알 수 있다 😁

MAG 는 확대, MIN 은 축소, WRAP 은 wrapping mode 일 것이니까! 😉

또한 인자로 주는 GL_NEAREST 와 GL_LINEAR 도 직관적으로 알 수 있다!

 

위에서 그림으로 친절하게 각 요소를 설명해 주고 있다. 위에서 설명했듯, 레벨을 어떻게 고를 것인지, 레벨 내에서 각 픽셀의 텍스쳐 값을 어떻게 결정할 것인지에 따라 인자의 이름이 나뉘고 있다.

 

그런데 사실 지금까지의 예시들은 매우 작위적이다. 위의 그림처럼, footprint 가 사각형이 아니라 찌그러진 모양일 수도 있다. 이 경우, 각 텍셀의 s, t 좌표는 눈으로 보기에는 같은 위치에 있는데 실제로는 다른 값이다. 즉, 아래와 같이 texture 가 표현된다 :

사실 각각의 픽셀은 각각 다른 footprint 를 가지고 있을 수 있다 😅

 

또한 위 그림처럼, 윗 부분은 minification 이, 아래 부분은 magnification 이 일어나는 케이스도 존재할 수 있다!

 

fragment shader 는 각각의 fragment 에 대해 계산을 진행한다. 즉, normal 과 texture coordinate 도 각 fragment 마다 인풋으로 들어온다. 이를 이용해서 색상을 결정할 것이다.

그런데 texture 는 uniform 으로 간주된다. 왜냐하면 각 frament 마다 다른 texture 를 사용하지는 않기 때문이다. 월드 변환에서 matrix 를 한 번에 쓰듯, texture 도 한꺼번에 주어질 것이다.

 

이제 코드를 보자. 일단 sampler2D 타입의 texture 가 있고, vec2 타입의 texture coordinate 가 보인다. 그리고 색상 결정은 딱 한줄로 끝난다.

그냥 라이브러리 함수를 호출하면 된다!

 

이게 바로 그 결과물이다. 맨 오른쪽에는 라이팅이 적용되어 좀 더 자연스러운 결과물이 보인다. Fragment Shader 가 lighting 을 수행하는데, 이는 다음 장에서 다룰 것이다 😉

 

음.. Ed Catmull 이라는 위대하신 분이... texture mapping, bicubic patches, anti-aliasing 등 여러 분야에 큰 영향을 미쳤다고 한다. 엄청난 분인 것 같다... 😵

Comments