현실적으로 수 많은 vertex와 색상 값을 설정하는 것은 오버헤드를 발생시킨다. 따라서 texture를 이용한다. texture란 object들을 더 세밀하게 만들기 위해 추가하는 2D(1D or 3D) image를 말한다.
예로 삼각형에 texture를 맵핑하기 위해서 삼각형 각 vertex가 texture의 어느부분과 일치하는지 명시해야한다. 각 vertex는 texture coordinate(텍스쳐 좌표계)를 가져야하는데 이는 texture 이미지의 어떤 부분을 샘플링하는지 연관되어있다. 그 후 fragment interpolation가 나머지 작업을 한다.
texture coordinate의 범위는 x와 y축에서 0부터 1까지이며 texture coordinate을 사용하여 texture 색상을 찾는 것을 샘플링이라고 한다. 텍스쳐 좌표계에서 (0, 0)은 텍스쳐 이미지의 좌하단 모서리이고 (1, 1)은 우상단 모서리이다.

위 그림을 보듯이 세개의 텍스쳐 좌표 지점을 볼 수 있다. 좌측 하단(0, 0), 우측 하단(1, 0), 중앙 상단(0.5, 1)을 텍스쳐 좌표로 사용. 이 세 텍스쳐 좌표만 vertex shader로 보내고 보내진 이 좌표들을 토대로 fragment shader가 각각의 fragment에 관한 텍스쳐 좌표를 보간(interpolate)한다.
float texCoords[] = {
0.0f, 0.0f, // lower-left corner
1.0f, 0.0f, // lower-right corner
0.5f, 1.0f // top-center corner
};
텍스쳐 샘플링은 정밀하지 않은 interpolation을 가지며 다양한 방식으로 된다. 더해서 텍스쳐를 어떻게 샘플링할 것인지 OpenGL에 알려주는 것도 프로그래머의 일이다.
앞서 말했듯이 텍스쳐 좌표의 범위는 (0, 0) ~ (1, 1)이다. 이를 벗어난다면? OpenGL은 기본적으로 이미지 반복으로 처리한다. 이에 대하여 다양한 옵션들을 제공한다.
GL_REPEAT: 디폴트 옵션, 이미지 반복GL_MIRRORED_REPEAT: 이미지 반복이지만 상이 바뀐다.GL_CLAMP_TO_EDGE: 범위를 벗어나면 텍스쳐 좌표의 끝지점이 이어진다.GL_CLAMP_TO_BORDER: 범위를 벗어나면 사용자 지정 경계 색상값으로 채워진다.
glTexParameter*함수에서 텍스쳐 좌표계(s, t, p(만약 3D 이미지라면), 이는 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(X), T(Y) 다 똑같이 설정. 마지막 인자는 어떤 모드로 텍스쳐 렙핑 할 것인지, 여기서 GL_MIRRORED_REPEAT으로 설정. 따라서 위 두 코드는 OpenGL에게 2D 텍스쳐를 사용하여 S, T 축 모두 범위 밖의 처리는 이미지가 상이 바뀌어 반복적으로 채워지게 설정하라는 것.
만약 GL_CLAMP_TO_BORDER로 설정하려면 색상값을 명시해줘야한다. fv를 전달하기에 그에 맞는 glTexParameterfv와 함께 옵션과 float array로 된 색상 값을 입력.
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
텍스쳐 좌표계는 해상도와 무관하지만 부동소수점 값일 수도 있다. 게다가 OpenGL은 텍스쳐 좌표에 맞는 맵핑을 위해 어느 텍스쳐 픽셀인지 계산해야한다. texture pixel을 texel이라 알려져있다. 매우 큰 object와 낮은 해상도의 texture를 가진 경우 이는 매우 중요해진다. OpenGL이 texture filtering에 관한 옵션을 가진 것을 예상할 것이며, 몇 가지 옵션들이 있는데 그 중 가장 중요한 것은 GL_LINEAR와 GL_NEAREST이다.
GL_NEAREST(nearest neighbor 또는 point 필터링이라 불러진다)는 OpenGL의 기본 디폴트 텍스쳐 필터링 옵션이다. 이를 설정했을시 OpenGL은 텍스쳐 좌표가 가장 가까운 texel을 선택한다.

GL_LINEAR((bi)linear 필터링이라 알려져있다) 는 텍스쳐 좌표의 이웃 texel들로부터 보간된(interpolated) 값을 가진다. texels 간 색상값을 근사치를 낸다.

각 texture filtering 방식의 시각적 효과는?

GL_NEARSET는 사각형 패턴의 결과가 나타나면서 픽셀이 잘 보이는반면 GL_LINEAR는 좀 더 부드러운 형태를 나타내면서 픽셀의 경계가 흐릿하다.
texture filtering은 magnifying과 minifying 연산을 설정할 수 있다. 아래와 같이 GL_NEAREST을 축소하고 GL_LINEAR를 확대하는 식으로 함수호출하여 설정 가능하다.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
엄청 큰 공간에 수천개의 objects와 각각 texture가 첨가됐다고 가정해보자. 보는 사람에게 가까운 objects처럼 고해상도 texture가 붙은 objects가 멀리 떨어져있을거고 멀리떨어져 있으므로 개수가 조금 적은 fragment을 가지므로 OpenGL은 고해상도 texture로부터 그 fragment에 관한 값을 찾는 것에 대한 어려움을 갖는다. texture의 큰 부분을 범위을 가지는 fragment에 관한 색상값을 골라야하기때문이다. 이는 작은 물체에 관해 굉장히 선명한 인공물 생산하며, 작은 물체에 고해상도의 텍스쳐를 사용함으로 메모리 사용의 낭비는 말할 필요도 없다.
이러한 문제를 해결하기위한 것이 mipmaps이다. mipmaps는 텍스쳐 이미지들을 모아놓은 것인데 그 이미지들은 서로 다른 사이즈를 가진 똑같은 이미지들이다. 각 이미지들은 이전 이미지들의 사이즈 두배 작은 것들이다.

OpenGL은 objects의 거리에 따라서 최적의 텍스쳐를 mipmaps에서 찾아 사용할 것이다. 보는 사람으로부터 멀리 떨어진 물체에 조금더 낮은 해상도의 텍스쳐를 사용함으로 이질감을 줄인다. 따라서 OpenGL은 알맞는 texel을 샘플링할 수 있고 mipmap에서 그 부분을 샘플링할 때 더 적은 cache 메모리가 들어간다.
각 텍스쳐 이미지들에 대한 mipmap된 텍스쳐들을 모음을 수동으로 만드는 것은 복잡하다. 운좋게 하나의 함수 호출로 이 일을 할 수 있다. 텍스쳐를 생성한 뒤 glGenerateMipmaps을 호출한다.
렌더링 중 mipmap들에서 texture 스위칭이 일어날 때 OpenGL은 두 mipmap layer 사이에서 더 선명한 모서리들이 보이는 물체를 보여줄 수 있다. 일단 texture filtering과 같이 mipmap 단계 사이에 NEAREST와 LINEAR 옵션을 사용하여 filter를 줄 수 있다.
GL_NEAREST_ MIPMAP_NEAREST: 픽셀 사이즈에 맞는 nearest mipmap을 가지며 texture 샘플링에 관해서 nearest neighbor interpolation 사용GL_LINEAR_MIPMAP_NEAREST: 픽셀 사이즈에 맞는 nearest mipmap을 가지며 texture 샘플링에 관해서 linear interpolation 사용GL_NEAREST_MIPMAP_LINEAR: 픽셀 사이즈에 가장 가까운 두 mipmap들을 linear interpolation하며 샘플링에 관해서 nearest neighbor interpolation을 사용GL_LINEAR_MIPMAP_LINEAR: 픽셀 사이즈에 가장 가까운 두 mipmap들을 linear interpolation하며 샘플링에 관해서 linear interpolation 사용glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
흔한 실수 중 하나는 mipmap filtering 옵션에서 magnifying(확대) filter를 하는 것이다. mipmap은 주로 사이즈를 줄이는데 사용되기 때문이다. GL_INVALID_ENUM 에러코드 발생
Texture image는 여러가지 파일 형식으로 저장될 수 있다. 각각 파일형식에 맞는 구조와 데이터 순서가 있다. 그럼 어떻게 애플리케이션으로 가져올 것인가? 하나의 해결책은 하나의 파일형식의 선택하여, 예를들면 .PNG 그리고 그 파일형식을 바이트로 구성된 큰 배열로 변환하는 이미지 로더를 작성한다. 이는 크게 어렵지 않지만 여전히 번거롭다. 그리고 더 다양한 파일형식을 지원하고 싶다면? 각각의 파일형식에 맞는 이미지 로더를 작성해야한다.
또 다른 해결책은 여러 파일 형식을 지원하는 이미지 로딩 라이브러리를 사용하는 것.
std_image.h는 Sean Barrett이 작성한 유명한 단일 헤더 이미지 로딩 라이브러리이다. 다양한 파일 형식의 이미지를 로드할 수 있으며 프로젝트에 쉽게 통합 가능하다. github.com/nothings/stb/blob/master/stb_image.h 에서 다운 받을 수 있다. 간단하게 단일 헤더 파일만 받고 프로젝트에 추가 하면 된다.
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
STB_IMAGE_IMPLEMENTATION define하는 것은 전처리기가 관련 있는 definition 소스 코드만 가지면서 효과적으로 헤더파일을 cpp파일로 변환하기위해 헤더 파일을 수정한다. std_image.h를 include하고 컴파일 하면 된다.
이미지 로드하기 위해서 std_image.h를 가지고 stbi_load 함수를 사용
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height,
&nrChannels, 0);
함수 첫번째 인자는 이미지 파일 경로, 나머지 인자들은 std_image.h가 이미지의 폭, 높이 그리고 색상 채널 값을 채워줄 정수형 변수들을 넣는데 차후에 width, height, nrChannels을 사용할 예정이다.
다른 objects들과 마찬가지로 texture도 ID를 받는다.
unsigned int texture;
glGenTextures(1, &texture);
glGenTextures 함수의 첫번째 인자는 얼마나 많은 텍스쳐를 만들고 unsigned int 배열 저장할 것인지 두번째는 그 배열(unsigned int 배열). 다만, 여기서는 하나만 받으니 단일 변수로 처리했다. 다음도 다른 object들처럼 바인딩한다.
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으로 바인된 texture에는 영향을 주지 않는다.GL_RGBGL_RGB)과 데이터 타입(GL_UNSIGNED_BYTE). 여기서 RGB값을 가진 로드했고 chars(bytes)로 저장했다. 따라서 이 정보를 명시한다.glTexImage2D가 호출되는 순간 현재 바인딩된 texture object가 텍스쳐 이미지 첨가된채로 가진 상태이다. 그러나 로드한 텍스쳐 이미지의 기초 단계를 가졌을 뿐이며 mipmap 사용을 원한다면 모든 이미지들에 대해서 매뉴얼로 명시하거나 glGenerateMipmap 함수를 texture을 만들어 낸 후에 호출한다. 이는 자동으로 현재 바인딩된 texture에 관한 모든 mipmap을 만들어낸다.
texture와 그에 맞는 mipmap을 만들어낸 후 이미지 메모리를 해제한다.
stbi_image_free(data);
texture를 만들어내는 코드 전체적으로 보면
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// set the texture wrapping/filtering options (on currently bound 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);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load and generate the texture
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 함수로 직사각형 그린 vertices에 색상값과 텍스쳐 좌표계를 추가
float vertices[] = {
// positions // colors // texture coords
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left
};
이제 vertex attributes를 설정해줘야한다. 아래의 그림과 같이 맞춰야한다.

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float),
(void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
직사각형에 맞게 맞춘 코드도 8 * sizeof(float)으로 변경하고 아래에 위의 코드를 추가한다.
이제 vertex shader를 아래의 코드와 같이 수정한다.
#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;
}
이제 fragment shader에 전달해주고 fragment shader가 텍스쳐 오브젝트에 접근해야한다. 어떻게 전달할 것인가? GLSL은 sampler라 불리는 텍스쳐 오브젝트에 관한 빌트인 데이터 타입을 가지고 있다. 이 sampler 타입은 텍스쳐 타입을 접미사로 함수명 뒤에 붙는다. e.g. sampler1D, sampler3D, 여기서는 sampler2D. 나중에 texture를 할당하기 위해 uniform sampler2D로 선언한다.
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
texture의 색상값을 sampling하기 위해서 GLSL의 빌트인 texture 함수를 사용. 첫번째 인자는 texture sampler, 두번째 인자는 상응하는 texture 좌표를 갖는다. texture 함수는 우리가 이전에 설정했던 texture parameter들을 사용하여 상응하는 색상값을 샘플링한다. fragment shader의 output은 보간된(interpolated) 텍스쳐 좌표에서 필터링된(filtered) 텍스쳐 색상값이다.
이제 남은 것은 glDrawElements 함수 이전에 texture를 바인딩하고 자동으로 fragment shader의 sampler에 texture가 할당되는 것.
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

