텍스쳐
우리는 객체에 더 많은 디테일을 추가하기 위해 각 정점에 색상을 지정하여 흥미로운 이미지를 만들 수 있다는 것을 알게 되었습니다. 하지만 상당한 사실감을 얻으려면 많은 정점이 필요하고 그만큼 많은 색상을 지정해야 합니다. 이는 각 모델에 더 많은 정점이 필요하고 각 정점에 색상 속성이 있어야 하므로 상당한 추가 오버헤드를 발생시킵니다.
일반적으로 아티스트와 프로그래머들이 선호하는 방식은 텍스처를 사용하는 것입니다. 텍스처는 객체에 디테일을 더하기 위해 사용되는 2D 이미지입니다(1D 및 3D 텍스처도 존재합니다). 예를 들어, 멋진 벽돌 이미지가 그려진 종이를 3D 집 위에 깔끔하게 접어 붙여 집 외벽이 돌로 된 것처럼 보이게 하는 것을 텍스처라고 생각해 보세요. 하나의 이미지에 많은 디테일을 담을 수 있기 때문에, 추가적인 정점을 지정하지 않고도 객체가 매우 정교해 보이는 효과를 줄 수 있습니다.
이미지 외에도 텍스처는 셰이더로 전송할 다양한 종류의 데이터를 저장하는 데 사용할 수 있지만, 이는 다른 주제에서 다루도록 하겠습니다.
아래 이미지는 이전 장에서 다룬 삼각형에 벽돌 벽의 텍스처를 적용한 것입니다.

