Textures

The following pages will describe the concepts of OpenGL Textures, Frame Buffer Objects and Pixel Buffer Objects. These concepts apply equally to OpenGL and OpenGL|ES - differences between the two will be noted in the text or in the example source code.

Loading a texture from disk

Before going into technical details about textures in the graphics pipeline, it is useful to know how to actually load a texture into OpenGL.

A simple way to achieve this is to use the System.Drawing.Bitmap class (MSDN documentation). This class can decode BMP, GIF, EXIG, JPG, PNG and TIFF images into system memory, so the only thing we have to do is send the decoded data to OpenGL. Here is how:

using System.Drawing;
using System.Drawing.Imaging;
using OpenTK.Graphics.OpenGL;
 
static int LoadTexture(string filename)
{
    if (String.IsNullOrEmpty(filename))
        throw new ArgumentException(filename);
 
    int id = GL.GenTexture();
    GL.BindTexture(TextureTarget.Texture2D, id);
 
    // We will not upload mipmaps, so disable mipmapping (otherwise the texture will not appear).
    // We can use GL.GenerateMipmaps() or GL.Ext.GenerateMipmaps() to create
    // mipmaps automatically. In that case, use TextureMinFilter.LinearMipmapLinear to enable them.
    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
 
    Bitmap bmp = new Bitmap(filename);
    BitmapData bmp_data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
 
    GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, bmp_data.Width, bmp_data.Height, 0,
        OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, bmp_data.Scan0);
 
    bmp.UnlockBits(bmp_data);
 
    return id;
}

Now you can bind this texture id to a sampler (with GL.Uniform1) and use it in your shaders. If you are not using shaders, you should enable texturing (with GL.Enable) and bind the texture (GL.BindTexture) prior to rendering.

2D Texture differences

The most commonly used textures are 2-dimensional. There exist 3 kinds of 2D textures:

  1. Texture2D
    Power of two sized (POTS) E.g: 1024²
    These are supported on all OpenGL 1.2 drivers.

    • MipMaps are allowed.
    • All filter modes are allowed.
    • Texture Coordinates are addressed parametrically: [0.0f ... 1.0f]x[0.0f ... 1.0f]
    • All wrap modes are allowed.
    • Borders are supported. (Exception: S3TC Texture Compression does not allow borders)
  2. Texture2D
    Non power of two sized (NPOTS) E.g: 640*480.
    GL.SupportsExtension( "ARB_texture_non_power_of_two" ) must evaluate to true.

    • MipMaps are allowed.
    • All filter modes are allowed.
    • Texture Coordinates are addressed parametrically: [0.0f ... 1.0f]x[0.0f ... 1.0f]
    • All wrap modes are allowed.
    • Borders are supported. (Exception: S3TC Texture Compression does not allow borders)
  3. TextureRectangle
    Arbitrary size. E.g: 640*480.
    GL.SupportsExtension( "ARB_texture_rectangle" ) must evaluate to true.

    • MipMaps are not allowed.
    • Only Nearest and Linear filter modes are allowed. (default is Linear)
    • Texture Coordinates are addressed non-parametrically: [0..width]x[0..height]
    • Only Clamp and ClampToEdge wrap modes are allowed. (default is ClampToEdge)
    • Borders are not supported.

Note that 1 and 2 both use the same tokens. The only difference between them is the size.

BCn Texture Compression

Introduction
A widely available texture compression comes from S3, mostly due to Microsoft licensing it and including it into DirectX 7. It uses the file format .dds (DirectDraw Surface), which is basically a copy of the texture in video memory. Every graphics accelerator compatible with DirectX 7 or higher supports this texture compression.

The BCn Formats are:
BC1 = DXT1 = 8 Bytes per Block, Accuracy: R5G6B5 or R5G5B5A1 (0.5 Byte/Texel)
BC2 = DXT3 = 16 Bytes per Block, Accuracy: R5G6B5A8 (1 Byte/Texel)
BC3 = DXT5 = 16 Bytes per Block, Accuracy: R5G6B5A8 (1 Byte/Texel)
BC4 = Basically this is only the alpha channel from DXT5 without the color channel. CompressedRedRgtc1 (0.5 Byte/Texel)
BC5 = This is 2 channels of BC4, twice the size. CompressedRgRgtc1 (1 Byte/Texel)

BC1, 2 and 3 from EXT_texture_compression_s3tc (DirectX7+ Hardware)
BC4 and 5 from ARB_texture_compression_rtgc. (~DirectX10 Hardware)

The formats DXT2 and DXT4 do exist, but they include pre-multiplied Alpha which is problematic when blending with images with explicit Alpha (RGBA, DXT3/5, etc). That's why those formats have barely been used, and are partially not supported by hardware and export/import tools and they have no BCn number.

Compressed vs. Uncompressed

Texture compression encodes the whole Image into blocks of 4x4 Texel, instead of storing every single Texel of the Image. Thus the ideal compressed Texture dimension is a multiple of 4, like 640x480 or a power of 2, which can be nicely fit into these Blocks. This is the ideal and not a restriction, the specification allows any non-power-of-two dimension, but will internally use a 4x4 Block for a Texture with the size of 2x1 (the other Texels in the Block are undefined).

You probably guessed it already, there is a catch involved when reliably shrinking an image to 25% of it's uncompressed size: A lossy compression technique. This quality loss involved, which can be altered by tweaking the Filter options when compressing the image, is different to the one used in JPG compression. Although both formats - .dds and .jpg - are designed to compress an Image, the S3TC format was developed with graphics hardware in mind.

