Cubemaps
- 지금까지 2D 텍스처들을 사용해왔다.
- 다른 유형도 존재
이번에 다룰 유형은 여러 텍스처들을 하나의 텍스처로 매핑한 텍스처이다.
- 이를
cubemaps
라고 부른다.
- 이를
Cubemap은 각 면을 형성하는 2D 텍스처들을 포함하고 있는 텍스처이다.
- 유용한 특성: 방향 벡터를 사용하여 인덱싱/ 샘플링될 수 있다는점.
- ex) 중앙에 위치해 있는 방향 벡터의 원점과 1x1x1의 단위 큐브
- 이 cubemap으로부터 텍스처를 샘플링하는 것은 다음 이미지 처럼 보인다. (주황색 = 방향벡터)
- 방향 벡터의 크기(magnitude)는 상관없다.
- 방향 벡터만 제공된다면, OpenGL이 방향과 맞닿는 해당 텍셀을 얻고, 적절히 샘플링된 텍스처 값을 리턴한다.
- 이러한 cubemap을 첨부한 큐브도형에서 cubemap을 샘플링하는 방향 벡터는 (보간된) vertex 위치와 비슷하다.
- 이 방법으로 우리는 이 큐브가 원점에 존재한다면, 이 큐브의 실제 위치 벡터들을 사용하여 cubemap을 샘플링할 수 있다.
- 그런 다음 이 큐브의 vertex 위치를 텍스처 좌표로서 얻을 수 있다.
- 그 결과 cubemap의 적절한 각 face를 접근할 수 있는 텍스처 좌표를얻을 수 있다.
Creating a cubemap
- 적절한 텍스처를 생성하고, 적절한 텍스처 타겟에 바인딩해야한다.
GL_TEXTURE_CUBE_MAP
에 바인딩해야함.
1
2
3
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
- 6개의 텍스처를 생성해야하므로 6번 glTexImage2D 함수를 6번 호출해야한다.
- 텍스처 타겟 파라미터에 cubemap의 특정 면을 설정한다.
Texture target | Orientation |
---|---|
GL_TEXTURE_CUBE_MAP_POSITIVE_X | Right |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X | Left |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y | Top |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | Bottom |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z | Back |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | Front |
- 이 enum 변수들은 연속적으로 증가하는 int 변수이므로, 반복문으로 처리 가능하다.
POSITIVE_X
부터 시작하여 1씩증가시키자
1
2
3
4
5
6
7
8
9
10
int width, height, nrChannels;
unsigned char *data;
for(unsigned int i = 0; i < textures_faces.size(); i++)
{
data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
}
textures_faces
벡터는 cubemap 순서와 같게 텍스처의 경로를 가지고 있음.cubemap은 또한 wrapping, filtering method를 지정할 수 있다.
1
2
3
4
5
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
GL_TEXTURE_WRAP_R
은 텍스처의 3번째 차원(z와 동일)에 해당하는 R 좌표에 대한 wrapping method를 설정한다.- 두 면의 접합된 부분에 있는 텍스처 좌표가 정확한 면을 가리키지 않을 수 있으므로(일부 하드웨어들의 제한에 의해)
GL_CLAMP_TO_EDGE
를 사용하여 면 사이를 샘플링할때마다, OpenGL이 항상 그들의 모서리 값을 리턴하도록 해준다.
- 두 면의 접합된 부분에 있는 텍스처 좌표가 정확한 면을 가리키지 않을 수 있으므로(일부 하드웨어들의 제한에 의해)
그런 다음 이 cubemap을 사용할 오브젝트를 그리기 전에, 해당 텍스처 유닛을 활성화하고 렌더링하기 전에 cubemap을 바인딩한다.
fragment shader 내부에서 샘플러 타입인
samplerCube
를 사용해야한다.- 그리고 vec3 타입의 방향벡터를 사용하여 샘플링할 것이다.
1
2
3
4
5
6
7
in vec3 textureDir; // direction vector representing a 3D texture coordinate
uniform samplerCube cubemap; // cubemap texture sampler
void main()
{
FragColor = texture(cubemap, textureDir);
}
- 이 큐브맵으로, skybox 같은 흥미로운것을 구현할 수 있다.
Skybox
Skybox는 전체 scene을 둘러싸고 주변 환경 6개의 이미지를 가지고 있는 큰 큐브이다.
- 보통 비디오게임에서 skybox의 예는 산, 구름, 별이 빛나는 밤하늘 이미지
- 이러한 skybox 이미지들은 온라인에서 충분히 찾을 수 있음
- 일반적으로 아래와 같은 패턴을 취한다.
- 이 6개의 면들을 접어서 큐브를만들려면 거대한 풍경을 시뮬레이션할 수 있는 완전히 텍스처가 입혀진 큐브를 얻을 수 있다.
- 이러한 형식을 가진 일부 리소스들은 6개의 이미지들로 추출해야한다.
- 대부분의 경우 6개의 단일 텍스처로 제공된다.
- 위 skybox는 여기에서 다운받을 수 있다.
Loading a skybox
- 다운받은 텍스처의 경로가 들어있는 faces를 벡터로 받게 하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
unsigned int loadCubemap(vector<std::string> faces)
{
unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
int width, height, nrChannels;
for (unsigned int i = 0; i < faces.size(); i++)
{
unsigned char *data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
stbi_image_free(data);
}
else
{
std::cout << "Cubemap tex failed to load at path: " << faces[i] << std::endl;
stbi_image_free(data);
}
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
return textureID;
}
- 그다음, 이 함수를 호출하기전에 텍스처 경로를 vector에 선언
1
2
3
4
5
6
7
8
9
10
vector<std::string> faces;
{
"right.jpg",
"left.jpg",
"top.jpg",
"bottom.jpg",
"front.jpg",
"back.jpg"
};
unsigned int cubemapTexture = loadCubemap(faces);
- 이제 skybox를 cubemap으로서 불러오고, texture에 id를 저장했다.
- 이 것을 큐브에 바인딩할 수 있고, 언제든 배경으로 사용 가능하다.
Displaying a skybox
skybox는 cube 위에 그려지기 때문에 또 다른 VAO VBO가 필요하고, 다른 오브젝트들과 마찬가지로 vertex 세트가 필요하다.
cubemap은 큐브의 위치(local positions)를 텍스처 좌표로 사용하여 샘플링될 수 있다.
- 큐브가 원점(0,0,0)에 위치해있을 때, 각 위치 벡터들은 원점으로부터의 방향벡터와 동일하다.
- 그러므로 텍스처 좌표는 필요없다.
이 skybox를 렌더링하기 위해 새로운 shader 세트가 필요하다.
- 오직 하나의 vertex attribute만 필요하므로, vertex는 간단하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
TexCoords = aPos;
gl_Position = projection * view * vec4(aPos, 1.0);
}
- 이 vertex shader는 입력받은 위치 벡터를 fragment shader로 보낼 텍스처 좌표로 출력한다.
- fragment shader는 이들을 입력받아
samplerCube
를 샘플링할 것이다.
- fragment shader는 이들을 입력받아
1
2
3
4
5
6
7
8
9
10
11
#version 330 core
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
FragColor = texture(skybox, TexCoords);
}
vertex attribute의 위치벡터를 방향벡터로 취하고, 이 것들을 cubemap으로부터 텍스처 값을 샘플링하기 위해 사용한다.
렌더링은 간단히 cubemap 텍스처를 바인딩하면, skybox sampler 는 자동적으로 skybox cubemap으로 채워지게 된다.
- 먼저 scene에 skybox를 그리고
- depth 쓰기를 비활성화할 것이다. (항상 모든 오브젝트들의 뒤에 그려지게됨)
1
2
3
4
5
6
7
8
glDepthMask(GL_FALSE);
skyboxShader.use();
// ... set view and projection matrix
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... draw rest of the scene
플레이어를 중앙으로 둘러싼 skybox를 원하므로, 플레이어의 움직임이 영향을 주지 않도록해야한다.
- 현재 view 행렬은 skybox의 모든 위치들을 회전, 확대, 이동 시키므로, 플레이어가 움직이면 cubemap 또한 움직이게 된다.
- view 행렬의 이동부분을 지워 움직임이 skybox 위치 벡터에 영향을 주지않도록 해야한다.
4x4 변환 행렬의 좌측 상단 3x3을 취하면, 이동부분을 제거할 수 있다.
- 회전변환은 유지됨.
1
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
- 이제 결과는 다음과 같다.
An optimization
위 예제는, 모든 다른 오브젝트를 그리기 전에 맨 처음에 skybox를 렌더링한다.
- 이는 효율적이지 않음.
fragment shader를 화면의 각 픽셀들마다 실행해야함.
- skybox의 보이는 부분이 작아도 그럼
- early depth testing 을 사용하여 먼저 폐기해야한다.
그러므로, 마지막에 렌더링해야한다.
- depth 버퍼는 다른 오브젝트의 dpth로 채워지므로, early depth test를 통과한 skybox의 fragment들만 렌더링하면 된다.
- 이는 fragment shader 호출 횟수를 줄여준다.
- 문제는 skybox는 대부분 렌더링에 실패할 것이라는점.(1x1x1 큐브이므로)
- 만약 dpth testing 없이 렌더링하면 skybox가 다른 오브젝트들을 덮어씀
- depth buffer에 트릭을 써, skybox가 depth 값을 최댓값인
1.0
을 가지게 만들어서 앞에 드른 오브젝트들이 있는 곳은 test에 실패하도록 해야한다.
원근 분할(perspective division)이 vertex shader 가 실행된 후에
gl_Position
의xyz
좌표를w
요소로 나눔.- 나눗셈 결과
z
요소는 vertex의 depth 값과 동일하다. - 그러므로, 출력 위치의
z
요소를w
와 동일하게 설정하면, z 값을 항상1.0
으로 만들 수 있다. - z = w/w = 1.0
- 나눗셈 결과
1
2
3
4
5
6
void main()
{
TexCoords = aPos;
vec4 pos = projection * view * vec4(aPos, 1.0);
gl_Position = pos.xyww;
}
이러면 NDC 좌표에서 z값은 1.0으로 depth. 값의 최댓값을 가지게 된다.
- 결과적으로 오브젝트들이 없는 곳에서만 렌더링 된다.
depth 함수를 기본값인
GL_LESS
대신에GL_LEQUAL
로 설정해야한다.- depth buffer는 skybox에 대해
1.0
값으로 채워지므로, - skybox를 통과하게 만들기 위해
- depth buffer는 skybox에 대해
Environment mapping
이제 하나의 텍스처에 전체 주변환경이 매핑되었다.
이런 환경을 가진 cubemap을 사용하여, 오브젝트에 빛을 반사 혹은 굴절 시키는 특성을 줄 수 있음.
이렇게 cubemap을 사용하는 기술을
environment mapping
기술이라고 부름- 가장 많이 사용되는것:
reflection
,refraction
- 가장 많이 사용되는것:
Reflection
reflection은 오브젝트(혹은 오브젝트의 어느 부분)이 주변환경을 반사하는 특성이다.
- 시점의 각도를 기반으로 오브젝트의 컬러들은 환경과 동일하게 설정될 수 있다.
- ex) 거울을 반사하는 오브젝트 .
반사벡터(reflection vector)
를 계산하는 방법과 cubemap을 샘플링하기 위해 이 벡터를 사용하는 방법은 다음과 같다.
view 방향 벡터 I 를 기반으로 오브젝트의 법선 벡터 N에 따른 반사 벡터 R을 계산한다.
- GLSL의
reflect
내장 함수를 사용하여 계산 가능 - R은 cubemap을 인덱싱, 샘플링 하기 위한 방향 벡터로 사용된다.
- 최종 효과는 오브젝트가 skybox를 반사하는 것처럼 보인다.
- GLSL의
이미 scene에 skybox를 가지고 있기 때문에 reflection을 생성하는 것은 그리 어렵지 않다.
컨테이너에 반사 속성을 주기 위해 컨테이너에 사용된 fragment shader를 수정할 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 Position;
uniform vec3 cameraPos;
uniform samplerCube skybox;
void main()
{
vec3 I = normalize(Position - cameraPos);
vec3 R = reflect(I, normalize(Normal));
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
- I, R을 계산하고, R로 cubemap을 샘플링한다.
- fragment의 보간된 Normal과 Position 변수를 가지고 있으므로, vertex shader 또한 수정해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 Normal;
out vec3 Position;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
Normal = mat3(transpose(inverse(model))) * aNormal;
Position = vec3(model * vec4(aPos, 1.0));
gl_Position = projection * view * vec4(Position, 1.0);
}
- 법선 벡터를 법선 행렬로 변환
Position
: world-space 위치 벡터, fragment 에서 view 방향 벡터를 계산하기 위해법선을 vertex data에 추가하고 attribute pointer에도 수정해야한다. 또한 cameraPos 또한 넘겨줘야한다.
- 큐브맵을 렌더링하기전에 cubemap 텍스처도 바인딩해야한다.
1
2
3
glBindVertexArray(cubeVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
코드를 실행하면, 거울같은 컨테이너를 볼 수 있다.
- 둘러싼 skybox는 정확히 컨테이너에 반사되고 있다.
- 반사가 전체 오브젝트에 적용될 때, 이 오브젝트는 스틸이나 크롬같은 높은 반사율을 가진 material 오브젝트처럼 보인다.
- 하지만 실제 대부분의 모델들은 완전한 반사를하지 않는다.
reflection maps
: diffuse, specular map 들 처럼, 이 map은 fragment의 반사율을 결정하기 위해 샘플링할 수 있는 텍스처 이미지- 이를 이용하여 모델의 어느 부분이 어떠한 세기를 가진 반사율을 보여줄지 결정할 수 있다.
Refraction
환경 매핑의 또다른 형태는 refraction(굴절)이라고 불리고 반사와 비슷하다.
- 굴절은 material의 변화에 따라 빛의 방향이 달라지는 것을 말한다.
- 굴절은 흔히 빛이 직선으로 통과하지 않고 휘어지는 물과 같은 표면에서 볼 수 있다.
굴절: snell’s law
view 벡터 I, 법선 벡터 N, 굴절벡터 R이 있다.
- view 벡터의 방향이 휘어진게 R로 Cubemap을 샘플링한다.
굴절은 GLSL의
refract
함수를 통해 쉽게 구현될 수 있다.- 이 함수는 법선 벡터와 view 방향, 그리고 refractive indices 사이의 비율을 인자로 받는다.
굴절 index는 material의 빛이 왜곡/ 휘어지는 정도를 결정한다.
- 각 material들은 자신만의 고유한
refactive index
를 가진다. - 가장 많이 쓰이는 index는 아래 표에서 볼 수 있다.
- 각 material들은 자신만의 고유한
Material | Refractive index |
---|---|
Air | 1.00 |
Water | 1.33 |
Ice | 1.309 |
Glass | 1.52 |
Diamond | 2.42 |
빛이 통과하는 두 material 사이의 비율을 계산하기 위해 이 index를 사용한다.
- 현재 예제의 경우 공기 => 유리 라고 가정함 즉, 비율은 1.0/1.52 = 0.658
fragment를 다음과 같이 수정해보자
1
2
3
4
5
6
7
8
void main()
{
float ratio = 1.00 / 1.52;
vec3 I = normalize(Position - cameraPos);
vec3 R = refract(I, normalize(Normal), ratio);
FragColor = vec4(texture(skybox, R).rgb, 1.0);
}
- 이 refractive index들을 바꾸면 다른 비주얼 결과를 만들 수 있다.
- 빛, 반사, 굴절, vertex 움직임의 올바른 결합을 통해 물 그래픽을 만들 수 있다.
- 물리적인 계산 결과를 위해, 물체를 떠날 때 빛을 다시 굴절시켜주어야한다.
- 지금은 간단히 한쪽 면만 굴절시킨다.
Dynamic environment maps
- 지금은 skybox를 정적인 이미지의 결합으로 사용함.
- 즉, 움직이는 오브젝트를 반사/굴절 시키지않음
- 위 예제는 여러 오브젝트가 있어도 skybox만 비침
Framebuffer를 사용하면 6개의 다른 각도로 찍어 scene 텍스처 생성 가능
- 이는 비용이 비싸다. => 성능적으로 큰 패널티
- 이를
동적 환경 매핑(dynamic environment mapping)
이라함 - 주변환경을 cubemap으로 동적으로 생성, 이를 환경에 매핑
- 보통의 응용프로그램은 가능한 skybox를 사용하고, 가능한 경우 동적 환경 맵을 생성하기 위해 가능한 한 큐브맵을 미리 렌더링하려고 한다.
- 동적 환경 매핑을 성능저하 없이 실제 렌더링 응용 프로그램에서 작동하도록하려면, 많은 트릭이 필요