Notice
Recent Posts
Recent Comments
Link
«   2024/03   »
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
Archives
Today
Total
관리 메뉴

게임공장

[Learn OpenGL 번역] 5-6. 고급 OpenGL - Cubemaps 본문

OpenGL

[Learn OpenGL 번역] 5-6. 고급 OpenGL - Cubemaps

짱승_ 2018. 8. 15. 16:43

Cubemaps

고급 OpenGL/Cubemaps

  지금까지 우리는 2D 텍스처들을 사용해왔었습니다. 하지만 우리가 아직 다루어보지 못한 많은 텍스처 유형이 존재합니다. 이번 강좌에서 다룰 텍스처 유형은 여러 텍스처들을 하나의 텍스처로 매핑한 텍스처입니다. 이를 cubemaps라고 부릅니다.


  Cubemap은 기본적으로 큐브의 각 면을 형성하는 2D 텍스처들을 포함하고 있는 텍스처입니다. 이러한 큐브의 중점이 무엇인지 궁금할 것입니다. 그냥 6개의 텍스처를 사용하면 되지 왜 6개의 텍스처를 하나의 텍스처로 만들어 사용하는 것일까요? Cubemap은 유용한 특성을 가지고 있습니다. 방향 벡터를 사용하여 인덱싱/샘플링될 수 있는 점입니다. 중앙에 위치해 있는 방향벡터의 원점과 1x1x1의 단위 큐브를 가지고 있다고 생각해보세요. 이 cube map으로부터 텍스처를 샘플링하는 것은 다음 그림처럼 보입니다. 주황색 벡터는 방향 벡터입니다.


Indexing/Sampling from a cubemap in OpenGL
  방향 벡터의 크기는 상관없습니다. 방향만 제공된다면 OpenGL 이 방향과 맞닿는 해당 텍셀을 얻습니다. 그리고 적절히 샘플링된 텍스처 값을 리턴합니다.

  이러한 cubemap을 첨부한 큐브 도형을 가지고 있다면 이 cubemap을 샘플링하는 방향 벡터는 cube의 (보간된) vertex 위치와 비슷합니다. 이 방법으로 우리는 이 큐브가 원점에 존재한다면 이 큐브의 실제 위치 벡터들을 사용하여 cubemap을 샘플링할 수 있습니다. 그런 다음 우리는 이 큐브의 vertex 위치를 텍스처 좌표로서 얻을 수 있습니다. 그 결과 cubemap의 적절한 각 face(면)를 접근할 수 있는 텍스처 좌표를 얻을 수 있습니다.

Cubemap 생성

  Cubemap은 다른 텍스처들과 같은 텍스처이므로 생성하기 위해서 텍스처 연산을 실행하기 전에 텍스처를 생성하고 적절한 텍스처 타겟에 바인딩합니다. 이번에는 GL_TEXTURE_CUBE_MAP에 바인딩합니다.


unsigned int textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);

  Cubemap은 6개의 텍스처로 이루어져있기 때문에 glTexImage2D 함수를 6번 호출해야합니다. 이 함수는 저번과 동일하게 사용하지만 텍스처 타겟 파라미터에 cubemap의 특정 면을 설정합니다. 기본적으로 OpenGL에게 우리가 생성할 텍스처가 cubemap의 어떠한 면에 해당하는지를 알려주는 것입니다. 이는 우리가 cubemap의 각 면마다 glTexImage2D 함수를 호출해야한다는 것을 의미합니다.


  6개의 면을 가지고 있기때문에 OpenGL은 cubemap의 면들을 타겟팅할 수 있도록 6개의 특별한 텍스처 타겟을 제공해줍니다.


텍스처 타겟 방향
GL_TEXTURE_CUBE_MAP_POSITIVE_X 오른쪽
GL_TEXTURE_CUBE_MAP_NEGATIVE_X 왼쪽
GL_TEXTURE_CUBE_MAP_POSITIVE_Y
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 아래
GL_TEXTURE_CUBE_MAP_POSITIVE_Z
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z

  다른 많은 OpenGL의 enum 변수들과 마찬가지로 점점 연속적으로 증가하는 int 형 변수이므로 텍스처의 vector 배열을 가지고 있다면 이들을 반복문으로 돌려 GL_TEXTURE_CUBE_MAP_POSITIVE_X로 시작하여 이 변수를 1씩 증가시켜가면서 효율적으로 모든 텍스처 타겟들을 설정할 수 있습니다.


