Sampler (GLSL)

From OpenGL.org
Revision as of 12:46, 17 May 2011 by Zyx 2000 (Talk | contribs) (Non-uniform flow control)

Jump to: navigation, search

A sampler is a set of GLSL variable types. Variables of one of the sampler types must be uniforms or as function parameters. Each sampler in a program represents a single texture of a particular texture type. The type of the sampler corresponds to the type of the texture that can be used by that sampler.

Sampler types

There are a number of sampler types. The various sampler types are separated into 3 categories, based on the basic data type of the Image Format of the texture that they sample from. These are floating-point, signed integer, and unsigned integer. Floating-point also covers normalized integer formats.

The name of the sampler type in GLSL reflects this grouping. The names are very similar to the names of the vector types in GLSL. Floating-point vectors do not have a prefix; they are just "vec". Signed integer vectors are "ivec", and unsigned integer vectors are "uvec". So for samplers, floating-point samplers begin with "sampler". Signed integer samplers begin with "isampler", and unsigned integer samplers begin with "usampler".

For the sake of clarity, when you see a g preceding "sampler" in a sampler name, it represents any of the 3 possible prefixes (nothing for float, i for signed integer, and u for unsigned integer).

The rest of the sampler's name refers to the texture type that it is a sampler of. The names map as follows:

  • gsampler1D: GL_TEXTURE_1D
  • gsampler2D: GL_TEXTURE_2D
  • gsampler3D: GL_TEXTURE_3D
  • gsamplerCube: GL_TEXTURE_CUBE_MAP
  • gsampler2DRect: GL_TEXTURE_RECTANGLE
  • gsampler1DArray: GL_TEXTURE_1D_ARRAY
  • gsampler2DArray: GL_TEXTURE_2D_ARRAY
  • gsamplerCubeArray: GL_TEXTURE_CUBE_MAP_ARRAY
  • gsamplerBuffer: GL_TEXTURE_BUFFER
  • gsampler2DMS: GL_TEXTURE_2D_MULTISAMPLE
  • gsampler2DMSArray: GL_TEXTURE_2D_MULTISAMPLE_ARRAY

Shadow samplers

If a texture is a depth texture and has the depth comparison activated, it cannot be used with a normal sampler. Attempting to do so is an error. Such textures must be used with a shadow sampler. This type changes the texture lookup functions (see below) to increase the vector size of the texture coordinate by one. This extra value is used to compare against the value sampled from the texture.

Because cubemap arrays take 4D texture coordinates, the texture lookup functions take an additional parameter, instead of expanding the vector texture coordinate size.

The result of accessing a shadow texture is always a single float value. This value is on the range [0, 1], which is proportional to the number of samples in the shadow texture that pass the comparison. Therefore, if the resulting value is 0.25, then only 1 out of every 4 values in the shadow comparison passed.

Notice that none of these types have the g prefix. This is because shadow samplers can only be used with textures with depth components. And those are all floating-point (or normalized integer) image formats.

  • sampler1DShadow: GL_TEXTURE_1D
  • sampler2DShadow: GL_TEXTURE_2D
  • samplerCubeShadow: GL_TEXTURE_CUBE_MAP
  • sampler2DRectShadow: GL_TEXTURE_RECTANGLE
  • sampler1DArrayShadow: GL_TEXTURE_1D_ARRAY
  • sampler2DArrayShadow: GL_TEXTURE_2D_ARRAY
  • samplerCubeArrayShadow: GL_TEXTURE_CUBE_MAP_ARRAY

Language Definition

A variable of sampler can only be defined in one of two ways. It can be defined as a function parameter or as a uniform variable.

uniform sampler2D texture1;
 
void Function(in sampler2D myTexture);

Samplers do not have a value. They can not be set by expressions and, with one exception, they cannot be a part of any expression. While they can be function parameters, you can only use them with parameters defined as "in".

Texture lookup functions

The only place where you can use a sampler is in one of the GLSL standard library's texture lookup functions. These functions access the texture referred to by the sampler. They take a texture coordinate as parameters.

