Multi-Texturing with GLSL

From LWJGL
Jump to: navigation, search

After floundering about in snippets of non-compile-able code, statements to this effect and statements to that, I finally came across NeHe article 21 by Florian Rudolf who in three lines explained how to make multiple textures accessible in fragment shaders. This tutorial explains the steps involved. It will end in a runnable program that loads two textures into a fragment shader where it mixes the colors of the textures and indexes one texture and further alters the rendered object based upon the result of the color indexed (displacement texturing). Full code can be found here: [1]

Rather than walk through all the code, which has been covered in earlier tutorials, I will point to the last tutorial which sets up shaders to access uniform variables, and which links back into older tutorials: [2]


The two areas I will walk through are setting up the textures, and initializing shader variables via LWJGL.

Setting up Textures

1) First we need two textures. I have two 256 square png images. One is the NeHe logo used to texture the crate in NeHe’s texturing tutorial (lesson 6); the other is divided into four squares, red, green, blue, and yellow.

2) Next we need to consider how we are to read these png images so we can put the data contained in them into openGL textures. I have used fast png which can be downloaded from Sourceforge: [3]. You will need to add the jar to your classpath.

In NetBeans, right click on your project sack in the Projects panel and open Properties. Select the Libraries option from the left panel, and Compile from the right. Add the fastpng jar alongside the lwjgl and the lwjgl_util jars which are (presumably) already there. A full demonstration of how to set up LWJGL on NetBeans can be found here: [4].

import net.sourceforge.fastpng.PNGDecoder; imports fastpng.


4) The two textures will be referenced in the program via:
private int tex01=0;//first texture
private int tex02=0;//second texture

found at the head of the Game class.

5) Each of these variables is initialized in the Game constructor via calls to setupTextures, made with the image name the texture is to be associated with. If the textures are not initialized with values greater than 0 there has been a problem… which I must say I haven’t handled very well…
tex01=setupTextures("assets/nehe.png");
tex02=setupTextures("assets/colorMap.png");
if(tex01<1 || tex02<1)

 System.out.println("Error initializing textures");


6) The first thing setupTextures does is to grab unique integer values that will be assigned to tex01 and tex02. Essentially, what happens is openGL is asked for a unique integer identifier that it will associate with a texture’s (image’s) data. The value will initially be stored in the IntBuffer tmp.
IntBuffer tmp=BufferUtils.createIntBuffer(1);
GL11.glGenTextures(tmp);
tmp.rewind();

7) Then we use fastpng to decode the png image and place the data into a ByteBuffer.
try {

 InputStream in = new FileInputStream(filename);
PNGDecoder decoder=new PNGDecoder(in);
ByteBuffer data=ByteBuffer.allocateDirect(4*decoder.getWidth()*decoder.getHeight());
decoder.decode(data, decoder.getWidth()*4, PNGDecoder.TextureFormat.RGBA);
data.rewind();

8) Binding the texture is a state thing… it means all further calls dealing with texture objects are dealing with the texture object that is currently bound.
GL11.glBindTexture(GL11.GL_TEXTURE_2D, tmp.get(0));

9) Of the bound texture we set the min and mag filters and importantly, the data that will be associated with the bound texture. Basically, openGL puts the data somewhere and associates the unique integer id held in tmp with it.
GL11.glTexParameteri(GL11.GL_TEXTURE_2D,GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D,GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GL11.glTexImage2D(GL11.GL_TEXTURE_2D,0,GL11.GL_RGBA,decoder.getWidth(),decoder.getHeight(),0,GL11.GL_RGBA,GL11.GL_UNSIGNED_BYTE,data);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 4);

10) Because openGL puts the data somewhere and associates the unique id with it, the work done by fastpng and the ByteBuffer can all be lost to the garbage collector when setupTextures returns… we only need the unique id for future reference into the texture data.
tmp.rewind();
return tmp.get(0);


11) Note that FileNotFoundExceptions are caught. So if you get one you need to either alter filename or the location of your image.

12) Now the textures are set up we need to examine how to use the unique ids to access the texture data from the fragment shader. Ideally this would be as simple as setting up any other uniform variable, but shadow has her way… “In the light universe I have been shadow. Maybe in the dark zone, I shall be light.” Kai… Lexx.


