Textures
Textures consume the largest amount of the available memory and bandwidth in many applications. One of the best places to look for improvements when short on memory or bandwidth to optimize texture size, format and usage. Careless use of texturing can degrade the frame rate and can even result in inferior image quality.
Use texture compression whenever possible (T1)
Texture compression brings several benefits that result from textures taking up less
memory: compressed textures use less of the available memory bandwidth, reduces download
time, and increases the efficiency of the texture cache. The
texture_compression_s3tc
and texture_compression_latc
extensions both provide block-based lossy texture compression that can be decompressed
efficiently in hardware. The S3TC extension gives 8:1 or 4:1 compression ratio and is
suitable for color images with 3 or 4 channels (with or without alpha) with relatively
low-frequency data. Photographs and other images that compress satisfactory with JPEG are
great candidates for S3TC. Images with hard and crisp edges are less good candidates for
S3TC and may appear slightly blurred and noisy. The LATC extension yields a 2:1 compression
ratio, but improves on the quality and can be useful for high resolution normal maps. The
third coordinate is derived in the fragment shader—be sure to benchmark if the application
can afford this trade-off between memory and computations! Unlike S3TC, the channels in LATC
are compressed separately, and quantization is less hard. Using texture compression does not
always result in lower perceived image quality, and with these extensions one can experiment
with increasing the texture resolution for the same memory. There are off-line tools to
compress textures (even if a GL extension supports compressing them on-the-fly). Search the
NVIDIA developer website for "Texture Tools".
Use mipmaps when appropriate (T2)
Mipmaps should be used when there is not an obvious one-to-one mapping between texels and framebuffer pixels. If texture minification occurs in a scene and there are no mipmaps to access, texture cache utilization will be poor due to the sparse sampling. If texture minification occurs more often than not, then the texture size may be too large to begin with. Coloring the mipmap levels differently can provide a visual clue to the amount of minification that is occurring. When reducing the texture size, it may also be worthwhile to perform experiments to see if some degree of magnification is visually acceptable and if it improves frame rate.
Although the GenerateMipmap
function is convenient, it should not be the
only option for generating a mipmap chain. This function emphasizes execution speed over
image quality by using a simple box filter. Generating mipmaps off-line using more advanced
filters (e.g. Lanczos/Sinc) will often yield improved image quality at no extra cost.
However, GenerateMipmap
may be preferable when generating textures
dynamically due to speed. One of the only situations where you do not want to use mipmaps is
if there is always a one-to-one mapping between texels and pixels. This is sometimes the
case in 3D, but more often the case for 2D user interfaces. Recall that a mipmapped texture
takes up 33% more storage than un-mipmapped, but they can provide much better performance
and even better image quality through reduced aliasing.
Use the smallest possible textures size (T3)
Always use the smallest possible texture size for any content that gives acceptable image quality. The appropriate size of a texture should be determined by the size of the framebuffer and the way the textured geometry is projected onto it. But even when you have a one-to-one mapping between texels and framebuffer pixels, there may be cases where a smaller size can be used. For instance, when blending a texture on the existing content in the entire framebuffer, the texture does not necessarily have to be the same width and height as the framebuffer. It may be the case that a significantly smaller texture that is magnified will produce results that are good enough. The bandwidth that is saved from using a smaller and more appropriately sized texture can instead be spent where it actually contributes to better image quality or performance.
Use the smallest possible texture format and data type (T4)
If hardware accelerated texture compression cannot be used for some textures, then consider
using a texture format with fewer components and/or fewer bits per component. Textures for
user interface elements sometimes have hard edges or color gradients that result in inferior
image quality when compressed. The S3TC algorithm make assumptions that changes are smooth
and colors values can be quantized. If these assumptions do not fit a particular image, but
the number of unique colors is still low, then experiment with storing these in a packed
texture format using 16 bit/texel (e.g. UNSIGNED_SHORT_5_6_5
). Although the
colors are remapped with less accuracy it may not be noticeable in the final application.
Grayscale images should be stored as LUMINANCE
and tinted images can
sometimes be stored the same way with the added cost of a dot product with the tint color.
If normal maps do not compress satisfactory with the LATC
format, then it
may be possible to store two of the normals coordinates in uncompressed
LUMINANCE_ALPHA
and derive the third in a shader assuming the direction
(sign) of the normal is implicit (as is the case of a heightmap terrain).
When optimizing uncompressed textures, the exception case that 24-bit (RGB) textures are not necessarily faster to download or smaller in memory than 32-bit (RGBA) on most GPUs. In this case, it may be possible to use the last component for something useful. For instance, if there already is an 8-bit greyscale texture that is needed at the same time as an opaque color texture, that single component texture can be stored in the unused alpha component of a 32-bit (RGBA). The component could define a specular/reflectance map that describe where and to what degree light is reflected. This is useful for terrain satellite imagery or land cover textures with water/snow/ice areas or for car textures with their metal and glass surfaces or for textures for buildings with glass windows.
Store multiple images in each texture object (T5)
There is no requirement that there is a one-to-one mapping between an image and a texture object. Textures objects can contain multiple distinct images. These are sometimes referred to as a "texture atlas" or a "texture page". The geometry defines texture coordinates that only reference a subset of the texture. Texture atlases are useful for minimizing state changes and enables larger batches when rendering. For example, residential houses and office buildings and factories might all use distinct texture images. But the geometry and vertex layout for each is most likely identical so these could share the same buffer object. If the distinct images are stored in a texture atlas instead of as separate textures, then these different kinds of buildings can all be rendered more efficiently in the same draw call (G7). The texture object could be a 2D texture, a cubemap texture or an array texture.
Note that if mipmapping is enabled, the sub-textures in an atlas must have a border wide
enough to ensure that smaller mipmaps are not generated using texels from neighboring
images. And if texture tiling (REPEAT
or
MIRRORED_REPEAT
) is needed for a sub-image then it may be better to store
it outside the texture atlas.
Emulating either wrapping mode in a shader by manipulating texture coordinates is possible, but not free. A cubemap texture can sometimes be useful since wrapping and filtering apply per face, but the texture coordinates used must be remapped to a vec3 which may be inconvenient. If all the sub-images have the same or similar size, format and type (e.g. image icons), the images are a good candidate for the array texture extension if supported. Array textures may be more appropriate here than a 2D texture atlas where mipmapping and wrapping restrictions have to be taken into consideration.
Float textures are always expensive (T6)
Textures with a floating-point format should be avoided whenever possible. If these textures are simply being used to represent a larger range of values, it may be possible to replace these with fixed point textures and scaling instructions. For instance, unsigned 16-bit integers cannot even accurately be represented by half-precision floats (FP16). These would have to be stored using single precision (FP32) leading to twice the memory and bandwidth requirements. It might be better to store these values in two components using 8 bits (LA8) and spend ALU instructions to unpack them in a shader.
Floating-point textures may not support anything better than nearest filtering.
Prefer power-of-two (POT) textures in most cases (T7)
Although Non-Power-of-Two (NPOT) textures are supported in ES2 they come with a
CLAMP_TO_EDGE
restriction on the wrapping mode (unless relaxed by an
extension). More importantly, they cannot be mipmapped (unless relaxed by an extension). For
that reason, POT textures should be used when there is not significant memory and bandwidth
to be saved from using NPOT. However, an NPOT texture may be padded internally to
accommodate alignment restrictions in hardware and that the amount of memory saved might not
be quite as large as the width and height suggests. As a rule of thumb, only large (i.e.,
hundreds of texels) NPOT textures will effectively save a significant amount of memory over
POT textures.
Update textures sparingly (T8)
Writing to GPU resources can be expensive—it applies to textures as well. If texture updates are required, then determine if they really need to be updated per frame or if the same texture can be reused for several frames. For environment maps, unless the lighting or the objects in the environment have been transformed (e.g., moved/rotated) sufficiently to invalidate the previous map, the visual difference may not be noticeable but the performance improvement can be. The same applies to the depth texture(s) used for shadow mapping algorithms.
Update textures efficiently (T9)
When updating an existing texture, use TexSubImage
instead of re-defining
its entire contents with TexImage
when possible.
When using TexSubImage
it is important to specify the same texture
format and data type with which that the texture object was defined; otherwise, there may
be an expensive conversion as texels are being updated.
If the application is only updating from a sub-rectangle of pixels in client memory, then
remember that the driver has no knowledge about the stride of pixels in your image. When the
width of the image rectangle differs from the texture width, this normally requires a loop
through single pixel rows calling TexSubImage
repeatedly while updating the
client memory offsets with pointer arithmetic. In this case, the
unpack_subimage
extension can be used (if supported) to set the
UNPACK_ROW_LENGTH
pixelstore parameter to update the entire region with
one TexSubImage
call.
Partition rendering based on alpha blending/testing (T10)
It is sometimes possible to improve performance by splitting up the rendering based on whether alpha blending or alpha testing is required. Use separate draw calls for opaque geometry so these can be rendered with maximum efficiency with blending disabled. Perform other draw calls for transparent geometry with alpha blending enabled, taking into account the draw ordering that transparent geometry requires. As always, benchmarks should be run to determine if this improves or reduces the frame rate since you will be batching less by splitting up draw calls.
Filter textures appropriately (T11)
Do not automatically set expensive texture filters and enable anisotropic filtering. Remember that nearest-neighbor filtering always fetches one texel, bilinear filtering fetches up to four texels and trilinear fetches up to eight texels. However, it can be incorrect to draw assumptions about the performance cost based on this. Bilinear filtering may not cost four times as much as nearest filtering, and trilinear can be more or less than twice as expensive as bilinear. Even though textures have mipmaps, it does not automatically mean trilinear filtering should be used. That decision should be made entirely from observing the images from the running application. Only then can a judgment be made if any abrupt changes between mipmap levels are visually disturbing enough to justify the cost of interpolating the mipmaps with trilinear filtering. The same applies to anisotropic filtering, which is significantly more expensive and bandwidth intensive than bilinear or trilinear filtering. If the angle between the textured primitives and the projection plane (e.g. near plane) is never very large, there is nothing to be gained from sampling anisotrophically and there is potentially lower performance. Therefore, an application should start off with the simplest possible texture filtering and only enable more expensive filtering after users have inspected the output images. It might be worthwhile to benchmark the changes and take notes along the way. This will provide a better indication of the relative cost of filtering method and if concessions must be made if the performance budget is exceeded.
Try to exploit texture tiling (T12)
It is common for images to contain the same repeated pattern of pixels. Or an image might
repeat a few patterns that are close enough in similarity that they could be replaced with a
single pattern without impacting image quality. Tiling textures saves on memory and
bandwidth. Some image processing applications can identify repeated patterns and can crop
them so they can be tiled infinitely without seams when using textures with the
REPEAT
wrap mode. Sometimes even a quarter of a tile or shingle may be
sufficient to store while using MIRRORED_REPEAT
. Consider if tiling
variation can be restored or achieved with multi-texturing, using for instance a less
expensive grey-scale texture that repeats at a different frequency to modulate the texels
from the tiled texture.
Use framebuffer objects (FBO) for dynamically generated textures (T13)
OpenGL ES comes with functions for copying previously rendered pixels from a framebuffer
into a texture (TexCopyImage
, TexCopySubImage
). These
functions should be avoided whenever possible for performance reasons. It is better to bind
a framebuffer object with a texture attachment and render directly to the texture. Make sure
you check for framebuffer completeness.
Not all pixel formats are color-renderable. Formats with 3 or 4 components in 16 or 32
bits are color-renderable in OpenGL ES 2.0, but LUMINANCE and/or ALPHA may require a
fall-back to TexCopyImage
functions.