ZTK's picture

Setting mipmaps manually and toggling through mipmap filters

Hello everyone,

This is a tutorial on how to manually set mipmaps. With generated mipmaps, it can be hard to see the difference between the various filter options. In this tutorial, we will be creating a checkerboard texture, at various dimensions. There will be a 16x16 blue on white checkerboard to start with. Then we will make an 8x8 red on white board. Then 4x4 yellow on white, 2x2 green on white, and finally a 1x1 purple pixel. This will make it very easy to see when we render a new mipmap, as the color difference will be obvious.

These blogs unfortunately don't allow for uploads of bitmaps, but I just created the textures in visual studio using the built in bitmap editor (screenshot below). See the code snippet below for how I named them and where I put them.

We will first load up bitmaps for our images:

        Bitmap bitmap_blue_white_16_16 = new Bitmap(@"..\..\img\bwboard.bmp");
        Bitmap bitmap_red_white_8_8 = new Bitmap(@"..\..\img\rwboard.bmp");
        Bitmap bitmap_yellow_white_4_4 = new Bitmap(@"..\..\img\ywboard.bmp");
        Bitmap bitmap_green_white_2_2 = new Bitmap(@"..\..\img\gwboard.bmp");
        Bitmap bitmap_purple_white_1_1 = new Bitmap(@"..\..\img\pwboard.bmp");

Then let's generate our texture, bind it, and assign to a uniform to our basic shader:

            GL.ActiveTexture(TextureUnit.Texture1);
            GL.GenTextures(1, out texture);
            GL.BindTexture(TextureTarget.Texture2D, texture);
            GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle, "MyTexture"), texture);

Now the fun part: Let's assign mipmaps at various levels to it. Note that if you skip any level, the whole thing renders as black!

            AssignMipMap(bitmap_blue_white_16_16, 0);
            AssignMipMap(bitmap_red_white_8_8, 1);
            AssignMipMap(bitmap_yellow_white_4_4, 2);
            AssignMipMap(bitmap_green_white_2_2, 3);
            AssignMipMap(bitmap_purple_white_1_1, 4);

Here's how the AssignMipMap function works:

        private void AssignMipMap(Bitmap bitmap, int level)
        {
            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, level, PixelInternalFormat.Rgba, data.Width, data.Height, 0,
            OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);
 
            bitmap.UnlockBits(data);
        }

In addition to our standard code for walking around, let's let the user switch the filter on the fly so he can see how the different filters look:

            // switch mipmap filter here
            if (Keyboard[Key.N])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
            else if (Keyboard[Key.L])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
            else if (Keyboard[Key.Number1])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.NearestMipmapNearest);
            else if (Keyboard[Key.Number2])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.NearestMipmapLinear);
            else if (Keyboard[Key.Number3])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapNearest);
            else if (Keyboard[Key.Number4])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);