There are several kinds of texture functions. Some texture functions do not take certain kinds of samplers. Some of them are not available on all shader stages.

The use of g has the same meaning as previously. Types in bold face represent a range of types, as explained in the description beneath the function.

Texture functions through rectangle samplers always take texture coordinates in texel space. Unless otherwise specified, all other texture coordinates are normalized.

Texture size retrieval

The size of a texture can be retrieved by calling this function:

 ivec textureSize(gsampler sampler, int lod);

This function retrieves the size of the given LOD of the texture bound to sampler. The sampler can be of any type. For sampler types that refer to textures without mipmaps (eg: samplerRect), no lod value is needed.

The return value, ivec, depends on the type of sampler. For images that are 1-dimensional, the function returns an int. For 2D images, the function returns ivec2, and for 3D ivec3. The array types return one extra coordinate (eg: 1D arrays return ivec2), with the last coordinate being the number of elements in the array of images.

Basic texture access

To sample the texture with normalized texture coordinates, this function is used:

 gvec texture(gsampler sampler, vec texCoord[, float bias]);

This samples the texture given by sampler, at the location texCoord, with an optional LOD bias value of bias. For sampler types that cannot have LODs, the bias parameter is removed.

Sampling from shadow samplers return a "float", representing the result of the comparison. Sampling from other kinds of samplers returns a gvec4, matching the type of gsampler. This function does not work with multisample or buffer samplers.

The size of the vec type of texCoord depends on the dimensionality of sampler. A 1D sampler takes a "float", 2D samplers take "vec2", etc. Array samplers add one additional coordinate for the array level to sample. Shadow samplers add one additional coordinate for the sampler comparison value. Array and shadow samplers add two coordinates: the array level followed by the comparison value. So vec when used with a sampler1DArrayShadow is a "vec3".

Offset texture access

You can add a texel offset to texture coordinates sampled with texture functions. This is useful for sampling from a collection of images all on one texture. This is done with this function:

 gvec textureOffset(gsampler sampler, vec texCoord, ivec offset[, float bias]);

The texCoord will have its value offset by offset texels before performing the lookup. Cubemap, multisample, and buffer samplers are not allowed as types for gsampler.

The type ivec has the same type, but in integers, as vec. So when accessing a 2D sampler, vec is "vec2" and ivec is "ivec2". When accessing a 1D sampler, vec is "float" and ivec is "int".

The value of offset must be a constant expression. That is, it must be a compile-time constant.

There are minimum and maximum values for the coordinates of offset. These are queried through GL_MIN_PROGRAM_TEXEL_OFFSET and GL_MAX_PROGRAM_TEXEL_OFFSET.

Lod texture access

If you want to prevent mipmapping altogether and just access from a single mipmap layer, you may use this function:

 gvec textureLod(gsampler sampler, vec texCoord, float lod);

This will sample the texture at the mip level lod. 0 means the base level (as set by the appropriate texture function). Sampler types that forbid mipmaps (rectangle, buffer, etc), multisample samplers, and shadow cubemap array samplers cannot be used with texture LOD functions.

Projective texture access

Projecting a texture onto a surface is often a useful technique. To perform this kind of texture access, this function is available:

 gvec textureProj(gsampler sampler, vec projTexCoord[, float bias]);

The difference between this function and texture is that vec is one component larger than it would be for texture. The previous components are divided by the last components, and this value is used to access the texture.

Array, cubemap, multisample, and buffer samplers cannot be used with this function.

An important note on shadow samplers. The comparison value is still part of the texture coordinate, stored in the next to last component. Because it is part of the texture coordinate, it too is divided by the last coordinate before the comparison. This means that you must pre-multiply the actual value by the projection value.

Gradient texture access

Being able to bias the mipmap LOD or specify the specific mipmap to use is useful, but it is also useful to change the mipmap lod computation in more subtle ways. This function allows you to specify the two gradients for how the texture coordinates change locally:

 gvec textureGrad(gsampler sampler, vec texCoord, gradvec dTdx, gradvec dTdy);

