Post Lists

2018년 6월 24일 일요일

Getting Started - Textures (7) 번역

위의 자료를 번역한 내용입니다.
--------------------------------------

Textures

우리의 오브젝트들에 좀 더 세부사항을 추가하기 위해서, 좀 더 흥미로운 이미지를 만들려고 각각의 vertex에 색을 사용하는 것을 배웠었다. 그러나, 좀 더 현실성을 얻귀애헛, 우리는 많은 정점들을 가져야만 한다. 그래서 우리는 많은 색을 정할 수가 있다. 이것은 상당한 추가 오버헤드를 필요로한다. 왜냐하면 각 모델들이 더 많은 정점들을 필요로하고, 각각의 정점에 대해 color attribute도 또한 필요로 하기 때문이다.

예술가들과 프로그래머들이 일반적으로 선호하는 것은 텍스쳐를 사용하는 것이다. 텍스쳐는 2D 이미지이다. (심지어 1D 와 3D 텍스쳐들도 존재한다.) 이것은 오브젝트에 세부사항을 추가하기 위해 사용된다. 텍스쳐를 멋진 벽돌 이미지를 가진 종이 한장으로 생각해보아라. 그리고 예를들어 너의 3D 집에 잘 접혀져있다고. 그것은 너이 집이 돌로 된 인테리어를 가진 것처럼 보인다. 왜냐하면 하나의 이미지에 많은 세부사항을 넣을 수 있기 때문에, 우리는 많은 정점들을 명시해야하는 것 없이 객체가 매우 세부적이게 되는 환영을 줄 수 있따.

Green Box
이미지들을 제쳐두고, 텍스쳐들은 또한 쉐이더에 보낼 많은 데이터 집합들을 저장하는데 사용할 수 있다. 그러나, 다른 주제들을 위해서 그것을 남겨둘 것이다.

아래에서 너는 이전 튜토리얼로부터 삼각형에 사상된 벽돌로 된 벽의 텍스쳐 이미지를 볼것이다.

텍스쳐를 삼각형에 mapping하기 위해서, 우리는 삼각형의 각 정점에 대해, 그것이 어떤 텍스쳐의 부분과 일치하는지를 말할 필요가 있다. 각각의 정점은 따라서 무슨 텍스쳐 이미지의 부분이 sample되어야하는지를 명시하는 것들과 사상된 텍스쳐 좌표를 가져야만 한다. 텍스쳐 좌표는 텍스쳐 이미지의 하단 왼쪽 구석에서 (0,0)으로 시작하고, 텍스쳐 이미지의 위쪽 오른쪽 모서리에서 (1,1)이다. 다음의 이미지는 어떻게 텍스쳐 좌표를 삼각형에 map하는지를 보여준다.

우리는 세 개의 텍스쳐 좌표점을 삼각혀에 대해 명시한다. 우리는 삼각형의 왼쪽 하단이 텍스쳐의 왼쪽 하단과 일치하기를 원한다. 그래서, 우리는 삼각형의 하단 왼쪽의 정점에 대해, 텍스쳐 좌표 (0,0)을 사용한다. 같은 것이 오른쪽 하단에 (1,0) 텍스쳐 좌표를 적용한다. 삼각형의 상단은 텍스쳐 이미지의 상단 중앙부와 일치해야 한다. 그래서 우리는  (0.5, 1.0)을 그것의 텍스쳐 좌표로서 가진다. 우리는 오찍 3 개의 텍스쳐 좌표를 vertex shader에게 넘긴다. 그리고 그 쉐이더는  그러한 것들을 fragment shader로 넘긴다. fragment shader는 각 fragment에 대해 모든 texture 좌표들을 깔끔하게 interpolate한다.

최종 텍스쳐 좌표는 이것과 같아 보일 것이다. :

float texCoords = 
{
0.0f, 0.0f, // lower-left corner
1.0f, 0.0f, // lower-right corner
0.5f, 1.0f // top-center corner
};

텍스쳐 sampling은 느슨한 interpretation을 가진다. 그리고 많은 다른 방식들로 처리 될 수 있다. OpenGL에게 어떻게 그것의 텍스쳐를 sample하도록 하는지 말하는 것은 따라서 우리의 일이다.

Texture Wrapping
텍스쳐 좌표들은 보통 (0,0)에서 (1,1)의 범위를 가지지만, 만약 우리가 이 범위 밖으로 좌표를 명시하면 어떨까? OpenGL의 기본 행동은 텍스쳐의 이미지를 반복시키는 것이다. (우리는 기본적으로 부동소수점 텍스쳐 좌표의 정수부분을 무시한다.), 그러나 OpenGL이 제공하는 좀 더 많은 옵션들이 있다.