A bilinear Texture lookup usually reads 2x2 Texels from the Texture and interpolates those 4 Texels to get the final Color. Since a Block consists of 4x4 Texels, there is a good chance that all 4 Texels - which must be examined for the bilinear lookup - are in the same Block. This means that the worst case scenario involves reading 4 Blocks, but usually only 1-2 Blocks are used to achieve the bilinear lookup. When using uncompressed Textures, every bilinear lookup requires reading 4 Texels.

If you do the maths now you will notice that the compressed image actually needs 16 Bytes for 1 Block of RGBA Color, but the uncompressed 4 Texels of RGBA need 16 Bytes too. And yes, if you would only draw a single Pixel on the screen all this would not bring any noticable performance gains, actually it would be slower if multiple Blocks must be read to do the lookup.

However in OpenGL you typically draw more than a single Pixel, at least a Triangle. When the Triangle is rasterized, alot of Pixels will be very close to each other, which means their 2x2 lookup is very likely in the same 4x4 Block used by the last lookup, or a close neighbour. Graphic cards usually support this locality by using a small amount of memory in the chip for a dedicated Texture Cache. If a Cache hit is made, the cost for reading the Texels is very low, compared to reading from Video Memory.

That's why S3TC does decrease render times: the earlier mentioned 16 Bytes of a DXTn Block contain 16 Texels (1 Byte per Texel), while 16 Bytes of uncompressed Texture only contain 4 Texels (4 Bytes per Texel). Alot more data is stored in the 16 Bytes of DXTn, and alot of lookups will be able to use the fast Texture Cache. The game Quake 3 Arena's Framerate increases by ~20% when using compressed Textures, compared to using uncompressed Textures.

Restrictions
Although you might be convinced now that Texture Compression is something worth looking into, do handle it with care. After all, it's a lossy compression Technique which introduces compression Artifacts into the Texture. For Textures that are close to the Viewer this will be noticed, that's why 2D Elements which are drawn very close to the near Plane - like the Mouse Cursor, Fonts or User Interface Elements like the Health display - are usually done with uncompressed Textures, which do not suffer from Artifacts.
As a rule of thumb, do not use Texture Compression where 1 Texel in the Texture will map to 1 Pixel on the Screen.

Using OpenTK.Utilities .dds loader
At the time of writing, the .dds loader included with OpenTK can handle compressed 2D Textures and compressed Cube Maps. Keep in mind that the loader expects a valid OpenGL Context to be present. It will only read the file from disk and upload all MipMap levels to OpenGL. It will NOT set minification/magnification filter or wrapping mode, because it cannot guess how you intent to use it.

void LoadFromDisk( string filename, bool flip, out int texturehandle, out TextureTarget dimension)

Input Parameter: filename
A string used to locate the DDS file on the harddisk, note that escape-sequences like "\n" are NOT stripped from the string.

Input Parameter: flip
The DDS format is designed to be used with DirectX, and that defines GL.TexCoord2(0.0, 0.0) at top-left, while OpenGL uses bottom-left. If you wish to use the default OpenGL Texture Matrix, the Image must be flipped before loading it as Texture into OpenGL.

Output Parameter: texturehandle
If there occured any error while loading, the loader will return "0" in this parameter. If >0 it's a valid Texture that can be used with GL.BindTexture.

Output Parameter: dimension
This parameter is used to identify what was loaded, currently it can return "Invalid", "Texture2D" or "TextureCube".

Example Usage

TextureTarget ImageTextureTarget;
int ImageTextureHandle;
ImageDDS.LoadFromDisk( @"YourTexture.dds", true, out ImageTextureHandle, out ImageTextureTarget );
if ( ImageTextureHandle == 0 || ImageTextureTarget == TextureTarget.Invalid )
   // loading failed
 
// load succeeded, Texture can be used.
GL.BindTexture( ImageTextureTarget, ImageTextureHandle );
GL.TexParameter( ImageTextureTarget, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Linear );
int[] MipMapCount = new int[1];
GL.GetTexParameter( ImageTextureTarget, GetTextureParameter.TextureMaxLevel, out MipMapCount[0] );
if ( MipMapCount == 0 ) // if no MipMaps are present, use linear Filter
  GL.TexParameter( ImageTextureTarget, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.Linear );
else // MipMaps are present, use trilinear Filter
  GL.TexParameter( ImageTextureTarget, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.LinearMipmapLinear );

Remember that you must first GL.Enable the states Texture2D or TextureCube, before using the Texture in drawing.

Useful links:

ATi Compressonator:
http://ati.amd.com/developer/compressonator.html

nVidia's Photoshop Plugin:
http://developer.nvidia.com/object/photoshop_dds_plugins.html

nVidia's GPU-accelerated Texture Tools:
http://developer.nvidia.com/object/texture_tools.html

Detailed comparison of uncompressed vs. compressed Images:
http://www.digit-life.com/articles/reviews3tcfxt1/

OpenGL Extension Specification:
http://www.opengl.org/registry/specs/EXT/texture_compression_s3tc.txt

Microsoft's .dds file format specification (was used to build the OpenTK .dds loader)
http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dx81_c/...

DXT Compression using CUDA
http://developer.download.nvidia.com/compute/cuda/sdk/website/projects/d...

Real-Time YCoCg-DXT Compression
http://news.developer.nvidia.com/2007/10/real-time-ycocg.html

Last Update of the Links: January 2008