OpenGL Texture loading stb_image.h Texture Unit 텍스처 로딩 라이브러리 텍스처 유닛 [Learn OpenGL 쉬운 번역]

728x90
반응형
※ 본 포스팅은 learnopengl.com을 번역 및 가감한 포스팅이며, learnopengl에서는 번역 작업 참여를 적극 장려하고 있습니다!
아래 링크에서 원문을 확인할 수 있습니다.

 

Loading and creating textures

texture를 실제로 사용하려면 texture 이미지 파일을 애플리케이션에 가져와야겠죠? 이미지는 jpg, png 등 여러 파일 형식이 있고, 각각 파일 구조가 다릅니다. 근데 어떻게 애플리케이션에 가져올까요? 한 가지 방법은 특정 파일 형식에 맞춰서 byte 배열로 변환하는 loader를 만드는 겁니다. 그럼 파일 종류마다 각각의 loader가 필요하겠죠?

 

하지만 번거롭게 하기보단 이미 있는 이미지 로드 라이브러리를 사용하는게 안정적이고, 효율적일 겁니다. 그래서 이번에는 stb_image.h 라이브러리를 사용하도록 합니다.

 

stb_image.h

stb_image.h는 Sean Barrett라는 개발자가 만든 유명한 이미지 로딩 라이브러리라고 합니다. 웬만한 이미지를 로드할 수 있는 라이브러리이며 아래 링크에서 다운로드 받으면 됩니다. 단일 헤더 파일로 되어있기 때문에 stb_image.h라는 파일을 만들어서 내용을 붙여넣기 하면 됩니다. 

 

 

그런 다음 C++파일을 만들어서 아래 코드를 작성하세요.

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

STB_IMAGE_IMPLEMENTATION을 정의하면 전처리기가 헤더 파일을 수정하여 관련된 정의 소스 코드만 포함시켜 버립니다. 사실상  cpp 소스로 만들어서 사용하는 것이죠. 이렇게 설정을 완료했으면, 필요한 스크립트에 헤더를 포함시킵니다.

 

https://learnopengl.com/img/textures/container.jpg

 

위 링크에서 나무 재질 texture 이미지를 받을 수 있는데 이걸 앞으로 이용할 겁니다. 

texture 에서 이미지를 사용하기 위해서 stb_image.h의 stbi_load 함수를 사용합니다.

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);

첫 번째로 이미지 파일의 위치를 받고, 2,3,4 번째 매개변수는 이미지의 너비, 높이 색상 채널 수를 넘겨줍니다. 이건 texture 이미지를 생성할 때 반영됩니다.

 

Generating a texture

texture도 object처럼 ID를 담아서 참조하는 식입니다. texture ID를 생성합시다.

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

glGenTextures 함수는 생성할 texture object 수를 첫 번째로 받고, 생성한 texture object를 두번째 인자 주소에 저장합니다. 일반적으로 여러 개이면 배열에 담지만 여기서는 단일 변수입니다. 

여느 object처럼 texture 또한 binding 을 한 후 현재 binding된 texture object에 대해서 조작합니다.

 

glBindTexture(GL_TEXTURE_2D, texture);

binding된 texture object에 로드한 이미지를 가지고 texture image를 생성합니다. 

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