* GL_REPEAT : 텍스쳐에 대한 기본 행동들, 텍스쳐 이미지들을 반복
* GL_MIRRORED_REPEAT : GL_REPEAT와 같지만, 각각의 반복과 함께, 이미지를 반사시킨다.
* GL_CLAMP_TO_EDGE : 0과 1사이의 좌표를 clamp시킨다. 결과는 더 큰 좌표들은 edge로 clamped된다. 그것은 쫙 펴지는 edge pattern을 만든다.
* GL_CLAMP_TO_BORDER : 범위 밖의 좌표들은 지금 사용자-명시된 border color들이 주어진다.

기본 범위 밖의 텍스쳐 좌표들을 사용할 때, 각 옵션들은 다른 시각적 결과물을 가진다. 샘플 텍스쳐 이미지에서 어떤지 봐보자.

앞 서 언급한 옵션들의 각각은 좌표 축마다 설정될 수 있다. (s,t (그리고 3D 텍스쳐를 사용한다면 r) 들은 x,y,z와 동일하다.) glTexParameter* 함수와 함께:

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 mode를 넘기기를 요구한다. 이 경우에, OpenGL은 그것의 텍스쳐 wrapping option을 현재 활성화된 텍스쳐를 GL_MIRRORED_REPEAT로 설정한다.

만약 우리가 GL_CLAMP_TO_BORDER 옵션을 선택한다면, 우리는 border color을 명시해야만 한다. 이것은 glTexParamet의 함수와 동일한 fv를 사용하여 처리된다. GL_TEXTURE_BORDER_COLOR와 함께, 우리가 가장자리의 color 값의 float 배열에서 넘기는 옵션으로서:

float borderColor[] = {1.f, 1.f, 0.f, 1.f};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

Texture Filtering
텍스쳐 좌표들은 해상도에 의존하지 않지만, 어느 부동소수점 값이 될 수 있다. 따라서, OpenGL은 어떤 텍스쳐 pixel을 (texel이라고 또한 알려진) 텍스쳐 좌표에 mapping시킬지를 알아내야만 한다. 이것은 특별하게 너가 매우 큰 오브젝트를 가지고, 낮은 해상도 텍스쳐를 가진다면 중요해진다. 너는 아마다 지금까지 OpenGl이 이 texture filtering에 대한 옵션을 가지고 있다는 것을 추축했다. 이용가능한 몇 가지 옵션들이 있지만, 지금, 우리는 가장 중요한 옵션들인 GL_NEAREST와 GL_LINEAR에 대해 이야기 할 것이다.

GL_NEAREST (nearest neighbor filtering으로 또한 알려진)은 OpenGL의 기본 텍스쳐 filtering이다. GL_NEAREST로 설정될 때, OpenGL은 텍스쳐의 좌표에 가장 가까운 중앙 픽셀을 선택한다. 아래에서 너는 4개의 픽셀들을 볼 수 있는데, 그 픽셀들에서, 그 십자가 모양은 정확한 텍스쳐 좌표를 나타낸다. 왼쪽 상단 texel은 텍스쳐 좌표에 대해 가장 가까운 중앙을 가지고 있고, 그러므로 sampled color로 선택 되어진다.

GL_LINEAR ((bi)linear filtering으로 또한 알려진)은 텍스쳐 좌표의 인접한 texel들로부터 텍셀 사이의 값을 근사시켜 interpolated된 값을 가져온다. texel의 중앙에 대한 텍스쳐좌표의 거리가 작을수록, texel의 색은 sampled color에 더욱 많이 나타난다. 아래에서 우리는 인접한 픽셀들의 혼합된 색이 반환된 것을 볼 수 있다.

그러나 그러한 texture filtering method의 시각적 효과는 무엇인가? 큰 오브젝트에 낮은 해상도를 가진 텍스쳐를 사용할 때, 이 방법들이 어떻게 작동하는지를 봐보자 (텍스쳐는 그러므로 위쪽으로 스케일되고, 개별 texel들은 두드러진다.)

GL_NEAREST는 blocked pattern들을 만든다. 그 패턴에서, 우리는 명백히 GL_LINEAR가 개별 픽셀들이 덜 보이는 더 부드러운 패턴으로 만들어내는 반면에 텍스쳐를 형성하는 픽셀들을 볼 수 있다. GL_LINEAR는 좀 더 현실적인 output을 만들어내지만, 몇 개발자들은 좀 더 8-bit 모습을 선호하고, 결과적으로 GL_NEARST 옵션을 고른다.

