LearnOpenGL(05) - 텍스처
작성날짜 2022/06/27
이전글: LearnOpenGL(04) - Shaders
다음글:
개요
우리는 오브젝트에 디테일을 더하는 법을 배웠습니다. 각 버텍스에 색을 사용하여 이미지를 덜 지루하게 만들었습니다. 하지만 사실적인 이미지를 만들기 위해서는 색을 적용할 수많은 버텍스들이 필요합니다. 모델에서 너무 많은 버텍스와 컬러 어트리뷰트를 가지게 된다면 상당한 부하가 발생할 것입니다.
아티스트와 프로그래머가 선호하는 것은 텍스처를 사용하는 것입니다. 텍스처란 오브젝트에 디테일을 더하는 2D 이미지(1D나 3D 텍스처도 존재합니다)입니다. 멋진 벽돌 무늬의 종이를 한 장 생각해보세요. 이제 그 종이를 잘 접어서 당신의 3D 집에 붙여보세요. 이제 당신의 집은 근사한 벽돌 집이 되었습니다. 이와 같은 걸 텍스처라고 합니다. 버텍스를 추가하는 대신 이미지를 사용해 오브젝트에 섬세한 디테일을 추가할 수 있습니다.
텍스처는 방금 말한 역할 외에 셰이더에 전달하기 위한 대량의 데이터 콜렉션으로도 쓰입니다. 하지만 이 주제는 다루지 않겠습니다.
하단에 지난 챕터에서 만든 삼각형에 벽돌 벽 텍스처를 맵핑한 이미지가 있습니다.
삼각형에 텍스처를 연결(map)하기 위해서는 각 버텍스가 텍스처의 어느 부분에 해당하는지 알려줘야 합니다. 각 버텍스는 텍스처 이미지의 어느 부분에서 색을 가져올지(sample from) 좌료를 가지고 있어야 합니다. 나머지는 fragment 보간으로 나머지 fragment들의 색을 자연스럽게 처리해줍니다.
텍스처 좌표의 범위는 0부터 1 사이의 x축과 y축 값입니다. 텍스처 좌표를 이용해서 텍스처 컬러를 가져오는 것을 샘플링(Sampling)이라고 합니다. 텍스처 좌표는 좌측 하단부터 (0, 0)으로 시작하여 우측 상단이 (1, 1)이 됩니다. 즉 삼각형에서 텍스처 좌표는 다음과 같습니다.
삼각형에서 3개의 좌표를 지정했습니다. 삼각형의 좌측 하단이 텍스처의 좌측 하단과 연결되어야 하므로 삼각형의 좌측 하단 버텍스에 대해 (0, 0) 텍스처 좌표를 사용할 것입니다. 같은 방식으로 우측 하단은 (1, 0) 텍스처 좌표를 가지게 됩니다. 삼각형의 꼭대기는 텍스처 이미지의 중앙 상단과 연결되어야 하니 (0.5, 1.0) 텍스처 좌표가 됩니다. 버텍스 셰이더에 3개의 텍스처 좌표만 넘겨주면 프레그먼트 셰이더가 각 프레그먼트에 대해 텍스처 좌표를 깔끔하게 보간해줍니다.
최종 텍스처 좌표는 다음과 같습니다.
float texCoords[] = {
0.0f, 0.0f, // lower-left corner
1.0f, 0.0f, // lower-right corner
0.5f, 1.0f // top-center corner
};
텍스처 샘플링은 느슨한 해석 과정을 가지고 있으므로 여러가지 방법으로 처리할 수 있습니다. 따라서 OpenGL에게 텍스처 샘플링을 어떻게 해야 할지 알려주는 건 우리의 몫입니다.
텍스처 랩핑
텍스처 좌표는 일반적으로 (0, 0)에서 (1, 1)사이의 값을 가집니다. 만약 범위 밖의 값을 가지게 되면 어떻게 될까요? OpenGL의 기본 동작은 텍스처 이미지를 반복하는 겁니다(기본적으로 실수 값 중 정수 부분은 무시합니다). 그리고 다른 옵션도 있습니다.
- GL_REPEAT: 기본 동작으로 텍스처를 반복합니다.
- GL_MIRRORED_REPEAT: GL_REPEAT과 동일하지만 반복될 때마다 이미지를 반전시킵니다.
- GL_CLAMP_TO_EDGE: 좌표를 0과 1사이로 고정합니다. 이미지의 끝부분이 늘어난 것처럼 됩니다.
- GL_CLAMP_TO_BORDER: 범위 밖은 유저가 선택한 색으로 채워집니다.
각 옵션들은 서로 다른 결과를 보여줍니다.
glTexParameter 함수를 사용하면 앞서 말한 옵션들을 각 좌표축 별로 설정할 수 있습니다.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
첫번째 인수로는 대상 텍스처를 넘겨줍니다, 2D 텍스처로 작업하고 있으므로 대상 텍스처는 GL_TEXTURE_2D가 됩니다. 두번째 인수는 텍스처의 어느 축을 사용할지 넘겨줍니다. 마지막 인수로는 어떤 텍스처 랩핑 모드를 사용할지 넘겨줍니다.
GL_CLAMP_TO_BORDER 옵션을 선택하는 경우엔 border color를 선택하야 합니다. float vector 버전의 glTexParameter 함수를 GL_TEXTURE_BORDER_COLOR 옵션과 border color를 나타내는 float array를 함께 사용하여 설정할 수 있습니다.
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
텍스처 필터링
텍스처 좌표는 해상도에 영향받지 않지만 다른 실수 값은 영향을 받습니다. OpenGL은 어느 텍스처의 픽셀(텍셀이라고도 합니다)이 텍스처 좌표에 연결될지 알아야 합니다. 이 부분은 굉장히 큰 오브젝트와 낮은 해상도의 텍스처를 갖고 있는 경우에 특히 중요합니다. 이제 OpenGL이 텍스처 필터링 옵션을 가지고 있다는 걸 예상하고 계실겁니다. 여러가지 다른 옵션들이 있지만 지금 당장은 가장 중요한 GL_NEAREST 그리고 GL_LINEAR 옵션에 대해 설명하겠습니다.
GL_NEAEST(nearest neighbor 또는 point filtering이라고도 합니다)는 OpenGL의 기본 텍스처 필터링 방법입니다. GL_NEAREST 옵션을 설정하면 텍셀 중에 중앙이 텍스처 좌표에서 가장 가까운 것을 선택합니다. 아래의 이미지에서 4개의 픽셀들 중 십자표시가 있는 곳이 텍스처 좌표를 나타냅니다. 우측 상단에 있는 텍셀의 중심이 텍스처 좌표에서 가장 가까우므로 해당 텍셀의 색이 샘플링됩니다.
이런 텍스처 필터링 방법들이 시각적으로 어떤 영향을 끼칠까요? 저 해상도의 텍스처를 커다란 오브젝트에 적용할 때, 각 텍스처 필터링 기법에 따라 어떤 결과가 나오는지 확인해봅시다.
GL_NEAREST를 사용하면 네모난 패턴이 생겼지만 픽셀들을 명확하게 볼 수 있고 GL_LINEAR의 경우엔 더 부드럽지만 각 픽셀들을 구분하기 어렵습니다. GL_LINEAR가 좀 더 현실적이지만 8-bit처럼 보이는 GL_NEAREST를 선호하는 개발자들도 있습니다.
텍스처 필터링은 확대나 축소 기능(크기를 키우거나 줄일 때)으로 사용할 수 있습니다. 예를 들어 텍스처를 축소할 때는 Nearest 필터링을 사용하고 확대할 때는 Linear 필터링을 사용할 수 있습니다. 랩핑 메소드와 비슷하게 생긴 glTexParameter 메소드로 필터링 방법을 지정할 수 있습니다.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
밉맵
수천개의 오브젝트로 들어찬 커다란 방을 상상해보세요. 심지어 각각의 오브젝트에 텍스처까지 붙어있다고 상상해보세요. 만약 멀리 떨어진 오브젝트의 텍스처가 가까이에 있는 오브젝트와 똑같은 고해상도의 텍스처를 사용하면 어떨까요? 멀리 떨어져 있는 오브젝트는 fragment가 적은 수만 생성되기 때문에 OpenGL은 fragment의 색상을 넓은 텍스처 부분에서 샘플링하게 됩니다. 샘플링할 텍스처 영역이 넓어질수록 여러가지 색을 사용할테니 올바른 색을 샘플링하는 것이 어렵습니다. 이런 과정을 거치고도 화면상의 작은 결과물을 생성하므로 메모리가 낭비되는 건 말할 필요도 없습니다.
이런 문제를 해결하기 위해 OpenGL은 밉맵이라는 개념을 사용합니다. 밉맵은 기본적으로 이전 텍스처보다 절반의 크기(넓이가 아닌 높이와 너비)를 텍스처들의 집합입니다. 밉맵의 발상은 이해하기 쉽습니다: 오브젝트와 뷰어 사이의 거리가 일정 거리가 되면, OpenGL은 해당 거리에 맞는 밉맵 텍스처를 사용합니다. 오브젝트가 멀리 떨어져 있으면 저해상도의 텍스처를 사용해도 알아차리기 힘들기 때문입니다. 밉맵을 사용하면 OpenGL이 올바른 텍셀을 샘플링할 수 있게되고 사용하는 메모리도 적어집니다. 밉맵이 적용된 텍스처는 이렇게 생겼습니다.
밉맵화된 텍스처 콜렉션을 직접 만드는 건 번거롭습니다. 하지만 다행히 glGenerateMipmap 메소드 호출 한번으로 OpenGL이 밉맵을 생성해줍니다.
렌더링 중 밉맵 레벨을 바꿀 때, 두 밉맵 레이어 가장자리의 날카로운 끝부분이 보일 수도 있습니다. 이런 부분도 다른 텍스처 필터링처럼 NEARSET와 LINEAR 필터링으로 필터링할 수 있습니다. 밉맵 레벨 사이를 필터링하는 방법을 지정하기 위해서 기존 필터링 메소드를 다음 중 하나로 바꿔야합니다.
- GL_NEAREST_MIPMAP_NEARSET: 픽셀 사이즈가 가장 비슷한 밉맵을 사용합니다, 또한 가까운 이웃 보간(nearest neighbor interpolation)법으로 텍스처 샘플링을 합니다.
- GL_LINEAR_MIPMAP_NEARSET: 가장 가까운 밉맵 레벨을 사용합니다, 또한 선형 보간(linear interpolation)법으로 텍스처 샘플링을 합니다.
- GL_NEARSET_MIPMAP_LINEAR: 픽셀 사이즈가 가장 비슷한 두 밉맵을 선형 보간합니다, 또한 가까운 이웃 보간(nearest neighbor interpolation)법으로 텍스처 샘플링을 합니다.
- GL_LINEAR_MIPMAP_LINEAR: 픽셀 사이즈가 가장 비슷한 두 밉맵을 선형 보간합니다, 또한 선형 보간(linear interpolation)법으로 텍스처 샘플링을 합니다.
텍스처 필터링처럼 glTexParameteri메소드로 위의 4가지 필터링 방법 중에 선택이 가능합니다.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
자주 하는 실수는 밉맵 필터링 옵션 중 하나를 확대 필터로 설정하는 것입니다. 밉맵은 주로 텍스처를 다운 스케링일할 때 사용되므로 효과가 없습니다. 텍스처 확대는 밉맵을 사용하지 않으며 이 옵션은 GL_INVALID_ENUM 에러를 발생시킵니다.