Friday, April 24, 2015

OpenGL Enviornment Mapping



Now that direct lighting contribution was calculated correctly, it was time for some indirect effects. I am talking about environmental specular effect also known as Environment mapping.  Another important technique that will be covered automatically is Cube mapping as it’s a preliminary step for the Environment mapping. 

Before that, here is the output to begin with. Its without environment map:


The goal is to first map 6 textures onto a 6 faces of a cube to create an illusion of surroundings during the rendering. There are plenty of tricks that make this possible. First thing first, we make sure we have the required cube data in the vertex buffer. I started with creating new class for Skybox object. As for the sake of simplicity I decided to keep only one instance of skybox available, hence made the class singleton.  Skybox geometry was nothing but a regular cube with following coordinates: 


       vertices[0] = VertexP(glm::vec3(-1,-1,1));
       vertices[1] = VertexP(glm::vec3(1,-1,1));
       vertices[2] = VertexP(glm::vec3(1,1,1));
       vertices[3] = VertexP(glm::vec3(-1,1,1));
       vertices[4] = VertexP(glm::vec3(-1,-1,-1));
       vertices[5] = VertexP(glm::vec3(1,-1,-1));
       vertices[6] = VertexP(glm::vec3(1,1,-1));
       vertices[7] = VertexP(glm::vec3(-1,1,-1))

Note that although we are planning to assign texture to the cube there is no mention for the texture coordinates for the cube. The main reason for this is, when the cube is centered at (0,0,0) then the position vector itself is a direction vector from the origin which can be used to do get the corresponding texture value at the cube position. 

The main purpose of skybox is to have never reachable horizon for the player. It also gives player sense of immersion & scale of the whole world. Basically, we want to make sure that no matter what player does & wherever he goes, skybox is always the farthest object which he can never reach. Does that ring any bell? To make sure that skybox is always rendered last or to back of all rendered objects, we need to make sure that depth value is always 1.0 for skybox. How do we do that? Easiest way to achieve this by recollecting how perspective divide happens. We know that, homogenous coordinates are expressed in terms of xyzw. During perspective divide, z component is calculated by doing z/w. Hence, if we can set z component to w, during perspective divide, it will result into w/w = 1. 

Resulting NDC will always have value of 1, which is maximum depth value. Hence, whenever skybox is rendered z component will always be equal to 1 which is the maximum depth value. All this is done as follows : 

void main()
{
       mat4 wvp = matProj * matView * matWorld;
       vec4 Pos = wvp * vec4(in_Position, 1.0f);
       gl_Position = Pos.xyww;

       vs_outTex = in_Position;
}

Word of caution, now that we are setting skybox’s depth value to be always equal to 1, hence it becomes necessary to make sure that depth test for Skybox rendering is set to GL_LEQUAL. 

With skybox correctly setup, next step is to use the skybox texture as a lookup for environment mapping. But before that, lets have a look at function which is used to generate cube map texture id for OpenGL. 

GLint TextureManager::LoadCubemapFromFile(const std::string& dir)
{
       // we work on the assumption that folder contains specifically named textures
       // forming a cubemap!
       // Do some book keeping first by storing textures in the folder into a vector
       std::vector<std::string> vecCubemapTextures;
       vecCubemapTextures.push_back(dir + '/' + "posx.jpg");
       vecCubemapTextures.push_back(dir + '/' + "negx.jpg");
       vecCubemapTextures.push_back(dir + '/' + "posy.jpg");
       vecCubemapTextures.push_back(dir + '/' + "negy.jpg");
       vecCubemapTextures.push_back(dir + '/' + "posz.jpg");
       vecCubemapTextures.push_back(dir + '/' + "negz.jpg");

       GLuint cubemapID;
       glGenTextures(1, &cubemapID);
       glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapID);

       for (int i = 0 ; i<vecCubemapTextures.size() ; i++)
       {
           FIBITMAP* bitmap = LoadTextureFromFreeImage(vecCubemapTextures[i]);
           if (bitmap)
           {
               glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA, FreeImage_GetWidth(bitmap), FreeImage_GetHeight(bitmap), 0, GL_BGRA, GL_UNSIGNED_BYTE, (void*)FreeImage_GetBits(bitmap));
               FreeImage_Unload(bitmap);
           }
       }

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

       glBindTexture(GL_TEXTURE_CUBE_MAP, 0);

       return cubemapID;
}

Code works on the assumption that cubemap has texture names such as posx.jpg, negx.jpg etc for easy loading. I make sure that different cubemaps are kept in different folders & that folder name is passed as an argument to load the cubemap. 

Alright, so now back to environment mapping.  Environment mapping is used to show case reflection phenomenon. In graphics, we can achieve this using reflection vector. Reflection vector is calculated for an Eye vector which is nothing but camera lookat vector reflected around the normal at the surface at that point. 

We use in build glsl function “reflect” to do the job for us. Here is the snippet : 

      // calculate reflection vector for environment mapping..
      vec3 R = reflect(Eye, normalize(vs_outNormal));
      vec4 reflectionColor = vec4(texture(texture_cubeMap, R));


If we output only reflection color as a final fragment color then we get output like this 


And here are some of the outputs Added with overall illumination equation. 


And few more ... 



Cheers!  

No comments: