Home [learn-opengl] Advanced OpenGL: Cubemaps
Post
Cancel

[learn-opengl] Advanced OpenGL: Cubemaps

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 targetOrientation
GL_TEXTURE_CUBE_MAP_POSITIVE_XRight
GL_TEXTURE_CUBE_MAP_NEGATIVE_XLeft
GL_TEXTURE_CUBE_MAP_POSITIVE_YTop
GL_TEXTURE_CUBE_MAP_NEGATIVE_YBottom
GL_TEXTURE_CUBE_MAP_POSITIVE_ZBack
GL_TEXTURE_CUBE_MAP_NEGATIVE_ZFront
  • 이 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 세트가 필요하다.

  • vertex data

  • 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를 샘플링할 것이다.
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_Positionxyz 좌표를 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를 통과하게 만들기 위해

전체 소스

Environment mapping

  • 이제 하나의 텍스처에 전체 주변환경이 매핑되었다.

  • 이런 환경을 가진 cubemap을 사용하여, 오브젝트에 빛을 반사 혹은 굴절 시키는 특성을 줄 수 있음.

  • 이렇게 cubemap을 사용하는 기술을 environment mapping 기술이라고 부름

    • 가장 많이 사용되는것: reflection, refraction

Reflection

  • reflection은 오브젝트(혹은 오브젝트의 어느 부분)이 주변환경을 반사하는 특성이다.

    • 시점의 각도를 기반으로 오브젝트의 컬러들은 환경과 동일하게 설정될 수 있다.
    • ex) 거울을 반사하는 오브젝트 .
  • 반사벡터(reflection vector)를 계산하는 방법과 cubemap을 샘플링하기 위해 이 벡터를 사용하는 방법은 다음과 같다.

  • view 방향 벡터 I 를 기반으로 오브젝트의 법선 벡터 N에 따른 반사 벡터 R을 계산한다.

    • GLSL의 reflect 내장 함수를 사용하여 계산 가능
    • R은 cubemap을 인덱싱, 샘플링 하기 위한 방향 벡터로 사용된다.
    • 최종 효과는 오브젝트가 skybox를 반사하는 것처럼 보인다.
  • 이미 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는 아래 표에서 볼 수 있다.
MaterialRefractive index
Air1.00
Water1.33
Ice1.309
Glass1.52
Diamond2.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를 사용하고, 가능한 경우 동적 환경 맵을 생성하기 위해 가능한 한 큐브맵을 미리 렌더링하려고 한다.
    • 동적 환경 매핑을 성능저하 없이 실제 렌더링 응용 프로그램에서 작동하도록하려면, 많은 트릭이 필요

출처

Cubemaps: 원문

Cubemaps: 번역본

This post is licensed under CC BY 4.0 by the author.

[learn-opengl] Advanced OpenGL: Framebuffers

[learn-opengl] Advanced OpenGL: Advanced Data