OpenGL

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

짱승_ 2018. 7. 15. 19:57

Textures

시작하기/Textures

  객체에 더 많은 상세사항을 추가하기 위해 각 vertex에 컬러를 사용하여 흥미로운 이미지를 만들 수 있음을 배웠습니다. 하지만 사실적인 느낌을 얻기 위해서는 많은 vertex를 가져야 하므로 많은 색상을 지정해야 합니다. 각 모델에는 더 많은 vertex들이 필요하고 각 vertex는 컬러 attriubute를 필요로 하기 때문에 상당한 오버헤드를 야기시킵니다.


  아티스트와 프로그래머가 일반적으로 선호하는 것은 텍스처(texture)를 사용하는 것입니다. 텍스처는 오브젝트에 세부 정보를 추가하는 데에 사용되는 2D Image(1D, 3D 텍스처도 존재합니다)입니다. 예를 들어 멋있는 벽돌 이미지가 있는 종이 조각처럼 여러분의 3D 집에 텍스처를 깔끔하게 접어서 집의 외관이 돌로 되어 있는 것처럼 보이게 하는 것을 생각해보세요. 하나의 이미지에 많은 세부 사항을 삽입할 수 있기 때문에 vertex를 추가하지 않아도 오브젝트가 매우 세밀하게 묘사되어 있다는 착각을 할 수 있습니다.

  이미지를 제외하고 텍스처는 많은 양의 데이터를 저장하여 shader에 보낼 수 있지만 이에 대한 내용은 나중에 다루도록 하겠습니다.

  아래에서 이전 강좌의 삼각형에 brick wall 텍스처 이미지를 매핑한 것을 볼 수 있습니다.



  텍스처를 삼각형에 매핑하기 위해 삼각형의 각 vertex에 텍스처의 어느 부분이 해당하는지 알려주어야 합니다. 따라서 각 vertex에는 샘플링 할 텍스처 이미지의 영역을 지정하는 텍스처 좌표(texture coordinate)가 있어야 합니다. Fragment 보간은 다른 fragment들에 대해 나머지 작업을 수행합니다.


  텍스처 좌표의 범위는 xy축 상의 0에서 1까지입니다(2D 텍스처 이미지를 사용한다는 것을 생각하세요). 텍스처 좌표를 사용하여 텍스처 컬러를 가져 오는 것을 sampling이라고 합니다. 텍스처 좌표는 텍스처 이미지의 좌측 하단 (0,0)부터 우측 상단 (1,1)까지 입니다. 다음 이미지는 텍스처 좌표를 삼각형에 매핑하는 방법을 보여줍니다.



  삼각형에 대해 3개의 텍스처 좌표 포인트를 지정합니다. 우리는 삼각형의 좌측 하단이 텍스처의 좌측 하단과 일치하도록 삼각형의 좌측 하단 vertex에 (0,0) 텍스처 좌표를 사용합니다. 우측 하단에도 마찬가지로 (1,0) 텍스처 좌표를 사용합니다. 삼각형의 위쪽은 텍스처 이미지의 상단 중앙과 일치해야하므로 (0.5,1.0) 텍스처 좌표를 사용합니다. 우리는 vertex shader에 3개의 텍스처 좌표를 전달하기만 하면 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의 기본 동작은 텍스처 이미지를 반복하는 것입니다(기본적으로 텍스처 좌표의 정수 부분을 무시합니다). 하지만 OpenGL에서 제공하는 많은 옵션들이 있습니다.


  • GL_REPEAT: 텍스처의 기본 동작입니다. 이미지를 반복합니다.
  • GL_MIRRORED_REPEAT: GL_REPEAT와 같지만 반복할때마다 이미지를 반대로 뒤집습니다.
  • GL_CLAMP_TO_EDGE: 01 사이의 좌표를 고정합니다. 결과적으로 큰 좌표가 가장자리에 고정되어 가장자리의 패턴이 늘어나게 됩니다.
  • 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 텍스처를 사용하기 때문에 타겟을 GL_TEXTURE_2D로 설정하였습니다. 두 번째 파라미터는 우리가 설정할 옵션과 어떤 축에 적용할 것인지 지정합니다. WRAP 옵션을 설정하고 S, T 축 모두에 적용하려고 합니다. 마지막 파라미터는 텍스처 wrapping 모드를 설정해야하며 이 경우에는 GL_MIRRORED_REPEAT을 사용하여 현재 활성화된 텍스처의 wrapping 옵션을 설정합니다.


  만약 GL_CLAMP_TO_BORDER 옵션을 선택하면 테두리 색도 추가로 설정해주어야 합니다. 이는 fv를 사용하는 glTexParameter 함수를 호출하여 파라미터로 GL_TEXTURE_BORDER_COLOR 옵션을 넣어 수행할 수 있습니다. 이 옵션은 테두리의 컬러 값을 float 배열로 전달합니다.


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_NEARESTGL_LINEAR에 대해 설명하겠습니다.


  GL_NEAREST (nearest neighbor filtering라고도 불림)는 OpenGL의 기본적인 텍스처 필터링 방법입니다. GL_NEAREST로 설정하면 OpenGL은 가운데가 텍스처 좌표에 가장 가까운 픽셀을 선택합니다. 아래에서 4개의 픽셀을 볼 수 있는데 여기서 십자가가 정확한 텍스처 좌표를 나타냅니다. 좌측 상단의 텍셀의 중심이 텍스처 좌표와 가장 가깝기 때문에 샘플링 된 색상으로 선택됩니다.



  GL_LINEAR ((bi)linear filtering이라고도 불림) 은 텍스처 좌표의 이웃한 텍셀에서 보간된 값을 가져와 텍셀 사이의 색상의 근사치를 가져온다. 텍스처 좌표에서 텍셀의 중심까지의 거리가 가까울수록 해당 텍셀의 색상이 샘플링 된 색상에 더많이 혼합됩니다. 아래에서 우리는 인접한 픽셀의 컬러들이 혼합된 색상이 반환됨을 알 수 있습니다.



  하지만 이러한 텍스처 필터링 방법들의 시각적인 효과는 무엇일까요? 큰 오브젝트에 해상도가 낮은 텍스처를 사용할 때 이러한 방법이 어떻게 작동하는지 봅시다(텍스처의 크기가 확장되고 각각의 텍셀들이 눈에 띕니다).



  GL_NEAREST는 텍스처를 형성하는 픽셀들을 명확히 볼 수 있는 차단된 패턴을 생성하는 반면 GL_LINEAR는 개별 픽셀들이 덜 보이는 더 매끄러운 패턴을 생성합니다. GL_LINEAR가 좀더 현실감있는 결과를 산출하지만 일부 개발자들은 8비트 룩을 선호하므로 GL_NEAREST 옵션을 선택합니다.


  텍스처 필터링은 확대(magnifying)축소(minifying) 작업(스케일 업 혹은 다운)에 대해 설정할 수 있으므로 예를 들어 텍스처가 축소될 때 nearest neighbor filtering을 사용하고 텍스처가 확대될 때는 linear filtering을 사용할 수 있습니다. 따라서 우리는 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은 텍스처의 대부분을 차지하는 fragment를 위한 텍스처 색상을 선택해야하므로 고해상도 텍스처에서 해당 fragment의 올바른 색상 값을 가져오는 데에 어려움을 겪습니다. 이렇게 하면 작은 물체에 고해상도 텍스처를 사용하여 메모리 낭비는 물론이고 작은 물체에는 결함이 보일 수 있습니다.


  이 문제를 해결하기 위해 OpenGL은 기본적으로 이전 텍스처보다 2배 작은 텍스처 이미지를 다음 텍스처로 가지는 mipmaps라는 개념을 사용합니다. mipmap의 개념은 이해하기 쉽습니다. 시점에서 특정 거리의 임계 값을 넘으면 OpenGL은 오브젝트까지의 거리에 가장 적합한 mipmap 텍스처를 사용하게 됩니다. 물체가 멀리 떨어져 있기 때문에 작은 해상도의 텍스처는 사용자에게 눈에 잘 띄지 않습니다. 또한 mipmap은 성능 향상에도 도움이 됩니다. mipmap 텍스처가 어떻게 생겼는지 자세히 살펴 보겠습니다.



  각 텍스처 이미지에 대한 mipmap 텍스처 모음을 생성하는 것은 직접하기에는 번거롭지만 운 좋게도 OpenGL은 텍스처를 생성한 후 glGenerateMipmaps 함수를 호출하면 모든 작ㅈ업을 수행할 수 있습니다. 나중에 텍스처 강좌에서 이 함수의 사용법을 볼 수 있습니다.


  렌더링 중에 mipmap의 레벨을 전환할 때 OpenGL은 두 mipmap 레이어 사이에 가장자리가 선명하게 나타나는 것과 같은 일부 결함을 나타낼 수도 있습니다. 일반적인 텍스처 필터링과 마찬가지로 mipmap 레벨을 전환하기 위해 NEARESTLINEAR 필터링을 사용하여 mipmap 레벨 사이를 필터링 할 수도 있습니다. mipmap 레벨 사이의 필터링 방법을 지정하기 위해 원래의 필터링 방법을 다음 네 가지 옵션 중 하나로 대체할 수 있습니다.


  • GL_NEAREST_MIPMAP_NEAREST: nearest neighbor 보간법으로 mipmap을 필터링하고 텍스처 샘플링도 nearest neghbor 보간법을 사용합니다.
  • GL_LINEAR_MIPMAP_NEAREST: nearest neighbor 보간법으로 mipmap을 필터링하고 텍스처 샘플링은 linear 보간법을 사용합니다.
  • GL_NEAREST_MIPMAP_LINEAR: linear 보간법으로 mipmap을 필터링하고 텍스처 샘플링은 nearest neighbor 보간법을 사용합니다.
  • 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는 가장 많이 쓰이는 파일 형식을 로드할 수 있고 프로젝트에 쉽게 통합할 수 있는 Sean Barrett의 매우 인기있는 싱글 헤더 이미지로드 라이브러리입니다. stb_image.h여기에서 다운로드할 수 있습니다. 하나의 헤더 파일을 다운로드하여 프로젝트에 stb_image.h로 추가하고 새로운 C++ 파일을 만들어 다음 코드를 추가하세요.


