ZTK's picture

Loading multiple textures and passing through to a shader

Hello 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!

Images

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
Profet's picture

If you cast the TextureUnit #0 to an integer, you'll notice that you don't get 0 but something like 33984.

From OpenTK sources:

public enum TextureUnit : int
{
        Texture0 = ((int)0x84C0),
        Texture1 = ((int)0x84C1),
        Texture2 = ((int)0x84C2),
        Texture3 = ((int)0x84C3),
        Texture4 = ((int)0x84C4),
        Texture5 = ((int)0x84C5),
        Texture6 = ((int)0x84C6),
        Texture7 = ((int)0x84C7),
        Texture8 = ((int)0x84C8),
        Texture9 = ((int)0x84C9),
        Texture10 = ((int)0x84CA),
        Texture11 = ((int)0x84CB),
        Texture12 = ((int)0x84CC),
        Texture13 = ((int)0x84CD),
        Texture14 = ((int)0x84CE),
        Texture15 = ((int)0x84CF),
        Texture16 = ((int)0x84D0),
        Texture17 = ((int)0x84D1),
        Texture18 = ((int)0x84D2),
        Texture19 = ((int)0x84D3),
        Texture20 = ((int)0x84D4),
        Texture21 = ((int)0x84D5),
        Texture22 = ((int)0x84D6),
        Texture23 = ((int)0x84D7),
        Texture24 = ((int)0x84D8),
        Texture25 = ((int)0x84D9),
        Texture26 = ((int)0x84DA),
        Texture27 = ((int)0x84DB),
        Texture28 = ((int)0x84DC),
        Texture29 = ((int)0x84DD),
        Texture30 = ((int)0x84DE),
        Texture31 = ((int)0x84DF),
}

And 0x84C0 makes in decimal.... 33984 ! The following values are consecutive so we can subtract Texture0 from the current textureunit to get it's id :)

I don't know why this enum doesn't start with zero, but if you look into OpenTK.Graphics.ES11 you'll find the textureunit in a global enum (public enum All : int), but my knowledge into OpenGL isn't deep enough to tell if that comes from an early implementation of opengl.

Profet's picture

double post.. sorry :)

puklaus's picture

[removed]