위와 같은 결과물이 나온다.
texture 색상값과 vertex 색상값을 섞을 수 있다. 간단하게 fragment shader에서 texture 색상값의 결과와 vertex 색상값을 곱한다.
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
그럼 아래와 같은 결과물을 볼 수 있다.

위에서 glUniform을 사용하여 값을 할당하는 것도 아닌데 왜 sampler2D가 uniform으로 선언되는지 궁금했을 것이다. glUniform1i을 사용하여 실제로 location 값을 texture sampler에 할당할 수 있고 fragment shader에서 여러 texture들을 한번에 설정할 수 있다. texture의 위치는 texture unit이라고 더 흔하게 말한다. 하나의 texture의 디폴트 texture unit은 0이다. 이는 default active texture unit이다. 그래서 이전에 location을 할당할 필요가 없었다. 주의할 것은 모든 그래픽 드라이버들이 default texture unit을 할당하지 않는다. 바로 전에 했던 예시들이 렌더링 안될 수도 있다.
texture unit의 주 목적은 shader에서 더 많은 texture를 사용하게 해주는 것. texture unit을 sampler에서 할당하는 것으로 그 상응하는 texture unit이 먼저 활성화되는 한 여러 texture들이 한번에 바인딩된다. glBindTexture와 같이 texture unit을 전달받는 glActiveTexture을 사용하여 texture unit들을 활성화할 수 있다.
glActiveTexture(GL_TEXTURE0); // activate texture unit first
glBindTexture(GL_TEXTURE_2D, texture);
texture unit을 활성화한 후 다음 glBindTexture 호출은 현재 활성화된 texture unit에 texture를 바인딩한다. texture unit GL_TEXTURE0은 항상 디폴트로서 활성화되어 있다. 따라서 이전 예시들에서 glBindTexture를 사용할 때 활성화할 필요가 없었다.
OpenGL은 적어도 최저 16개의 texture unit을 사용할 수 있게 한다. 따라서 GL_TEXTURE0부터 GL_TEXTURE15를 사용해서 활성화할 수 있다. 또한 GL_TEXTURE8을 GL_TEXTURE0 + 8로 가질 수 있다. 이는 여러 texture unit을 루프로 가질 때 유용하다.
또 다른 sampler를 가지기 위해서 fragment shader를 수정해본다.
#version 330 core
...
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord),
texture(texture2, TexCoord), 0.2);
}
최종 색상 값은 texture1과 texture2, 두 텍스쳐가 조합된 색. GLSL의 빌트인 mix함수는 입력의 두 값을 세번째 인자 값을 기본으로하여 선형적으로 보간(linearly interpolate)한다. 세번째 인자 값이 0.0이라면 첫번째 입력된 texture를 반환, 1.0이면 두번째 texture를 반환. 0.2는 첫번째 입력값은 80퍼센트 두번째는 20퍼센트로 두 텍스쳐가 섞여서 결과로 나온다.
이제 또 다른 texture를 생성하자. 먼저 또 다른 texture object 생성, 이미지 로드 그리고 glTexImage2D함수로 texture를 만든다.
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 이미지는 alpha값을 가지므로 이미지 데이터 인자로 GL_RGBA를 넣는다.
두번째 texture를 사용하기 위해서 해당하는 texture unit에 두 texture들을 바인딩함으로 렌더링 절차를 조금 바꿔야한다.
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를 사용하여 어떤 texture unit이 설정된 각 sampler에 각 shader sampler가 속하는지 OpenGL에 알려줘야한다. 이는 한번만 설정하고 render loop전에 할 수 있다.
shader.use(); // don’t forget to activate the shader first!
glUniform1i(glGetUniformLocation(shader.ID, "texture1"), 0); // manually
shader.setInt("texture2", 1); // or with shader class
while(...)
{
[...]
}
glUniform1i을 통해 sampler들을 설정함으로 각 uniform sampler을 올바른 texture unit에 일치하게 하하면 아래의 결과를 갖는다.

위 아래가 뒤집힌 결과를 갖는데 이는 OpenGL이 y축에서 (0, 0) 좌표가 이미지의 아래쪽으로 인식한다. 이미지는 y축의 제일 위로 (0, 0) 좌표점을 가진다. 다행히도 stb_image.h 는 이미지 로딩 중 y축을 뒤집을 수 있다. 코드는 아래와 같다.
stbi_set_flip_vertically_on_load(true);
아래와 같이 뒤집혀있지 않고 제대로 나타나는 것을 확인할 수 있다.