int width, height, nrChannels;
unsigned char *data;  
for(GLuint 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라는 이름을 가진 vector를 가지고 있는데 이 것은 cubemap을 위한 모든 텍스처들의 위치를 위 표 순서대로 가지고 있습니다. 이는 현재 바인딩된 cubemap의 각 면에 텍스처를 생성합니다.


  Cubemap은 다른 텍스처와 다를게 없는 텍스처이기 때문에 wrapping, filtering method를 지정할 수 있습니다.


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을 바인딩합니다. 평상시의 2D 텍스처와 별반 다를것이 없습니다.


  Fragment shader 내부에서 우리는 다른 샘플러 타입인 samplerCube를 사용해야 합니다. 이 타입은 texture 하마수를 사용하여 샘플링을 하는 것은 동일하지만 vec2 대신에 vec3의 방향 벡터를 사용합니다. cubemap을 사용하는 fragment shader의 예는 다음과 같습니다.


in vec3 textureDir; // 3D 텍스처 좌표를 나타내는 방향 벡터
uniform samplerCube cubemap; // Cubemap 텍스처 샘플러

void main()
{             
    FragColor = texture(cubemap, textureDir);
}  

  하지만 여전히 왜 굳이 이렇게 하는지 궁금할 것입니다. Cubemap을 사용하면 구현하기 쉬운 흥미로운 기술들이 존재합니다. 그들 중 하나가 skybox를 생성하는 것입니다.

Skybox

  Skybox는 전체 scene을 둘러싸고 주변 환경 6개의 이미지를 가지고 있는 (큰) 큐브입니다. 플레이어가 실제로 그 환경에 있는듯한 착각을 들게 만듭니다. 비디오 게임에서 skybox의 예는 산, 구름, 별이 빛나는 밤하늘 이미지 같은 것들입니다. 별이 빛나는 밤하늘 이미지를 사용하여 다음 엘더스크롤 3편의 스크린샷에서 볼 수 있는 효과를 만들 수 있습니다.


Image of morrowind with a skybox

  여러분은 아마 이제 이와 같은 skybox가 cubemap에 적합하다고 생각하실 것입니다. 우리는 6개의 면을 가지고 있고 이 면마다 텍스처를 입혀야 합니다. 이전의 이미지에서 그들은 밤하늘의 여러 이미지를 사용했습니다. 플레이어가 큰 세계에 있는 것과 같은 착시를 만들어주기 위해말이죠. 하지만 실제로는 아주 작은 박스안에 있는 것이지요.


  일반적으로 이러한 skybox 이미지들을 온라인에서 충분히 찾을 수 있습니다. 예를들어 이 웹사이트에는 아주 많은 skybox들이 있습니다. 이러한 skybox 이미지들은 일반적으로 다음과 같은 패턴을 취합니다.


Image of a skybox for a cubemap in OpenGL

  이 6개의 면들을 접어서 큐브를 만들면 거대한 풍경을 시뮬레이션할 수 있는 완전히 텍스처가 입혀진 큐브를 얻을 수 있습니다. 이러한 형식을 가진 일부 리소스들은 여러분이 직접 6개의 이미지들로 추출해야합니다. 하지만 대부분의 경우에 6개의 단일 텍스처 이미지로서 제공됩니다.


  특히 이 (퀄리티가 좋은) skybox는 우리가 사용할 것이며 여기에서 다운로드 받을 수 있습니다. This particular (high-quality) skybox is what we'll use for our scene and can be downloaded here.

Skybox 불러오기

  Skybox는 그자체로 단지 cubemap이기 때문에 skybox를 불러오는 것은 전에 보았던 것과 크게 다르지 않습니다. skybox를 불러오기 위해 6개의 텍스처 위치를 가지고 있는 vector를 받아들이는 다음과 같은 함수를 사용할 것입니다.


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 texture 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;
}  

  이 함수 자체로만은 그렇게 놀랍지 않습니다. 이는 기본적으로 모두 우리가 이전 섹션에서 봤던 cubemap 코드들을 하나의 관리가능한 함수로 합친 것입니다.


  그런 다음 우리가 이 함수를 호출하기 전에 적절한 텍스처 경로를 vector에 불러올 것입니다.


