Saturday, 2 January 2016

Texture mapping (part 13)

Now that we have the basic logic for displaying a 3D object it is time to slowly make it look nicer.

We'll start by adding a texture map to our object. We'll be replacing the funky color logic here. It would be easy to combine the two but I see little point in doing so. The color of an object is usually uniform (lighting not withstanding) and the texture map replaces that function. But there are nice things you can do with mixing color in especially to create certain lighting effects. Such colors more often then not aren't set per vertex but instead set as uniforms.

That reminds me about a quick sidestep I'm not sure I made clear enough in previous examples. Our shaders have 4 prefixes for "global" (global to the shader) variables:
  • uniform, this means the value of the variable does not change and is generally set from outside of the shader, i.e. we set in in code
  • layout, which points to one of our attributes in our vertex buffer object(s)
  • out, which is an output variable which allows us to set variables and give them to the next shader in the pipeline. As mentioned before, these variables are often interpolated 
  • in, which is an input variable which should match the output of the previous shader in our pipeline. So the output of our vertex shader becomes the input of our fragment shader.
There aren't many things we need to change from our previous example.

Our texture map

First we need to load our texture map, I've added a box texture I grabbed somewhere off of the internet where we have different textures for each side of our box and placed this into our resources folder and then re-introduced a few things from our earlier tutorials.

First in our engine.h I've defined an enumeration that I use in conjunction with an array of textures. This is just convenience. Seeing we only have one texture it is also overkill but I like to be prepared:)
enum texture_types {
  TEXT_BOXTEXTURE,
  TEXT_COUNT
};
Then in engine.c I've defined the texture array into which we'll load our texture ids:
GLuint textures[TEXT_COUNT] = { 0 };
Now we need to load our texture, I've reintroduced my setTexture helper function and added the required logic at the end of our load_objects function:
void setTexture(GLuint pTexture, GLint pFilter, GLint pWrap) {
  glBindTexture(GL_TEXTURE_2D, pTexture);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, pFilter);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, pFilter);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, pWrap);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, pWrap);  
};

void load_objects() {
  int x, y, comp;
  unsigned char * data;

  ...

  // Now lets load our textures, note that this does not relate to our VAO state
  glGenTextures(TEXT_COUNT, textures);
  
  // and we load our box texture into textures[TEXT_BOXTEXTURE]
  data = stbi_load("boxtexture.jpg", &x, &y, &comp, 4);
  if (data == 0) {
    engineErrCallback(-1, "Couldn't load boxtexture.jpg");
  } else {
    setTexture(textures[TEXT_BOXTEXTURE], GL_LINEAR, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, x, y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
 
    stbi_image_free(data);
  };
};

void unload_objects() {
  glDeleteTextures(TEXT_COUNT, textures);
  glDeleteBuffers(2, VBOs);
  glDeleteVertexArrays(1, &VAO);
};
Note also that we delete our textures on our unload.

Changes to our model

Next we need to change our model data to contain coordinates within our texture for each vertex. Note that our coordinates range from (0.0, 0.0) - (1.0, 1.0) and are scaled up to the resolution of the texture image. (0.0, 0.0) is the top left of the image and (1.0, 1.0) is the bottom right. When loading the texture we told it to clamp to the edge but you can also set it to wrap the texture so it can be used as a pattern.

Now we do have a problem in the way we apply our texture because we only have 8 vertexs, we'll come back to that later. We'll change our vertex structure to hold a texture coordinate T instead of our color C and then adjust our vertex array:
typedef struct vertex {
  vec3    V;          // position of our vertice (XYZ)
  vec2    T;          // texture coordinates (XY)
} vertex;

vertex vertices[] = {
  -0.5,  0.5,  0.5, 1.0 / 3.0,       0.0,          // vertex 0
   0.5,  0.5,  0.5, 2.0 / 3.0,       0.0,          // vertex 1
   0.5, -0.5,  0.5, 2.0 / 3.0, 1.0 / 4.0,          // vertex 2
  -0.5, -0.5,  0.5, 1.0 / 3.0, 1.0 / 4.0,          // vertex 3
   0.5,  0.5, -0.5, 1.0 / 3.0, 1.0 / 2.0,          // vertex 4
  -0.5,  0.5, -0.5, 2.0 / 3.0, 1.0 / 2.0,          // vertex 5
  -0.5, -0.5, -0.5, 2.0 / 3.0, 3.0 / 4.0,          // vertex 6
   0.5, -0.5, -0.5, 1.0 / 3.0, 3.0 / 4.0,          // vertex 7
};
Our index array stays the way it is for now but we do need to change our vertex attribute pointers in our load_objects function:
  // now we need to configure our attributes, we use one for our position and one for our color attribute 
  glEnableVertexAttribArray(0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid *) 0);
  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid *) sizeof(vec3));
Note, the only change here is that our 2nd attribute is now 2 floats instead of 3 (the 2nd parameter in the last call).

Changes to our shaders

Now we need to update our shader code, again only small changes are needed. Our vertex shader now looks like this:
#version 330

layout (location=0) in vec3 positions;
layout (location=1) in vec2 texcoords;

uniform mat4 mvp;              // our model-view-projection matrix

out vec2 coords;

void main(void) {
  // load up our values
  vec4 V = vec4(positions, 1.0);
  coords = texcoords;
  
  // projection of V
  gl_Position = mvp * V;
}
The only change here is that our 2nd attribute has been renamed and is now a vec2 and our output variable is changed in unison to output a texture coordinate instead of a color. Just like with our color variable the values will be interpolated between the vertices before being sent to our fragment shader.

Our fragment shader has a few more changes:
#version 330

uniform sampler2D boxtexture;

