눈팅하는 게임개발자 블로그

OpenGL Getting Started 2-6 Texture 본문

공부한거/OpenGL

OpenGL Getting Started 2-6 Texture

Palamore 2020. 9. 24. 19:11

원본 사이트

learnopengl.com/Getting-started/Shaders

 

LearnOpenGL - Shaders

Shaders Getting-started/Shaders As mentioned in the Hello Triangle chapter, shaders are little programs that rest on the GPU. These programs are run for each specific section of the graphics pipeline. In a basic sense, shaders are nothing more than program

learnopengl.com

번역 사이트

heinleinsgame.tistory.com/9?category=757483

 

[Learn OpenGL 번역] 2-6. 시작하기 - Textures

Textures 시작하기/Textures 객체에 더 많은 상세사항을 추가하기 위해 각 vertex에 컬러를 사용하여 흥미로운 이미지를 만들 수 있음을 배웠습니다. 하지만 사실적인 느낌을 얻기 위해서는 많은 vertex�

heinleinsgame.tistory.com

 

각 vertex에 색상을 지정하여 컬러를 입힐 수 있다는 것 까지는 알았으나

아티스트와 프로그래머가 일반적으로 선호하는 것은 Texture를 사용하는 것이다.

Texture

Texture는 오브젝트에 세부 정보를 추가하는 데 사용되는 2D Image(또는 1, 3D)를 말한다.

하나의 이미지에 많은 세부사항을 삽입할 수 있기에 vertex를 추가하지 않아도

오브젝트가 매우 세밀하게 묘사되어 있는 것처럼 보여줄 수 있다.

 

텍스처를 삼각형에 매핑하기 위해 삼각형의 각 vertex에 텍스처의 어느 부분이 해당하는 지 알려줄 필요가 있다.

따라서 각 vertex에는 샘플링할 텍스처 이미지의 영역을 지정하는 텍스처 좌표가 있어야 한다.

이를 Texture Coordinate, 줄여서 Texcoord라고 많이들 부른다.

 

텍스처 좌표의 범위는 x, y축의 0~1 사이다. (2D 텍스처 이미지를 사용하는 경우)

좌표를 사용하여 텍스처 컬러를 가져오는 것을 sampling이라 한다.

텍스처 좌표는 텍스처 이미지의 좌측 하단(0, 0)부터 우측 상단(1, 1)까지이다.

다음 이미지는 텍스처 좌표를 삼각형에 매핑하는 방법을 보여준다.

삼각형의 좌측 하단이 텍스처의 좌측 하단과 일치하도록

삼각형의 좌측 하단 vertex에 (0, 0) 텍스처 좌표를 사용.

우측에도 마찬가지로 vertex에 (1, 0) 텍스처 좌표를 사용한다.

삼각형의 위쪽은 텍스처 이미지의 상단 중앙과 일치해야 하므로 (0.5, 1.0) 텍스처 좌표를 사용한다.

 

매핑한 좌표들을 shader에 전달하기만 하면 vertex shader는 그것들을 fragment shader에 전달하고

fragment shader는 모든 텍스처 좌표를 각 fragment에 깔끔하게 보간한다.

 

위 삼각형의 텍스처 좌표는 다음과 같다.

Float texCoords[] = {
	0.0f, 0.0f,
	1.0f, 0.0f,
	0.5f, 1.0f
};

텍스처 샘플링은 느슨한 해석을 가지고 있으며 여러가지 방법으로 수행될 수 있다.

따라서 OpenGL에게 텍스처를 sample하는 방법을 알려줘야 한다.

 

Texture Wrapping

텍스처 좌표는 일반적으로 (0, 0)에서 (1, 1)까지이지만 범위 밖의 좌표를 지정하게 되면 OpenGL은

기본적으로 텍스처 이미지를 반복하게 된다. (정수 부분을 무시한다.)

하지만 다른 옵션들이 존재하기도 한다.

 

GL_REPEAT : 디폴트 동작, 이미지 반복