vector<std::string> faces;
{
    "right.jpg",
    "left.jpg",
    "top.jpg",
    "bottom.jpg",
    "front.jpg",
    "back.jpg"
};
unsigned int cubemapTexture = loadCubemap(faces);  

  우리는 이제 skybox를 cubemap으로서 불러오고 cubemapTexture에 id를 저장해놓았습니다. 이제 이 것을 큐브에 바인딩할 수 있고 결국에는 언제든 배경으로 사용할 수 있습니다.

Skybox 그리기

  Skybox는 cube 위에 그려지기 때문에 우리는 또다른 VAO, VBO가 필요하고 다른 오브젝트들과 마찬가지로 vertex 세트가 필요합니다. 이 vertex 데이터를 여기에서 얻을 수 있습니다.


  3D 큐브의 텍스처로 사용되는 cubemap은 큐브의 위치를 텍스처 좌표로 사용하여 샘플링될 수 있습니다. 큐브가 원점(0,0,0)에 위치해있을 때 각 위치 벡터들은 원점으로부터의 방향 벡터와 동일합니다. 이 방향 벡터는 정확히 우리가 해당 텍스처 값을 얻기 위해 필요한 것입니다. 이런 이유로 우리는 오직 위치 벡터만을 제공해주면 되고 텍스처 좌표는 필요 없습니다.


  Skybox를 렌더링하기 위해 우리는 그리 복잡하지 않은 새로운 shader 세트가 필요합니다. 우리는 오직 하나의 vertex attribute만 필요하므로 vertex shader는 꽤 간단합니다.


#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는 이들은 입력 받아 samplerCude를 샘플링할 것입니다.


#version 330 core
out vec4 FragColor;

in vec3 TexCoords;

uniform samplerCube skybox;

void main()
{    
    FragColor = texture(skybox, TexCoords);
}

  이 fragment shader는 비교적 간단합니다. vertex attribute의 위치 벡터를 텍스처의 방향 벡터로 취하고 이 것들을 cubemap으로부터 텍스처 값을 샘플링하기 위해 사용합니다.


  우리가 cubemap 텍스처를 가지고 있으므로 skybox 렌더링은 쉽습니다. 우리는 간단히 cubemap 텍스처를 바인딩하면 이 skybox sampler는 자동적으로 skybox cubemap으로 채워지게 됩니다. 이 skybox를 그리기 위해 우리는 가장 먼저 scene에 skybox를 그릴 것이고 depth 작성을 비활성화할 것입니다. 이렇게 하면 이 skybox는 항상 모든 오브젝트들의 뒤에 그려지게 됩니다.


glDepthMask(GL_FALSE);
skyboxShader.use();
// ... view, projection 행렬 설정
glBindVertexArray(skyboxVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays(GL_TRIANGLES, 0, 36);
glDepthMask(GL_TRUE);
// ... scene의 나머지 그리기

  이를 실행해본다면 여러분은 어려움에 닥칠 것입니다. 우리는 플레이어를 중앙으로 둘러싼 skybox를 원하므로 플레이어가 얼마나 움직였는지는 신경쓰지 않아야 합니다. 하지만 현재 view 행렬은 skybox의 모든 위치들을 회전시키고 확대, 이동 시키므로 플레이어가 움직이면 cubemap도 같이 움직입니다. 우리가 view 행렬의 이동 부분을 지워야 움직임이 skytbox 위치 벡터에 영향을 주지 않습니다.


  저번 강좌에서 4x4 변환 행렬의 좌측 상단 3x3 행렬을 취하면 이동 부분을 없앨 수 있다고 언급했었습니다. 이를 간단히 view 행렬을 3x3 행렬로 변환한 후 다시 4x4 행렬로 변환함으로써 이를 수행할 수 있습니다.


glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));  

  이는 어떠한 이동이든 없애줍니다. 하지만 모든 회전 변환은 유지하므로 사용자는 여전히 scene을 둘러볼 수 있습니다.


  scene을 둘러보면 scene의 현실적임이 극적으로 증가했음을 알 수 있습니다. 결과는 다음과 같이 보일 것입니다.


Image of a skybox in an OpenGL scene

  여러가지 skybox들로 실험해보고 여러분의 scene에 어떠한 영향을 주는지 확인해보세요.

