[Learn OpenGL 번역] 2-6. 시작하기 - Textures
Textures
시작하기/Textures
객체에 더 많은 상세사항을 추가하기 위해 각 vertex에 컬러를 사용하여 흥미로운 이미지를 만들 수 있음을 배웠습니다. 하지만 사실적인 느낌을 얻기 위해서는 많은 vertex를 가져야 하므로 많은 색상을 지정해야 합니다. 각 모델에는 더 많은 vertex들이 필요하고 각 vertex는 컬러 attriubute를 필요로 하기 때문에 상당한 오버헤드를 야기시킵니다.
아티스트와 프로그래머가 일반적으로 선호하는 것은
아래에서 이전 강좌의 삼각형에 brick wall 텍스처 이미지를 매핑한 것을 볼 수 있습니다.
텍스처를 삼각형에 매핑하기 위해 삼각형의 각 vertex에 텍스처의 어느 부분이 해당하는지 알려주어야 합니다. 따라서 각 vertex에는 샘플링 할 텍스처 이미지의 영역을 지정하는
텍스처 좌표의 범위는 x
와 y
축 상의 0
에서 1
까지입니다(2D 텍스처 이미지를 사용한다는 것을 생각하세요). 텍스처 좌표를 사용하여 텍스처 컬러를 가져 오는 것을 (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:
0
과1
사이의 좌표를 고정합니다. 결과적으로 큰 좌표가 가장자리에 고정되어 가장자리의 패턴이 늘어나게 됩니다. - GL_CLAMP_TO_BORDER: 범위 밖의 좌표에 사용자가 지정한 테두리 색이 지정됩니다.
기본 범위 밖의 텍스처 좌표를 사용할 때 각 옵션은 다른 출력을 보여줍니다. 샘플 텍스처 이미지에서 어떻게 보이는지 봅시다.
앞서 언급한 각 옵션들은 s
, t
(3D 텍스처를 사용한다면 r
)는 x
, y
, z
와 같습니다)별로 설정할 수 있습니다.
glTexParameter i(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameter i(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
를 사용하는
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameter fv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
Texture Filtering
텍스처 좌표는 해상도에 의존하지 않지만 실수 값이 될 수 있으므로 OpenGL은 텍스처 좌표를 매핑할 텍스처 픽셀(
GL_NEAREST (
GL_LINEAR (
하지만 이러한 텍스처 필터링 방법들의 시각적인 효과는 무엇일까요? 큰 오브젝트에 해상도가 낮은 텍스처를 사용할 때 이러한 방법이 어떻게 작동하는지 봅시다(텍스처의 크기가 확장되고 각각의 텍셀들이 눈에 띕니다).
GL_NEAREST는 텍스처를 형성하는 픽셀들을 명확히 볼 수 있는 차단된 패턴을 생성하는 반면 GL_LINEAR는 개별 픽셀들이 덜 보이는 더 매끄러운 패턴을 생성합니다. GL_LINEAR가 좀더 현실감있는 결과를 산출하지만 일부 개발자들은 8비트 룩을 선호하므로 GL_NEAREST 옵션을 선택합니다.
텍스처 필터링은
glTexParameter i(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameter i(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Mipmaps
수천개의 오브젝트가 있는 넒은 공간이 있고 각각에 텍스처가 첨부된 경우를 상상해보세요. 시점에 가까이에 있는 물체와 동일한 고해상도 텍스처가 사용된 멀리있는 물체가 있을 것입니다. 오브젝트가 멀리 떨어져 있어서 몇개의 fragment만 생성하기 때문에 OpenGL은 텍스처의 대부분을 차지하는 fragment를 위한 텍스처 색상을 선택해야하므로 고해상도 텍스처에서 해당 fragment의 올바른 색상 값을 가져오는 데에 어려움을 겪습니다. 이렇게 하면 작은 물체에 고해상도 텍스처를 사용하여 메모리 낭비는 물론이고 작은 물체에는 결함이 보일 수 있습니다.
이 문제를 해결하기 위해 OpenGL은 기본적으로 이전 텍스처보다 2배 작은 텍스처 이미지를 다음 텍스처로 가지는
각 텍스처 이미지에 대한 mipmap 텍스처 모음을 생성하는 것은 직접하기에는 번거롭지만 운 좋게도 OpenGL은 텍스처를 생성한 후
렌더링 중에 mipmap의 레벨을 전환할 때 OpenGL은 두 mipmap 레이어 사이에 가장자리가 선명하게 나타나는 것과 같은 일부 결함을 나타낼 수도 있습니다. 일반적인 텍스처 필터링과 마찬가지로 mipmap 레벨을 전환하기 위해 NEAREST 및 LINEAR 필터링을 사용하여 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 보간법을 사용합니다.
텍스처 필터링과 마찬가지로
glTexParameter i(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameter i(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
를 사용하여 이미지를 로드하려면
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);
unsigned int
배열에 텍스처들을 저장합니다(이 경우에는 하나의 unsigned int
). 다른 객체들과 마찬가지로 바인딩 해야합니다. 그래야 그 후에 텍스처 명령이 현재 바인딩된 텍스처를 대상으로 설정할 수 있습니다.
glBindTexture (GL_TEXTURE_2D, texture);
이제 텍스처가 바인딩 되었으므로 이전에 로드된 이미지 데이터를 사용하여 텍스처를 생성할 수 있습니다.
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_1D나 GL_TEXTURE_3D로 바인딩된 객체에는 아무런 영향을 끼치지 않습니다).
- 두 번째 파라미터는 우리가 생성하는 텍스처의 mipmap 레벨을 수동으로 지정하고 싶을 때 지정합니다. 하지만 우리는 베이스 레벨일
0
로 남겨두겠습니다. - 세 번째 파라미터는 OpenGL에게 우리가 저장하고 싶은 텍스처가 어떤 포멧을 가져야 할지 알려줍니다. 우리의 이미지는 오직
RGB
값만 가지고 있으므로 텍스처를RGB
값과 함께 저장할 것입니다. - 네 번째, 다섯 번째 파라미터는 결과 텍스처의 너비와 높이를 설정합니다. 우리는 이미지를 로딩할 때 이미 저장해두었으므로 해당 변수들을 사용할 것입니다.
- 그 다음 파라미터는 항상
0
을 지정해야합니다. - 일곱 번째, 여덟 번째 파라미터는 원본 이미지의 포멧과 데이터타입을 지정합니다. 우리는
RGB
값이 있는 이미지를 로드했고char
s(bytes)로 저장하였으므로 해당하는 값으로 설정합니다. - 마지막 파라미터는 실제 이미지 데이터입니다.
텍스처와 해당 mipmap들을 생성한 후 이미지의 메모리를 반환하는 것이 좋습니다.
stbi_image_free(data);
따라서 텍스처 생성의 전체적인 과정은 다음과 같습니다.
unsigned int texture;
glGenTextures (1, &texture);
glBindTexture (GL_TEXTURE_2D, texture);
// 텍스처 wrapping/filtering 옵션 설정(현재 바인딩된 텍스처 객체에 대해)
glTexParameter i(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameter i(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameter i(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameter i(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 강좌에서
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 포멧을 다시 알려주어야 합니다.
glVertexAttribPointer (2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnable VertexAttribArray (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은 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의
이제 남은 일들은
glBindTexture (GL_TEXTURE_2D, texture);
glBindVertexArray (VAO);
glDrawElements (GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
모든 것을 올바르게 수행했다면 다음 이미지를 볼 수 있을 것입니다.
사각형이 완전히 흰색이나 검은색이라면 아마 오류가 발생한 것일 가능성이 있습니다. shader 로그를 확인하고 여러분의 코드와 소스 코드를 비교해보세요.
좀 펑키한 것을 만들기 위해 최종 텍스처 컬러와 vertex 컬러를 혼합할 수 있습니다. fragment shader에서 간단히 최종 텍스처 컬러와 vertex 컬러를 곱하는 것으로 두 컬러를 혼합할 수 있습니다.
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
결과는 vertex 컬러와 텍스처 컬러가 혼합된 것이어야 합니다.
우리의 컨테이너는 디스코를 좋아하는 것 같군요.
Texture Units
sampler2D
변수가 uniform인지 궁금할 것입니다. 0
입니다. 이는 기본으로 활성화된 텍스처 유닛이므로 이전의 섹션에서는 위치 값을 할당할 필요가 없었습니다. 모든 그래픽 드라이버가 기본 텍스처 유닛을 할당하는 것은 아니라는 사실을 알고 있어야 합니다. 그렇기 때문에 이전의 섹션에서 아마 렌더링이 안되었던 분들이 계셨을 수도 있습니다.
텍스처 유닛의 주 목적은 shader에서 하나 이상의 텍스처를 사용할 수 있도록 해주는 것입니다. sampler에 텍스처 유닛을 할당함으로써 해당 텍스처 유닛을 활성화하기만 하면 여러 텍스처들을 동시에 바인딩할 수 있습니다.
glActiveTexture (GL_TEXTURE0); // 텍스처를 바인딩하기 전에 먼저 텍스처 유닛을 활성화
glBindTexture (GL_TEXTURE_2D, texture);
텍스처 유닛을 활성화한 후에 호출되는
다른 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의 0.01.0
이라면 두 번째 텍스처 컬러를 리턴합니다. 0.2
값은 첫 번째 텍스처 컬러의 80%
, 두 번째 텍스처 컬러의 20%
를 리턴하여 결과적으로 두 개의 텍스처를 혼합하게 됩니다.
우리는 이제 다른 텍스처를 로드하고 생성해야 합니다. 여러분은 아마 이제 이 단계가 친숙할 것입니다. 새로운 텍스처 객체를 생성하고 이미지를 불러온 후
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);
또한
ourShader.use(); // uniform을 설정하기 전에 shader를 활성화해야 한다는 것을 잊지마세요!
glUniform 1i(glGetUniformLocation (ourShader.ID, "texture1"), 0); // 직접 설정
ourShader.setInt("texture2", 1); // 혹은 shader 클래스를 활용
while(...)
{
[...]
}
텍스처가 거꾸로 뒤집혀있다는 것을 알 수 있습니다. 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 변수를 사용하세요. 위, 아래 방향키를 사용하여 컨테이너와 얼굴의 보이는 정도를 조절할 수 있도록 해보세요: 해답