Texture filtering은 연산들을 늘이거나 줄이거나에 대해 설정될 수 있다. (스케일 업 또는 다운할 때) 그래서 너는 예를들어, 텍스쳐가 scale downward할 때, nearest neighbor filtering을 사용할 수 있고, upscaled texture에 대해 linear filtering을 사용할 수 있다. 우리는 따라서 glTexParameter*로 두 옵션들에 대해 filtering method를 명시해야만 한다. 그 코드는 wrapping method를 설정하는것과 유사하다.

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

Mipmaps
 우리가 수 천개의 오브젝트들이 있는 큰 방을 가졌다고 생각해보자. 거기에 각각 텍스쳐가 부착되어있고. 저 멀리에 보는 사람에 가까운 객체로서, 부착된 같은 높은 해상도의 텍스쳐를 가진 객체들이 있을 것이다. 오브젝트들이 멀리있고 아마도 몇 개의 fragments들만을 만들어내기 때문에, OpenGL은 고해상도의 텍스쳐로부터 그것의 fragment에 대한 올바른 color 값을 가져오는데 어려움이 있다. 왜냐하면 그것은 텍스쳐의 큰 부분에서 fragment에 대한 텍스쳐 color를 골라야 하기 때문이다. 이것은 작은 오브젝트들에 대해서는 가시적인 인공물을 만들것이다. 작은 오브젝트들에 높은 해상도 텍스쳐들을 사용하기위해 메모리의 낭비는 말할 것도 없이.

이 문제를 해결하기 위해서, OpenGL은 기본적으로 각각 나중의 텍스쳐가 이전의 것과 비교해서 두 배 더 작은 텍스쳐 이미지들의 집합인 mipmpas이라 불리는 개념을 사용한다.  mipmap뒤에 있는 아이디어는 이해하기에 쉬울 것이다 : viewer로부터 어떤 먼 거리의 threshold 이후에, OpenGL은 그 객체에 대한 거리에 가장 잘 맞는 다른 mipmap 텍스쳐를 사용할 것이다. 왜냐하면 그 객체가 멀리 있기 때문에, 더 작은 해상도는 사용자에게 눈에 띄지 않을 것이다. 또한, mipmaps들은 추가적인 보너스 특징이 있는데, 그것은 성능에 또한 좋다는 것이다. mipmapped texture가 어떻게 보이는지 자세히 봐보자.

각 texture image에 대해 mipmapped 텍스쳐들의 모음을 만드는 것을 수동으로 하기에는 번거롭지만, 운 좋게도 OpenGL은 우리가 한 텍스쳐를 만든 후에 glGenerateMipmaps라는 단일의 호출로 우리를 위해 모든 일을 할 수가 있다. 텍스쳐 튜토리얼에서 나중에, 우리는 이 함수의 사용을 볼 것이다.

렌더링 하는 동안에, mipmaps levels들 사이에서 바뀔 때, OpenGL은 두 개의 mipmap layer들 사이에서 보이는 날카로운 모서리 같은 몇 몇 인공물들을 보여줄지도 모른다. 일반적인 texture filtering처럼, NEAREST와 LINEAR 필터링을 사용하여 mipmap level들 사이를 filter하는 것이 또한 가능하다. mipmap levels사이의 filtering method를 명시하기 위해, 우리는 원래의 filtering methods들을 다음 네 가지 옵션들 중 하나로 대체할 수 있다.

* GL_NEAREST_MIPMAP_NEAREST : 픽셀 사이즈를 맞추기 위해 가장 가까운 mipmap을 가져오고, 텍스쳐 샘플링을 위해 nearest neighbor interpolation을 사용
* GL_LINEAR_MIPMAP_NEAREST : 가장 가까운 mipmap level을 가져오고, linear interpolation을 샘플한다.
* GL_NEAREST_MIPMAP_LINEAR : 한 픽셀의 크기와 가장 잘 맞는 두 개의 mipmap사이에서 선형으로 interpolate하고, nearest neighbor interpolation으로 샘플링
* GL_LINEAR_MIPMAP_LINEAR : 두 개의 가장 가까운 mipmap사이에서 선형으로 interpolate하고, linear interpolation으로 텍스쳐를 샘플링

텍스쳐 filtering처럼, 우리는 glTexParameteri를 사용하여 4개의 앞서 언급된 방법들 중 하나로 필터링 방법을 설정할 수 있다 :

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