삼각형에 텍스처를 매핑하려면 삼각형의 각 정점에 텍스처의 어느 부분에 해당하는지 알려줘야 합니다. 따라서 각 정점에는 텍스처 이미지의 어느 부분을 샘플링할지 지정하는 텍스처 좌표(texture coordinate)가 연결되어야 합니다. 그런 다음 프래그먼트 보간이 나머지 프래그먼트에 대해 작업을 수행합니다.
텍스처 좌표는 x축과 y축에서 0부터 1까지의 범위를 갖습니다(2D 텍스처 이미지를 사용한다는 점을 기억하세요). 텍스처 좌표를 사용하여 텍스처 색상을 추출하는 것을 샘플링(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의 기본 동작은 텍스처 이미지를 반복하는 것입니다(기본적으로 부동 소수점 텍스처 좌표의 정수 부분을 무시합니다). 하지만 OpenGL은 더 많은 옵션을 제공합니다.
GL_REPEAT: 텍스처의 기본 동작입니다. 텍스처 이미지를 반복합니다.GL_MIRRORED_REPEAT:GL_REPEAT와 동일하지만, 반복할 때마다 이미지를 좌우 반전시킵니다.GL_CLAMP_TO_EDGE: 좌표를 0에서 1 사이로 제한합니다. 결과적으로 더 높은 좌표는 가장자리에 고정되어 가장자리 패턴이 늘어나는 현상이 발생합니다.GL_CLAMP_TO_BORDER: 이제 범위를 벗어난 좌표에는 사용자가 지정한 테두리 색상이 적용됩니다.
각 옵션은 기본 범위를 벗어난 텍스처 좌표를 사용할 때 서로 다른 시각적 결과를 보여줍니다. 샘플 텍스처 이미지에서 이러한 결과가 어떻게 나타나는지 살펴보겠습니다. (원본 이미지: Hólger Rezende)

앞서 언급한 각 옵션은 glTexParameter* 함수를 사용하여 좌표축(s, t (3D 텍스처를 사용하는 경우 r 포함), x, y, z에 해당)별로 설정할 수 있습니다.
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입니다. 두 번째 인수는 설정할 옵션과 적용할 텍스처 축을 지정해야 합니다. 여기서는 S축과 T축 모두에 대해 설정을 구성하려고 합니다. 마지막 인수는 원하는 텍스처 래핑 모드를 전달해야 합니다. 이 경우 OpenGL은 현재 활성화된 텍스처에 GL_MIRRORED_REPEAT 옵션을 적용합니다.
GL_CLAMP_TO_BORDER 옵션을 선택하는 경우 테두리 색상도 지정해야 합니다. 이는 glTexParameter 함수의 fv 버전에 해당하는 함수를 사용하고, 옵션으로 GL_TEXTURE_BORDER_COLOR를 지정하며, 테두리 색상 값을 담은 float 배열을 전달함으로써 수행됩니다.
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
텍스쳐 필터링
텍스처 좌표는 해상도에 의존하지 않고 임의의 부동 소수점 값이 될 수 있으므로, OpenGL은 해당 텍스처 좌표를 어떤 텍스처 픽셀(텍셀(texel)이라고도 함)에 매핑해야 하는지 파악해야 합니다. 이는 특히 크기가 매우 큰 객체와 저해상도 텍스처를 사용하는 경우에 중요해집니다. 짐작하셨겠지만, OpenGL에는 이러한 텍스처 필터링을 위한 옵션도 있습니다. 여러 옵션이 있지만, 여기서는 가장 중요한 옵션인 GL_NEAREST와 GL_LINEAR에 대해 살펴보겠습니다.
GL_NEAREST(최근접 이웃 또는 점 필터링이라고도 함)는 OpenGL의 기본 텍스처 필터링 방식입니다. GL_NEAREST로 설정하면 OpenGL은 텍스처 좌표에 가장 가까운 중심을 가진 텍셀을 선택합니다. 아래 그림에서 십자 표시는 정확한 텍스처 좌표를 나타내는 4개의 픽셀을 보여줍니다. 왼쪽 상단의 텍셀이 텍스처 좌표에 가장 가까운 중심을 가지고 있으므로 샘플링된 색상으로 선택됩니다.

GL_LINEAR(또는 선형 필터링((bi)linear filtering))는 텍스처 좌표의 인접한 텍셀에서 보간된 값을 가져와 텍셀 사이의 색상을 근사합니다. 텍스처 좌표에서 텍셀 중심까지의 거리가 작을수록 해당 텍셀의 색상이 샘플링된 색상에 더 많이 기여합니다. 아래 예시에서 인접한 픽셀의 색상이 혼합된 값이 반환되는 것을 볼 수 있습니다.

그렇다면 이러한 텍스처 필터링 방법의 시각적 효과는 어떨까요? 해상도가 낮은 텍스처를 큰 객체에 적용할 때(텍스처가 확대되어 개별 텍셀이 눈에 띄게 됨) 이러한 방법들이 어떻게 작동하는지 살펴보겠습니다.

GL_NEAREST는 텍스처를 구성하는 픽셀이 명확하게 보이는 블록 패턴을 생성하는 반면, GL_LINEAR는 개별 픽셀이 덜 보이는 더 부드러운 패턴을 생성합니다. GL_LINEAR는 더 사실적인 출력을 제공하지만, 일부 개발자는 8비트 느낌을 선호하여 GL_NEAREST 옵션을 선택하기도 합니다.
텍스처 필터링은 확대(magnifying) 및 축소(minifying) 작업(크기 조정 시)에 대해 따로 설정할 수 있으므로, 예를 들어 텍스처 크기를 줄일 때는 최근접 이웃 필터링을 사용하고 확대할 때는 선형 필터링을 사용할 수 있습니다. 따라서 glTexParameter*를 통해 두 가지 옵션 모두에 대한 필터링 방법을 지정해야 합니다. 코드는 래핑 방법을 설정하는 코드와 유사합니다.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
밉맵
수천 개의 객체가 있는 넓은 방을 상상해 보세요. 각 객체에는 텍스처가 적용되어 있습니다. 멀리 있는 객체들도 가까이 있는 객체들과 동일한 고해상도 텍스처를 가지고 있을 것입니다. 객체가 멀리 있고, 생성되는 프래그먼트가 적을 경우, OpenGL은 고해상도 텍스처에서 해당 프래그먼트에 맞는 색상 값을 가져오는 데 어려움을 겪습니다. 텍스처의 상당 부분을 차지하는 프래그먼트에 대한 색상 값을 선택해야 하기 때문입니다. 이로 인해 작은 객체에서 눈에 띄는 아티팩트가 발생할 뿐만 아니라, 작은 객체에 고해상도 텍스처를 사용하는 것은 메모리 대역폭 낭비로 이어집니다.
이 문제를 해결하기 위해 OpenGL은 밉맵(mipmaps)이라는 개념을 사용합니다. 밉맵은 기본적으로 텍스처 이미지들의 모음으로, 각 텍스처는 이전 텍스처보다 크기가 절반씩 작습니다. 밉맵의 원리는 간단합니다. 뷰어에서 특정 거리 임계값을 초과하면 OpenGL은 객체와의 거리에 가장 적합한 다른 밉맵 텍스처를 사용합니다. 객체가 멀리 떨어져 있기 때문에 해상도가 낮아도 사용자는 알아차리지 못합니다. 따라서 OpenGL은 적절한 텍셀을 샘플링할 수 있으며, 밉맵의 해당 부분을 샘플링할 때 캐시 메모리 사용량도 줄어듭니다. 밉맵이 적용된 텍스처가 어떻게 생겼는지 자세히 살펴보겠습니다.

각 텍스처 이미지에 대해 밉맵이 적용된 텍스처 모음을 수동으로 생성하는 것은 번거롭지만, 다행히 OpenGL은 텍스처를 생성한 후 glGenerateMipmap을 한 번만 호출하면 모든 작업을 자동으로 처리해 줍니다.
OpenGL은 렌더링 중 밉맵 레벨을 전환할 때 두 밉맵 레이어 사이에 날카로운 경계가 나타나는 등의 아티팩트가 나타날 수 있습니다. 일반적인 텍스처 필터링과 마찬가지로, NEAREST 및 LINEAR 필터링을 사용하여 밉맵 레벨 간 전환을 위한 필터링을 적용할 수 있습니다. 밉맵 레벨 간 필터링 방법을 지정하려면 기존 필터링 방법을 다음 네 가지 옵션 중 하나로 대체하면 됩니다.
GL_NEAREST_MIPMAP_NEAREST: 픽셀 크기와 일치하는 가장 가까운 밉맵을 가져오고 텍스처 샘플링에 최근접 이웃 보간법을 사용합니다.GL_LINEAR_MIPMAP_NEAREST: 가장 가까운 밉맵 레벨을 가져와 선형 보간법을 사용하여 해당 레벨을 샘플링합니다.GL_NEAREST_MIPMAP_LINEAR: 픽셀 크기와 가장 유사한 두 밉맵 사이를 선형적으로 보간하고, 최근접 이웃 보간법을 통해 보간된 레벨을 샘플링합니다.GL_LINEAR_MIPMAP_LINEAR: 가장 가까운 두 밉맵 사이를 선형 보간하고 선형 보간을 통해 보간된 레벨을 샘플링합니다.
텍스처 필터링과 마찬가지로 glTexParameteri를 사용하여 앞서 언급한 4가지 방법 중 하나로 필터링 방법을 설정할 수 있습니다.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
흔히 저지르는 실수 중 하나는 밉맵 필터링 옵션 중 하나를 확대 필터로 설정하는 것입니다. 밉맵은 주로 텍스처 크기를 줄일 때 사용되므로, 텍스처 확대에는 밉맵이 적용되지 않습니다. 따라서 밉맵 필터링 옵션을 지정하면 OpenGL GL_INVALID_ENUM 오류 코드가 발생합니다.
텍스처 불러오기 및 생성
텍스처를 실제로 사용하려면 먼저 텍스처 이미지를 애플리케이션에 불러와야 합니다. 텍스처 이미지는 수십 가지 파일 형식으로 저장될 수 있으며, 각 형식마다 고유한 구조와 데이터 순서를 가지고 있습니다. 그렇다면 이러한 이미지를 애플리케이션에서 어떻게 불러올 수 있을까요? 한 가지 방법은 사용하려는 파일 형식(예: .PNG)을 선택하고 해당 형식을 큰 바이트 배열로 변환하는 자체 이미지 로더를 작성하는 것입니다. 자체 이미지 로더를 작성하는 것이 아주 어렵지는 않지만, 여전히 번거롭고, 더 많은 파일 형식을 지원해야 한다면 어떻게 될까요? 지원하려는 각 형식마다 이미지 로더를 작성해야 합니다.
또 다른 해결책이자 아마도 좋은 방법은 여러 인기 있는 이미지 형식을 지원하고 모든 어려운 작업을 대신 처리해주는 이미지 로딩 라이브러리를 사용하는 것입니다. stb_image.h와 같은 라이브러리가 그 예입니다.
stb_image.h
stb_image.h는 Sean Barrett이 개발한 매우 인기 있는 단일 헤더 이미지 로딩 라이브러리로, 대부분의 인기 있는 파일 형식을 로드할 수 있으며 프로젝트에 쉽게 통합할 수 있습니다. stb_image.h는 여기에서 다운로드할 수 있습니다. 단일 헤더 파일을 다운로드하고 프로젝트에 stb_image.h로 추가한 다음, 다음 코드가 포함된 C++ 파일을 추가로 생성하면 됩니다.
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
STB_IMAGE_IMPLEMENTATION을 정의하면 전처리기가 헤더 파일을 수정하여 관련 정의 소스 코드만 포함하도록 만듭니다. 이렇게 하면 헤더 파일이 사실상 .cpp 파일이 됩니다. 이제 프로그램의 적절한 위치에 stb_image.h를 포함하고 컴파일하면 됩니다.
다음 텍스처 섹션에서는 나무 상자 이미지를 사용하겠습니다. stb_image.h를 사용하여 이미지를 로드하려면 stbi_load 함수를 사용합니다.
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
이 함수는 먼저 이미지 파일의 위치를 입력으로 받습니다. 그런 다음 두 번째, 세 번째, 네 번째 인수로 세 개의 정수를 입력받아야 하는데, stb_image.h 파일은 이 값들을 결과 이미지의 너비, 높이, 그리고 색상 채널 수로 채웁니다. 이미지의 너비와 높이는 나중에 텍스처를 생성하는 데 필요합니다.
텍스쳐 생성하기
OpenGL의 다른 객체들과 마찬가지로 텍스처도 ID로 참조됩니다. 이제 텍스처 ID를 만들어 보겠습니다.
unsigned int texture;
glGenTextures(1, &texture);
glGenTextures 함수는 먼저 생성할 텍스처의 개수를 입력으로 받아 두 번째 인수로 제공된 부호 없는 정수 배열에 저장합니다(이 예에서는 단일 부호 없는 정수). 다른 객체와 마찬가지로 바인딩해야 이후의 텍스처 명령이 현재 바인딩된 텍스처를 구성하게 됩니다.
glBindTexture(GL_TEXTURE_2D, texture);
텍스처 바인딩이 완료되었으므로 이전에 로드한 이미지 데이터를 사용하여 텍스처 생성을 시작할 수 있습니다. 텍스처는 glTexImage2D 함수를 사용하여 생성됩니다.
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
이 함수는 매개변수가 상당히 많기 때문에 단계별로 살펴보겠습니다.
- 첫 번째 인수는 텍스처 대상을 지정합니다. 이 값을
GL_TEXTURE_2D로 설정하면 이 작업은 현재 바인딩된 텍스처 객체의 동일한 대상에 텍스처를 생성합니다(따라서GL_TEXTURE_1D또는GL_TEXTURE_3D대상에 바인딩된 텍스처는 영향을 받지 않습니다). - 두 번째 인수는 텍스처를 생성할 밉맵 레벨을 지정합니다. 각 밉맵 레벨을 수동으로 설정하려는 경우에 사용하지만, 여기서는 기본값인 0으로 둡니다.
- 세 번째 인수는 OpenGL에게 텍스처를 어떤 형식으로 저장할지 알려줍니다. 우리 이미지는 RGB 값만 가지고 있으므로 텍스처도 RGB 값으로 저장합니다.
- 네 번째와 다섯 번째 인수는 결과 텍스처의 너비와 높이를 설정합니다. 이 값들은 이미지를 불러올 때 이미 저장해 두었으므로 해당 변수를 사용합니다.
- 다음 인수는 항상 0이어야 합니다(기존 설정 때문입니다).
- 7번째와 8번째 인수는 소스 이미지의 형식과 데이터 유형을 지정합니다. 우리는 이미지를 RGB 값으로 불러와서 문자(바이트)로 저장했으므로 해당 값을 전달합니다.
- 마지막 인수는 실제 이미지 데이터입니다.
glTexImage2D 함수가 호출되면 현재 바인딩된 텍스처 객체에 텍스처 이미지가 연결됩니다. 하지만 현재는 텍스처 이미지의 기본 레벨만 로드되어 있으므로, 밉맵을 사용하려면 모든 다른 이미지를 수동으로 지정해야 합니다(두 번째 인수를 계속 증가시키는 방식). 또는 텍스처 생성 후 glGenerateMipmap 함수를 호출하여 현재 바인딩된 텍스처에 필요한 모든 밉맵을 자동으로 생성할 수도 있습니다.
텍스처와 해당 밉맵 생성이 완료되면 이미지 메모리를 해제하는 것이 좋습니다.
stbi_image_free(data);
텍스처를 생성하는 전체 과정은 대략 다음과 같습니다.
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// (현재 바인딩된 텍스처 객체에 대해) 텍스처 래핑/필터링 옵션을 설정합니다.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 텍스처를 불러오고 생성합니다.
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
텍스처 적용하기
다음 섹션에서는 안녕 삼각형 챕터의 마지막 부분에서 glDrawElements를 사용하여 그린 직사각형 모양을 사용합니다. OpenGL에 텍스처 샘플링 방법을 알려주기 위해 정점 데이터에 텍스처 좌표를 추가해야 합니다.
정점 속성을 하나 더 추가했으므로 OpenGL에 새로운 정점 형식을 다시 알려야 합니다.

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
참고로 이전 두 정점 속성의 stride 매개변수도 8 * sizeof(float)로 조정해야 합니다.
다음으로, 정점 셰이더가 텍스처 좌표를 정점 속성으로 받아들이고 해당 좌표를 프래그먼트 셰이더로 전달하도록 수정해야 합니다.
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
}
그리고 프래그먼트 셰이더는 정점 셰이더의 TexCoord 출력 변수를 입력 변수로 받아들여야 합니다.
프래그먼트 셰이더도 텍스처 객체에 접근해야 하는데, 어떻게 텍스처 객체를 프래그먼트 셰이더로 전달할까요? GLSL에는 텍스처 객체를 위한 내장 데이터 타입인 샘플러(sampler)가 있습니다. 샘플러는 접미사로 원하는 텍스처 타입을 붙입니다. 예를 들어 sampler1D, sampler3D 또는 이 경우에는 sampler2D입니다. 따라서 프래그먼트 셰이더에 텍스처를 추가하려면 유니폼 변수 sampler2D를 선언하고, 나중에 OpenGL 코드에서 이 변수에 텍스처를 할당하면 됩니다.
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
텍스처의 색상을 샘플링하기 위해 GLSL의 내장 함수인 texture를 사용합니다. 이 함수는 첫 번째 인수로 텍스처 샘플러를, 두 번째 인수로 해당 텍스처 좌표를 받습니다. texture 함수는 이전에 설정한 텍스처의 래핑/필터링 옵션을 사용하여 해당 색상 값을 샘플링합니다. 따라서 이 프래그먼트 셰이더의 출력은 (보간된) 텍스처 좌표에서의 (필터링된) 텍스처 색상입니다.
이제 남은 작업은 glDrawElements를 호출하기 전에 텍스처를 바인딩하는 것뿐입니다. 그러면 glDrawElements가 자동으로 텍스처를 프래그먼트 셰이더의 샘플러에 할당합니다.
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
모든 단계를 제대로 따라했다면 다음 이미지가 표시될 것입니다.