in vec2 coords;
out vec4 fragcolor;

void main() {
  fragcolor = texture(boxtexture, coords);  
  if (fragcolor.a < 0.5) {
    discard;
  } 
}
Here we see the definition of a sampler2D uniform which is what we'll bind our texture to.
It is then used in our shader function together with our coordinate input variable "coords" to lookup our color.
The alpha check isn't needed for what we're doing now but a nice one to have, this allows us to do render shapes like leafs on a tree without having an elaborate model (see my tree generation blog post from a few months back).

Finally when loading our shader we need to grab the ID of our boxtexture sampler uniform in our load_shaders function:
  ...
  boxTextureId = glGetUniformLocation(program, "boxtexture");
  if (boxTextureId < 0) {
    engineErrCallback(boxTextureId, "Unknown uniform boxtexture");
  };
  ...

Rendering our cube

Finally to render our cube with a texture we need to bind our texture and inform our shader to use it. As I mentioned before, the texture is not part of our VAO state and thus not loaded automatically when we bind our VAO. This is actually a handy thing because we could reuse our cube model to render multiple cubes with different textures.

All we need to add is a little bit of code to our engine_render function when we select our shader:
  ...
  // select our shader
  glUseProgram(program);
  glUniformMatrix4fv(mvpId, 1, false, (const GLfloat *) mvp.m);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, textures[TEXT_BOXTEXTURE]);
  glUniform1i(boxTextureId, 0);
Those last 3 lines:
  • make texture0 active
  • bind our texture to texture0
  • assign texture0 to our boxtexture uniform
And voila, we have a rotating textured box:

Correctly texturing our cube

However we're not there yet, only the front and back of the box is properly textured. The sides, top and bottom are all wrong. Here we have a problem that we often run into because we need different texture coordinates for the same position when rendering the other sides of the cube.

What we see in many 3D formats is that our index array actually contains multiple indexes for each vertex of the model pointing to separate position and texture coordinate arrays. This is not something OpenGL is designed to do.

Our only option is to duplicate vertices. This may result in some more data but its a small price to pay. I'm creating duplicates each time a vertex is used for a different side of our cube, you could remove some that are exactly the same, but in our next session we'll see there is a good reason for this.

So here are our updated arrays:
vertex vertices[] = {
  // front
  -0.5,  0.5,  0.5, 1.0 / 3.0,       0.0,          // vertex 0
   0.5,  0.5,  0.5, 2.0 / 3.0,       0.0,          // vertex 1
   0.5, -0.5,  0.5, 2.0 / 3.0, 1.0 / 4.0,          // vertex 2
  -0.5, -0.5,  0.5, 1.0 / 3.0, 1.0 / 4.0,          // vertex 3

  // back
   0.5,  0.5, -0.5, 1.0 / 3.0, 1.0 / 2.0,          // vertex 4
  -0.5,  0.5, -0.5, 2.0 / 3.0, 1.0 / 2.0,          // vertex 5
  -0.5, -0.5, -0.5, 2.0 / 3.0, 3.0 / 4.0,          // vertex 6
   0.5, -0.5, -0.5, 1.0 / 3.0, 3.0 / 4.0,          // vertex 7
   
  // left
  -0.5,  0.5, -0.5, 1.0 / 3.0, 1.0 / 4.0,          // vertex 8  (5)
  -0.5,  0.5,  0.5, 2.0 / 3.0, 1.0 / 4.0,          // vertex 9  (0)
  -0.5, -0.5,  0.5, 2.0 / 3.0, 2.0 / 4.0,          // vertex 10 (3)
  -0.5, -0.5, -0.5, 1.0 / 3.0, 2.0 / 4.0,          // vertex 11 (6)

  // right
   0.5,  0.5,  0.5, 1.0 / 3.0, 1.0 / 4.0,          // vertex 12 (1)
   0.5,  0.5, -0.5, 2.0 / 3.0, 1.0 / 4.0,          // vertex 13 (4)
   0.5, -0.5, -0.5, 2.0 / 3.0, 2.0 / 4.0,          // vertex 14 (7)
   0.5, -0.5,  0.5, 1.0 / 3.0, 2.0 / 4.0,          // vertex 15 (2)

  // top
  -0.5,  0.5, -0.5,       0.0,       0.0,          // vertex 16 (5)
   0.5,  0.5, -0.5, 1.0 / 3.0,       0.0,          // vertex 17 (4)
   0.5,  0.5,  0.5, 1.0 / 3.0, 1.0 / 4.0,          // vertex 18 (1)
  -0.5,  0.5,  0.5,       0.0, 1.0 / 4.0,          // vertex 19 (0)

  // bottom
  -0.5, -0.5,  0.5, 2.0 / 3.0,       0.0,          // vertex 20 (3)
   0.5, -0.5,  0.5, 3.0 / 3.0,       0.0,          // vertex 21 (2)
   0.5, -0.5, -0.5, 3.0 / 3.0, 1.0 / 4.0,          // vertex 22 (7)
  -0.5, -0.5, -0.5, 2.0 / 3.0, 1.0 / 4.0,          // vertex 23 (6)
};

// and now define our indices that make up our triangles
GLint indices[] = {
  // front
   0,  1,  2,
   0,  2,  3,

  // back
   4,  5,  6, 
   4,  6,  7,  

  // left
   8,  9, 10,
   8, 10, 11,
  
  // right
  12, 13, 14,
  12, 14, 15,
  
  // top
  16, 17, 18,
  16, 18, 19,
  
  // bottom
  20, 21, 22,
  20, 22, 23,
};
And our end result:

Download the source code here

What's next

In our next tutorial we'll start looking at basic lighting techniques.

No comments:

Post a Comment