흔한 실수는 magnification filter로서 mipmap filtering option들 중 하나로 설정하는 것이다. 이것은 mipmaps들이 우선적으로 텍스쳐가 downscaled될 때를 위해 사용되기 때문에 어떤 효과도 갖지 않는다 : 텍스쳐 magnification은 mipmap을 사용하지 않고, 그것에게 mipmap filtering option을 주는 것은 OpenGL에게 GL_INVALID_ENUM 에러 코드를 발생시킨다.

Loading and creating textures
 실제로 텍스쳐를 사용하기 위해 우리가 해야할 첫 번째의 것은 그것들을 우리의 프로그램으로 불러오는 것이다. 텍스쳐 이미지들은 수십 개의 파일 포맷들로 저장될 수 있다. 그들의 각각의 데이터 구조와 순서를 가진채. 그래서 어떻게 우리는 그러한 이미지들을 우리의 프로그램으로 불러올 수 있는가? 한 해결책은 우리가 사용하고 싶은 file format을 사용하는 것이다. 가령 .png를 그리고 이미지 format을 큰 바이트의 배열로 변환하기 위해 우리의 이미지 loader를 작성하는 것이다. 너 자신의 image loader를 작성하는 것은 어렵지 않지만, 여전히 번거로운 일이고 너가 좀 더 많은 파일 포맷들을 지원하면 어떨 것인가? 너는 그러면 너가 지원하고자 하는 각각의 포맷에 대한 이미지 로더를 작성해야만 할 것이다.

다른 해결책은, 아마도 좋은 것인데, 몇 가지 인기있는 포맷들을 지원하고 우리를 위해 모든 어려운 일들을 할 이미지로드 라이브러리를 사용하는 것이다. stb_image.h같은 라이브러리.

stb_image.h
stb_image.h는 가장 인기있는 파일 포맷들을 load할 수 있고, 너의 프로젝트에 통합하기에 쉬운 Sean Barrett에 의해 작성된 인기있는 단일 헤더 이미지 로드 라이브러리 이다. stb_image.h는 여기에서 다운로드 되어질 수 있다. 간단히, 단일 헤더파일을 다운로드하고, 그것의 너의 프로젝트에 stb_image.h로서 추가해라. 그리고 다음의 코드로 부가적인 C++ file을 만들어라:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

STB_IMAGE_IMPLEMENTATION을 정의 함으로써 전처리기는 헤더파일을 수정하여, 그것인 오직 관련 정의 소스코드만 포함하도록 한다. 이것은 효과적으로 헤더파일을 .cpp 파일로 바꾼다. 간단히 stb_image.h를 너의 프로그램 어딘가에서 include하고 컴파일해라.

다음 텍스쳐 부분을 위해, 우리는 wooden container의 이미지를 사용할 것이다. stb_image.h를 사용하여 이미지를 load하기 위해 우리는 그것의 stbi_load 함수를 사용한다:

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

그 함수는 첫 째로, 이미지 파일의 위치를 인풋으로서 받는다. 그리고나서 그것은 너가 세 개의 integer들을 주는 것을 기대한다. stb_image.h는 최종 이미지의 width, height, color channels들로 그것들을 채울 것이다. 우리는 이미지의 너비와 높이를 나중에 텍스쳐를 생성하기 위해 필요하다.

Generating a texture
OpenGL에서 이전의 오브젝트들 처럼, 텍스쳐들은 ID로 참조된다. 하나를 만들어 보자:

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

glGenTextures 함수는 처음에 얼마나 많은 텍스쳐를 생성하길 원하는 가를 인풋으로서 받고, 그것들을 unsigned int array에 저장한다. 그것의 두 번째 인자로서 (우리의 경우에 단일의 unsigned int). 다른 오브젝트들 처럼 우리는 그것을 바인드 할 필요가 있다. 그래서 어떤 이후의 텍스쳐 명령어들은 현재 bound texture를 설정할 것이다 :

glBindTexture(GL_TEXTURE_2D, texture);

텍스쳐가 bound됐으니, 우리는 이전의 불러와진 이미지 데이터를 사용하여 텍스쳐를 생성할 수 있다. 텍스쳐들은 glTexImage2D로 생성된다 :