최적화

  지금 우리는 모든 다른 오브젝트를 그리기 전에 맨 터음에 skybox를 먼저 렌더링합니다. 이 것을 잘 동작하지만 효율적이지는 않습니다. skybox를 제일 처음에 그리면 우리는 fragment shader를 화면의 각 픽셀들마다 실행해야 합니다. 심지어 skybox의 보이는 부분이 아주 작을지라도 말이죠. early depth testing을 사용하여 쉽게 폐기된 fragment들은 우리를 구해줄 것입니다.


  그래서 성능 향상을 위해 우리는 skybox를 마지막에 렌더링할 것입니다. 그러면 depth buffer는 완전히 다른 오브젝트들의 depth 값으로 채워지므로 우리는 오직 early depth test를 통과한 skybox의 fragment들만 렌더링하면 됩니다. 이는 비약적으로 fragment shader 호출 횟수를 줄일 수 있습니다. 문제는 skybox 는 대부분 렌더링에 실패할 것이라는 점입니다. 그저 1x1x1 큐브이기 때문이죠. 단순히 depth testing 없이 렌더링하는 것은 해법이 아닙니다. 그러면 skybox가 모든 다른 오브젝트들을 덮어씌울 것이기 때문이죠. 우리는 depth buffer에 트릭을 써서 skybox가 depth 값을 최댓값인 1.0을 가지고있다고 믿게 만들어서 앞에 다른 오브젝트들이 있는 곳은 test에 실패하도록 해야합니다.


  좌표 시스템 강좌에서 우리는 perspective division이 vertex shader가 실행된 후에 gl_Positionxyz 좌표를 w 요소로 나눔으로써 수행된다고 언급했었습니다. 또한 우리는 depth testing 강좌에서 나눗셈의 결과 z 요소는 vertex의 depth 값과 동일하다고 말했었습니다. 이 정보를 사용하여 우리는 출력 위치의 z 요소를 w 요소와 동일하게 설정하여 z 값이 항상 1.0이 될 수 있도록 만들 수 있습니다. perspective division이 수행될 때 z 요소는 w / w = 1.0으로 변환되기 때문입니다.


void main()
{
    TexCoords = aPos;
    vec4 pos = projection * view * vec4(aPos, 1.0);
    gl_Position = pos.xyww;
}  

  결과 NDC 좌표는 1.0z 값을 가지게 됩니다. 이는 depth 값의 최댓값입니다. 이 skybox는 결과적으로 오직 다른 오브젝트들이 없는 곳에서만 렌더링되게 됩니다(skybox 앞에 있는 모든 것들은 depth testing에서 통과하게 됩니다).


  우리는 depth 함수를 기본값인 GL_LESS 대신에 GL_LEQUAL로 설정해야 합니다. depth buffer는 skybox에 대해 1.0 값으로 채워지므로 우리는 skybox를 통과하게 만들기 위해 less than이 아닌 less than or equal로 수정해야 합니다.


  여기에서 최적화된 전체 소스 코드를 확인할 수 있습니다.

환경 매핑

  이제 우리는 하나의 텍스처가 매핑된 환경 오브젝트를 가지고 있고 이를 skybox 이상의 것들에 대해 사용할 수 있습니다. 환경과 cubemap을 사용하여 오브젝트에 빛을 반사 혹은 굴절 시키는 특성을 줄 수 있습니다. 이렇게 환경 cubemap을 사용하는 기술을 denvironment mapping 기술이라고 부르고 가장 많이 사용되는 것이 reflection(반사)refraction(굴절)입니다.

Reflection(반사)

  Reflection은 오브젝트(혹은 오브젝트의 어느 부분)이 주변 환경을 반사(reflect)하는 특성입니다. 시점의 각도를 기반으로 오브젝트의 컬러들은 환경과 동일하게 설정될 수 있습니다. 예를 들어 거울은 반사하는 오브젝트입니다. 시점의 각도에 따라 주변을 반사시키죠.


  Reflection의 기본은 그리 어렵지 않습니다. 다음 이미지는 반사 벡터(reflection vector)를 계산하는 방법과 cubemap을 샘플링하기 위해 이 벡터를 사용하는 방법을 보여줍니다.