#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

 STB_IMAGE_IMPLEMENTATION을 정의함으로써 전처리기는 헤더 파일을 관련된 정의 소스 코드만 포함하도록 하여 헤더 파일을 효과적으로 .cpp 파일로 변환합니다. 이제 stb_image.h를 프로그램의 어딘가에 포함시키고 컴파일하세요.


  다음 텍스처 섹션에서는 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 개의 정수형 변수를 두 번째, 세 번째, 네 번째 파라미터로 받습니다. 나중에 텍스처를 생성하기 위해 이미지의 너비와 높이가 필요합니다.

텍스처 생성

  이전의 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);

  파라미터가 꽤 많은 큰 함수이기 때문에 하나하나 살펴보도록 하겠습니다.


  • 첫 번째 파라미터는 텍스처 타겟을 지정합니다. 이를 GL_TEXTURE_2D로 설정한다는 것은 현재 GL_TEXTURE_2D로 바인딩된 텍스처 객체에 텍스처를 생성하겠다는 뜻입니다(그래서 GL_TEXTURE_1DGL_TEXTURE_3D로 바인딩된 객체에는 아무런 영향을 끼치지 않습니다).
  • 두 번째 파라미터는 우리가 생성하는 텍스처의 mipmap 레벨을 수동으로 지정하고 싶을 때 지정합니다. 하지만 우리는 베이스 레벨일 0로 남겨두겠습니다.
  • 세 번째 파라미터는 OpenGL에게 우리가 저장하고 싶은 텍스처가 어떤 포멧을 가져야 할지 알려줍니다. 우리의 이미지는 오직 RGB 값만 가지고 있으므로 텍스처를 RGB 값과 함께 저장할 것입니다.
  • 네 번째, 다섯 번째 파라미터는 결과 텍스처의 너비와 높이를 설정합니다. 우리는 이미지를 로딩할 때 이미 저장해두었으므로 해당 변수들을 사용할 것입니다.
  • 그 다음 파라미터는 항상 0을 지정해야합니다.
  • 일곱 번째, 여덟 번째 파라미터는 원본 이미지의 포멧과 데이터타입을 지정합니다. 우리는 RGB 값이 있는 이미지를 로드했고 chars(bytes)로 저장하였으므로 해당하는 값으로 설정합니다.
  • 마지막 파라미터는 실제 이미지 데이터입니다.

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