GL_MIRRORED_REPEAT : GL_REPEAT과 같지만 반복할 때마다 이미지를 반대로 뒤집는다.

GL_CLAMP_TO_EDGE : 0과 1 사이의 좌표를 고정한다. 결과적으로 큰 좌표가 가장자리에 고정되어

가장자리의 패턴이 늘어나게 된다.

GL_CLAMP_TO_BORDER : 범위 밖의 좌표에 사용자가 지정한 테두리 색이 지정된다.

 

기본 범위 밖의 텍스처 좌표를 사용할 때 옵션의 각 출력은 다음과 같다.

위의 옵션들은 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 텍스처를 사용한다.

두 번째 파라미터는 설정할 옵션과 어떤 축에 적용할 것인지 지정.(WRAP 옵션, S축에 지정)

세 번째 파라미터는 텍스처 wrapping 모드를 설정해야 하며, 이 경우에는 GL_MIRRORED_REPEAT을 사용한다.

 

만약 GL_CLAMP_TO_BORDER 옵션을 선택하면 테두리 색도 추가로 설정해줘야 한다.

이는 fv를 사용하는 glTexParameter함수를 호출하여 파라미터로 GL_TEXTURE_BORDER_COLOR 옵션을 넣어 사용할 수 있다.

Float borderColor[] = {1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

 

Texture Filtering

텍스처 좌표는 기본적으로 해상도에 의존하지 않지만 실수 값이 될 수 있다.

따라서 OpenGL은 텍스처 좌표를 매핑할 텍스처 픽셀(texel)을 찾아야 한다.

 

이는 매우 큰 물체에 낮은 해상도의 텍스처가 있는 경우 특히 중요하다.

OpenGL은 이를 위한 texture filtering 옵션이 존재한다.

 

GL_NEAREST(nearest neighbor filtering 으로도 불림)은 OpenGL의 기본적인 텍스처 필터링 방법이다.

GL_NEAREST로 설정하면 OpenGL은 픽셀의 가운데가 텍스처 좌표에 가장 가까운 픽셀을 선택한다.

위의 경우 파란색의 중앙에 가장 가깝게 찍혔으므로 파란색 픽셀을 선택하여 리턴.

 

GL_LINEAR(bilinear filtering 으로도 불림)은 텍스처 좌표의 이웃한 텍셀에서 보간된 값을 가져와 텍셀 사이의 색상 근사치를 가져온다.

텍스처 좌표에서 텍셀의 중심까지의 거리가 가까울수록 해당 텍셀의 색상이 더 많이 혼합된 상태로 샘플링된다.

찍힌 좌표 주변의 텍셀 색상들을 보간하여 리턴.

NEAREST는 텍스처를 형성하는 픽셀들을 명확히 볼 수 있는 차단된 패턴을 생성하는 반면

LINEAR은 개별 픽셀들이 덜 보이는 더 매끄러운 패턴을 생성한다.

LINEAR이 좀 더 현실적인 결과를 얻을 수 있지만

일부 개발자들은 8비트 룩을 선호하므로 NEAREST 옵션을 선택하기도 한다.

 

텍스처 필터링은 확대 및 축소 작업이 이루어질 때 설정할 수 있으므로 

텍스처가 축소될 때 NEAREST를 사용하고 확대될 때는 LINEAR을 사용할 수 있다.

따라서 glTexParameter함수를 통해 두 옵션 모두에 대한 필터링 방법을 지정해야 한다.

코드는 wrapping 방법을 설정하는 것과 비슷하다.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

 

Mipmaps

같은 오브젝트에 같은 텍스처가 첨부된 경우라도

가까이 있는 경우와 멀리 있는 경우에 따라 생기는 fragment의 숫자에 차이가 나게 되면

멀리 있는 오브젝트의 경우 작은 물체에 고해상도 텍스처를 사용한 것이 된다.

이 경우 메모리 낭비가 발생하면서도 멀리 있는 작은 물체에 결함이 보일 수 있다.

이 문제를 위해 OpenGL은 기본적으로 이전 텍스처보다 2배 작은 텍스처 이미지를 다음 텍스처로 가지는

mipmap이라는 개념을 사용한다.

mipmap의 개념은 어렵지 않은데,

물체가 멀리 떨어져 있기 때문에 작은 해상도의 텍스처는 사용자의 눈에 잘 띄지 않는다는 점을 이용해

작은 텍스처를 멀리 떨어져 있는 물체에 적용하고 이 방법으로 성능 향상에 도움이 될 수도 있다.

 

각 텍스처 이미지에 대한 mipmap 텍스처 모음을 생성하는 것은 번거롭지만

OpenGL의 glGenerateMipmaps 함수를 사용하면 모든 작업을 수행할 수 있다.

 

일반적인 텍스처 필터링과 마찬가지로 mipmap레벨을 전환하기 위해

NEAREST 및 LINEAR 필터링을 사용하여 레벨 사이를 필터링 할 수도 있다.

 

mipmap 레벨 사이의 필터링 방법을 지정하기 위해 원래의 필터링 방법은 4가지 옵션이 있다.

GL_NEAREST_MIPMAP_NEAREST : NEAREST 보간법으로 mipmap을 필터링하고 텍스처 샘플링도 

NEAREST 보간법을 사용한다.

GL_LINEAR_MIPMAP_NEAREST : NEAREST 보간법으로 mipmap을 필터링하고 텍스처 샘플링은

LINEAR 보간법을 사용한다.

GL_NEAREST_MIPMAP_LINEAR : LINEAR 보간법으로 mipmap을 필터링하고 텍스처 샘플링은

NEAREST 보간법을 사용한다.

GL_LINEAR_MIPMAP_LINEAR : 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);

 