This gives a nice visual distinction between the four kinds of filters (six if you don't want to use mipmaps).

Here is the entire code sample:

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 MipMapExplorer
{
 
    class Game : GameWindow
    {
        private Matrix4 modelviewMatrix, projectionMatrix, translationMatrix;
 
        int fragmentShaderHandle, shaderProgramHandle, texture;
 
        Bitmap bitmap_blue_white_16_16 = new Bitmap(@"..\..\img\bwboard.bmp");
        Bitmap bitmap_red_white_8_8 = new Bitmap(@"..\..\img\rwboard.bmp");
        Bitmap bitmap_yellow_white_4_4 = new Bitmap(@"..\..\img\ywboard.bmp");
        Bitmap bitmap_green_white_2_2 = new Bitmap(@"..\..\img\gwboard.bmp");
        Bitmap bitmap_purple_white_1_1 = new Bitmap(@"..\..\img\pwboard.bmp");
 
        string fragmentShaderSource = @"
#version 130
uniform sampler2D MyTexture;
 
void main(void) {
// draw the texture
gl_FragColor = texture2D( MyTexture, 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, "Mipmap explorer")
        {
            VSync = VSyncMode.On;
        }
 
        /// <summary>Load resources here.</summary>
        /// <param name="e">Not used.</param>
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            CreateShaders();
 
            // set a dark base color
            GL.ClearColor(0.4f, 0.4f, 0.36f, 0.0f);
 
            GL.Enable(EnableCap.DepthTest);
            GL.Enable(EnableCap.Texture2D);
            GL.Hint(HintTarget.PerspectiveCorrectionHint, HintMode.Nicest);
 
            GL.ActiveTexture(TextureUnit.Texture1);
            GL.GenTextures(1, out texture);
            GL.BindTexture(TextureTarget.Texture2D, texture);
            GL.Uniform1(GL.GetUniformLocation(shaderProgramHandle, "MyTexture"), texture);
 
            AssignMipMap(bitmap_blue_white_16_16, 0);
            AssignMipMap(bitmap_red_white_8_8, 1);
            AssignMipMap(bitmap_yellow_white_4_4, 2);
            AssignMipMap(bitmap_green_white_2_2, 3);
            AssignMipMap(bitmap_purple_white_1_1, 4);
 
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.NearestMipmapNearest);
 
            // set the camera location from which we will look at the model
            modelviewMatrix = Matrix4.LookAt(new Vector3(0, 2, 7), new Vector3(0, 2, 0), new Vector3(0, 1, 0));
 
            Console.WriteLine("Arrow keys to move around");
            Console.WriteLine("N: GL_NEAREST");
            Console.WriteLine("L: GL_LINEAR");
            Console.WriteLine("1: NEAREST_MIPMAP_NEAREST");
            Console.WriteLine("2: NEAREST_MIPMAP_LINEAR");
            Console.WriteLine("3: LINEAR_MIPMAP_NEAREST");
            Console.WriteLine("4: LINEAR_MIPMAP_LINEAR");
        }
 
        private void AssignMipMap(Bitmap bitmap, int level)
        {
            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, level, PixelInternalFormat.Rgba, data.Width, data.Height, 0,
            OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);
 
            bitmap.UnlockBits(data);
        }
 
        /// <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)
        {
            float viewZ, rotZ;
            base.OnUpdateFrame(e);
            if (Keyboard[Key.Escape])
                Exit();
            if (Keyboard[Key.Up])
            {
                viewZ = 0.1f;
                translationMatrix = Matrix4.CreateTranslation(0f, 0f, viewZ);
                modelviewMatrix = modelviewMatrix * translationMatrix;
            }
            if (Keyboard[Key.Down])
            {
                viewZ = -0.1f;
                translationMatrix = Matrix4.CreateTranslation(0f, 0f, viewZ);
                modelviewMatrix = modelviewMatrix * translationMatrix;
            }
            if (Keyboard[Key.Left])
            {
                rotZ = -0.02f;
                translationMatrix = Matrix4.CreateRotationY(rotZ);
                modelviewMatrix = modelviewMatrix * translationMatrix;
            }
            if (Keyboard[Key.Right])
            {
                rotZ = 0.02f;
                translationMatrix = Matrix4.CreateRotationY(rotZ);
                modelviewMatrix = modelviewMatrix * translationMatrix;
            }
 
            // switch mipmap filter here
            if (Keyboard[Key.N])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
            else if (Keyboard[Key.L])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
            else if (Keyboard[Key.Number1])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.NearestMipmapNearest);
            else if (Keyboard[Key.Number2])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.NearestMipmapLinear);
            else if (Keyboard[Key.Number3])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapNearest);
            else if (Keyboard[Key.Number4])
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
        }
 
        /// <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);
 
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadMatrix(ref modelviewMatrix);
            DrawFloor();
 
            SwapBuffers();
        }
 
        private void DrawFloor()
        {
            GL.BindTexture(TextureTarget.Texture2D, texture);
            GL.Begin(BeginMode.Quads);
 
            for (int z = -20; z <= 20; z = z + 2)
            {
                for (int x = -20; x <= 20; x = x + 2)
                {
                    GL.TexCoord2(0.0f, 1.0f); GL.Vertex3(-1.0f + x, 0.0f, -1.0f + z);
                    GL.TexCoord2(1.0f, 1.0f); GL.Vertex3(1.0f + x, 0.0f, -1.0f + z);
                    GL.TexCoord2(1.0f, 0.0f); GL.Vertex3(1.0f + x, 0.0f, 1.0f + z);
                    GL.TexCoord2(0.0f, 0.0f); GL.Vertex3(-1.0f + x, 0.0f, 1.0f + z);
                }
            }
            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);
            }
        }
    }
}
Images

Comments

Comment viewing options

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

Very useful information, thank you for sharing! This could allow people dealing with custom file formats to load embedded mipmaps and possibly supply better quality textures that way. Nice work!