텍스처 적용

  다음 섹션에서 우리는 Hello Triangle 강좌에서 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를 추가했기 때문에 OpenGL에게 새로운 vertex 포멧을 다시 알려주어야 합니다.


Image of VBO with interleaved position, color and texture data with strides and offsets shown for configuring vertex attribute pointers.

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

  이전의 2개의 vertex attribute가 있었을 때의 stride 파라미터를 8 * sizeof(float)로 수정해야합니다.


  그 다음 vertex attribute로서 텍스처 좌표를 받을수 있도록 vertex shader를 수정해야합니다. 그러면 텍스처 좌표는 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는 텍스처 객체에 접근해야지만 어떻게 텍스처 객체를 fragment shader에 보낼 수 있을까요? GLSL은 sampler라고 불리는 텍스처 객체에 대한 데이터 타입을 가지고 있습니다. sampler는 텍스처의 타입에 대한 접미사를 가지고 있습니다. 예를 들어 sampler1D, sampler3D 가 있고 우리의 경우에는 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);

  모든 것을 올바르게 수행했다면 다음 이미지를 볼 수 있을 것입니다.



  사각형이 완전히 흰색이나 검은색이라면 아마 오류가 발생한 것일 가능성이 있습니다. shader 로그를 확인하고 여러분의 코드와 소스 코드를 비교해보세요.

  텍스처가 동작하지 않거나 검은색으로 나타난다면 강좌를 계속해서 읽고 여러분만의 방법으로 작업해보세요. 마지막 예제에서는 반드시 동작해야합니다. 어떠한 드라이버에서는 항상 각 sampler uniform에 잠시 뒤에 언급할 텍스처 유닛을 할당해야만 동작할 수도 있습니다.

  좀 펑키한 것을 만들기 위해 최종 텍스처 컬러와 vertex 컬러를 혼합할 수 있습니다. fragment shader에서 간단히 최종 텍스처 컬러와 vertex 컬러를 곱하는 것으로 두 컬러를 혼합할 수 있습니다.


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

  결과는 vertex 컬러와 텍스처 컬러가 혼합된 것이어야 합니다.