glTextImage2D(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또는 _3D 타겟으로 바운딩된 어떤 텍스쳐든 영향을 받지 않는다.)
* 두 번째 인자는 너가 만들고자하는 텍스쳐의 mipmap level를 명시한다. 만약 너가 각각의 mipmap level을 개별적으로 설정하고 싶다면, 우리는 그것을 기본 level인 0으로 둘 것이다.
* 세 번째 인자는 OpenGL에게 어떤 포맷의 종류로 텍스쳐를 저장하고 싶은지를 말한다. 우리의 이미지는 오직 RGB 값들을 가진다. 그래서 우리는 텍스쳐를 RGB 값들로 또한 저장할 것이다.
* 4, 5번째 인자는 최종 텍스쳐의 너비와 높이를 설정한다. 우리는 그것들을 이미지를 로드할 때 이미 저장했었다. 그래서 우리는 똑같은 변수를 사용할 것이다.
* 다음 인자는 항상 0이 되어야만 한다. (legacy stuff)
* 7, 8번째 인자는 소스 이미지의 foramt과 data type을 명시한다. 우리는 RGB 값을 가진 이미지를 로드했고, 그것들을 chars(bytes)로 저장했다. 그래서 우리는 그에 상응하는 값들을 넘길 것이다.
* 마지막 인자는 실제 이미지 데이터이다.

일단 glTexImage2D가 호출된다면, 현재 바운딩된 텍스쳐 객체는 지금 그것에 부착된 텍스쳐 이미지를 갖는다. 그러나, 현재 그것은 로드된 텍스쳐의 이미지의 기본 수준만을 가지고 있다. 그래서 만약 우리가 mipmap을 사용하길 원한다면 모든 다른 이미지들을 수동으로 명시해야만 한다. (지속적으로 두 번째 인자를 증가시기면서) 또는, 우리는 glGenerateMIpMap을 텍스쳐를 생성한 후에 호출 할 수 있다. 이것은 자동적으로 모든 요구된 mipmap들을 현재 바운딩된 텍스쳐에 대햐여 생성할 것이다.

텍스쳐와 그것에 일치하는 mipmaps들을 생성하고난 후, 이미지 메모리를 해제하는 것은 좋은 습관이다:

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);
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\n";
}
stbi_image_free(data);

Applying textures
다가오는 섹션들에 대해서, 우리는 Hello Triangle 튜토리얼에서의 마지막 부분에 있는 glDrawElements로 그려지는 사각형 모양을 사용할 것이다. 우리는 OpenGL에게 어떻게 텍스쳐를 샘플링할지를 알려줄 필요가 있다. 그래서 우리는 vertex data를 texture 좌표로 업데이트 해야만 할 것이다.

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 attribute를 더 했기 때문에, 우리는 다시 OpenGl에게 새로운 vertex foramt을 알려야만 한다.

Vertex1 attributes, XYZ, RGB, ST
Stride = 32
Position offset = 0, Color offset = 12, Texture Offset, 24

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

우리가 이전의 두 개의 vertex attributes들의 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 output 변수를 input 변수로서 받아야만 한다.

fragment shader는 또한 texture object에 접근해야만 한다. 그러나 어떻게 texture object를 fragment shader로 넘길 것인가? GLSL은 예를들어 sampler1D, sampler3D 또는 우리의 경우에 sampler2D같은 우리가 원하는 텍스쳐 타입을 후위표기를 한, sampler라고 불리는 텍스쳐 객체에 대한 내장 데이터 타입을 가지고 있다. 우리는 그리고나서 간단하게  우리가 나중에 텍스쳐를 할당할 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);
}

텍스쳐의 color를 샘플링 하기 위해, 우리는 GLSL의 내장 texture 함수를 사용한다. 그것은 첫 번째 인자로서, 텍스쳐 sampler를 받고, 두 번째 인자로서 그에 상응하는 텍스쳐 좌표를 받는다. 그 texture function은 그리고나서 우리가 이전에 설정한 텍스쳐 파라미터를 사용하여 그에 상응하는 color value를 샘플링한다. 이 fragment shader의 output은 그리고나서 (interpolated된) 텍스쳐의 좌표에서 텍스터의 (filtered된) color이다.

지금 남아있는 할 일은, glDrawElements를 호출하기전에 텍스쳐를 bind하는 것이다. 그리고 그것은 자동적으로 텍스쳐를 fragment shader의 샘플러에 할당한다:

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

너가 모든 것을 옳게 했다면, 다음의 이미지를 볼 것이다.

 

만약 너의 사각형이 완전히 흰색이거나 검정색이라면, 너는 아마도 하다가 에러를 만들었다는 것이다. 너의 쉐이더 log들을 확인하고, 코드와 프로그램의 소스코드를 비교해봐라.

