GLSL Tutorial: Texturing

From LWJGL
Jump to: navigation, search

Thus far I have only dealt with straight up colours in GLSL, which is quite useless for the vast majority of games. This tutorial will quickly cover how to access and sample textures in GLSL. Then I will give a quick explanation and example of the fabled multi-texturing.

The Shaders

The majority of the work for texturing is done in the fragment shader, all the vertex shader does is to pass on the per-vertex texture coordinates. Unless you wanted to do some funky distortion of texture position, there should not be any transforming or operation performed on these coordinates. So, the vertex shader

#version 110

void main() {
    gl_Position = ftransform(); //Transform the vertex position
    gl_TexCoord[0] = gl_MultiTexCoord0;
    //glTexCoord is an openGL defined varying array of vec4. Different elements in the array can be used for multi-texturing with 
    //different textures, each requiring their own coordinates.
    //gl_MultiTexCoord0 is an openGl defined attribute vec4 containing the texture coordinates for unit 0 (I'll explain units soon) that
    //you give with calls to glTexCoord2f, glTexCoordPointer etc. gl_MultiTexCoord1 contains unit 1, gl_MultiTexCoord2  unit 2 etc.
}

In this example, I don't use any colour values, but their is no reason you can't and they can be a real bonus for cool lighting.

The fragment shader has two jobs when it comes to texturing: sampling the texture and setting the frag colout to this value. When I say sampling, all I mean is finding the colour of a particular pixel in the texture, also known as a texel. No matter how complex this may sound, GLSL makes it quite easy allowing us to work on what we want.

#version 110

uniform sampler2D texture1; //Remember back to my first tutorial (if you read it). Samplers are data types used to access textures. //To use textures from your main program, this must be uniform. 

void main() {
    gl_FragColor = texture2D(texture1, gl_TexCoord[0].st); //And that is all we need.
    //gl_FragColor you already should know. texture2D is a built in function used to retrieve a particular texel from a sampler. It
    //takes a sampler2D and a vec2 argument. If your confused about the .st : remember you can access elements in a vector using s, t
    //p, q. But these can also be used to get more than one element at a time. So st gives a vec2 of the first and second elements.
    //It probably won't surprise you that similar samplers also exist for 1D and 3D textures. They are:
    //  Type: sampler1D  Function: texture1D(sampler1D, float)
    //  Type: sampler3D  Function: texture3D(sampler3D, vec3)
    // But there are a whole host of functions to achieve various texture sampling objectives such as projection and offsets. These I
    // have never used and will not go into. They are there if you want to research.
}

So we have our simple shaders that transform our primitives and draw textures onto them. Let's just tell GLSL which textures to use and then we're done.


Binding Textures to Samplers

First up: texture units. Modern graphics cards have several spaces to store textures and openGL requires each implementation to support at least a certain number (I cannot remember the number but it is more than enough for most of our needs. There are certainly 33 including 0 enums in LWJGL to identify them). As of GL13, a method exists to tell openGL which space to use by setting the current texture unit.

   GL13.glActiveTexture(int unit); 

Where unit is one of GL13.GL_TEXTURE0, GL13.GL_TEXTURE1, GL13.GL_TEXTURE2, GL13.GL_TEXTURE3, GL13.GL_TEXTURE4 etc. Since we are only rendering a single texture per primitive at the moment, it will be enough to call GL13.glActiveTexture(GL13.GL_TEXTURE0) somewhere in the initiation of your program although I believe this is the default anyway. One more thing I will say is that:

       GL_TEXTUREn == GL_TEXTURE0 + n; 

Where n is a positive integer. This is very useful for loops where you have a lot of textures for each primitive.

Next we must tell GLSL which texture unit our sampler should sample from. This is done in exactly the same way as any other uniform variable if we imagine that the sampler is just an integer. So:

public void setTextureUnit0(int programId) {
    //Please note your program must be linked before calling this and I would advise the program be in use also.
    int loc = GL20.glGetUniformLocation(programId, "texture1");
    //First of all, we retrieve the location of the sampler in memory.
    GL20.glUniform1i(loc, 0);
    //Then we pass the 0 value to the sampler meaning it is to use texture unit 0.
}

Now, when you bind your texture using glBindTexture, it will be bound to texture unit 0 and when your shader runs, the sampler will get its values from texture unit 0, ie your texture. Please note that unless you plan on using the same texture unit for different purposes from primitive to primitive, you should only have to set the sampler's value once in your initiation steps. I would advise using a unique texture unit for each purpose. For example 0, 1, 2 and 3 are colour textures, 4 is a gloss map and 5 is an alpha map. This makes everything much simpler in my opinion and is easier to debug and not create bugs in the first place. If you don't know what I'm talking about, read the next section.


Multi-Texturing

You may already have guessed that texture units are the key to multi-texturing, but one thing at a time. First the fragment shader. The example I will be using involves giving each primitive two textures and a value which is the translucency of the first and opacity of the second. Ie we will be blending the two together to get our final colour. This could be used for to create a fading wall from transparent to opaque, such as in an interrogation room, or in sprites (As in a texture that changes over time). For simplicity's sake, the texture coordinates will be the same for each texture as this means we won't have to change our vertex shader. The fragment shader:

#version 110

uniform sampler2D texture1;
uniform sampler2D texture2; //Look at that, two samplers.
uniform float translucency;
//This is a float between 0 and 1 inclusive specifying how much of the second texture we want, and how much of the first we don't.
//Another implementation could have each vertex specifying an attribute variable for this and then passing it on with a varying. If 
//this was the case, fading could start at once side and work its way across for example.

void main() {
    vec4 colour1 = texture2D(texture1, gl_TexCoord[0].st);
    vec4 colour2 = texture2D(texture2, gl_TexCoord[0].st);
    float translucency1 = float(1) - translucency; //Note the cast to float as this can cause problems.
    colour1 *= translucency1;
    colour2 *= translucency;
    gl_FragColor = colour1 + colour2; //Vector addition is per component.
    //Don't worry if you don't understand the blending stuff, the important bit is the two samplers and two calls to texture2D.
}

Now, again in the initiation section of your program, tell the samplers which texture units to use.

public void setTextureUnits(int programId) {
    //Your program must be linked before calling this and I would advise the program be in use also.
    int loc = GL20.glGetUniformLocation(programId, "texture1");
    GL20.glUniform1i(loc, 0); //Texture Unit 0 for sampler1.
    int loc2 = GL20.glGetUniformLocation(programId, "texture2");
    GL20.glUniform1i(loc2, 1); //Texture Unit 1 for sampler2.
}

The texture binding portion of your rendering code must be a little more complex, as openGL must know which texture unit we are binding to before we bind the texture. So:

public void bindTextures(int texture1Id, int texture2Id) {
    GL13.glActiveTexture(GL13.GL_TEXTURE0);
    GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture1Id);
    GL13.glActiveTexture(GL13.GL_TEXTURE1);
    GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture2Id);
}

This binds texture1 to texture unit 0, which our sampler1 is using, and texture2 to texture unit 1, which our sampler2 is using. Before running this example (if you wish to) make sure you give translucency a value, otherwise it will default to 1 (I think) and you will only have the second texture showing.