Cubemaps
우리는 지금동안 2D 텍스쳐들을 사용하고 있지만, 우리가 아직 알아보지 않은 더 많은 텍스쳐 타입들이 있다. 이 튜토리얼에서 우리는 하나의 텍스쳐에 여러가지 텍스쳐들의 조합이 매핑되는 텍스쳐 타입에 대해서 이야기할 것이다; cube map
cubemap은 기본적으로 6개의 개별적인 2D 텍스쳐들로 구성된 한 텍스쳐이다. 그것은 한 큐브의 한 면을 형성한다: textured cube. 너는 그러한 큐브의 요점이 무엇인지를 궁금할지도 모른다. 왜 6개의 개별 텍스쳐를 사용하는 대신에 6개의 개별 텍스쳐들을 하나의 개체로 결합하느라 씨름하는가? 음, 큐브 맵들은 그것들이 direction vector를 이용하여 인덱싱되고/샘플링 되어질 수 있다는 유용한 특징을 가진다. 우리가 중심에 방향벡터의 원점이 있는 1x1x1의 단위 큐브를 가졌다고 상상해라. 오렌지 색의 방향벡터를 가진 그 큐브 맵으로부터 한 텍스쳐 값을 샘플링하는 것은이것처럼 보인다:
Green Box
방향벡터의 크기는 중요하지 않다. 한 방향이 지원되는 한, OpenGL은 방향이 부딪히는(결국에) 그에 상응하는 texels를 가져오고, 적절히 샘플링된 텍스쳐 값을 반환한다.
만약 우리가 그러한 큐브맵을 붙일 cube shape를 가진다고 상상한다면, 그 큐브맵을 샘플링할 방향 벡터는 큐브의 (보간된) vertex position과 유사해야 할 것이다. 이러한 방식으로 큐브가 원점에 중심이 있는 한 큐브의 실제 위치 벡터를 사용하여 큐브맵을 사용할 수 있다. 우리는 그러고나서 큐브의 vertex positions로서 모든 정점의 텍스쳐좌표를 가져올 수 있다. 그 결과는 큐브맵의 적절한 각각의 face texture에 접근하는 텍스쳐 좌표이다.
Creating a cubemap
한 큐브맵은 어떤 다른 텍스쳐와 같은 텍스쳐이다. 그래서 하나를 만들기 위해서, 우리는 텍스쳐 연산을 더 하기 전에 한 텍스쳐를 생성하고 그것을 적절한 텍스쳐 타켓에 바인드 시켜야 한다. 이번에 GL_TEXTURE_CUBE_MAP으로 바인드시킨다 :
unsigned int textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
cubemap은 6개의 텍스쳐로 이루어지기 때문에, 각 면에 대해 하나씩, 우리는 glTexImage2D를 그것들의 파라미터를 이전의 튜토리얼과 비슷한 값으로 설정하여 6번 호출해야만 한다. 그러나 이번에, 우리는 cubemap의 특정한 면에 대해 texture target parameter를 설정해야마 ㄴ한다. 기본적으로 이것은 OpenGL에게 우리가 큐브맵의 어떤 면에 대해 텍스쳐를 만들지를 말하는 것이다. 이것은 우리가 cubemap의 각 면에 대해 한 번씩 glTexImage2D를 호출해야만 하는 것을 의미한다.
우리가 6개의 면을 가졌기 때문에, OpenGL은 우리에게 큐브맵의 한 면을 타게팅 하는 것에 대해 6개의 특별한 텍스쳐 타켓을 제공한다:
- 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
OpenGL의 많은 enums(열거형)처럼, 그것들의 scene뒤에 있는 int 값은 선형으로 증가한다. 그래서 만약 우리가 텍스쳐 위치의 한 배열이나 또는 한 벡터를 가진다면, 우리는 그것들을 GL_TEXTURE_CUBE_MAP_POSITIVE_X로 시작하여 그 열거형을 한 번의 반복마다 증가시켜서 그것들에 대해 루프를 돌 수 있다. 이것은 효과적으로 모든 텍스쳐 타겟에 대해 루프를 돌게 한다:
int t_width, t_height, t_nrChannels; unsigned char* t_data; for (GLuint i = 0; i < textures_faces.size(); ++i) { data = stbi_load(textures_faces[i].c_str(), &t_width, &t_height, &t_nrChannels, 0); glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, t_width, t_height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); }
여기에서 우리는 테이블에서 주어진 순서대로 큐브맵에 대해 요구되는 모든 텍스쳐의 위치를 포함하는 textures_Faces라고 불려지는 한 벡터를 가진다. 이것은 현재 바인드된 큐브맵의 각 면에 대해 한 텍스쳐를 생성한다.
cubemap은 어떤 다른 텍스쳐와 같은 텍스쳐이기 때문에, 우리는 또한 그것의 wrapping과 filtering methods를 명시해야할 것이다:
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을 두려워하지 말아라. 이것은 간단히 텍스쳐의 R위치에 대해 wrapping method를 설정한다. 그 R좌표는 텍스쳐의 3번째 차원과 동일하다 (positions에 대해 z 값 처럼). 우리는 wrapping method를 GL_CLAMP_TO_EDGE로 설정한다. 정확히 두 면 사이에 있는 텍스쳐 좌표들이 정확한 면에 맞지 않을 것이기 때문이다. (몇 가지 하드웨어 제한 떄문에) 그래서 GL_CLAMP_TO_EDGE를 사용하여, OpenGL은 항상 우리가 면 사이에서 샘플링할 때마다 그것들의 edge를 반환한다.
그러고나서 cubemap을 사용할 오브젝트들을 그리기 전에, 우리는 그에 상응하는 texture unit을 활성화하고, 렌더링 전에 cubemap을 바인드 시킨다. 일반적인 2D 텍스쳐와 비교해서 큰 차이는 없다.
fragment shader안에서, 우리는 또한 texture function을 사용하여 sample할 samplerCube 타입의 다른 샘플러를 사용해야만 한다. 그러나 vec2 대신에 이번에 vec3 direction vector
를 사용하여서. cubemap을 사용하는 fragment shader의 한 예시는 이것과 같다:
in vec3 textureDir; // direction vector representing a 3D texture coordinate uniform samplerCube cubemap; // cubemap texture sampler void main() { FragColor = texture(cubemap, textureDir); }
그것은 여전히 훌륭하지만 왜 성가신가?. cubemap으로 구현하기에 더 쉬운 몇 가지 흥미로운 기술들이 있다는 것이다. 그러한 테크닉들 중 하나는 skybox를 만드는 것이다.
Skybox
skybox는 전체 scene과 주변 환경의 6개의 이미지들을 포함하는 (큰) 큐브이다. 이것은 플레이어에게 그가 있는 환경이 실제보다 더 크다는 환각을 준다. 비디오 게임들에서 사용되는 스카이박스들의 몇 몇 예시들은 산, 구름 또는 별이 빛나는 밤의 이미지들이다. 별이 있는 밤의 하늘 이미지를 사용한 스카이 박스 예시는 엘더스크롤3 게임의 다음 스크린샷에서 볼 수 있다:
너는 아마도 이제 이것과 같은 스카이박슫르이 큐브맵들과 완벽히 어울린다고 추측했다: 6개의 면을 가지고 면마다 texture되어야 하는 큐브를 가지고 있따. 이전의 이미지에서, 그들은 플레이어가 큰 공간에 있다는 환각을 주기 위해 몇 가지 밤 하늘의 이미지들을 사용해싿. 반면에 그는 실제로는 아주 작은 박스 안에 있다.
보통 이러한 것과 같은 스카이박스들을 찾을 수 있는 자료들이 충분히 있다. 예를들어 이 웹사이트는 풍부한 스카이 박스들을 가진다. 이러한 스카이박스 이미지들은 보통 다음의 패턴을 가진다:
너가 그 6개의 면을 정육면체로 접는다면, 너는 큰 배경을 재현한 완전히 텍스쳐가 입혀진 큐브를 얻게된다. 몇몇 리소스들은 이것과 같은 포맷으로 된 스카이박스들을 제공한다. 그 경우에 너는 6개의 이미지들을 수동으로 추출해야만 하지만, 대부분의 경우에 6개의 단일의 텍스쳐 이미지들로서 제공된다.
이 특별한 (고품질) 스카이박스는 우리의 scene을 위해 사용할 것이고, 여기에서 다운로드 되어질 수 있다.
Loading a skybox
skybox는 그 자체로 단지 cubemap이기 때문에, skybox를 불러오는 것은 우리가 이전에 봤던 것과 많이 다르지 않다. skybox를 불러오기 위해 우리는 6개의 텍스쳐 위치를 가진 한 벡터를 받는 다음의 함수를 사용할 것이다:
unsigned int loadCubemap(std::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); else std::cout << "Cubemap texture failed to load at path: " << faces[i] << '\n'; stbi_image_free(data); } 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); return textureID; }
그 함수는 그 자체로 놀랍지 않을 것이다. 그것은 기본적으로 이전 섹션에서 우리가 본 큐브맵 코드이지만, 하나의 관리 가능한 함수에 결합되어 있다.
그러고나서 우리가 이 함수를 호출하기 전에, 우리는 cubemap enums가 명시된 순서로 한 벡터에 적절한 텍스쳐 경로를 불러와야 할 것이다:
std::vector<std::string> skyfaces = { "Model/skybox/right.jpg", "Model/skybox/left.jpg", "Model/skybox/top.jpg", "Model/skybox/bottom.jpg", "Model/skybox/front.jpg", "Model/skybox/back.jpg", }; unsigned int cubemapTexture = loadCubemap(skyfaces);
우리는 이제 skybox를 그것의 id인 cubemapTexture를 가진 하나의 큐브맵으로서 불러왔다. 우리는 이제 그것을 마지막으로 항상 배경으로서 우리가 사용해온 별로인 clear color를 교체하기 위해 한 큐브에 바인드 시킬 수 있다.
Displaying a skybox
skybox는 하나의 큐브에서 그려지기 때문에, 우리는 어떤 다른 오브젝트 처럼 또 다른 VAO, VBO와 정점들의 세트가 필요하다. 너는 그것의 정점 데이터를 여기에서 얻을 수 있다.
3d cube를 텍스쳐입히기 위해 사용되는 큐브맵은 큐브의 위치들을 텍스쳐 좌표로서 사용하여 샘플링 된다. 큐브가 원점 (0,0,0)에 중심이 있을 때, 그것의 포지션 벡터들 각각은 원점으로부터의 방향 벡터가 된다. 이 방향 벡터는 정확히 특정한 큐브의 포지션에서 그에 대응되는 텍스쳐값을 얻기 위해 우리가 필요한 것이다. 이러한 이유 때문에, 우리는 오직 position vectors만을 공급하면되고, 텍스쳐 좌표는 필요하지 않다.
skybox를 렌더링하기 위해서, 우리는 너무 복잡하지 않은 새로운 쉐이더 set이 필요 하다. 우리는 오직 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의 흥미로운 부분은 우리가 들어오는 position vectors를 fragment shader에 대해 나가는 texture 좌표로서 설정하는 것이라는 것을 유의해라. fragment shader는 그러고나서 samplerCube를 샘플링하는 입력으로서 이것을 취한다:
#version 330 core out vec4 FragColor; in vec3 TexCoords; uniform samplerCube skybox; void main() { FragColor = texture(skybox, TexCoords); }
그 fragment shader는 상대적으로 간단하다. 우리는 vertex attribute의 position vector를 텍스쳐의 방향 벡터로 취하고, cubemap으로부터 텍스쳐 값을 샘플링하기 위해 그것들을 사용한다.
스카이박스를 렌더링하는 것은 쉽다. 우리가 cubemap texture를 가졌으니, 우리는 간단히 cubemap texture를 바인드하고, skybox sampler는 자동적으로 그 skybox cubemap으로 채워진다. skybox를 그리기 위해서, 우리는 그것을 scene의 첫 번째 오브젝트로서 그릴 것이고, depth writing을 비활성화 할 것이다. 이 방식으로 스카이박스는 항상 모든 다른 오브젝트들이ㅡ 배경에 그려질 것이다:
만야 너가 이것을 작동시킨다면, 너는 어려움에 빠질 것이다. 우리는 스카이박스가 플레이어 주변에 중심을 잡길 원한다. 그 플레이어가 아무리 멀리 간다할지라도, 그 사이박스는 가까워지지 않게 하도록, 주변 환경이 매우 크다는 인상을 주면서. 현재 view matrix는 그러나 모든 스카이박스의 위치를 회전, scaling 그리고 평행이동하여 변환한다. 그래서 만약 플레이어가 움직인다면, 그 큐브맵도 또한 움직인다. 우리는 그 view matrix의 평행이동 부분을 제외하고 싶다. 그래서 움직임은 스카이박스의 위치 벡터에 영향을 받지 않는다.
너는 basic lighting tutorial에서 우리가 transformation matrices의 translation section을 4x4행렬의 왼쪽 상단의 3x3 행렬을 취하여 없애는 것을 깅거할지도 모른다. 이것은 효과적으로 평행이동 컴포넌트들을 제거한다. 우리는 이것을 view matrix를 3x3 matrix로 변환하여 얻을 수 있다:
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));
이것은 어떠한 평행이동을 제거하지만, 모든 회전 transformation을 유지한다. 그래서 사용자는 여전히 scene을 둘러볼 수 있다.
그 결과는 우리의 skybox 때문에, 끊임없이 거대해 보이는 scene이다. 만약 너가 기본 컨테이너 주위로 날아다닌다면, 너는 즉시 크기에 대한 감을 얻는다. 이것은 극적으로 scene의 현실성을 높인다. 그 결과는 이것 처럼 보인다:
다른 스카이박스들로 실험을 해보고, 그것들이 너의 scene의 look과 feel에 엄청난 영향을 어떻게 갖는지를 보아라.
An optimization
지금 당장, 우리는 scene에서 모든 다른 오브젝트들을 렌더링하기전에 처음에 skybox를 렌더링 했다. 이것은 훌륭히 작동하지만, 너무 효율적이지 않다. 만약 우리가 처음에 skybox를렌더링 한다면, skybox의 오직 작은 부분만이 실제로 보일지라도, 우리가 스크린에 있는 각 픽셀에 대해 fragment shader를 작동시킬 것이다; 귀중한 bandwidth를 절약하는 early depth testing을 사용하여 쉽게 버려질 수 있는 fragments들을 말한다.
그래서 우리에게 조금의 성능 향상을 위해서, 우리는 skybox를 마지막에 렌더링 할 것이다. 이 방식으로, 그 depth buffer는 완전히 모든 오브젝트의 depth values로 채워지고, 그래서 우리는 오직 그 early depth test가 통과하는 곳에서 skybox의 fragments를 랜더링하기만 하면 된다. 이것은 훌륭하게 fragment shader에 대한 호출을 줄인다. 문제는, 스카이박스가 1x1x1의 정육면체 이기 때문에 대부분 실패할 확률이 높을 것이라는 것이다. 이것은 대부분의 depth test를 실패하게 만든다. depth testing 없이 그것을 렌더링하는것은 해결책이 아니다. 왜냐하면 스카이박스는 scene에서 다른 오브젝트들을 덮어 쓸 것이기 때문이다. 우리는 depth buffer를 속여서 skybox가 최대 1.0의 depth value를 갖도록 해야할 것이다. 이것은 그것 앞에 다른 오브젝트들이 있는 곳에서 depth test를 실패하게 만들기 위해서이다.
coordinate systems 튜토리얼에서, 우리는 vertex shader가 작동한 후에 perspective division이 수행된다고 말했었다. 이것은 gl_Positions의 xyz좌표를 그것의 w component로 나눈다. 우리는 또한 depth testing 튜토리얼로부터 그 최종적으로 나눈 후의 z component가 그 정점의 depth value와 동일하다는 것을 안다. 이 정보를 사용하여,우리는 output position의 z component를 그것의 w component와 같게 설정할 수 있다. 그리고 이것은 z component가 항상 1.0이 되도록 만들 것이다. 왜냐하면 perspective division이 적용될 때, 그것의 z componentsms w / w = 1.0으로 바뀐다:
void main() { TexCoords = aPos; vec4 pos = projection * view * vec4(aPos, 1.0); gl_Position = pos.xyww; }
그러고나서 그 최종 NDC는 항상 1.0과 같은 z 값을 가질 것이다: 최대 depth value이다. 그 스카이박스는 결과적으로 오직 어떠한 보이는 오브젝트들이 없는 곳에서 렌더링 될 것이다. (그러고나서, 그것은 depth test를 통과할 것이고, 모든 다른 것은 그 skybox의 앞에 있다)
우리는 depth function을 디폴트인 GL_LESS 대신에 GL_LEQUAL로 설정하여 조금 바꿔야만 한다. 그 depth buffer는 skybox에 대해 1.0으로 채워질 것이다. 그래서, 우리는 skybox가 depth buffer와 동일하거나 더 작은 값들로 depth tests를 통과하도록 해야만 한다. less 대신에.
너는 소스코드의 좀 더 최적화된 버전을 여기에서 볼 수 있다.
Environment mapping
우리는 이제 단일 텍스쳐 오브젝트에 매핑된 전체의 주변 환경을 가지고, 우리는 그냥 스카이박스 이상을 위해서 그 정보를 사용할 수 있다. 한 환경을가진 cubemap을 사용하여, 우리는 오브젝트들에게 반사 또는 굴절 특징을 줄 수 있다. 이것과 같은 environment cubemap을사용하는 기법들은 environment mapping 기법이라고 불려지고, 두 개의 가장 인기있는 것들이 reflection(반사)와 refraction(굴절)이다.
Reflection
반사는 한 오브젝트(또는 한 오브젝트의 부분)가 그것의 주변환경을 반사시키는 특징이다. 예를들어, 오브젝트의 컬러들은 보는 사람의 각을 기반으로 하여 그것의 환경과 다소 동일하다. 예를들어 거울은 반사체이다: 그것은 보는 사람의 각도를 기반으로 그것의 주변환경을 반사한다.
반사의 기본은 그렇게 어렵지 않다. 다음의 이미지는 어떻게 우리가 반사 벡터를 계산할 수 있고, cubemap으로부터 샘플링하기 위해 그 벡터를 어떻게 사용하는지를 보여준다.
우리는 view direction vector I를 기반으로 오브젝트의 법선 벡터 N를 중심으로하는 반사 벡터 R을계산한다. 우리는 이 반사벡터를 GLSL의 내장 reflect 함수를 사용하여 계산할 수 있다. 그 최종 벡터 R은 그러고나서 환경의 color value를 반환하여 cubemap을 인덱싱/샘플링하는 방향벡터로 사용된다. 최종 효과는 오브젝트는 그 스카이박스를 반사시키는 것처럼 보인다.
우리는 이미 우리의 scene에서 skybox 설정을 가졌기 때문에, 반사를 만드는 것은 그렇게 어렵지 않다. 우리는 컨테이너에 의해 사용되는 fragment shader를 그 컨테이너에게 반사 특징을 주기 위해 바꿀 것이다:
#version 330 core out vec4 FragColor; in vec3 Normal; in vec3 Position; uniform vec3 cameraPos; uniform samplerCube skybox; void main() { vec3 l = normalize(Position - cameraPos); vec3 R = reflect(l, normalize(Normal)); FragColor = vec4(texture(skybox, R).rgb, 1.0); }
우리는 처음에 view/camera direction vector l을 계산한다. 그리고 우리가 skybox cubemap으로부터 샘플링하기 위해 사용할 reflect vector R를 계산하기 위해 이것을 사용한다. 우리가 다시 fragment의 보간된 Normal과 Position 변수를 가지고 있다는 것을 주목해라. 그래서 우리는 그 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); }
우리는 법선 벡터를 사용할 것이다. 그래서 우리는 또 다시 normal matrix로 그것들을 변형하길 원한다. Position output vector는 world-space position vector이다. vertex shader의 Position output은 fragment shader에서 view direction vector를 계산하기 위해서 사용된다.
우리가 normals 사용하기 때문에, 너는 vertex data를 업데이트 시켜야만 하고, attribute pointers를 또한 업데이트 시켜야만 한다. 또한 cameraPos uniform을 설정하도록 해라.
글고나서 우리는 컨테이너를렌더링하기 전에 cubemap texture를 바인드 시키길 원한다:
glBindVertexArray(cubeVAO); glActiveTexture(GL_TEXTURE0); reflectShader.use(); reflectShader.setMat4("view", view); reflectShader.setMat4("projection", projection); glm::mat4 rmodel(1.0); rmodel = glm::translate(rmodel, glm::vec3(-1.f, 0.f, 0.3f)); rmodel = glm::scale(rmodel, glm::vec3(0.5)); reflectShader.setMat4("model", rmodel); reflectShader.setVec3("cameraPos", camera.Position); glDrawArrays(GL_TRIANGLES, 0, 36);
너의 코드를 컴파일하고 실행시키는 것은 완벽한 거울처럼 작동하는 컨테이너를 준다. 주변 skybox는 완벽히 컨테이너에 반사된다:
너는 여기에서 완전한 소스코드를 볼 수 있다.
반사가 전체 오브젝트에 적용될 때 (컨테이너 처럼), 그 오브젝트는 마치 그것이 steel 또는 chrome 처럼 보이는 것 같다. 만약 우리가 model loading 튜토리얼에서 사용했던 nanosuit model를 불러온다면, 우리는 전적으로 chrome으로 만들어지도록 보이는 효과를 얻는다:
이것은 꽤 멋져 보이지만, 사실 대부분의 모델들은 모두 완전히 반사적이지않다. 우리는 예를들어 모델들에게 추가적인 세부사항을더해주는 reflection maps을 도입할 수 있다. diffuse and specular maps처럼, reflection maps는 한 fragment의 reflectivity(반사성)을 결정하기 위해 샘플링할 수있는 텍스쳐 이미지이다. 이러한 reflection maps를 사용하여, 우리는 그 모델의 어떤부분이 반사를 할지 그리고 어느 강도로 할지를 결정할 수 있다. 이 튜토리얼의 exercise에서 이전에 우리가 만들었던 model loader에서 reflection maps를도입하는 것은 너에게 달려있다. 이것은 nanosuit model의 detail을 증가시킨다.
Refraction
environment mappin의 또 다른 형태는 refraction(굴절)이라고 불려지고, 반사와 유사하다. 굴절은 빛이 흘러들어오는 material의 변화때문에, 빛의 방향에서의 변화이다. 굴절은 우리가 흔히 물과 같은 표면에서 보는 것이다. 거기에서 빛은 직선으로 들어오지 않고, 조금 휘어진다. 그것은 마치 물에 반쯤 잠긴 너의 팔을 보는 것과 같다.
굴절은 Snell's law에 의해 묘사된다. environment map과 함께 다음과 같이 보이는:
또 다시, 우리는 view vector I, 법선 벡터 N 그리고 이번에 최종 굴절 벡터 R을 가진다. 너도 볼 수 있듯이, view vector의 방향은 다소 구부려진다. 이 최종 휘어진 vector R은 그러고나서 cubemap으로부터 샘플링하기 위해 사용된다.
굴절은 쉽게 GLSL의 내장 refract 함수를 사용하여 구현되어 질 수 있다. 그 함수는 normal vector와 view direction과 두 개의 material의 굴절 indices 사이의 비율을 원한다.
굴절 인덱스(refractive index)는 한 material에 대해 빛이 왜곡/휘어지는 양을 결정한다. 거기에서 각 material은 그것 자신의 refractive index를 갖는다. 가장흔한 refractive indices의 목록은 다음의 테이블에서 주어진다:
- AIr : 1.00
- Water = 1.33
- Ice = 1.309
- Glass = 1.52
- Diamond = 2.42
우리는 빛이 통과하는 materials 사이의 비율을 계산하기 위해 이러한 refractive indices를 사용한다. 우리의 경우에, light/view 광선은 공중에서 glass로 간다. (만약 우리가 container가 유리로 만들어졌다고 가정한다면) 그래서 그 비율은 1.00/1.52 = 0.658이 된다.
우리는 이미 bind된 cubemap을 가지고 있고, normals과 함께 vertex data를 넣어주고, camera position을 uniform으로서 설정한다. 우리가 바꿔야할 유일한 것은 fragment shader이다:
너는 lighting, reflection, refraction과 vertex movement의 올바른 조합으로, 꽤 깔끔한 water graphics를 만들 수 있다는 것을 상상할 수 있다. 물리적으로 정확한 결과를 위해, 우리는 또한 빛이 그 오브젝트를 떠날 때 다시 굴절시켜야 하는 것에 유의해라; 이제 우리는 간단히 대부분의 목적에 좋은 single-side refraction을 사용했다.
Dynamic environment maps
지금 당장, 우리는 보기 훌륭한 skybox로서 이미지들의 정적인 조합을 사용하고 있다. 그러나 그것은 그럴듯하게 움직이는 오브젝트와 함께 실제의 scene을 포함하지 않는다. 우리는 실제로 이것을 이제까지 눈치채지 못했다. 왜냐하면 우리는 오직 단일의 오브젝트를 사용했기 때문이다. 만약 우리가 다양한 주변 오브젝트들이 함께 있는 거울 같은 오브젝트들을 가졌다면, 오직 스카이박스가 그 미러에 보일것이다. 마치 scene에서 유일한 오브젝트인 것 처럼.
framebuffers를 사용하여, 의문이 드는 오브젝트로부터 모두 다른 6개의 각도에 대해 scene의 한 텍스쳐를 만드는 것이 가능하고, 그러한 것들을 render iteration마다 cubemap에 저장하는 것이 가능하다. 우리는 그러고나서 이 (동적으로 생성된) cubemap을 모든 다른 오브젝트들을 포함하기 위해 현실적인 반사와 굴절 표면을 만들어 내기 위해 사용할 수 있다. 이것은 dynamic environment mapping이라고 불리는데, 우리가 동적으로 한 오브젝트의 주변의 상황이 들어있는 cubemap을 만들고, 그것을 evironment map으로 사용할 수 있기 때문이다.
그것은 보기 훌륭한 반면에, 한 가지 엄청난 불이익을 가지고 있다: 우리는 한 environment map을 사용하여 오브젝트마다 6번 scene을 렌더링 해야만 한다. 그리고 이것은 너의 프로그램에서 엄청난 성능 저하이다. 현대 프로그램들은 가능한한 skybox를 많이 사용하려고 노력하고, cubemap을 미리 컴파일 하려고한다. 그것들이 어느정도 여전히 dynamic environment maps를 사용할 수 있는 곳에서. dynamic environment mapping이 훌륭한 기법이지만, 그것은 많은 성능저하 없이 실제 렌더링 프로그램에서 작동시키기는데 많은 훌륭한 트릭과 hacks를 요구한다.
Exercises
- reflection maps을 우리가 model loading tutorials에서 만든 model loader에 도입하려고 해라. 너는 여기에서 reflection maps을가진 업그레이드된 nanosuit model를 볼 수 있따. 주의해야할 몇가지가 있다: - Assimp는 대부분의 오브젝트 포맷에서 reflection maps를 좋아하지 않는것처럼 보인다. 그래서 우리는 reflection maps를 ambient maps로 저장하여 조금 속임수를 썼었다. 너는 materials를 로딩할 때 aiTextureType_AMBIENT로 명시하여 reflection maps를 불러올 수 있다. - 나는 어느정도 성급하게 specular texture images로부터 reflection map texture를만들었었다. 그래서 그 reflection maps는 어떤 장소에서 그 모델에 대해 정확히 매핑 하지 않을 것이다. - model loader는 그 자체로 쉐이더에서 3개의 texture units을 가지기 때문에, 너는 skybox를 4번째 text unit으로 바인드 시켜야 할 것이다. 우리가 또한 같은 shader에서 skybox로부터 샘플링 할 것이기 때문이다.
- 만약 너가 옳게 했다면, 그것은 이렇게 보일 것이다.
=============================================
skybox에 대해 reflection 기능을 기존의 lighting shader에 적용할려고 많은 애를 먹었다.
그래도 결과적으로 구현을 했다.
shader를 구현하기 위해서 여러가지 bool 표현이 필요해서, 그것을 확인하기 위해서 비트연산자를 통해서 문제를 해결하기도 했다.
댓글 없음:
댓글 쓰기