Red Box
만약 너의 텍스쳐 코드가 작동하지 않거나, 완전히 검정색으로 나온다면, 계속해서 읽고, 마지막 예제가 작동하게 해보아라. 몇몇 드라이버에서, 각각의 샘플러 uniform에 텍스쳐 unit을 항상 할당하는 것이 요구된다. 우리는 이것을 이 튜토리얼의 나중에 이야기 할 것이다.

먼진 것을 얻기 위해, 우리는 또한 vertex colors들과 최종 texture colors들을 합칠 수 있다. 우리는 간단히 최종 텍스쳐 색과 vertex color를 두 색들을 합치기 위해 fragment shader에서 곱할 수 있다.

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

결과는 vertex's color와 texture's coor의 혼합이여야 한다.

 

Texture Units
너는 아마도 왜 sampler2D 변수가 uniform인지 궁금해 했을 것이다. 우리가 그것에게 glUniform과 함꼐 값을 할당하지 않았는데도. glUniform1i를 사용하여, 우리는 실제로 location value에 texture sampler를 할당할 수 있다. 그래서 우리는 fragment shader에서 한 번에 많은 텍스쳐들을 설정할 수 있다. 이 한 텍스쳐의 위치는 좀 더 흔하게 texture unit으로 알려져 있다. 한 텍스쳐에 대한 default texture unit은 0이다. 이것은 default active texture unit이다. 그래서 우리는 이전 섹션에서 위치를 할당할 필요가 없었다.; 모든 그래픽 드라이버들이 default texture unit을 할당하지는 않는다는 것을 주목해라. 그래서 이전 섹션이 너의 것에서는 렌더링되지 않았을 수도 있다.

texture units의 주된 목적은 우리가 쉐이더들에서 1개 이상의 텍스쳐를 사용하게 하는 것이다. texture units들을 samplers에 할당하여, 우리는 처음에 우리가 상응하는 텍스쳐 unit을 활성화 시키는한, 한 번에 많은 텍스쳐들을 bind 시킬 수 있다. glBindTexture처럼, glActiveTexture를 사용하여, 우리는 우리가 사용하고자 하는 텍스쳐 유닛에 넘겨지는 texture units들을 활성화 시킬 수 있다 :

glActiveTexture(GL_TEXTURE0); // activate the texture unit first before binding texture
glBindTexture(GL_TEXTURE_2D, texture);

한 texture unit을 활성화 한 후에, 이후의 glBindTexture 호출은 그 texture를 현재 활성화된 active texture unit에 바인드 시킬 것이다. Texture unit GL_TEXTURE0은 항상 기본적으로 활성화 되어져있다. 그래서 우리는 이전 예제에서, glBindTexture를 사용할 때, 어떤 texture units들을 활성화 할 필요가 없었다.

Green Box
OpenGL은 적어도 GL_TEXTURE0~15까지 사용하여 너가 활성화하여 너가 사용할 수 있는 최소 16개의 texture units들을 가질 것이다. 그것들은 예를들어, GL_TEXTURE0 + 8같은 것을 통해서 GL_TEXTURE8을 얻을 수 있도록 정의되어졌다. 이것은 우리가 몇 가지 texture units들을 반복시켜야 할 때 유용하다.

그러나 우리는 여전히 다른 샘프러를 받기위해 fragment shader를 수정할 필요가 있다. 이것은 상대적으로 지금 간단하다:

#version 330 core
~
uniform sampler2D texture1
uniform sampler2D texture2;

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

---- 
변형

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

최종 output color는 지금 두 texture lookups들의 조합이다. GLSL의 내장 mix 함수는 두 값들을 input으로 받아서, 그것의 세 번째 인자를 기반으로 하여 선형으로 그것들 사이를 interpolate한다. 만약 세 번째 value가 0.0이라면, 첫 번째 input을 반환한다. 만약 그것이 1.0이라면, 두 번째 input value를 반환한다. 0.2 값은 첫 번째 input color의 80% 그리고 두 번째 input color의 20%를 반환한다. 이것은 우리의 두 텍스쳐의 혼합을 만든다.

우리는 이제 다른 텍스쳐를 불러와서 만들기를 원한다. 너는 이제 그 단계들에 친숙해야만 한다. 다른 텍스쳐 오브젝트를 만들고, 이미지를 불러오고 glTexImage2D를 이용하여 최종 텍스쳐를 생성하도록 해라. 두 번째 텍스쳐에 대해, 우리는 OpenGL 배우는 동안의 너의 얼굴 표정 이미지를 사용할 것이다.

두 번째 텍스쳐를 사용하기 위해서 (그리고 첫 번째 것도) 우리는 두 텍스쳐를 상응하는 texture unit으로 바인딩함으로써, 렌더링 절차를 조금 바꿔야만 한다.

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

