Some graphics libraries expect textures to have resolutions that are powers of 2, like 1024 by 256. Texture looks up are faster if you follow this constraint. Let's explore a few reasons why this might be.
What is \(5 \ll 1\)? The value 5 is being shifted left 1 bit. In binary, that's \(101_2 \ll 1\), which is \(1010_2\) or \(10_{10}\). Extending this, \(5 \ll 2\) is \(10100_2\) or \(20_{10}\). \(5 \ll 3\) is \(101000_2\) or \(40_{10}\).
Shifting a binary number leftward is the same as multiplying by a power of 2. A left-shift of 1 bit is the same as multiplying by 2. A left-shift of 2 bits is the same as multiplying by 4. A left-shift of 3 bits is the same as multiplying by 8. A left-shift of \(n\) bits is the same as multiplying by \(2^n\).
If you've got integer texture coordinates row
and column
and an image of a particular width stored in row-major order, then you turn your 2D coordinates into a 1D index into the pixel buffer using this equation:
$$ i = \mathrm{row} \times \mathrm{width} + \mathrm{column} $$
However, if you know the width is \(2^n\), then you can express this multiplication as a left-shift:
$$ i = \mathrm{row} \ll n + \mathrm{column} $$
Shifting is generally faster than multiplication in hardware. That's why many graphics libraries either require or suggest textures have resolutions that are powers of 2. WebGL allows non-power of 2 textures (NPOT), but texel lookups may be slower.
Additional, you cannot use gl.REPEAT
or gl.MIRRORED_REPEAT
for wrapping coordinates on NPOT textures. Only gl.CLAMP_TO_EDGE
. Why might this be? Suppose you have a texture that is 64 texels wide. Column 50 on the first tile repeats on the next tile at column 50 + 64. And then again at column 50 + 64 + 64. These are the binary representations of the coordinates on the first four tiles that map to 50:
$$ \begin{array}{lcr} 50 &=& 110010_2 \\ 50 + 64 \times 1 &=& 1110010_2 \\ 50 + 64 \times 2 &=& 10110010_2 \\ 50 + 64 \times 3 &=& 11110010_2 \end{array} $$
You can see from the binary representations how to get the coordinates back in the [0, 64) range: you mask out the six lower-order bits. The mask is 111111, which is 63 in decimal. In general, you calculate your in-range coordinate like this:
$$ s' = s \text{ & } (\mathrm{width} - 1) $$
Mirroring only works when the size is a power of 2 because only then will masking put the texture coordinates back in range.
If you have an NPOT texture and are considering using it in a renderer, you have several options:
Image
class doesn't give you much direct control over pixels. However, its canvas
element does. You draw an NPOT texture into a POT canvas and extract the new image with this function:function padToPot(image) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = powerOfTwoCeiling(image.width);
canvas.height = powerOfTwoCeiling(image.height);
context.drawImage(image, 0, 0);
return context.getImageData(0, 0, canvas.width, canvas.height);
}
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, powerOfTwoCeiling(image.width), powerOfTwoCeiling(image.height), 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, image.width, image.height, gl.RGBA, gl.UNSIGNED_BYTE, image);
The code in several of these solutions requires you to compute power-of-2 ceilings, such as these:
$$ \begin{align} 97 &\rightarrow 128 \\ 129 &\rightarrow 256 \\ 1992 &\rightarrow 2048 \\ \end{align} $$
You compute the nearest power of 2 that's greater than or equal to a number by taking the number's base-2 logarithm, rounding up, and raising 2 to that power. This function does the trick:
function powerOfTwoCeiling(x) {
return Math.pow(2, Math.ceil(Math.log2(x)));
}
The options that involve padding the image generally require you to adapt the texture coordinates since they require changing the image resolution. The proportions need to account for the new resolution.
Someday these limitations on NPOT textures may go away. The practical concerns that led to these limitations might not even be relevant on modern hardware, yet libraries and developers continue to abide by them for backward compatibility.