Setting up uniform sampler2D

1) If you do not know how to set up GLSL shaders in LWJGL look here: [5].

2) If you look at the file.frag code you find two uniform variables of type sampler2D; sampler01 will reference the texture data held by tex01, etc. sampler2D types are GLSL specific designed to reference two dimensional textures. The first thing we need do is access these variables from within the tick method, which we do in the same manner we accessed the uniform variable pos in this tutorial dealing with setting up uniform variables: [6].

3) Using the shader and all that is required to enable this must precede initializing the int sampler01. The call to glGetUniformLocationARB is a request to your graphics card… basically saying “you… graphics card, you compiled and linked the shader so you must know where you have put the uniform variables I declared… give me access to them”, to which the graphics card responds in the manner openGL responds when handling textures… “yeah, I know where the variable is but you are not getting it, you can have this unique id and when you use it I will look up where I have put the variable.”


if(useShader){

 ARBShaderObjects.glUseProgramObjectARB(shader);

}
int sampler01=ARBShaderObjects.glGetUniformLocationARB(
shader, "sampler01");
if(sampler01<1)

 System.out.println(“Error accessing sampler01”);

4) Nowadays graphics cards have banks for holding different textures… the card can sample data from these different banks in the same pass. What we have to do is put tex01 and tex02 into different banks. Hence the call to GL13.glActiveTexture(GL13.GL_TEXTURE0); This is a call to set the openGL state to work with the first texture bank GL_TEXTURE0. The call to GL13.glActiveTexture(GL13.GL_TEXTURE1); in the next bloc of code is a call to set the openGL state to work with the second texture bank.

5) Next we bind the state to work with the texture we want to put in that bank. This also binds the texture being worked with to the bank we are working with.
GL11.glBindTexture(GL11.GL_TEXTURE_2D, tex01);


6) Then we associate the uniform sampler2D id got from the graphics card with the bank that now holds the texture we want. The uniform sampler2D sampler01 found in the fragment shader now references the first texture bank which contains tex01. Essentially tex01 and sampler01 end up referencing the same texture but they are not related to each other directly. This differs from the other uniform variables found in the tutorial linked above where the CPU side variable is used to refer to the GPU side variables when it initializes them. Note that the 0 refers to GL_TEXTURE0
ARBShaderObjects.glUniform1iARB(sampler01, 0);

7) The story is yet incomplete. Not only do we want the fragment shader to be able to access the individual textures, but we also want each fragment processed to be able to sample the textures at the correct place in the texture. Examine the vertex shader file.vert:
void main(){

   gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;
gl_TexCoord[0]=gl_MultiTexCoord0;

}


8) glMultiTexCoord0 is an attribute variable containing the values set by the glTexCoord calls found inbetween the glBegin/glEnd bloc. Assigning this value to gl_TexCoord[0] kind of like pins each vertex to the desired position within the texture. When the fragments are created for the fragment shader their positions are calculated relative to these pinned vertex points and thus also access the relevant texels in the texture.

9) Now we visit the fragment shader file.frag. Here we find the two sampler variables that have been bound to the texture bank containing the relevant texture.
uniform sampler2D sampler01;
uniform sampler2D sampler02;


10) The vec3 variable theColor gets set with the color value extracted from sampler01 texture tex01 at the coordinates defined by gl_TexCoord[0]’s s and t values. These values, as already noted, are set by the graphics card when it creates the fragments based upon the position of each fragment relative to the pinned vertices. Remembering that each fragment is processed independently by this fragment shader.
vec3 theColor=vec3(texture2D(sampler01, (gl_TexCoord[0].st)));

11) The vec3 variable theColor2 is set with the color value extracted from the sample02 texture tex02 using the same set of coordinates. Next we check on the value set in theColor2[0] (theColor.r). If the red value is greater than 0.5 we reset theColor2 to white.

12) Lastly we set the final fragment color in gl_FragColor. Specifically, we multiply the color set in theColor2 (either its native color or white depending on the red value of the native color) with the color value found in tex01.