Image of how to calculate reflection.

  View 방향 벡터 I ¯ 를 기반으로 오브젝트의 법선 벡터 N ¯ 에 따른 반사 벡터 R ¯ 을 계산합니다. GLSL의 reflect 함수를 사용하여 이 반사 벡터를 계산할 수 있습니다. 결과 벡터 R ¯ 은 cubemap을 인덱싱/샘플링하기 위한 방향 벡터로서 사용됩니다. 최종 효과는 오브젝트가 skybox를 반사하는 것처럼 보입니다.


  우리는 이미 scene에 skybox를 가지고 있기때문에 reflection을 생성하는 것은 그리 어렵지 않습니다. 우리는 컨테이너에 반사 속성을 주기 위해 컨테이너에 사용된 fragment shader를 수정할 것입니다.


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

  먼저 view/camera 방향 벡터 I를 계산하고 반사 벡터 R을 계산하기 위해 사용합니다. 이 반사 벡터는 skybox cubemap을 샘플링하기 위해 사용될 것입니다. 우리는 fragment의 보간된 NormalPosition 변수를 가지고 있으므로 vertex shader 또한 수정해야한다는 것을 알아두세요.


#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 * model * vec4(aPos, 1.0);
}  

  우리는 법선 벡터를 사용하고 있으므로 단위 벡터로 이들을 변환하기를 원합니다. Position 출력 벡터는 world-space 위치 벡터입니다. 이 Position 출력은 fragment shader에서 view 방향 벡터를 계산하기 위해 쓰입니다.


  법선을 사용하기 때문에 vertex data를 수정하고 attribute pointer 또한 수정해주어야 합니다. 또한 cameraPos uniform도 설정해주어야 합니다.


  그런 다음 우리는 컨테이너를 렌더링하기 전에 cubemap 텍스처를 바인딩해야합니다.


glBindVertexArray(cubeVAO);
glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture);  		
glDrawArrays(GL_TRIANGLES, 0, 36);	  

  컴파일 후 코드를 실행해보면 완벽한 거울같은 컨테이너를 볼 수 있습니다. 둘러싼 skybox는 정확히 컨테이너에 반사되고 있습니다.


Image of a cube reflecting a skybox via cubemaps via environment mapping.

  여기에서 전체 소스 코드를 확인할 수 있습니다.


  반사가 전체 오브젝트(컨테이너 같은)에 적용될 때 이 오브젝트는 스틸이나 크롬같은 높은 반사율을 가진 material 오브젝트처럼 보입니다. model 불러오기 강좌에서 사용했던 nanosuit 모델을 불러온다면 전체적으로 크롬으로 이루어진듯한 효과를 볼 수 있습니다.


Image of a Crysis Nanosuit reflecting a skybox via cubemaps via environment mapping.

  이는 꽤 멋집니다. 하지만 현실에서 대부분의 모델들은 완전한 반사를 하지 않습니다. 예를 들어 모델에 또다른 추가 디테일을 주는 reflection maps를 소개합니다. diffuse, specular map들 처럼 reflection map은 fragment의 반사율을 결정하기 위해 샘플링할 수 있는 텍스처 이미지입니다. 이 reflection map을 이용하여 모델의 어느 부분이 어떠한 세기를 가진 반사율을 보여줄지 결정할 수 있습니다. 이 강좌의 연습에서 모델 로더에서 reflection map을 구현해보도록 할 것입니다. 이는 nanosuit 모델의 디테일을 좀 더 살려줄 것입니다.

Refraction(굴절)

  환경 매핑의 또다른 형태는 refraction(굴절)이라고 불리고 반사와 비슷합니다. 굴절은 material의 변화에 따라 빛의 방향이 달라지는 것을 말합니다. 굴절은 흔히 빛이 직선으로 통과하지 않고 휘어지는 물과 같은 표면에서 볼 수 있습니다.


  환경 맵과 함께 굴절은 Snell's law에 설명이 잘 되어있습니다.


Image explaining refraction of light for use with cubemaps.

  다시 우리는 view 벡터 I ¯ , 법선 벡터 N ¯ , 그리고 굴절 벡터 R ¯ 을 가지고 있습니다. 보시다시피 view 벡터의 방향은 약간 휘어집니다. 이 휘어진 벡터 R ¯ 은 cubemp을 샘플링합니다.


  굴절은 GLSL의 refract 함수를 통해 쉽게 구현될 수 있습니다. 이 함수는 법선 벡터와 view 방향 그리고 두 refractive indices 사이의 비율을 인자로 받습니다.


  굴절 index는 material의 빛이 왜곡/휘어지는 정도를 결정합니다. 각 material들은 자신만의 고유한 refractive index를 가지고 있습니다. 가장 많이 쓰이는 refractive index들을 다음 표에 나타내었습니다.


