We will now create model loader and renderer for Autodesk® 3ds model format which is a simple yet efficient binary model format for storing digital assets.
The code for this recipe is contained in the Chapter5/3DsViewer
folder. This recipe will be using the Drawing a 2D image in a window using a fragment shader and the SOIL image loading library recipe from Chapter 1, Introduction to Modern OpenGL, for loading the 3ds mesh file's textures using the SOIL
image loading library.
The steps required to implement a 3ds file viewer are as follows:
C3dsLoader
class. Then call the C3dsLoader::Load3DS
function passing it the name of the mesh file and a set of vectors to store the submeshes, vertices, normals, uvs, indices, and materials.if(!loader.Load3DS(mesh_filename.c_str( ), meshes, vertices, normals, uvs, faces, indices, materials)) { cout<<"Cannot load the 3ds mesh"<<endl; exit(EXIT_FAILURE); }
for(size_t k=0;k<materials.size();k++) { for(size_t m=0;m< materials[k]->textureMaps.size();m++) { GLuint id = 0; glGenTextures(1, &id); glBindTexture(GL_TEXTURE_2D, id); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); int texture_width = 0, texture_height = 0, channels=0; const string& filename = materials[k]->textureMaps[m]->filename; std::string full_filename = mesh_path; full_filename.append(filename); GLubyte* pData = SOIL_load_image(full_filename.c_str(), &texture_width, &texture_height, &channels, SOIL_LOAD_AUTO); if(pData == NULL) { cerr<<"Cannot load image: "<<full_filename.c_str()<<endl; exit(EXIT_FAILURE); } //Flip the image on Y axis int i,j; for( j = 0; j*2 < texture_height; ++j ) { int index1 = j * texture_width * channels; int index2 = (texture_height - 1 - j) * texture_width * channels; for( i = texture_width * channels; i > 0; --i ){ GLubyte temp = pData[index1]; pData[index1] = pData[index2]; pData[index2] = temp; ++index1; ++index2; } } GLenum format = GL_RGBA; switch(channels) { case 2: format = GL_RG32UI; break; case 3: format = GL_RGB; break; case 4: format = GL_RGBA; break; } glTexImage2D(GL_TEXTURE_2D, 0, format, texture_width, texture_height, 0, format, GL_UNSIGNED_BYTE, pData); SOIL_free_image_data(pData); textureMaps[filename]=id; } }
vertices
), texture coordinates (uvs
), per-vertex normals (normals
), and triangle indices (indices
) to GPU memory by allocating separate buffer objects for each attribute. Note that for easier handling of buffer objects, we bind a single vertex array object (vaoID
) first.glBindVertexArray(vaoID); glBindBuffer (GL_ARRAY_BUFFER, vboVerticesID); glBufferData (GL_ARRAY_BUFFER, sizeof(glm::vec3)* vertices.size(), &(vertices[0].x), GL_STATIC_DRAW); glEnableVertexAttribArray(shader["vVertex"]); glVertexAttribPointer(shader["vVertex"], 3, GL_FLOAT, GL_FALSE,0,0); glBindBuffer (GL_ARRAY_BUFFER, vboUVsID); glBufferData (GL_ARRAY_BUFFER, sizeof(glm::vec2)*uvs.size(), &(uvs[0].x), GL_STATIC_DRAW); glEnableVertexAttribArray(shader["vUV"]); glVertexAttribPointer(shader["vUV"],2,GL_FLOAT,GL_FALSE,0, 0); glBindBuffer (GL_ARRAY_BUFFER, vboNormalsID); glBufferData (GL_ARRAY_BUFFER, sizeof(glm::vec3)* normals.size(), &(normals[0].x), GL_STATIC_DRAW); glEnableVertexAttribArray(shader["vNormal"]); glVertexAttribPointer(shader["vNormal"], 3, GL_FLOAT, GL_FALSE, 0, 0);
GL_ELEMENT_ARRAY_BUFFER
so that we can render the whole mesh in a single call. However, if we have more than one material, we bind the appropriate submeshes separately. The glBufferData
call allocates the GPU memory, however, it is not initialized. In order to initialize the buffer object memory, we can use the glMapBuffer
function to obtain a direct pointer to the GPU memory. Using this pointer, we can then write to the GPU memory. An alternative to using glMapBuffer
is glBufferSubData
which can modify the GPU memory by copying contents from a CPU buffer.if(materials.size()==1) { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort)* 3*faces.size(), 0, GL_STATIC_DRAW); GLushort* pIndices = static_cast<GLushort*>(glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY)); for(size_t i=0;i<faces.size();i++) { *(pIndices++)=faces[i].a; *(pIndices++)=faces[i].b; *(pIndices++)=faces[i].c; } glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER); }
vUVout
.#version 330 core layout(location = 0) in vec3 vVertex; layout(location = 1) in vec3 vNormal; layout(location = 2) in vec2 vUV; smooth out vec2 vUVout; uniform mat4 P; uniform mat4 MV; uniform mat3 N; smooth out vec3 vEyeSpaceNormal; smooth out vec3 vEyeSpacePosition; void main() { vUVout=vUV; vEyeSpacePosition = (MV*vec4(vVertex,1)).xyz; vEyeSpaceNormal = N*vNormal; gl_Position = P*vec4(vEyeSpacePosition,1); }
#version 330 core uniform sampler2D textureMap; uniform float hasTexture; uniform vec3 light_position;//light position in object space uniform mat4 MV; smooth in vec3 vEyeSpaceNormal; smooth in vec3 vEyeSpacePosition; smooth in vec2 vUVout; layout(location=0) out vec4 vFragColor; const float k0 = 1.0;//constant attenuation const float k1 = 0.0;//linear attenuation const float k2 = 0.0;//quadratic attenuation void main() { vec4 vEyeSpaceLightPosition = (MV*vec4(light_position,1)); vec3 L = (vEyeSpaceLightPosition.xyz-vEyeSpacePosition); float d = length(L); L = normalize(L); float diffuse = max(0, dot(vEyeSpaceNormal, L)); float attenuationAmount = 1.0/(k0 + (k1*d) + (k2*d*d)); diffuse *= attenuationAmount; vFragColor = diffuse*mix(vec4(1),texture(textureMap, vUVout), hasTexture); }
glDrawElement
by using the indices attached to the GL_ELEMENT_ARRAY_BUFFER
binding point.glBindVertexArray(vaoID); { shader.Use(); glUniformMatrix4fv(shader("MV"), 1, GL_FALSE, glm::value_ptr(MV)); glUniformMatrix3fv(shader("N"), 1, GL_FALSE, glm::value_ptr(glm::inverseTranspose(glm::mat3(MV)))); glUniformMatrix4fv(shader("P"), 1, GL_FALSE, glm::value_ptr(P)); glUniform3fv(shader("light_position"),1, &(lightPosOS.x)); if(materials.size()==1) { GLint whichID[1]; glGetIntegerv(GL_TEXTURE_BINDING_2D, whichID); if(textureMaps.size()>0) { if(whichID[0] != textureMaps[materials[0]->textureMaps[0]->filename]) { glBindTexture(GL_TEXTURE_2D, textureMaps[materials[0]->textureMaps[0]->filename]); glUniform1f(shader("hasTexture"),1.0); } } else { glUniform1f(shader("hasTexture"),0.0); glUniform3fv(shader("diffuse_color"),1, materials[0]->diffuse); } glDrawElements(GL_TRIANGLES, meshes[0]->faces.size()*3, GL_UNSIGNED_SHORT, 0); }
sub_indices
array stored in the material to the glDrawElements
function to load those indices only.else { for(size_t i=0;i<materials.size();i++) { GLint whichID[1]; glGetIntegerv(GL_TEXTURE_BINDING_2D, whichID); if(materials[i]->textureMaps.size()>0) { if(whichID[0] != textureMaps[materials[i]->textureMaps[0]->filename]) { glBindTexture(GL_TEXTURE_2D, textureMaps[materials[i]->textureMaps[0]->filename]); } glUniform1f(shader("hasTexture"),1.0); } else { glUniform1f(shader("hasTexture"),0.0); } glUniform3fv(shader("diffuse_color"),1, materials[i]->diffuse); glDrawElements(GL_TRIANGLES, materials[i]->sub_indices.size(), GL_UNSIGNED_SHORT, &(materials[i]->sub_indices[0])); } } shader.UnUse();
The main component of this recipe is the C3dsLoader::Load3DS
function. The 3ds file is a binary file which is organized into a collection of chunks. Typically, a reader reads the first two bytes from the file which are stored in the chunk ID. The next four bytes store the chunk length in bytes. We continue reading chunks, and their lengths, and then store data appropriately into our vectors/variables until there are no more chunks and we pass reading the end of file. The 3ds specifications detail all of the chunks and their lengths as well as subchunks, as shown in the following figure:
Note that if there is a subchunk that we are interested in, we need to read the parent chunk as well, to move the file pointer to the appropriate offset in the file, for our required chunk. The loader first finds the total size of the 3ds mesh file in bytes. Then, it runs a while loop that checks to see if the current file pointer is within the file's size. If it is, it continues to read the first two bytes (the chunk's ID) and the next four bytes (the chunk's length).
while(infile.tellg() < fileSize) { infile.read(reinterpret_cast<char*>(&chunk_id), 2); infile.read(reinterpret_cast<char*>(&chunk_length), 4);
Then we start a big switch case with all of the required chunk IDs and then read the bytes from the respective chunks as desired.
switch(chunk_id) { case 0x4d4d: break; case 0x3d3d: break; case 0x4000: { std::string name = ""; char c = ' '; while(c!=' ') { infile.read(&c,1); name.push_back(c); } pMesh = new C3dsMesh(name); meshes.push_back(pMesh); } break; …//rest of the chunks }
All names (object name, material name, or texture map name) have to be read byte-by-byte until the null terminator character (