Texture
A texture is an OpenGL Object that contains one or more images that all have the same image format. A texture can be used in two ways. It can be the source of a texture access from a Shader, or it can be used as a render target.
Theory
For the purpose of this discussion, an image is defined as a single array of pixels of a certain dimensionality (1D, 2D, or 3D), with a particular size, and in a specific format.
A texture is a container of one or more images. But textures do not store arbitrary images; a texture has specific constraints on the images it can contain. There are three defining characteristics of a texture, each of the defining part of those constraints: the texture type, texture size, and the image format used for images in the texture. The texture type defines the arrangement of images within the texture. The size defines the size of the images in the texture. And the image format defines the format that all of these images share.
There are a number of different types of textures. These are:
- GL_TEXTURE_1D: Images in this texture all are 1-dimensional. They have width, but no height or depth.
- GL_TEXTURE_2D: Images in this texture all are 2-dimensional. They have width and height, but no depth.
- GL_TEXTURE_3D: Images in this texture all are 3-dimensional. They have width, height, and depth.
- GL_TEXTURE_RECTANGLE: The image in this texture (only one image. No mipmapping) is 2-dimensional. Texture coordinates used for these textures are not normalized.
- GL_TEXTURE_BUFFER: The image in this texture (only one image. No mipmapping) is 1-dimensional. The storage for this data comes from a Buffer Object.
- GL_TEXTURE_CUBE_MAP: There are exactly 6 distinct sets of 2D images, all of the same size. They act as 6 faces of a cube.
- GL_TEXTURE_1D_ARRAY: Images in this texture all are 1-dimensional. However, it contains multiple sets of 1-dimensional images, all within one texture. The array length is part of the texture's size.
- GL_TEXTURE_2D_ARRAY Array: Images in this texture all are 2-dimensional. However, it contains multiple sets of 2-dimensional images, all within one texture. The array length is part of the texture's size.
- GL_TEXTURE_2D_MULTISAMPLE: The image in this texture (only one image. No mipmapping) is 2-dimensional. Each pixel in these images contains multiple samples instead of just one value.
- GL_TEXTURE_2D_MULTISAMPLE_ARRAY: Combines 2D array and 2D multisample types. No mipmapping.
Texture sizes have a limit based on the GL implementation. For 1D and 2D textures (and any texture types that use similar dimensionality, like cubemaps) the max size of either dimension is GL_MAX_TEXTURE_SIZE. For array textures, the maximum array length is GL_MAX_ARRAY_TEXTURE_LAYERS. For 3D textures, no dimension can be greater than GL_MAX_3D_TEXTURE_SIZE in size.
Within these limits, the size of a texture can be any value. It is advised however, that you stick to powers-of-two for texture sizes, unless you have a significant need to use arbitrary sizes.
Mip maps
When a texture is directly applied to a surface, how many pixels of that texture (commonly called "texels") are used depends on the angle at which that surface is rendered. A texture mapped to a plane that is almost edge-on with the camera will only use a fraction of the pixels of the texture. Similarly, looking directly down on the texture from far away will show fewer texels than an up-close version.
The problem is with animation. When you slowly zoom out on a texture, you start to see aliasing artifacts appear. These are caused by sampling fewer than all of the texels; the choice of which texels are sampled changes between different frames of the animation. Even with linear filtering (see below), artifacts will appear as the camera zooms out.
To solve this problem, we employ mip maps. These are pre-shrunk versions of the full-sized image. Each mipmap is half the size of the previous one in the chain, using the largest dimension of the image . So a 64x16 2D texture can have 5 mip-maps: 32x8, 16x4, 8x2, 4x1, 2x1, and 1x1. OpenGL does not require that the entire mipmap chain is complete; you can specify what range of mipmaps in a texture are available.
Some texture types have multiple independent sets of mipmaps. Each face of a cubemap has its own set of mipmaps, as does each entry in an array texture. However, the texture as a whole only has one setting for which mipmaps are present. So if the texture is set up such that only the top 4 levels of mipmaps present, you must have them for all mipmap chains in the texture.
When sampling a texture (see below), the implementation will automatically select which mipmap to use based on the viewing angle, size of texture, and various other factors.
When using texture sizes that are not powers of two, the half-size of lower mipmaps is rounded down. So a 63x63 texture has as its next lowest mipmap level 31x31. And so on.
The base level of a mipmap chain is the largest one. It is also the one that defines the full size of the texture. OpenGL numbers this mipmap level as 0; the next largest mipmap level is 1, and so on.
The base level of a texture does not have to be loaded. As long as you specify the range of mipmaps correctly, you can leave out any mipmap levels you want.
Texture Objects
Textures in OpenGL are OpenGL Objects, and they follow the standard conventions of such. So they have the standard glGenTextures
, glBindTexture
, as you would expect.
The target parameter of glBindTexture
corresponds to the texture's type. So when you use a freshly generated texture name, the first bind helps define the type of the texture. It is not legal to bind an object to a different target than the one it was previously bound with. So if you generate a texture and bind it as GL_TEXTURE_1D, then you should continue to bind it as such.
As with any other kind of OpenGL object, it is legal to bind multiple objects to different targets. So you can have a GL_TEXTURE_1D bound while a GL_TEXTURE_2D_ARRAY is bound.
Binding a fresh object is all that is needed to give it a type. But to give it a size and format, we must call one of the glTexImage*
functions. This will allocate storage for a particular mipmap level of the texture. To allocate all of the mipmap levels of a texture, you can call the appropriate function in a loop, from base level 0 to the the maximum mipmap level (defined by the largest dimension of the texture).
These functions are defined as follows:
void glTexImage1D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, void *data ); void glTexImage2D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, void *data ); void glTexImage3D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, void *data ); void glTexImage2DMultisample( GLenum target, GLsizei samples, GLint internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations ); void glTexImage3DMultisample( GLenum target, GLsizei samples, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations );
The level parameter defines what mipmap level is being allocated in this function call. The width, height, and depth are specific to this particular mipmap level. For 1D array textures, the height means "number of entries in the array". The same goes for 2D array textures and the depth value.
The target value specifies what target will have this operation performed on it. Certain targets are not allowed under certain functions. glTexImage3D
can only be used with GL_TEXTURE_3D and GL_TEXTURE_2D_ARRAY targets. glTexImage2D
can only be used with GL_TEXTURE_2D, GL_TEXTURE_1D_ARRAY, GL_TEXTURE_RECTANGLE, and the 6 GL_TEXTURE_CUBE_MAP* faces (of the form GL_TEXTURE_CUBE_MAP_POSITIVE/NEGATIVE_X/Y/Z). glTexImage1D
can only be used with GL_TEXTURE_1D.
The two multisample glTexImage*
functions can only be used with GL_TEXTURE_2D_MULTISAMPLE, and GL_TEXTURE_2D_MULTISAMPLE_ARRAY, with the array version for the 3D version. These functions set the sample count in addition to the internal format and size of the texture.
The internalformat value is the format of the texture. Take care with this value when allocating mipmaps: this parameter must be the same for each of the subsequent calls.
The border parameter is for old, deprecated functionality. Any value other than 0 is an error.
The format, type, and data parameters are used for performing a Pixel Transfer operation. This allows one to create a texture and fill it with some data in one call. However, since you can only give it data for one mipmap, this is not as useful as expected. As with any pixel transfer operation, Pixel Buffer Objects can be used to feed OpenGL the data.
You do not need to fill in the texture's data in the same call that you create it in. If data is NULL, no pixel transfer will be done, and the texture's data is undefined. You should make sure to fill in the texture's data at some point in the future.
The multisample versions of these functions do not offer pixel transfer. This is because the image data of multisample textures cannot be updated from client data. It can only be filled in as a render target, and it can only be sourced as a texture from GLSL.
Texture Parameters
Texture objects have parameters. These parameters control many aspects of how the texture functions.
Texture parameters are set with the following functions:
void glTexParameter[if]( GLenum target, GLenum pname, T param); void glTexParameter[if]v( GLenum target, GLenum pname, T *params ); void glTexParameterI[i ui]v( GLenum target, GLenum pname, T *params );
These function set the parameter values param or params for the particular texture parameter pname in the texture bound to target.
Mipmap range
The texture parameters GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL (integer values) define the closed mipmap range of mipmaps that are to be considered available with this texture. Nothing can cause the sampling of mipmaps smaller than GL_TEXTURE_BASE_LEVEL and nothing can cause the sampling of mipmaps greater than GL_TEXTURE_MAX_LEVEL. This even filters into GLSL; the texture size functions will retrieve the size of GL_TEXTURE_BASE_LEVEL, rather than the size of mipmap level 0.
There is another pair of texture parameters, GL_TEXTURE_MAX_LOD and GL_TEXTURE_MIN_LOD (floating-point values). These do something similar: they clamp the value that computes which mipmap should be sampled from in a texture. However it does not cover everything; that is not the purpose of these parameters. So if your intent is to guarantee that you can never sample any other mipmap levels, what you want are the BASE_LEVEL and MAX_LEVEL, not the LODs here.
Remember: these parameters work for the entire texture. So all cubemap faces must have the same mipmaps defined, as must all array textures.
Automatic mipmap generation
It is often useful to auto-generate a mipmap set from just the base mipmap level in the previously defined range. How exactly the implementation does filtering for mipmap generation is implementation-dependent. You do not have to call the glTexImage*
functions to allocate storage for the mipmaps to be generated; OpenGL will handle that detail for you.
Before you can generate mipmaps, you must set the base mipmap level as above. And you must actually put data in that level. If you are generating mipmaps for cubemaps, you must have put data into all 6 faces of the cubemap.
Then, call this function:
void glGenerateMipmap( GLenum target );
This will cause the texture bound to target to have its mipmap levels below the base level auto-generated. The base level and max level range is observed; the base level will not change, and all levels below it will be generated, down to the max level. Any mipmap levels defined outside of the base/max range will not be changed.
Texture image modification
It is often useful to modify a texture after it is specified. If you wish to pass it data from the CPU, calling the glTexImage*
is a very heavyweight operation. If you only want to update a subsection of the texture, those functions will not help.
These functions are used to modify image data without reallocating it.
void glTexSubImage1D( GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, void *data ); void glTexSubImage2D( GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, void *data ); void glTexSubImage3D( GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, void *data );
These functions update a specific region of the given texture and mipmap level with new data. The xoffset, yoffset, zoffset, width, height, and depth define the region to be updated. The format, type, and data functions operate as for any Pixel Transfer operation.
Compressed textures
Standard Pixel Transfer operations can convert pixel data into compressed formats for you. These algorithms tend to be optimized for speed rather than image quality. Therefore, it is often desirable to pre-compress image data offline and upload it as compressed image data. A normal pixel transfer operation cannot handle this.
To perform such an operation, you must use the following functions:
void CompressedTexImage1D( GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border, GLsizei imageSize, void *data ); void CompressedTexImage2D( GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, void *data ); void CompressedTexImage3D( GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLsizei imageSize, void *data ); void CompressedTexSubImage1D( GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLsizei imageSize, void *data ); void CompressedTexSubImage2D( GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, void *data ); void CompressedTexSubImage3D( GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, void *data );
The glCompressedTexImage
functions work very much like the glTexImage
functions. They respecify a mipmap layer completely. The glCompressedTexSubImage
functions work like the glTexSubImage
, updating only the image data.
The target, level, xoffset, yoffset, zoffset, width, height, depth, and border fields function exactly as they did in the earlier functions. imageSize is the length of the byte array data. Since the format of the compressed image data must match the specific format of the texture, the implementation can deduce what this data means from the pointer and size alone.
The internalformat parameter must refer to a specific compressed format. The generic formats cannot be used here. And it is an error to call any of the glCompressedTexSubImage
formats on a texture that doesn't use a non-generic compressed format.
Though this is not technically a pixel transfer operations, a buffer bound to GL_UNPACK_BUFFER can still be used in place of a client memory pointer.
Texture image units
Binding textures in OpenGL is a little weird. There are two reasons to bind a texture object to the context: to change values in the object and to render something with it.
Changing the texture's stored state can be done with the above simple glBindTexture
call. However, actually rendering with a texture is a bit more complicated.
A texture can be bound to one or more locations. These locations are called texture image units. OpenGL contexts have a maximum number of texture image units, queriable from the constant GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS.
What image unit a glBindTexture
call binds the texture to depends on the current active texture image unit. This value is set by calling:
void glActiveTexture( GLenum texture );
The value of texture is GL_TEXTURE0 + i, where i is a number on the half-open range [0, GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS). This will cause the texture image unit i to be the current active image unit.
Each texture image unit supports bindings to all targets. So a 2D texture and an array texture can be bound to the same image unit, or different 2D texture can be bound in two different image units without affecting each other. So which texture gets used when rendering? In GLSL, this depends on the type of sampler that uses this texture image unit.
The glActiveTexture
function will affect the functioning of any function that takes a texture target as a parameter.
Texture sampling
Sampling is the process of fetching a value from a texture at a given position. GLSL controls much of the process of sampling, but there are many texture parameters that affect this as well.
Normalized texture coordinates
Locations in a texture are usually abstracted via the use of normalized texture coordinates. These are floating-point values where 0 means one edge of the texture and 1 means the opposite edge of the texture. This abstracts away the size of the texture, allowing different textures with different sizes to be used.
Filtering
Filtering is the process of accessing a particular sample from a texture. There are two cases for filtering: minification and magnification. Magnification means that the area of the fragment in texture space is smaller than a texel, and minification means that the area of the fragment in texture space is larger than a texel. Filtering for these two cases can be set independently.
The magnification filter is controlled by the GL_TEXTURE_MAG_FILTER texture parameter. This value can be GL_LINEAR or GL_NEAREST. If GL_NEAREST is used, then the implementation will select the texel nearest the texture coordinate; this is commonly called "point sampling"). If GL_LINEAR is used, the implementation will perform a weighted linear blend between the nearest adjacent samples.
The minification filter is controlled by the GL_TEXTURE_MIN_FILTER texture parameter. To understand these values better, it is important to discuss what the particular options are.
When doing minification, you can choose to use mipmapping or not. Using mipmapping means selecting between multiple mipmaps based on the angle and size of the texture relative to the screen. Whether you use mipmapping or not, you can still select between linear blending of the particular layer or nearest. And if you do use mipmapping, you can choose to either select a single mipmap to sample from, or you can sample the two adjacent mipmaps and linearly blend the resulting values to get the final result.
The OpenGL minification settings for these are as follows:
Param Setting | Linear within mip-level | Has mipmapping | Linear between mip-levels |
---|---|---|---|
GL_NEAREST | No | No | |
GL_LINEAR | Yes | No | |
GL_NEAREST_MIPMAP_NEAREST | No | Yes | No |
GL_LINEAR_MIPMAP_NEAREST | Yes | Yes | No |
GL_NEAREST_MIPMAP_LINEAR | No | Yes | Yes |
GL_LINEAR_MIPMAP_LINEAR | Yes | Yes | Yes |
A note on terminology. This discussion has refrained from using the common filtering terms "bilinear" and "trilinear." This is for a good reason; these terms are often misunderstood and do not carry over to all texture types.
Take the term "bilinear". This term is used because it refers to linear filtering in 2 axes: horizontally and vertically in a 2D texture. A monolinear would be filtering in one axis, and thus trilinear is filtering in 3 axes.
The problem is that what constitutes "bilinear" depends on the texture type. Or specifically, its dimensionality. Setting GL_TEXTURE_MAG_FILTER and MIN_FILTERs to GL_LINEAR will create monolinear filtering in a 1D texture, bilinear filtering in a 2D texture, and trilinear in a 3D texture. In all cases, it is simply doing a linear filter between the nearest samples; some texture types simply have more nearest samples than others.
Unfortunately, what most people think of as "trilinear" is not linear filtering of a 3D texture, but what in OpenGL terms is GL_LINEAR mag filter and GL_LINEAR_MIPMAP_LINEAR in the min filter in a 2D texture. That is, it is bilinear filtering of each appropriate mipmap level, and doing a third linear filter between the adjacent mipmap levels. Hence the term "trilinear".
This is easily confused with what is just GL_LINEAR for 3D textures. That is why OpenGL and this discussion does not use these terms.
Anisotropic filtering
Anisotropic filtering is an advanced filtering technique that takes more than one sample point and blends them together. Exactly how this is done is implementation-dependent, but the control is a specific value: the maximum number of samples that can be taken of the texture. More samples may slow down performance, but increase image quality. Then again, it may not, depending on the angle you're looking at the surface. Implementations only take extra samples when needed.
To use anisotropic filtering, set the GL_TEXTURE_MAX_ANISOTROPY_EXT parameter. This parameter is floating-point, and can be set between 1.0f and an implementation-defined maximum anisotropy (queried with GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT). Any value greater than 1.0f counts as a use of anisotropic filtering.
Anisotropic filtering is not a replacement for mipmaps or mipmap filtering. For best results, combine a anisotropic filtering with a GL_LINEAR_MIPMAP_LINEAR minification filter.
Comparison mode
Depth textures (textures that have a depth component image format), can be sampled in one of two ways. They can be sampled as a normal texture, which simply retrieves the depth value (with filtering applied). Or the texture can be accessed in comparison mode.
Comparison mode means that sampling from the texture requires a value to compare to those pulled from the texture; this value is called the reference value. The result of the comparison depends on the comparison function set in the texture. If the function succeeds, the resulting value is 1.0f; if it fails, it is 0.0f.
When linear filtering is used, the actual returned value is implementation-defined. However, the value will be on the range [0, 1] and will be proportional to the number of neighboring texels that pass the comparison based on the single given value.
If the texture is a normalized integer depth format, then the reference value is clamped to [0, 1], to match the values from the texture. Otherwise, the value is not clamped.
Using this mode requires two special settings. First, the sampler used in GLSL must be a shadow sampler. Second, the texture used in that sampler must have activated depth comparison mode. Attempting to use a texture without comparison with a shadow sampler, or vice-versa, will result in an error upon rendering.
To set the texture to comparison mode, set the GL_TEXTURE_COMPARE_MODE texture parameter to GL_COMPARE_REF_TO_TEXTURE with glTexParameteri. The comparison function to use when comparing the reference to the texture is set with the GL_TEXTURE_COMPARE_FUNC texture parameter. Acceptable values are GL_NEVER (always fails), GL_ALWAYS (always succeeds), GL_LESS, GL_LEQUAL, GL_EQUAL, GL_NOT_EQUAL, GL_GEQUAL, and GL_GREATER. The comparison works as follows:
ref OPERATOR texture
So GL_LESS will be true if the reference value is strictly less than the value pulled from the texture.
Edge value sampling
Normalized texture coordinates are not limited to values between 0.0 and 1.0. They can be any floating-point number. When a texture coordinate is not within the [0, 1] range, a heuristic must be employed to decide what the color value will be.
Each dimension of a texture can have a different heuristic. These are set by setting the texture parameters GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T, and GL_TEXTURE_WRAP_R, where S, T, and R are the first 3 texture coordinates in order. The possible heuristics are:
- GL_REPEAT: the texture coordinate wraps around the texture. So a texture coordinate of -0.2 becomes the equivalent of 0.8.
- GL_MIRRORED_REPEAT: the texture coordinate wraps around like a mirror. -0.2 becomes 0.2, -1.2 becomes 0.8, etc.
- GL_CLAMP_TO_EDGE: the texture coordinate is clamped to the [0, 1] range.
- GL_CLAMP_TO_BORDER: the texture coordinate is clamped to the [0, 1] range, but the edge texels are blended with a constant border color.
GLSL binding
Programs are one of the two users of textures. In order to use textures with a program, the program itself must use certain syntax to expose texture binding points.
Samplers
A sampler in GLSL is a uniform variable that represents an accessible texture. It cannot be set from within a program; it can only be set by the user of the program. Sampler types correspond to OpenGL texture types.
Samplers are used with GLSL texture access functions.
Programs and textures
The process of using textures with a program involves 2 halves. Texture objects are not directly associated with or attached to programs. Instead, program samplers reference texture image unit indices. And whatever textures are bound to those image units at the time of rendering are used by the program.
So the first step is to set the uniform value for the program samplers. For each sampler uniform, set its uniform value to an integer on the range [0, GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS). When the time comes to use the program directly, simply use glActiveTexture
and glBindTexture
to bind the textures of interest to these image units.
The textures bound to the image unit set in the sampler uniforms must match the sampler's type. So a sampler1D
will look to the GL_TEXTURE_1D binding in the image unit it is set in.
Render targets
Through the use of a framebuffer object, individual images within a texture can be the destination for rendering.