The type of gradvec is a float-vector with a number of components equal to the dimensionality of the sampler type. A 1D sampler uses a "float", etc. Note that shadow sampler types do not add an additional coordinate to gradvec, so a sampler2DShadow is still "vec2".

This function works for sampler types that are not multisample, buffer texture, or cubemap array samplers, including those that do not have mipmaps (like sampler2DRect).

The values of dTdx and dTdy are vectors that represent the change of each texture coordinate per pixel of the window's X and Y coordinates.

Mixed texture accesses

There are texture functions for different combination of the above. These are:

  • textureProjOffset
  • textureProjLod
  • textureProjLodOffset
  • textureProjGrad
  • textureProjGradOffset
  • textureLodOffset
  • textureGradOffset

The variations that add parameters to the functions, everything except "Proj", add them in the order that the names appear in the function. So textureProjLodOffset has this signature:

 gvec textureProjLodOffset(gsampler sampler, vec projTexCoord, float lod, ivec offset);

The "Grad" and "Lod" functions do not have a bias parameter.

The accepted sampler types is the intersection of all sampler types that each function accepts. "Proj" cannot take cubemaps and "Lod" cannot take rectangles, so textureProjLod cannot take either type.

Direct texel fetches

The above texture functions all use filtering and normalized texture coordinates (except when using rectangle samplers). It is occassionally useful to forgo this and directly access a texel value with non-normalized coordinates. This is done through one of these functions:

 gvec texelFetch(gsampler sampler, ivec texCoord, int lod[, int sample]);
 gvec texelFetchOffset(gsampler sampler, ivec texCoord, int lod, ivec offset);

The texCoord parameter is an unnormalized texture coordinate. The lod specifies which mipmap to sample from; if the sampler type does not have mipmaps, this parameter will not be present. The offset version applies an integer texel offset to the texture coordinate before doing the texture access. As before, the offset must be a constant expression.

texelFetch is the only texturing function (besides textureSize) that takes multisample and buffer samplers. When using multisample samplers, the user must pass a sample parameter. This tells the system which sample to retrieve.

texelFetch does not take cubemap, cubemap array, or shadow samplers of any kind. texelFetchOffset forbids everything except 1D, 2D, 3D, rectangle, 1D, and 2D arrays.

Texture lookup in shader stages

Texture accessing is not limited to just fragment shaders, though this is the primary place where these functions are used. Any GLSL shader stage may access textures. However, there are certain caveats to using some functions.

Mipmapping works based off of the angle of the rendered primitive relative to the window. This makes sense in the context of a fragment shader. But in any other shader stage, there is no rendered primitive.

Because of this, the texture functions have slightly different behavior in non-fragment shader stages. The "Lod" and "Grad" functions all work as expected, as does the texelFetch functions. The result of using any other texturing functions for mipmapped textures outside of a fragment shader is undefined.

Non-uniform flow control

In fragment shaders, there is one other circumstance that can cause all of your non-"Lod" or "Grad" texture accesses to become undefined: non-uniform flow control.

Uniform flow control for a particular location in code means that, no matter how a shader is executed, the shader will follow the same path to get to that location of code. Consider the following GLSL code for a fragment shader:

void main()
{
  vec4 firstData = texture(someSampler, textureCoords);
 
  if(parameterValue < 20)
  {
      firstData += texture(someOtherSampler, otherTexCoords);
  }
 
  vec4 moreData = texture(thirdSampler, thirdTexCoords);
}

The first texture access happens in uniform flow control. Thus, the texture access produces definite results. However, both of the other two texture accesses are not in uniform flow. If the textures they are accessing use mipmapping or anisotropic filtering of any kind, then any texture function that is not "Lod" or "Grad" will retrieve undefined results.

Note: The GLSL compiler will not give you an error for this. It is perfectly legal GLSL code. It only becomes undefined when certain texture state is set on those textures. Specifically, if mipmap or anisotropic filtering is used.

There are two ways to solve this. The simplest is to restructure the code to always do the second texture access, but only add it based on the condition:

