우리가 무언가를 화면에 그리고자 할 때, 앞선 게시글에서는 버텍스 버퍼를 활용하여 찍을 점의 위치와 색상들을 하나하나 기입하여 사용해보았다.
하지만 그리고자 하는 대상이 굉장히 복잡해지게되면, 각 점을 프로그래머가 직접 일일히 다루기는 어려워진다. 이에, 그리고자 하는 이미지를 파일로 불러오고, 그 불러온 이미지를 특정 영역에 벽지를 바르듯이 붙여서 조금 더 복잡한 색상, 무늬를 표현해보고자 한다.
이 때 사용하는 벽지를 텍스쳐(Texture)라 할 수 있고 텍스쳐를 특정 영역에 붙여보는 작업을 해보자.
먼저 텍스처를 생성하고, 텍스처를 바인딩한다.
VBO를 생성하고, 현재 바인드되어있는 VAO에 바인드했던 것 처럼 텍스처도 동일한 과정을 거친다. 단, 텍스처는 GPU의 텍스처 슬롯에 바인드한다.
바인드하려는 텍스처 슬롯이 있다면 glActiveTexture를 통해 해당 텍스처 슬롯을 활성화한 후 바인드를 진행하고, 활성화없이 바로 바인드하면 기본 텍스처 슬롯(GL_TEXTURE0)에 바인드된다.
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glGenTextures 에서는 해당 텍스처 이름에 대한 선언만 이루어진다고 보면되고, 실제 생성되는 것은 바인드를 하면서 이루어진다.
텍스처가 저장될 공간을 할당하는 방식은 glTexStorage2D, glTexImage2D 함수가 있다.
둘 다 OpenGL에서 2D 텍스처를 생성하는 데 사용되지만, 메모리 할당 방식에 차이가 있다.
glTexStorage2D()와 glTexImage2D()glTexStorage2D(): 이 함수는 한 번에 텍스처 메모리를 할당하며, 텍스처의 크기, 포맷, Mipmap 레벨을 고정시킨다. 한 번 설정한 후에는 해당 텍스처의 크기와 포맷을 다시 변경할 수 없다. 텍스처의 메모리 할당이 더 효율적이기 때문에 성능 면에서 유리하다. 내부 메모리 데이터는 glTexSubImage2D() 함수를 통해 변경한다.glTexImage2D(): 이 함수는 매번 호출할 때마다 메모리를 할당다. 텍스처의 크기나 포맷을 유연하게 설정할 수 있으며, 필요에 따라 여러 번 호출해 텍스처 데이터를 변경할 수 있다. 그러나 이 방식은 매번 메모리를 재할당하는 데에 비용이 들기 때문에 성능이 떨어질 수 있다.유연성 측면에서는 고정적인 레이아웃을 다루는 glTexStorage보다는 glTexImage2D 함수가 유용하나, 성능적으로는 매번 할당과 해제를 진행하는 glTexImage2D 보다 glTexStorage2D가 유리하다.
바인드한 텍스처에 대한 이미지 데이터, 필터링 옵션, 래핑 옵션 등을 설정한다.
glTexImage2D 함수는 해당 텍스처 데이터를 GPU 메모리에 업로드한다.
void glTexImage2D(
GLenum target, // 텍스처의 대상 (2D 텍스처, CubeMap 등)
GLint level, // Mipmap 레벨 (보통 0, 기본 레벨)
GLint internalFormat, // 텍스처의 GPU 내부 저장 형식
GLsizei width, // 텍스처의 너비
GLsizei height, // 텍스처의 높이
GLint border, // 테두리의 크기 (항상 0이어야 함)
GLenum format, // 입력되는 이미지의 형식
GLenum type, // 입력되는 이미지 데이터 타입 (예: GL_UNSIGNED_BYTE)
const void* data // 실제 이미지 데이터
);
target: 텍스처의 대상입니다. GL_TEXTURE_2D는 2D 텍스처를 의미하며, 큐브맵을 사용할 경우 GL_TEXTURE_CUBE_MAP 같은 다른 값이 들어갑니다.텍스처 타겟과 타입
OpenGL에서 텍스처 타겟(또는 텍스처 종류)은 텍스처 데이터를 어떤 형식으로 GPU에 저장하고 사용할지를 결정하는 역할을 한다.
1. GL_TEXTURE_1D
2. GL_TEXTURE_2D
3. GL_TEXTURE_3D
4. GL_TEXTURE_CUBE_MAP
5. GL_TEXTURE_1D_ARRAY
6. GL_TEXTURE_2D_ARRAY
7. GL_TEXTURE_RECTANGLE
8. GL_TEXTURE_BUFFER
9. GL_TEXTURE_2D_MULTISAMPLE
10. GL_TEXTURE_2D_MULTISAMPLE_ARRAY
level: Mipmap 레벨을 의미합니다. Mipmap이란 텍스처를 여러 해상도로 저장해 가까운 곳에선 고해상도를, 멀리 있는 곳에선 저해상도 텍스처를 사용하는 방식인데, 일반적으로 0은 기본 레벨을 의미합니다.
internalFormat: GPU에 저장될 때 텍스처의 내부 형식을 지정합니다. 예를 들어 GL_RGB 또는 GL_RGBA 등이 있습니다. 이 값은 GPU 메모리에 저장될 데이터의 형식과 관련됩니다.
width & height: 텍스처 이미지의 너비와 높이를 의미합니다. 픽셀 단위로 지정합니다.
border: 텍스처에 테두리를 추가하는지 여부를 지정하는데, OpenGL에서는 항상 0이어야 합니다.
format: CPU 메모리에서 제공되는 이미지 데이터의 형식을 지정합니다. 예를 들어 GL_RGB는 이미지가 3개의 채널(빨강, 녹색, 파랑)로 구성되어 있음을 의미합니다. 이 값은 internalFormat과 달리, CPU에서 제공되는 데이터의 형식을 정의합니다.
type: CPU 메모리에서 제공되는 이미지 데이터의 타입을 지정합니다. 예를 들어 GL_UNSIGNED_BYTE는 각 채널이 8비트 정수로 표현됨을 의미합니다.
data: 실제 텍스처 이미지 데이터를 가리키는 포인터입니다. 예를 들어 stb_image 라이브러리를 사용하여 로드한 이미지의 데이터 포인터가 여기에 들어갑니다.
Mipmap은 텍스처의 고해상도 버전(original version) 외에도 여러 해상도의 텍스처 버전을 포함하는 방식. 저해상도로 텍스처를 사용하게 될 때 발생하는 텍셀 선택에서의 문제를 해소하기 위해 사용.
성능 향상:
시각적 품질 개선:
glGenerateMipmap 을 활용생성:
glGenerateMipmap 함수를 사용하여 Mipmap을 생성합니다. 텍스처가 바인딩된 상태에서 호출하면, 자동으로 필요한 레벨의 Mipmap을 생성합니다.glBindTexture(GL_TEXTURE_2D, textureID);
glGenerateMipmap(GL_TEXTURE_2D);
활성화:
glTexParameteri 함수를 사용하여 Mipmap을 사용할 수 있도록 설정합니다.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
필터링:
GL_LINEAR_MIPMAP_LINEAR를 사용하면 가장 부드러운 Mipmap 샘플링을 제공합니다.이처럼 밉맵은 저해상도, 작은 크기에 텍스처를 입힐 때 효과적인 방식이다.
사실 glGenerateMipmap 함수를 활용하면 런타임에 밉맵을 만들기때문에, 다소 느릴 수 있다. 성능을 중시한다면 미리 작성된 텍스처를 읽어들여서 사용하는 것이 좋을 것.
이미 생성된 밉맵을 OpenGL에 로드하려면, glTexSubImage2D()와 glTexStorage2D() 같은 함수들을 적절히 사용하여 텍스처와 밉맵 레벨들을 GPU에 업로드할 수 있다.
이 함수는 텍스처의 메모리 공간을 미리 할당하는 역할을 한다. 기본적으로 텍스처의 여러 레벨(밉맵 포함)을 미리 설정해두고, 그 후에 텍셀 데이터를 업로드하는 방식을 사용한다.
glTexStorage2D(GL_TEXTURE_2D, mipmapLevels, GL_RGBA8, width, height);
• GL_TEXTURE_2D: 2D 텍스처를 사용할 것임을 지정.
• mipmapLevels: 텍스처의 몇 개의 레벨을 사용할지 설정 (밉맵을 포함한 총 레벨 수).
• GL_RGBA8: 텍스처의 내부 형식, RGBA 포맷(8비트 정밀도) 사용.
• width, height: 텍스처의 기본 크기.
이렇게 하면 지정한 수의 밉맵 레벨들을 위한 공간이 할당되며, 각 레벨에 데이터를 로드할 수 있게 된다.
이 함수는 이미 할당된 텍스처의 특정 영역에 데이터를 업데이트할 때 사용된다. 즉, 미리 할당된 텍스처 메모리에 새로운 데이터를 넣을 수 있다. 특히 밉맵 레벨별로 텍스처 데이터를 업로드할 때 유용하다.
glTexSubImage2D(GL_TEXTURE_2D, level, xoffset, yoffset, width, height, format, type, data);
• GL_TEXTURE_2D: 2D 텍스처를 사용할 것임을 지정.
• level: 몇 번째 밉맵 레벨에 데이터를 쓸 것인지 지정.
• xoffset, yoffset: 텍스처 내에서 데이터를 쓸 위치 (0, 0으로 지정하면 텍스처 전체에 데이터를 쓴다).
• width, height: 업로드할 데이터의 크기.
• format: 데이터를 어떤 형식으로 업로드할지(RGBA 등).
• type: 데이터의 타입(GL_UNSIGNED_BYTE 등).
• data: 실제 텍셀 데이터.
밉맵 데이터를 업로드하는 방법
먼저 텍스처의 공간을 할당해야 한다. 이때 glTexStorage2D()를 사용하면 텍스처의 전체 공간을 한 번에 미리 할당할 수 있다. 이 함수는 이후 밉맵 레벨별로 데이터를 넣을 준비를 마친다.
그다음, 각 밉맵 레벨에 데이터를 넣는 작업을 진행해야 한다. 이때 glTexSubImage2D()를 사용하여 각 레벨의 텍스처 데이터를 순차적으로 업로드할 수 있다. 밉맵의 각 레벨은 기본 텍스처 크기의 절반으로 줄어들기 때문에, 각 레벨별로 크기에 맞춰 데이터를 넣어야 한다.
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// 밉맵을 포함한 텍스처 레벨들을 위한 공간을 할당
glTexStorage2D(GL_TEXTURE_2D, mipmapLevels, GL_RGBA8, baseWidth, baseHeight);
// 각 레벨별로 텍스처 데이터를 업로드
for (int level = 0; level < mipmapLevels; ++level) {
int width = baseWidth >> level;
int height = baseHeight >> level;
// 각 레벨에 맞는 텍셀 데이터를 로드해서 glTexSubImage2D를 사용해 업로드
glTexSubImage2D(GL_TEXTURE_2D,
level,
0, 0,
width, height,
GL_RGBA, GL_UNSIGNED_BYTE,
levelData[level]);
}
• 여기서 mipmapLevels는 텍스처의 레벨 개수이며, 각 레벨은 기본 크기의 절반으로 줄어든다.
• levelData[level]는 각 레벨에 해당하는 텍셀 데이터를 담고 있는 포인터다.
glGenerateMipmap vs. 미리 정의된 밉맵 사용
- glGenerateMipmap()은 런타임에 OpenGL이 자동으로 밉맵을 생성해주는 편리한 함수지만, 속도 면에서 느릴 수 있다.
- 미리 정의된 밉맵을 사용하는 경우, OpenGL이 런타임에 밉맵을 생성하지 않으므로 성능 향상에 유리하다. 특히 큰 텍스처를 사용할 때 미리 준비된 밉맵을 로드하는 방식이 효율적이다.
텍스처 정보를 전달해준 뒤, 그 텍스처의 필터링, 래핑 옵션을 설정해주는 glTexParameteri 함수를 사용하자. 필터링은 텍스쳐의 확대 축소 시 어떤식으로 텍셀을 선택하여 렌더링할지를 결정하는 옵션이다. 래핑 옵션은 텍스처 좌표를 넘어가는 경우 반복, 늘리기 등 어떤 옵션으로 그림을 그릴지 결정하는 옵션.
// 어떤 텍스쳐의 옵션설정인지 명확히 하기 위한 바인딩
glBindTexture(GL_TEXTURE_2D, textureID);
// 텍스처 필터링 설정 (확대와 축소 시)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 축소 시 선형 필터링
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 확대 시 선형 필터링
// 텍스처 래핑 모드 설정 (S축과 T축에서 반복)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
텍스처의 필터링은 텍스처가 화면에 확대되거나 축소될 때 어떤 방법으로 픽셀 값을 계산할지를 결정하는 방식.
1.1. 텍스처 확대 (Magnification) 필터링
텍스처가 화면에서 확대될 때 적용되는 필터링 모드다. 이때 텍셀의 값을 어떻게 결정할지를 선택할 수 있다.
• GL_NEAREST: 가장 가까운 텍셀 값을 사용한다. 텍스처가 확대될 때 픽셀들이 뚜렷하게 보이며, 계단 현상(Aliasing)이 발생할 수 있다.
• GL_LINEAR: 주변 텍셀들의 가중 평균을 사용하여 부드럽게 보이게 한다. 확대할 때 픽셀 간의 경계가 부드럽게 보이는 효과가 있다.
1.2. 텍스처 축소 (Minification) 필터링
텍스처가 화면에서 축소될 때 적용되는 필터링 모드다. 텍스처 축소는 더 많은 텍셀을 하나의 픽셀로 합치는 경우 발생하며, 이때 어떤 값을 사용할지 선택할 수 있다.
• GL_NEAREST: 텍스처 좌표에서 가장 가까운 텍셀 값을 그대로 사용한다.
• GL_LINEAR: 축소 시 주변 텍셀들의 가중 평균을 사용하여 부드럽게 보이도록 한다.
• GL_NEAREST_MIPMAP_NEAREST: 가장 가까운 mipmap(다중 해상도의 텍스처 레벨)에서 최적 텍셀 값을 선택하여 필터링한다.
• GL_LINEAR_MIPMAP_LINEAR: 여러 mipmap 레벨에서 텍셀 값을 이중 선형 보간으로 필터링한다. 이 방식은 텍스처 축소 시 가장 부드럽게 나타난다.
텍스처 좌표가 0.0에서 1.0 사이를 벗어났을 때 텍스처를 어떻게 처리할지를 결정하는 방식이다. 보통 텍스처 좌표는 0과 1 사이에서 정의되지만, 그 범위를 벗어날 경우 OpenGL이 어떤 식으로 그 좌표를 처리할지 정해야 한다. 이때 사용하는 것이 래핑 모드다.
래핑 모드는 텍스처의 각 축(S, T, R)에 대해 설정할 수 있으며, 주로 S와 T축이 사용된다.
• GL_REPEAT: 텍스처가 좌표를 벗어나면 텍스처의 처음부터 다시 반복한다. (타일링 효과)
• GL_MIRRORED_REPEAT: 텍스처가 좌표를 벗어나면 텍스처를 거울처럼 반전시켜 반복한다. (반사 타일링 효과)
• GL_CLAMP_TO_EDGE: 텍스처 좌표가 범위를 벗어나면 가장자리 텍셀 값을 계속 사용한다.
• GL_CLAMP_TO_BORDER: 텍스처 좌표가 범위를 벗어나면 텍스처 외부에 지정된 **단색(border color)**로 처리된다.
다른 텍스처도 생성하고 위와 같은 과정을 반복합니다.
텍스처가 여러 개일 경우, 이를 각각 다른 텍스처 유닛에 바인딩할 수 있습니다. 이를 위해 glActiveTexture(GL_TEXTURE0 + n)로 유닛을 활성화한 후, 해당 유닛에 텍스처를 바인드합니다.
glActiveTexture(GL_TEXTURE0); // Activate texture unit 0
glBindTexture(GL_TEXTURE_2D, textureID1);
glActiveTexture(GL_TEXTURE1); // Activate texture unit 1
glBindTexture(GL_TEXTURE_2D, textureID2);
glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0); // GL_TEXTURE0
glUniform1i(glGetUniformLocation(shaderProgram, "texture2"), 1); // GL_TEXTURE1
GLSL 셰이더에서의 텍스처
2D Texture는 GLSL에서 sampler2D라는 유니폼 변수로 설정된다.
uniform sampler2D tex1; // 코드의 texture1, GL_TEXTURE0 슬롯. uniform sampler2D tex2; // 코드의 texture2, GL_TEXTURE1 슬롯.
전 시간 만들었던 사각형 버텍스에 특정 색깔을 입력해주었던 것에서, 그 좌표에 해당하는 텍스처의 색상 값을 씌워서 벽지바르듯 사각형에 입혀보았다.

각 버텍스가 텍스처의 어느 위치에 해당하는지 지정해줘야하는데
float vertices[] =
{
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.5f, -0.5f, 0.0f, 0.f, 1.0f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
};
각 줄은 버텍스 하나의 정보이며, 앞에서부터 세 개는 위치, 세 개는 색상, 마지막 뒤 두 개는 텍스처 상의 2D 좌표이다.
물론 지금 색상은 텍스처 좌표의 픽셀 색상값을 가져오기때문에 따로 입력해줄 필요는 없다.