흔히 하는 실수 중 mipmap 필터링 옵션 중 하나를 확대(magnification)필터로 설정하는 것이다.

mipmap은 텍스처가 축소될 때(작아질 때에 사용됨으로써 의미를 가지는 개념이다.)주로 사용하므로

확대할 경우에는 mipmap은 의미가 없다. 옵션을 지정할 경우 GL_INVALID_ENUM 오류 코드를 생성한다.

 

텍스처 로드 및 생성

응용 프로그램에 텍스처를 로드하기 위해서 사용하는 방법으로

사용하고자 하는 파일 형식을 선택하고 .PNG 이미지 형식을 큰 바이트 배열로 변환하는 이미지 로더를 작성하는 방법이 있다.

이는 번거로운 작업이며 더 많은 파일 형식을 지원해야 할 경우 또 다른 이미지 로더를 작성해야 한다.

하지만 이미 다른 여러 형식을 지원하는 이미지 로딩 라이브러리를 사용하는 방법도 있다.

stb_image.h같은 라이브러리를 활용할 수 있다.

 

stb_image.h

stb_image.h는 가장 많이 쓰이는 파일 형식을 로드할 수 있고 프로젝트에 쉽게 통합할 수 있는

매우 인기 있는 싱글 헤더 이미지 로드 라이브러리이다.

STB_IMAGE_IMPLEMENTATION을 정의함으로써 전처리기는 헤더 파일을 관련 정의 소스 코드만

포함하도록 하여 헤더 파일을 효과적으로 .cpp파일로 변환한다.

 

이제 wooden container 이미지를 사용하는 데 stb_image.h를 사용하여 이미지를 로드하려면

stbi_load함수를 사용한다.

Int width, height, nrChannels;
Unsigned char *data = stbi_load(“container.jpg”, &width, &height, &nrChannels, 0);

첫 번째 파라미터는 이미지 파일의 경로. 다음으로

stb_image.h가 결과 이미지의 너비, 높이 및 컬러 채널의 수로 채울 3개의 정수형 변수를

2, 3, 4번째 파라미터로 받는다. 나중에 텍스처를 생성하기 위해 이미지의 너비와 높이가 필요하다.

 