사각형이 완전히 흰색이나 검은색으로 보인다면 코드 작성 과정에서 오류가 발생했을 가능성이 높습니다. 셰이더 로그를 확인하고 완성된 애플리케이션의 소스 코드와 비교해 보세요.
텍스처 코드가 작동하지 않거나 완전히 검은색으로 표시되는 경우, 계속 읽고 마지막 예제까지 따라해 보세요. 일부 드라이버에서는 각 샘플러 유니폼에 텍스처 유닛을 할당해야 하는데, 이 부분은 이 장에서 자세히 다루겠습니다.
좀 더 개성 있는 효과를 내려면 생성된 텍스처 색상과 정점 색상을 혼합할 수도 있습니다. 프래그먼트 셰이더에서 생성된 텍스처 색상과 정점 색상을 곱하기만 하면 두 색상이 혼합됩니다.
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
결과는 정점의 색상과 텍스처의 색상이 혼합된 형태가 되어야 합니다.

우리 컨테이너는 디스코를 좋아하는 것 같다고 할 수 있겠네요.
텍스처 유닛
glUniform을 사용하여 값을 할당하지 않았는데도 sampler2D 변수가 유니폼 변수인 이유가 궁금했을 것입니다. glUniform1i를 사용하면 텍스처 샘플러에 위치 값을 할당할 수 있으므로 프래그먼트 셰이더에서 여러 텍스처를 한 번에 설정할 수 있습니다. 이러한 텍스처의 위치는 일반적으로 텍스처 유닛이라고 합니다. 텍스처의 기본 텍스처 유닛은 0이며, 이는 기본 활성 텍스처 유닛이므로 이전 섹션에서 위치를 할당할 필요가 없었습니다. 모든 그래픽 드라이버가 기본 텍스처 유닛을 할당하는 것은 아니므로 이전 섹션이 제대로 렌더링되지 않을 수 있다는 점에 유의하십시오.
텍스처 유닛의 주요 목적은 하나의 프래그먼트 셰이더에서 여러 개의 텍스처를 사용할 수 있도록 하는 것입니다. 샘플러에 텍스처 유닛을 할당하면, 해당 텍스처 유닛을 먼저 활성화하기만 하면 여러 텍스처에 동시에 바인딩할 수 있습니다. glBindTexture와 마찬가지로 glActiveTexture를 사용하여 사용하려는 텍스처 유닛을 전달함으로써 텍스처 유닛을 활성화할 수 있습니다.
glActiveTexture(GL_TEXTURE0); // 텍스처를 바인딩하기 전에 먼저 텍스처 유닛을 활성화하세요.
glBindTexture(GL_TEXTURE_2D, texture);
정리하자면, 텍스쳐 유닛은 하나의 프래그먼트 셰이더에서 여러개의 이미지를 사용할 수 있게 해줍니다. 기존의 방법대로라면 바인드된 텍스쳐 하나만 사용할수 있었겠지만 텍스쳐 유닛을 사용한다면 여러가지 텍스쳐를 각각의 샘플러에 적용하여 여러개의 이미지를 사용할수 있게 됩니다.
텍스처 유닛을 활성화한 후, glBindTexture 함수를 사용하여 해당 텍스처를 현재 활성화된 텍스처 유닛에 바인딩합니다. 텍스처 유닛 GL_TEXTURE0은 기본적으로 항상 활성화되어 있으므로 이전 예제에서는 glBindTexture를 사용할 때 텍스처 유닛을 활성화할 필요가 없었습니다.
OpenGL은 GL_TEXTURE0부터 GL_TEXTURE15까지 사용할 수 있는 최소 16개의 텍스처 유닛을 제공합니다. 이 유닛들은 순서대로 정의되어 있으므로, 예를 들어 GL_TEXTURE0 + 8을 통해 GL_TEXTURE8도 사용할 수 있습니다. 이는 여러 텍스처 유닛을 순회해야 할 때 유용합니다.
그리고 프래그먼트 셰이더가 다른 샘플러를 받아들이도록 수정해야 합니다. 이제 이 작업은 비교적 간단할 것입니다.
#version 330 core
...
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
최종 출력 색상은 두 개의 텍스처 조회 값의 조합입니다. GLSL의 내장 함수인 mix는 두 값을 입력으로 받아 세 번째 인수를 기준으로 두 값 사이를 선형 보간합니다. 세 번째 값이 0.0이면 첫 번째 입력값을 반환하고, 1.0이면 두 번째 입력값을 반환합니다. 따라서 세 번째 값이 0.2이면 첫 번째 입력 색상의 80%와 두 번째 입력 색상의 20%가 반환되어 두 텍스처가 혼합된 색상이 됩니다.
이제 다른 텍스처를 불러오고 생성해 보겠습니다. 이제 이 단계들은 익숙해졌을 것입니다. 새로운 텍스처 객체를 생성하고, 이미지를 불러온 다음, glTexImage2D 함수를 사용하여 최종 텍스처를 생성하세요. 두 번째 텍스처로는 OpenGL 학습 중에 여러분이 지었던 표정 이미지를 사용하겠습니다.
unsigned char *data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
이번에는 알파(투명도) 채널이 포함된 .png 이미지를 로드한다는 점에 유의하십시오. 따라서 이미지 데이터에 알파 채널이 포함되어 있음을 GL_RGBA를 사용하여 지정해야 합니다. 그렇지 않으면 OpenGL이 이미지 데이터를 잘못 해석합니다.
두 번째 텍스처(그리고 첫 번째 텍스처)를 사용하려면 두 텍스처를 해당 텍스처 유닛에 바인딩하여 렌더링 절차를 약간 변경해야 합니다.
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
또한 glUniform1i를 사용하여 각 셰이더 샘플러가 어떤 텍스처 유닛에 속하는지 OpenGL에 알려줘야 합니다. 이 설정은 한 번만 하면 되므로 렌더링 루프에 들어가기 전에 수행할 수 있습니다.
ourShader.use(); // 유니폼을 설정하기 전에 셰이더를 활성화하는 것을 잊지 마세요!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 수동으로 설정하기
ourShader.setInt("texture2", 1); // 또는 셰이더 클래스를 사용하기
while(...)
{
[...]
}
glUniform1i를 통해 샘플러를 설정함으로써 각 유니폼 샘플러가 적절한 텍스처 유닛에 대응하게 되었습니다. 이제 다음과 같은 결과가 나타날 것입니다.