우리는 또한 OpenGL에게 glUniform1i를 사용하여 각 샘플러를 설정함으로써 각 쉐이더 샘플러가 어떤 texture unit에 속하는지를 말해주어야만 한다. 우리는 오직 이것을 한 번만 설정해야 한다. 그래서 우리는 이것을 render loop에 가기 전에 할 수 있다.

ourShader.use();
ourShader.setInt("texture1", 0); // or glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);
ourShader.setInt("texture2", 1); 

glUniform1i를 통해 샘플러들을 설정하여, 우리는 가각의 uniform sampler가 적절한 texture unit에 일치하도록 한다. 너는 다음의 결과를 얻을 것이다.

 

너는 아마도 텍스쳐가 거꾸로 뒤집어졌다는 것을 알아챘을 것이다. 이것은 OpenGL이 Y축에서의 0,0 좌표가 이미지의 하단 부가 되기를 원하기 떄문이다. 그러나 이미지들은 보통 Y축의 꼭대기에 0,0을 가진다. 운좋게도 우리에게, stb_image.h는 어떤 이미지를 로드하기전에, 다음 문장을 추가하여 이미지를 불러오는 동안 y축을 뒤집을 수 있다.

stbi_set_flip_vertically_on_load(true);


이미지를 불러올 때, stb_image.h가 y축을 뒤집으라고 말한 후에 너는 다음과 같은 결과를 얻는다.

만약 너가 한 행복한 container를 본다면, 너는 옳게 한 것이다. 소스코드와 비교해볼 수 있다.
--------------------------------------------
Test... setInt에서 "texture1" "texture2"라고 입력해야 하는데, "textuer1" "textuer2"라 입력해서
왜 안되는지 모르고 한 시간동안 정보만 찾아다녔다... 
shader 변수 오타는 내기 쉽고, 디버깅해서 찾기도 어려운 것 같다..







Exercise
 텍스쳐와 좀 더 편해지기 위해, 계속하기전에 이 문제들을 해내는 것이 충고되어진다.

* happy  face가 fragment shader를 바꾸어서 다른 방향/역 방향이 되도록 해라.
 

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
    vec4 t1 = texture(texture1, TexCoord);
    
    vec2 test = TexCoord;
    test.x *= -1;
    test.y *= -1;
    vec4 t2 = texture(texture2, test);

    FragColor = mix(t1, t2, 0.2);
}

TexCoord를 받아서, 그것의 x,y(Texture로 치면 s,t)를 -1을 곱해주었다. x축 대칭, y축대칭을 한 것이므로, 원점대칭 한 것으로 볼 수 있겠다.

이 튜토리얼의 솔루션을 봐보니

(1.0 - TexCoord.x)

위의 식을 통해서 대칭을 이루었는데, Texture의 st 좌표가 0~1의 범위이기 때문에, 저렇게 표현하는게 맞다. 나는 그것을 모른채 그냥 * -1을 했는데,
0 ~ 1, -1 ~ 0도 그렇게 표현이 된다. 즉 부호가 다르면 대칭의 위치에 찍히게 되는 것이다.


* 텍스쳐 좌표를 0.0f에서 1.0f 대신에 0.0f에서 2.0f 범위에서 명시하여, 다른 텍스쳐 wrapping 방식을 실험해라. 너는 그것의 모서리에서 납작해진 단일의 container image에서 4개의 웃는 얼굴들을 보일 수 있는지 보아라.

verteices에서 st좌표를 2.0으로 바꾸는 것보다, fragment shader에서 TexCoord에 곱하기 2를 하면 되니 그렇게 구현을 하고, 여러가지 wrapping 방식을 시험했다.

- GL_REPEAT

 

GL_REPEAT는 텍스쳐 좌표의 정수부분이 무시되도록 한다고 한다. GL은 소수 부분만을 사용하고, 그것으로 인해 반복하는 패턴을 만들어 낸다.

- GL_MIRRORED_REPEAT
 

거울에 비친것처럼 반복될 때, 좌우대칭이 된다. S에 대해서는 REPEAT를 하고 T에대해서는 GL_MIRRORED_REPEAT로 설정하여 각각 따로 해줄 수 있다.
S(REPEAT), T(MIRRORED_REPEAT)라면, 텍스쳐는 0,0부터 그려지므로, 0,0에서 올바른 이미지가 나타나고, 그것의 오른쪽 그러니까 x축쪽에는 같은 이미지를
반복시킨다. 그리고 나머지 위의 영역은 y축의 영역으로 반사시켜서 반복시킨다.