텍스처 생성

이전 OpenGL의 객체들과 마찬가지로 텍스처는 ID로 참조된다.

unsigned int texture;
glGenTextures(1, &texture);

glGenTextures함수는 생성하고자 하는 텍스처의 개수를 첫 번째 파라미터로 받고 

두 번째 파라미터로 주어진 unsigned int 배열에 텍스처들을 저장한다.(위의 경우 하나의 unsigned int 변수)

다른 객체들과 마찬가지로 바인딩 해야 한다. 그 후에 텍스처 명령이 현재 바인딩된 텍스처를 대상으로 설정할 수 있다.

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);

파라미터를 살펴 보자.

1. 텍스처 타겟을 지정, 이를 GL_TEXTURE_2D로 설정한다는 것은 현재 GL_TEXTURE_2D로 바인딩 된 텍스처 객체에 

텍스처를 생성하겠다는 의미가 된다.(다른 형식으로 바인딩 된 텍스처 객체에는 아무런 영향을 주지 않는다.)

2. 생성하는 텍스처의 mipmap 레벨을 수동으로 지정하고 싶을 때 지정한다. 지금은 베이스 레벨인 0으로 남겨둔다.

3. OpenGL에게 저장할 텍스처가 어떤 포맷을 가져야할지 알려준다. 지금은 RGB값만 가지고 있으므로 텍스처를

RGB값과 함께 저장한다.

4. 결과 텍스처의 너비를 설정한다. 

5. 결과 텍스처의 높이를 설정한다. 이미지를 로딩할 때 이미 저장해 둔 변수들을 사용한다.

6. 이 파라미터는 항상 0을 지정해야 한다.

7. 원본 이미지의 포맷을 지정. RGB 값이 있는 이미지를 로드.

8. 원본 이미지의 데이터 타입을 지정, 원본 이미지를 chars(bytes)로 저장하였으므로 해당하는 값으로 설정.

9. 실제 이미지 데이터를 가진 변수(byte 데이터)

 

glTexImage2D함수를 한번 호출하면 현재 바인딩된 텍스처 객체가 첨부된 텍스처 이미지를 가진다.

이는 베이스 레벨의 텍스처 이미지만 로드된 것이고

mipmap을 사용하고 싶다면 모든 이미지들을 직접 지정하거나

텍스처를 생성한 후 glGenerateMipmap함수를 사용해야 한다.

이 함수는 현재 바인딩 된 텍스처에 대해 필요한 모든 mipmap을 자동으로 생성해준다.

 

텍스처와 해당 mipmap들을 생성한 후 이미지의 메모리를 반환하는 것이 좋다.

Stbi_image_free(data);

 

텍스처 생성의 전체적인 과정은 다음과 같다.

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 텍스처 wrapping/filtering 옵션 설정(현재 바인딩된 텍스처 객체에 대해)
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);
// 텍스처 로드 및 생성
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에게 텍스처를 샘플하는 방법을 알려주어야 하므로

텍스처 좌표를 vertex 데이터에 추가한다.

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.0f, 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    // 좌측 상단
};

 

Vertex attribute를 생성해 바뀐 vertex format을 OpenGL에게 알려준다.

위의 경우 stride는 4(float) * 8, Offset은 12, 12, 8이 된다.

 

설정이 완료되었다면 vertex attribute로서 텍스처 좌표를 받을 수 있도록 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는 texCoord 출력 변수를 입력 변수로서 받게 된다.

또한 fragment shader는 텍스처 객체에 접근해야 하는데

fragment shader에 텍스처 객체를 보내는 방법으로

GLSL은 sampler라고 불리는 텍스처 객체에 대한 데이터 타입을 제공한다.

sampler는 텍스처의 타입에 대한 접미사를 가지고 있다.

예를 들어 sampler1D, sampler2D와 같은 것들이 있다.

텍스처를 집어 넣을 uniform sampler2D를 선언하기만 하면 텍스처를 fragment shader에 전달할 수 있다.