glTexImage2D 의 함수 인자는 다음과 같습니다.

 

 

  • 첫 번째 인수는 텍스처 타겟을 지정합니다. GL_TEXTURE_2D로 설정하면 이 작업이 현재 바인딩된 텍스처 객체의 같은 타겟에 텍스처를 생성한다는 의미입니다(따라서 GL_TEXTURE_1DGL_TEXTURE_3D에 바인딩된 텍스처는 영향을 받지 않습니다).
  • 두 번째 인수는 텍스처를 생성하고자 하는 mipmap 레벨을 지정합니다. 각 mipmap 레벨을 수동으로 설정할 수 있지만, 기본 레벨인 0으로 설정해둡니다.
  • 세 번째 인수는 OpenGL이 텍스처를 어떤 형식으로 저장할지를 지정합니다. 이미지가 RGB 값만을 가지고 있으므로, 텍스처도 RGB 값으로 저장합니다.
  • 네 번째와 다섯 번째 인수는 생성될 텍스처의 너비와 높이를 설정합니다. 이미지를 로드할 때 이 값을 저장해두었으므로, 해당 변수를 사용합니다.
  • 다음 인수는 항상 0이어야 합니다(일부 레거시 요소 때문입니다).
  • 일곱 번째와 여덟 번째 인수는 소스 이미지의 형식과 데이터 유형을 지정합니다. 이미지를 RGB 값으로 로드하고 이를 char(바이트)로 저장했으므로, 이에 해당하는 값을 전달합니다.
  • 마지막 인수는 실제 이미지 데이터입니다.

 

glTexImage2D를 정상 호출했다면 이제 texture object는 texture image를 가지고 있는 상태가 됩니다. 여기서 mipmap도 만들려면 glTexImage2D의 두 번째 매개변수 mipmap 레벨을 조정해서 하나씩 할 수도 있지만, 자동으로 하려면 glGenerateMipmap을 사용하면 필요한 모든 mipmap이 생성 됩니다. 

 

texture mipmap 생성을 완료하면 이미지 메모리는 지워주세요.

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 << "텍스처 로드 실패" << std::endl;
}

// 이미지 메모리 해제
stbi_image_free(data);

 

Applying textures

여기서는 이전에 그렸던 삼각형으로 사각형 모양을 만들었던 glDrawElements 코드를 발전시키려고 합니다. texture sampling을 위해서 vertex data에 texture coordinate를 추가합니다.

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

texture coordinate로 인해 vertex attribute가 추가되었으니 또 opengl에게 attribute의 형식을 알려줍시다. 

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

이제 각 attribute 마다 stride는 32 byte, 즉 8 * sizeof(float)로 설정해주어야합니다.

위 코드에서는 texture attribute에 대해서만 설정했지만, position, color 에 대해서도 동일한 stride로 수정해주어야합니다.

 

이제 vertex shader를 수정해서 texture 좌표를 vertex attribute에 추가하고 fragment 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는 이제 texture object에 접근해서 texture image를 가져와야합니다. 이를 위해 GLSL에는 texture object를 가져올 수 있는 타입이 있습니다. texture 유형을 접미사로 받는 변수로 sampler1D, sampler3D의 형태입니다. 여기서는 2D Texture를 사용하므로 sampler2D를 쓰면 됩니다. fragment shader에 texture를 추가하기 위해 uniform sampler2D를 선언하고 uniform에 할당해줍시다. 

#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 coordination을 받아서 좌표에 해당하는 texture에서 color를 sampling합니다. fragment shader에서는 texture coordination에 따라 보간되고 sampling 후 filtering까지 적용된 색상을 출력합니다. 

 

opengl에서는 glDrawElements로 그려야하는데, 그 전에 texture를 binding해서 texture를 fragment shader의 sampler에 할당시킵니다. 

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

실행 결과는 이렇게 나옵니다.

 

 

 

만약 오류가 생긴다면 전체 소스 코드를 보고 잘못된 곳을 확인해보세요.

 

텍스처가 작동하지 않거나 검게 표시되는 경우 밑에서 드라이버 별로 할당 방법이 다른데 그 방법이 필요할 수도 있습니다. sampler uniform에 texture unit을 할당하는 방법이죠.

 

fragment shader에서 texture 색과 vertex 색을 혼합해서 다음과 같이 출력할 수도 있습니다. 

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

이렇게 texture와 vertex color가 혼합되어 나옵니다.

디스코를 좋아하는 컨테이너.

 

Texture Units