텍스처가 뒤집혀 있는 것을 눈치채셨을 겁니다! 이는 OpenGL이 y축의 0.0 좌표가 이미지의 아래쪽에 있어야 한다고 예상하지만, 실제 이미지에서는 y축의 0.0이 위쪽에 있는 경우가 많기 때문입니다. 다행히 stb_image.h 헤더 파일에는 이미지를 로드하기 전에 다음 구문을 추가하여 y축을 뒤집을 수 있는 기능이 있습니다.
stbi_set_flip_vertically_on_load(true);
stb_image.h 파일에서 이미지를 로드할 때 y축을 뒤집도록 설정하면 다음과 같은 결과가 나타납니다.

정상적으로 작동하는 컨테이너가 하나라도 보이면 제대로 작업한 것입니다. 여기에서 소스 코드를 확인해 볼 수 있습니다.
연습 문제
텍스쳐에 더욱 익숙해지려면 다음 단계로 넘어가기 전에 이러한 연습을 해보는 것이 좋습니다.
- 프래그먼트 셰이더를 수정하여 웃는 얼굴만 반대쪽/반대 방향을 바라보도록 하세요 (해결 방법).
- 텍스처 좌표 범위를 0.0f에서 1.0f 대신 0.0f에서 2.0f로 지정하여 다양한 텍스처 래핑 방법을 실험해 보세요. 가장자리에 맞춰 클램핑된 하나의 컨테이너 이미지에 웃는 얼굴 4개를 표시할 수 있는지 확인해 보세요. (해결 방법, 결과) 그리고 다른 래핑 방법도 실험해 보세요.
- 텍스처 이미지의 중심 픽셀만 사각형 영역에 표시하여 텍스처 좌표를 변경함으로써 개별 픽셀이 보이도록 해보세요. 픽셀을 더 선명하게 보려면 텍스처 필터링 방식을
GL_NEAREST로 설정해 보세요. (해결 방법) - 혼합 함수의 세 번째 매개변수로 균일 변수를 사용하여 두 텍스처가 보이는 정도를 조절하세요. 또한 위아래 화살표 키를 사용하여 컨테이너 또는 웃는 얼굴이 보이는 정도를 변경할 수 있게 해보세요. (해결 방법)