#version 330 core
out vec4 FragColor;
  
in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
    FragColor = texture(ourTexture, TexCoord);
}

텍스처 컬러를 샘플링하기 위해 GLSL의 texture 함수를 사용하였다.

첫 번째 파라미터로 텍스처 sampler를

두 번째 파라미터로 해당 텍스처 좌표를 받는다.

다음 texture 함수는 앞서 설정했던 텍스처 파라미터를 사용하여 해당 컬러 값을 샘플링한다.

이 fragment shader의 출력은 보간된 텍스처, 좌표에서 필터링된 텍스처의 컬러가 된다.

 

이제 glDrawElements 함수를 호출하기 전에 텍스처를 바인딩하는 일만 남았다.

그러면 텍스처를 fragment shader의 sampler로 자동으로 할당하게 된다.

glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

결과 화면

 

최종 텍스처 컬러와 vertex 컬러를 혼합할 수도 있다.

FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);  

결과 화면

 

 

Texture units

glUniform 함수를 사용하여 값을 할당하지 않음에도 sampler2D 변수가 uniform인 이유가 있다.

glUniform 함수를 사용하여 실제로 텍스처 sampler의 위치값을 할당하여 fragment shader에서 동시에 어려 텍스처들을 설정할 수 있다.

이 텍스처의 위치는 흔히 texture unit이라고 알고 있는 것이다.

기본 텍스처 유닛은 0이다.

이는 기본으로 활성화된 텍스처 유닛이므로 이전의 섹션에서는 위치 값을 할당할 필요가 없었다.

모든 그래픽 드라이버가 기본 텍스처 유닛을 할당하는 것은 아니라는 사실을 알고 있어야 한다.

그렇기 때문에 이전의 섹션에서 아마 렌더링이 안되는 경우도 생길 수 있다.

 

텍스처 유닛의 주 목적은 shader에서 하나 이상의 텍스처를 사용할 수 있도록 해주는 것이다.

sampler에 텍스처 유닛을 할당함으로써 해당 텍스처 유닛을 활성화하기만 하면

여러 텍스처들을 동시에 바인딩할 수 있다.

glBindTexture함수와 마찬가지로 glActiveTexture함수에서 텍스처 유닛을 전달하여 호출함으로써

텍스처 유닛을 활성화할 수 있다.

glActiveTexture(GL_TEXTURE0); // 텍스처를 바인딩 하기 전에 먼저 텍스처 유닛을 활성화
glBindTexture(GL_TEXTURE_2D, texture);

 

텍스처 유닛을 활성화한 후에 호출되는 glBindTexture 함수는 해당 텍스처를

현재 활성화된 텍스처 유닛에 바인딩한다.

GL_TEXTURE0 텍스처 유닛은 항상 기본으로 활성화되므로 이전 예제에서

glBindTexture함수를 사용할 때 어떤 텍스처 유닛도 활성화할 필요가 없었던 것이다.

 

OpenGL은 최소 16개의 텍스처 유닛을 가지고 있다.

 

GL_TEXTURE0부터 GL_TEXTURE15까지 사용할 수 있다.

이들은 순서대로 선언되어 있으므로(주소가 연속되어 있다.)

GL_TEXTURE0 + 8과 같은 방식으로 GL_TEXTURE8에 접근하는 방식이 가능하다.

 

다른 sampler를 받기 위해 fragment shader를 수정해야 한다.

uniform sampler2D texture1;
uniform sampler2D texture2

void main()
{
	FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}

이 셰이더는 두 개의 텍스처를 혼합하여 색을 만들어낸다.

GLSL의 mix함수는 두 개의 텍스처 값을 받고

세 번째 파라미터를 기반으로 linear 보간법으로 두 개의 텍스처를 보간한다.

세 번째 값이 0.0이라면 첫 번째 텍스처 컬러를, 1.0이라면 두 번째 텍스처 컬러를 리턴한다.