이전에 sampler2D을 uniform으로 만들었는데 glUniform으로 값을 할당하지 않았습니다. 왜냐하면 texture는 보통 texture unit이라는 곳에 저장되는데, 드라이버에서 기본 texture unit 0번은 활성화를 시켜주기 때문에 굳이 따로 할당하지 않았습니다. 그래서 드라이버에 따라 texture unit에 기본적으로 할당되지 않아 렌더링되지 않았을 수도 있습니다. 

 

texture unit의 경우 shader에서 한 번에 여러 texture를 사용할 수 있게 합니다. sampler에 texture unit을 할당하면 활성화시 여러 texture를 한 번에 binding가능합니다. glBindTexture 이전에 glActiveTexture를 사용해서 texture unit을 활성화합니다.

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

texture unit을 활성화 하면 glBindTexture를 호출해서 활성화된 texture unit에 texture를 binding하면 됩니다. GL_TEXTURE0의 경우 보통 기본적으로 활성화 되어있는 상태였기 때문에 이전에는 따로 활성화하지 않았던 겁니다.

 

opengl에서 사용할 수 있는 texture unit는 최소 16개 입니다. 즉 GL_TEXTURE0 ~ GL_TEXTURE15 까지 활성화 가능한거죠. 메모리 상 주소가 순서대로 나열되어있어서 GL_TEXTURE0 + 8 하면 GL_TEXTURE8와 같습니다. 이걸 이용해서 여러 개를 처리하는 로직을 만들 수 있겠죠.

 

fragment shader를 수정해서 또 다른 sampler를 추가합니다.

#version 330 core
...

uniform sampler2D texture1;
uniform sampler2D texture2;

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

이제 최종적으로 출력되는 색은 두 texture를 mix한 결과가 나오게 됩니다. GLSL의 mix 함수는 두 값을 받아서 마지막 값을 기반으로 선형 보간을 합니다. 0.0이면 첫 번째 값, 1.0이면 두 번째 값을 100%로 해서 그 사이 값이라면 보간합니다. 현재 0.2 이므로 첫번째 80%, 두 번째 20%로 texture를 혼합한 결과가 나옵니다.

 

 

또 다른 texture를 로드해서 만듭시다.

위 링크의 이미지로 texture object를 생성하고 이미지를 load해서 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파일을 사용하는데 png파일은 alpha 채널값이 들어있습니다. 그래서 glTexImage2D로 texture 생성시 GL_RGBA를 사용해서 alpha값을 사용하도록 합니다. 안그러면 이미지를 잘못 읽을 수 있습니다.

 

texture를 여러 개 사용하기 위해 렌더링 절차를 변경합니다. texture 각각을 texture unit에 binding합니다.

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

그리고 shader의 sampler가 각각 어떤 texture unit과 연결되었는지 알려줘야합니다. 그럴려면 glUniformli를 사용해서 smapler 설정을 합니다. 이건 render loop 밖에서 한 번한 해주면 됩니다. 

ourShader.use(); // 유니폼을 설정하기 전에 셰이더를 활성화하는 것을 잊지 마세요!  
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 수동으로 설정
ourShader.setInt("texture2", 1); // 또는 셰이더 클래스 사용
  
while(...) 
{
    [...]
}

sampler 설정을 했다면, 이제 uniform sampler가 texture unit과 연결되어서 아래와 같이 나올 겁니다.

 

https://learnopengl.com/Getting-started/Textures

근데 texture가 뒤집어져 잇네요. 그 이유는 opengl에서 y축의 0.0이 가장 위부터 시작하기 때문입니다. stb_image로 이미지를 로드해올 때 이걸 뒤집을 수 있습니다. 이미지를 로드할 때 아래 코드를 추가하면 됩니다.

stbi_set_flip_vertically_on_load(true);

이렇게 y축 flip 명령을 해서 이미지를 뒤집어 불러옵니다.

https://learnopengl.com/Getting-started/Textures

 

 

오류가 있다면 전체 코드와 비교해서 해결해보세요. 

728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유