
Loading multiple textures and passing through to a shader
Posted Wednesday, 22 June, 2011 - 18:22 by ZTKHello everyone,
Many people seem to be confused as to how to load multiple textures. I'm writing this tutorial to give a concrete example of this. We will create a cube whose faces have four textures each. Each face is passed through to a fragment shader, that displays and tints each texture
The key point is this function, which will load textures
private void CreateTexture(out int texture, Bitmap bitmap) { // load texture GL.GenTextures(1, out texture); //Still required else TexImage2D will be applyed on the last bound texture GL.BindTexture(TextureTarget.Texture2D, texture); BitmapData data = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0); bitmap.UnlockBits(data); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); }
As well as this code which will bind the textures.
private void BindTexture(ref int textureId, TextureUnit textureUnit, string UniformName) { GL.ActiveTexture(textureUnit); GL.BindTexture(TextureTarget.Texture2D, textureId); GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle, UniformName), textureUnit - TextureUnit.Texture0); }
Note that we can't just pass textureUnit to GL.Uniform1. It has to be textureUnit - TextureUnit.Texture0. Another point of confusion on this function (that really confused me) is that the openTK tutorial has the texture object Id passed to GL.Uniform1. This is WRONG! You need to pass an int representing which textureUnit you are working with. So if you are on textureUnit.Texture4 (for example), then you'd pass "4" to GL.Uniform1. Alternatively, as Profet points out in the comments, you can do something like TextureUnit4 - TextureUnit0, which gives a result of "4".
These functions are called four times, like this:
CreateTexture(out texture0, bitmap0); CreateTexture(out texture1, bitmap1); CreateTexture(out texture2, bitmap2); CreateTexture(out texture3, bitmap3); BindTexture(ref texture0, TextureUnit.Texture0, "MyTexture0"); BindTexture(ref texture1, TextureUnit.Texture1, "MyTexture1"); BindTexture(ref texture2, TextureUnit.Texture2, "MyTexture2"); BindTexture(ref texture3, TextureUnit.Texture3, "MyTexture3");
One basic point for some readers, but one that I've seen some people confused about is this:
GL.GenTextures(1, out texture);
The first param is a "1". It's always "1", because we are generating one texture. The "1" here has nothing to do with which texture we are generating.
Finally, here is our shader code. Note that it is passed four textures, and tints each one differently. It looks at the horizontal coordinate of the fragment, and applies the first texture if it is in the first quarter, second texture in the second quarter, etc.. The shader has access to all our textures at once.
#version 150 uniform sampler2D MyTexture0; uniform sampler2D MyTexture1; uniform sampler2D MyTexture2; uniform sampler2D MyTexture3; void main(void) { if (gl_TexCoord[0].s < 0.25){ gl_FragColor = texture2D( MyTexture0, gl_TexCoord[0].st ); gl_FragColor[1] = gl_FragColor[1] * 0.90; } else if (gl_TexCoord[0].s < 0.5) { gl_FragColor = texture2D( MyTexture1, gl_TexCoord[0].st ); gl_FragColor[0] = gl_FragColor[0] * 0.90; } else if (gl_TexCoord[0].s < 0.75) { gl_FragColor = texture2D( MyTexture2, gl_TexCoord[0].st ); gl_FragColor[2] = gl_FragColor[2] * 0.90; } else { gl_FragColor = texture2D( MyTexture3, gl_TexCoord[0].st ); } }
Here the full code. Create a VS2010 solution and create four images called pic1, pic2, pic3, and pic4 respectively as bitmaps.
using System; using OpenTK; using OpenTK.Graphics; using OpenTK.Graphics.OpenGL; using OpenTK.Audio; using OpenTK.Audio.OpenAL; using OpenTK.Input; using System.Drawing; using System.Drawing.Imaging; using System.Diagnostics; namespace StarterKit { class Game : GameWindow { private Matrix4 modelviewMatrix, projectionMatrix; private Matrix4 rotationviewMatrix; int fragmentShaderHandle, shaderProgramHandle, texture0, texture1, texture2, texture3; const float rotation_speed = 18.0f; float angle; Bitmap bitmap0 = new Bitmap(@"..\..\img\pic1.bmp"); Bitmap bitmap1 = new Bitmap(@"..\..\img\pic2.bmp"); Bitmap bitmap2 = new Bitmap(@"..\..\img\pic3.bmp"); Bitmap bitmap3 = new Bitmap(@"..\..\img\pic4.bmp"); string fragmentShaderSource = @" #version 150 uniform sampler2D MyTexture0; uniform sampler2D MyTexture1; uniform sampler2D MyTexture2; uniform sampler2D MyTexture3; void main(void) { if (gl_TexCoord[0].s < 0.25){ gl_FragColor = texture2D( MyTexture0, gl_TexCoord[0].st ); gl_FragColor[1] = gl_FragColor[1] * 0.90; } else if (gl_TexCoord[0].s < 0.5) { gl_FragColor = texture2D( MyTexture1, gl_TexCoord[0].st ); gl_FragColor[0] = gl_FragColor[0] * 0.90; } else if (gl_TexCoord[0].s < 0.75) { gl_FragColor = texture2D( MyTexture2, gl_TexCoord[0].st ); gl_FragColor[2] = gl_FragColor[2] * 0.90; } else { gl_FragColor = texture2D( MyTexture3, gl_TexCoord[0].st ); } }" ; void CreateShaders() { fragmentShaderHandle = GL.CreateShader(ShaderType.FragmentShader); GL.ShaderSource(fragmentShaderHandle, fragmentShaderSource); GL.CompileShader(fragmentShaderHandle); Debug.WriteLine(GL.GetShaderInfoLog(fragmentShaderHandle)); // Create program shaderProgramHandle = GL.CreateProgram(); GL.AttachShader(shaderProgramHandle, fragmentShaderHandle); GL.LinkProgram(shaderProgramHandle); Debug.WriteLine(GL.GetProgramInfoLog(shaderProgramHandle)); GL.UseProgram(shaderProgramHandle); float aspectRatio = ClientSize.Width / (float)(ClientSize.Height); Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, aspectRatio, 1, 100, out projectionMatrix); } /// <summary>Creates a 800x600 window with the specified title.</summary> public Game() : base(800, 600, GraphicsMode.Default, "OpenTK Quick Start Sample") { VSync = VSyncMode.On; } /// <summary>Load resources here.</summary> /// <param name="e">Not used.</param> protected override void OnLoad(EventArgs e) { CreateShaders(); base.OnLoad(e); GL.ClearColor(0.0f, 0.4f, 0.0f, 0.0f); GL.Enable(EnableCap.DepthTest); GL.Enable(EnableCap.Texture2D); GL.Hint(HintTarget.PerspectiveCorrectionHint, HintMode.Nicest); CreateTexture(out texture0, bitmap0); CreateTexture(out texture1, bitmap1); CreateTexture(out texture2, bitmap2); CreateTexture(out texture3, bitmap3); BindTexture(ref texture0, TextureUnit.Texture0, "MyTexture0"); BindTexture(ref texture1, TextureUnit.Texture1, "MyTexture1"); BindTexture(ref texture2, TextureUnit.Texture2, "MyTexture2"); BindTexture(ref texture3, TextureUnit.Texture3, "MyTexture3"); modelviewMatrix = Matrix4.LookAt(0, 4, 7, 0, 0, 0, 0, 1, 0); rotationviewMatrix = Matrix4.CreateRotationX((float)Math.PI / 100);// *Matrix4.CreateRotationX((float)Math.PI / 10000); } private void CreateTexture(out int texture, Bitmap bitmap) { // load texture GL.GenTextures(1, out texture); //Still required else TexImage2D will be applyed on the last bound texture GL.BindTexture(TextureTarget.Texture2D, texture); BitmapData data = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, data.Width, data.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0); bitmap.UnlockBits(data); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); } /// <summary> /// Called when your window is resized. Set your viewport here. It is also /// a good place to set up your projection matrix (which probably changes /// along when the aspect ratio of your window). /// </summary> /// <param name="e">Not used.</param> protected override void OnResize(EventArgs e) { base.OnResize(e); GL.Viewport(ClientRectangle.X, ClientRectangle.Y, ClientRectangle.Width, ClientRectangle.Height); projectionMatrix = Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, Width / (float)Height, 1.0f, 64.0f); GL.MatrixMode(MatrixMode.Projection); GL.LoadMatrix(ref projectionMatrix); } /// <summary> /// Called when it is time to setup the next frame. Add you game logic here. /// </summary> /// <param name="e">Contains timing information for framerate independent logic.</param> protected override void OnUpdateFrame(FrameEventArgs e) { base.OnUpdateFrame(e); if (Keyboard[Key.Escape]) Exit(); } /// <summary> /// Called when it is time to render the next frame. Add your rendering code here. /// </summary> /// <param name="e">Contains timing information.</param> protected override void OnRenderFrame(FrameEventArgs e) { base.OnRenderFrame(e); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); angle += rotation_speed * (float)e.Time; float translateby = (float)Math.Sin(DateTime.Now.Second) * 0.05f; rotationviewMatrix = Matrix4.CreateTranslation(0f, 0f, translateby); GL.MatrixMode(MatrixMode.Modelview); GL.LoadMatrix(ref modelviewMatrix); GL.Rotate(angle, 0.0f, 1.0f, 0.0f); DrawCube(); SwapBuffers(); } private void BindTexture(ref int textureId, TextureUnit textureUnit, string UniformName) { GL.ActiveTexture(textureUnit); GL.BindTexture(TextureTarget.Texture2D, textureId); GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle, UniformName), textureUnit - TextureUnit.Texture0); } private void DrawCube() { GL.Begin(BeginMode.Quads); GL.Color3(Color.Silver); GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f, -1.0f, -1.0f); GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-1.0f, 1.0f, -1.0f); GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(1.0f, 1.0f, -1.0f); GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(1.0f, -1.0f, -1.0f); GL.Color3(Color.Honeydew); GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f, -1.0f, -1.0f); GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, -1.0f, -1.0f); GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(1.0f, -1.0f, 1.0f); GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f, -1.0f, 1.0f); GL.Color3(Color.Goldenrod); GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f, -1.0f, -1.0f); GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-1.0f, -1.0f, 1.0f); GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(-1.0f, 1.0f, 1.0f); GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f, 1.0f, -1.0f); GL.Color3(Color.DodgerBlue); GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f, -1.0f, 1.0f); GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, -1.0f, 1.0f); GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(1.0f, 1.0f, 1.0f); GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f, 1.0f, 1.0f); GL.Color3(Color.Purple); GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f, 1.0f, -1.0f); GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(-1.0f, 1.0f, 1.0f); GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(1.0f, 1.0f, 1.0f); GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(1.0f, 1.0f, -1.0f); GL.Color3(Color.ForestGreen); GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(1.0f, -1.0f, -1.0f); GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f, 1.0f, -1.0f); GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(1.0f, 1.0f, 1.0f); GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(1.0f, -1.0f, 1.0f); GL.End(); } /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { // The 'using' idiom guarantees proper resource cleanup. // We request 30 UpdateFrame events per second, and unlimited // RenderFrame events (as fast as the computer can handle). using (Game game = new Game()) { game.Run(30.0); } } } }
A big thank you to Profet and tksuoran for clearing up errors in this!
- ZTK's blog
- Login or register to post comments


Comments
Re: Loading multiple textures and passing through to a ...
Nice tutorial about an obscure part of the shader API !
However, it ocured that some things was a bit misleading for me at first, then I would add my contribution :
1) Multitexturing purpose
Multiple TextureUnit is only required when using multiples textures at the same time in a single shader. Use the same TU for all your project textures if you don't use multitexturing.
2) GL.Uniform1 parameter
I found the following usage of GL.Uniform1 very deceptive since the "GenTexture" output id is used instead of the TextureUnit id.
GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle, UniformName), texture);That seems to be correct (and works) here because the order of texture generation exactly matches the id of used TextureUnit but that's not practical in a dynamic engine where resource generation is unpredictable.
That means that if you are activating TextureUnit.Texture1, you must pass 1 as last parameter to this function.
In a more dynamic way and to avoid passing an additional integer argument to CreateAndBindTexture(), the following code might be used to retrieve the current textureUnit id :
3) Dynamic binding
The current example works fine when textures are bound on program's load, but CreateAndBindTexture becomes unpractical when we have to switch textures, as when we render multiple objects using different textures with a single shader.
Then the CreateAndBindTexture method becomes :
And the drawing part:
4) TextureUnit ids
The following assumption is not correct outside of this tutorial:
GL.ActiveTexture has to start with TextureUnit.Texture1.
You are free to use any TextureUnit you want, but that was required in this tutorial to match the id generated by the GenTexture function.
5) TextureUnit limitations
Be aware that number of available TextureUnits is hadware dependant, recent have up to 32 units since older might only have 2 of them !
Re: Loading multiple textures and passing through to a ...
As Profet pointed out, the original post uses GL.Uniform1() incorrectly.
Uniform1() for texture sampler uniforms takes in texture units, not texture objects.
ActiveTexture() selects texture unit, where texture object is then bound.
Also you can use texture0 just fine - you just need to pass in texture unit, not texture object.
Re: Loading multiple textures and passing through to a ...
Here is some pseudo code:
Re: Loading multiple textures and passing through to a ...
Thank you both very much Profet and tksuoran!
point 1. I don't fully understand. Isn't this a use of multitexturing? After all, each of the four textures is used in the shader, as each texture is tinted differently... Am I missing something?
point 2. Thank you very much! I was confused about this. Your code works great! However, my usage came from the example in the openTK sample brower. Please look at Julia Set Fractal code that comes in the openTK sample browser.. It has the following code
int TextureObject; GL.ActiveTexture(TextureUnit.Texture0); // select TMU0 GL.GenTextures(1, out TextureObject); GL.Uniform1(GL.GetUniformLocation(ProgramObject, "COLORTABLE"), TextureObject); // <<-- here they are using textureobject from gentextures!!Am I missing something? It seems in this example they are using the textureObject not the texture unit. Is this a mistake in the example or am I misunderstanding?
point 3. This works perfectly. Thank you so much for the clarificaiton.
point 4. Thank you for helping me understand this. I was very confused, this helps a lot!
point 5. That makes sense.
Re: Loading multiple textures and passing through to a ...
Consider rendering a car with the following textures:
Texture object 1: Font
Texture object 2: Environment map
Texture object 3: Shadow map
Texture object 4: Skydome
Texture object 5: Car colour texture
Texture object 6: Car normal map
When you render chrome bits of car, you might do the following:
- Texture unit 0 - Texture object 2 environment map
- Texture unit 1 - Texture object 3 shadow map
When you render other parts of the car, you might do the following
- Texture unit 0 - texture object 5 car colour texture
- Texture unit 1 - texture object 6 car normal map
- Texture unit 2 - texture object 3 shadow map
And so on. Accidentally, you can use texture unit X for texture object X - for the first few texture objects. But there is quite limited number of texture units, while you can have hundreds of textures: Add 100 to the texture object names 1..6 above and see what it looks like then. Also, 0 is not a texture object name like other texture object names, so trying to bind texture object name 0 to texture unit 0 is not going to work.
A side note: The above example leaves some room for optimization. You should try to bind same texture object to same unit and not keep switching, so binding shadow map always to texture unit 0 for example may help to get a bit better performance.
Re: Loading multiple textures and passing through to a ...
Thank you tksuroan, I see what you mean.
What I had thought was that textureunits0, 1, 2... etc were in 1-1 correspondence with the actual textures used. So I thought that if you had 20 textures, you would have to use 20 textureunits too.
I guess what I don't understand then is how can I use multiple textures in a single active textureunit, to do what you say. I will do some research into how to do this.
Re: Loading multiple textures and passing through to a ...
Multitexturing means that you have more than one texture unit, while each texture unit can be using only one texture object.
You can have multiple textures in one texture unit, but not at the same time. So you can have two draw calls with different textures bound to unit 0. But this is not multitexturing.
Re: Loading multiple textures and passing through to a ...
Makes sense. So if I want a shader that is passed 4 different textures (for example to blend them or something) and use them all IN ONE PASS, then I would have to use multitexturing as in the original example, right?
Re: Loading multiple textures and passing through to a ...
Yes exactly, multitexturing is the action to blend several textures into a final one, and for that you have to use as many textureunits.
However, as I mentioned in my first post, never use the GenTexture's id as parameter for Uniform1 function, this tutorial is misleading !
To my knowledge, there are few reasons to use multiple TextureUnits :
- multitexturing.
- optimization, if you have a texture that's used very-very-very often, you can reserve a given TU for it to prevent rebinding again and again the same texture (but it might be impractical due to the TU count limitation on some hardware, and the speed gain might also be hardly noticeable).
Re: Loading multiple textures and passing through to a ...
Thank you Profet, I've updated the original post with your corrections to remove the errors you brought up. If I may ask one more question, why is it that you have to do:
GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle, UniformName), textureUnit - TextureUnit.Texture0);Why can't you do:
GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle, UniformName), textureUnit);I'm suprised you can't pass a variable of type TextureUnit to the Uniform1 function, and that you have to do a subtraction instead...