우리의 컨테이너는 디스코를 좋아하는 것 같군요.

Texture Units

  glUniform 함수를 사용하여 값을 할당하지 않음에도 불구하고 왜 sampler2D 변수가 uniform인지 궁금할 것입니다. glUniform1i 함수를 사용하여 실제로 텍스처 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_TEXTURE8GL_TEXTURE0 + 8 과 같은 형식으로도 접근 가능합니다. 이는 여러 텍스처 유닛들에 접근해야할 때 유용합니다.

  다른 sampler를 받기 위해 fragment shader를 수정해야 합니다. 이는 비교적 쉽습니다.


#version 330 core
...

uniform sampler2D texture1;
uniform sampler2D texture2;

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

  최종 출력 컬러는 이제 두 개의 텍스처를 혼합한 것입니다. GLSL의 mix 함수는 입력으로 두 개의 값을 받고 세 번째 파라미터를 기반으로 linear 보간법으로 그들을 보간합니다. 세 번째 값이 0.01.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);
}

  이제 우리는 alpha(투명도) 채널을 가지고 있는 .png 이미지를 로드합니다. 이는 GL_RGBA를 사용하여 alpha 채널을 포함하고 있는 이미지 데이터라는 것을 명시해야 한다는 것을 의미합니다. 그러지 않으면 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 함수를 사용하여 각 sampler를 설정함으로써 OpenGL에게 각 shader sampler가 속하는 텍스처 유닛이 어떤 것인지를 알려주어야 합니다. 오직 한번만 설정하면 되므로 렌더링 루프에 들어가기 전에 할 수 있습니다.


ourShader.use(); // uniform을 설정하기 전에 shader를 활성화해야 한다는 것을 잊지마세요!  
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 직접 설정
ourShader.setInt("texture2", 1); // 혹은 shader 클래스를 활용
  
while(...) 
{
    [...]
}

  glUniform1i 함수를 통해 sampler를 설정함으로써 각 uniform sampler를 적절한 텍스처 유닛과 맞춥니다. 다음과 같은 결과를 볼 수 있습니다.



  텍스처가 거꾸로 뒤집혀있다는 것을 알 수 있습니다. OpenGL이 y 축의 0.0 좌표를 이미지의 아래쪽으로 인식하기 때문입니다. 하지만 대부분의 이미지는 0.0는 y축의 맨 위를 가리킵니다. 운좋게도 stb_image.h는 이미지를 로드하기 전에 다음과 같은 상태를 추가하면 이미지를 로딩하는 동안 뒤집을 수 있습니다.


stbi_set_flip_vertically_on_load(true);  

  stb_image.h 에게 뒤집으라고 말 한 후 이미지를 로드하면 다음과 같은 결과를 볼 수 있습니다.



  행복한 컨테이너를 볼 수 있다면 여러분은 오바르게 한 것입니다. 소스 코드와 비교해보세요.

연습

  텍스처 사용에 있어서 응용을 위해 계속 진행하기 전에 연습을 수행하는 것을 권장합니다.


  • 오직 행복한 얼굴만 다른/반대의 방향으로 보이게 fragment shader를 수정하세요: 해답
  • 텍스처 좌표를 0.0f부터 1.0f까지가 아닌 0.0f부터 2.0f까지로 지정하여 텍스터 wrapping 방법들의 차이점을 직접 확인해보세요. 가장자리에 고정되는 방법을 사용한다면 4개의 스마일 얼굴을 볼 수 있을 것입니다: 해답, 결과. 다른 방법들도 확인할 수 있을 것입니다.
  • 사각형에 각각의 픽셀들이 보일 수 있도록 텍스처 좌표를 수정하여 텍스처 이미지의 픽셀들의 중앙 색상만 보일 수 있도록 하세요. 텍스처 filtering 방법을 GL_NEAREST로 설정하여 픽셀이 좀더 선명해지는 것을 확인해보세요: 해답
  • mix 함수의 세 번째 파라미터(두 텍스처의 비율)로 uniform 변수를 사용하세요. 위, 아래 방향키를 사용하여 컨테이너와 얼굴의 보이는 정도를 조절할 수 있도록 해보세요: 해답




출처 : https://learnopengl.com, Joey de Vries

반응형