Material Refractive index
공기 1.00
1.33
얼음 1.309
유리 1.52
다이아몬드 2.42

  빛이 통과하는 두 material 사이의 비율을 계산하기 위해 이 refractive index들을 사용합니다. 우리의 경우엔 빛/view 광선이 공기에서 유리로 향합니다(컨테이너가 유리로 만들어져있다고 가정합니다). 그래서 이 비율은 1.001.52=0.658 입니다.


  이미 cubemap이 바인딩되어있고 법선과 함께 vertex data도 가지고 있고 unform으로 camera 위치도 설정했습니다. 우리가 오직 해야할 일은 fragment shader를 수정하는 것 뿐입니다.


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들을 바꾸면 완전히 다른 비주얼 결과를 만들 수 있습니다. 컴파일 후 프로그램을 실행시켜보면 기본적인 컨테이너이기 때문에 흥미로운 효과를 볼 수 없을 것입니다. 동일한 shader를 nanosuit 모델에 사용해보면 유리 같은 오브젝트처럼 보일 것입니다.


Image of environment maps using refraction in OpenGL

  빛, 반사, 굴절, vertex 움직임의 올바른 결합을 사용하여 여러분은 말끔한 물 그래픽을 만들 수 있습니다. 물리적인 계산 결과를 위해 우리는 물체를 떠날 때 빛을 다시 굴절시켜주어야 한다는 것을 알아두세요. 지금은 간단히 한쪽 면만 굴절시킵니다. 이는 대부분의 목적에 충분합니다.

동적 환경 매핑

  지금은 skybox를 정적인 이미지의 결합으로 사용했습니다. 하지만 움직이는 오브젝트를 반사/굴절시키지는 않습니다. 우리는 지금까지 알아채리지 못했습니다. 우리는 오직 하나의 오브젝트만 사용했기 때문이죠. 만약 우리가 주변의 여러 오브젝트와 하나의 거울같은 오브젝트를 가지고 있다면 오직 skybox만 거울에 비칠 것입니다.


  Framebuffer를 사용하면 오브젝트로부터 6개의 다른 각도에 해당하는 scene의 텍스처를 생성할 수 있습니다. 그리고 이들을 반복문이 돌 때마다 cubemap에 저장할 수 있습니다. 그런 다음 우리는 이 (동적으로 생성된) cubemap을 현실적인 반사, 굴절 면을 생성하기 위해 사용할 수 있습니다. 이를 동적 환경 매핑(dynamic environment mapping)이라고 불립니다. 오브젝트 주변의 환경을 cubemap으로 동적으로 생성하고 이를 환경 매핑에 사용하기 때문입니다.


  이 것이 멋지게 보이는 반면에 아주 큰 단점 하나를 가지고 있습니다. 우리는 environment map을 사용하는 오브젝트 당 6번 scene을 렌더링해야 합니다. 이는 성능적으로 큰 패널티를 가져옵니다. 현대 응용 프로그램은 가능한 skybos를 사용하려고 하고 미리 컴파일된 cubemap을 사용하려고 노력 중입니다. 동적 환경 매핑이 훌륭한 기술인 반면에 성능 저하 없이 렌더링하기위해 많은 복잡한 트릭들을 필요로 합니다.

연습

  • Model 불러오기 강좌에서 생성했던 Model loader에 reflection map을 도입해보세요. 업그레이드된 reflection map을 가지고 있는 nanosuit는 여기에서 다운받을 수 있습니다. 다음은 이를 위한 설명입니다.
    • Assimp는 대부분의 format에서 reflection map처럼 보지 않기 때문에 reflection map을 ambient maps로 저장하여 약간의 트릭을 써야 합니다. 여러분은 material을 불러올 때 aiTextureType_AMBIENT 타입을 설정하여 relfection map을 불러올 수 있습니다.
    • 제가 급하게 specular 텍스처 이미지에서 reflection map을 만들었으므로 reflection map은 이 모델에 대한 정환한 map이 아닙니다.
    • Model loader는 그 자체로 3개의 텍스처 유닛을 가지고 있기 때문에 여러분은 skybox를 4번째 텍스처 유닛에 바인딩해야 합니다. 동일한 shader에서 skybox를 샘플링할 것이기 때문이죠.
  • 여러분이 올바르게 했다면 이 것과 같이 보일 것입니다.



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

반응형