reference에 의하면
GL_MIRRORED_REPEAT는 만약 텍스쳐 좌표가 짝수라면, 텍스쳐의 좌표의 소수 부분으로 설정되도록하고, 만약 홀수라면, 텍스쳐 좌표가 1-frac(텍스쳐좌표)가 되도록한다.
frac(텍스쳐좌표)는 텍스쳐 좌표의 소수 부분을 나타낸다. 

- GL_CLAMP_TO_EDGE
 

GL_CLAMPE_TO_EDGE는 텍스쳐 좌표가 [1/2N, 1 - 1/2N]의 범위로 납작해지게 만든다. 여기에서 N은 clamping 방향에서의 텍스쳐 크기이다.
여기에서 smile 텍스쳐이미지의 좌표는 2배하여, 2.0이 되었다. 따라서, 그 범위 2.0에서 1/4씩 줄어들므로 저렇게 1,0의 절반으로 들어가게 된다.

- GL_CLAMP_TO_BORDER
 

레퍼런스에 의하면 GL_CALMP_TO_BORDER는 GL_CLAMP_TO_EDGE와 유사한 방식으로 텍스쳐 좌표를 평가한다. 그러나, GL_CLAMP_TO_EDGE 모드에서 clampping이 발생하는 경우에서, 가져와진 texel data는 GL_TEXTURE_BORDER_COLOR로 대체 되어진다.

BORDER_COLOR를 설정하기 위해 다음과 같은 명령어가 필요하다.

const float bordercolor[] = { 0.98f, 0.88f, 0.f , 1.f};
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bordercolor);

Texture2의 border가 노란색으로 잘 칠해지는지 확인하기 위해, mix에서 alpha를 서로 0.5씩 절반으로 주었다.

 

다른 텍스쳐를 불러오고 GL_CLAMP_TO_EDGE로 하고 투명도를 각각 반 씩 주었다. 아마도 내 추축은
texture1이 희미해지면서 흰색이 된거 아닐까 생각이 든다.


* 사각형에서 텍스쳐 이미지의 오직 중앙 픽셀들만 보이려고 해라. 각 픽셀들이 텍스쳐 좌표를 바꿈으로써 보이도록 하는 방식으로. 픽셀들을 좀 더 선명하게 보기위해, GL_NEAREST를 texture filtering method에 설정하도록 해라.

float firstTri[] =
{
// positions          // colors           // texture coords
0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   0.55f, 0.55f,   // top right
0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   0.55f, 0.45f,   // bottom right
-0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.45f, 0.45f,   // bottom left
-0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.45f, 0.55f    // top left 
};

텍스쳐좌표를 저렇게 바꾸어, 확대만 시킨다.

- GL_LINEAR 적용

- GL_NEAREST 적용

 

확실히 GL_NEAREST가 2D 비트 게임 같아보이고, GL_LINEAR가 interpolated된 값을 가져오기 때문에 더 부드러워 보인다.



* 두 텍스쳐가 보이는 양을 다르게 하기 위해, mix 함수의 세 번째 인자로서 uniform 변수를 사용해라. 얼마나 많은 container 또는 smiley face가 보이도록 바꾸기 위해서 위아래 화살표키를 사용해라.


float alpha;

void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, 1);

if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS)
{
alpha = alpha > 1.0 ? 1.0 : alpha + 0.1;

}

if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS)
{
alpha = alpha < 0.0 ? 0.0 : alpha - 0.1;
}
}

// Render Loop
while (!glfwWindowShouldClose(window))
{
// Input
processInput(window);

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
// draw our first triangle
shader1.use();
shader1.setFloat("alpha", alpha);
glBindVertexArray(VAO[0]); 
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

// check and call events and swap the buffers.
glfwSwapBuffers(window);
glfwPollEvents(); 
}

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D texture1;
uniform sampler2D texture2;
uniform float alpha;

void main()
{
    vec4 t1 = texture(texture1, TexCoord);
    vec4 t2 = texture(texture2, TexCoord);

    FragColor = mix(t1, t2, alpha);
}

이 코드에서 화살표 위아래를 누르면, 너무나 빠르게 alpha 값이 바뀌는데,
해결방법은 alpha 값을 더해주는 것을 더욱 작은값으로 해주거나, bool flag를 이용하여, 한 번씩만 누르게 하면 된다.
그러니까, 한 번 누르면 키보드를 다시 떼는것이 일어나고 나서 누르는 것을 인식하게 하면 된다.

댓글 없음:

댓글 쓰기