void main()
{
  vec4 firstData = texture(someSampler, textureCoords);
 
  vec4 addingData = texture(someOtherSampler, otherTexCoords);
  vec4 moreData = texture(thirdSampler, thirdTexCoords);
 
  if(parameterValue < 20)
  {
      firstData += addingData;
  }
}

The other alternative, which must be used in cases that are not so simple to resolve, is to get gradients for each pair of texture coordinates the texture coordinates with the dFdx and dFdy functions. Then use those gradients when fetching the textures.

void main()
{
  vec4 firstData = texture(someSampler, textureCoords);
 
  vec2 otherTexDx = dFdx(otherTexCoords);
  vec2 otherTexDy = dFdy(otherTexCoords);
  vec2 thirdTexDx = dFdx(thirdTexCoords);
  vec2 thirdTexDy = dFdy(thirdTexCoords);
 
  if(parameterValue < 20)
  {
      firstData += textureGrad(someOtherSampler, otherTexCoords, otherTexDx, otherTexDy);
  }
 
  vec4 moreData = textureGrad(thirdSampler, thirdTexCoords, thirdTexDx, thirdTexDy);
}

It is important to get the gradients before going into non-uniform flow code. The dFdx and dFdy become just as useless as the texture calls within non-uniform flow control code.

Binding textures to samplers

Uniforms of sampler types are used in GLSL to represent a texture of a particular kind. Therefore, sampler types represent textures. The way a program is associated with textures is somewhat unintuitive. The mapping is done with the rendering context.

Recall that the OpenGL rendering context has multiple independent bind locations for textures. The function to switch which bind location is current is glActiveTexture. The bind locations are named in this function GL_TEXTURE0, GL_TEXTURE1, etc. Alternatively, you can use bind locations of the form GL_TEXTURE0 + i, where i is a texture unit number between 0 and the implementation-defined constant GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS.

This means you can bind 4 textures to texture units 0, 1, 3, and 8, so long as GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS for the implementation is greater than 8.

The value of a sampler uniform in a program is not a texture object, but a texture unit index. So you set the texture unit index for each sampler in a program. Then you bind the textures you wish to use to those texture units. Bind the program to the context, and rendering with that program will use those textures. To set the value of a sampler, use glUniformi or glUniformiv for an array of samplers.

Here is an example of how to do this.

//Initial program setup.
glLinkProgram(program); //Initial link
 
GLint baseImageLoc = glGetUniformLocation(program, "baseImage");
GLint normalMapLoc = glGetUniformLocation(program, "normalMap");
GLint shadowMapLoc = glGetUniformLocation(program, "shadowMap");
 
glUseProgram(program);
glUniformi(baseImageLoc, 0); //Texture unit 0 is for base images.
glUniformi(normalMapLoc, 2); //Texture unit 2 is for normal maps.
glUniformi(shadowMapLoc, 4); //Texture unit 4 is for shadow maps.
 
//When rendering an objectwith this program.
glActiveTexture(GL_TEXTURE0 + 0);
glBindTexture(object1BaseImage);
glActiveTexture(GL_TEXTURE0 + 2);
glBindTexture(object1NormalMap);
glActiveTexture(GL_TEXTURE0 + 4);
glBindTexture(shadowMap);
 
glUseProgram(program);
 
//Render stuff
glDraw*();
 
//Render another object with some different textures.
glActiveTexture(GL_TEXTURE0 + 0);
glBindTexture(object2BaseImage);
glActiveTexture(GL_TEXTURE0 + 2);
glBindTexture(object2NormalMap);
 
//Use the same shadow map, so no need to unbind/bind.
//Render stuff
glDraw*();

Notice that the shadow map does not need to be changed here. This helps minimize the number of state changes. Your program could even have a global convention that texture image unit 4 is always used for shadow maps. Since all objects used the same shadow map, you can bind the shadow map before you start rendering anything, and you only have to bind it once per frame.

If a program doesn't use certain texture image units, it is still fine to have a texture bound to them. They will not affect the rendering in any way.

External links