Skip to content

Level of detail emulation: lod per pixel calculation, tile selection and third axis interpolation factor computation.

Axi edited this page Aug 31, 2021 · 4 revisions

The N64 hardware supports the use of textures whose level of detail decreases as the object moves away from the camera. The process consists of three parts.

Lod per pixel calculation.

The level of detail per pixel is calculated by evaluating the change in texture coordinates s and t per change in screen coordinates ,x and y. A standard formula would go lod = max(sqrt((ds/dx)^2 + (ds/dy)^2), sqrt((dt/dx)^2 + (dt/dy)^2)), i.e., the maximum between the euclidean norms of the gradients of each texture coordinate. A computationally fast and good approximation would be using the L-infinity norm, or simply put, picking the maximum component of the gradient.

The current implementation goes like this, where vLodTexCoord is a vector holding the texture coordinates and uScreenScale is a scale factor so that non-native resolutions keep the original looks.

mediump vec2 dx = abs(dFdx(vLodTexCoord)) * uScreenScale;
mediump vec2 dy = abs(dFdy(vLodTexCoord)) * uScreenScale;
mediump float lod = max(max(dx.x, dx.y), max(dy.x, dy.y));

Base tile index and interpolation factor.

The lod just calculated roughly indicates how many texels correspond to each screen pixel. Since each mip-map is usually half the size of the previous texture, the base-two logarithm will show the desired tile index. However, this process reveals two difficulities: what to do if the logarithm is negative (magnification) or bigger than the existing tile indices? These can be avoided by clamping the lod between a minimum value of 1 and maximum value of 2^max_tile - eps, where eps represents the smallest possible value in fixed point format. When magnifying, it is possible to extrapolate using the two most detailed textures or use a detail texture. This cases will be discussed later.

mediump float min_lod = 1.0;
mediump float max_lod = pow(2.0, float(uMaxTile)) - 1.0 / 32.0;
mediump float lod_clamp = min(max(lod, min_lod), max_lod);

Finally, this logarithm is split into its integral and fractional parts. A high-level approach could simply go,

mediump float lod_log = log2(lod_clamp);
mediump int lod_tile = int(lod_log);
mediump float lod_frac = fract(lod_log);

While GLSL offers functions like log2(), the N64 was unable to compute the this logarithm. Its integral part posed no issues as it means checking the position in which the most significant bit of the value is, so the value above can be used. The fractional part, on the other hand, had to be approximated.

Minification

When minifying (lod>=1), the fractional part is linearly interpolated between the lower and upper powers of two.

lod_frac = (lod - 2^lod_tile) / (2^(lod_tile + 1) - 2^(lod_tile))

Using properties of powers, this expression can be simplified into,

lod_frac = (lod - 2^lod_tile) / (2^lod_tile) = lod / 2^(lod_tile) - 1

Magnification

So far, we avoided magnification by forcing lod to be greater than 1. However the N64 offers two alternatives.

Sharpen extrapolation

The first alternative consists of using lod_tile=0 but allowing lod_frac to go to negative values when lod<1.0. The approximation used is

lod_frac = lod - 1

which maps an lod near 1 to lod_frac near 0 and an lod near 0 to an lod_frac near -1. The initial lod value is clamped to a minimum value to avoid excessive extrapolation.

Detail texture

The second alternative consists of using a special texture. As a result, all other texture levels have their index increased by one. The approximation used is as above, but is increased by one unit because the most detailed texture is at index 1.

lod_frac = lod

This maps an lod near 1 to lod_frac near 1 and an lod near 0 to an lod_frac near 0. The initial lod value is also clamped to a minimum value to avoid excessive use of the detail texture in the mixture.

A special case

Normally, if sharpen and detail modes are disabled, it would be impossible to enter into magnification territory. However, if a texture has only one level, the level of detail will be clamped to 1-eps.

lod = min(max(lod, lod_min), lod_max) = min(max(lod, 1), 1-eps) = 1-eps

Therefore, magnification can also occur in this case, so it must be considered.

Current implementation

The minimum level of detail can be modified as,

mediump float min_lod = uTextureDetail != 0 ? uMinLod : 1.0;

The steps mentioned above can be summed up in,

lowp int lod_tile = clamp(int(log2(lod_clamp)), 0 , max_tile);
lowp int tile0 = 0;					
lowp int tile1 = 1;						
if (uEnableLod != 0) {						
  if (lod_clamp < 1.0 && uTextureDetail == 0) {		
    tile0 = 0;							
    tile1 = 0;							
  } else if (lod_clamp >= 1.0 && uTextureDetail == 2) {		
    tile0 = lod_tile + 1;					
    tile1 = lod_tile + 2;						
  } else {								
    tile0 = lod_tile;								
    tile1 = lod_tile + 1;								
  }											
}										
mediump float lod_frac = lod_clamp / pow(2.0, float(lod_tile));		
if (uTextureDetail == 1 || lod_clamp >= 1.0) {				
  lod_frac = clamp(lod_frac - 1.0, -1.0, 1.0);				
}							

The aftermath

At last, this values are sent to the texture engine and color combiner to fetch texels from the appropiate level and perform the third axis interpolation respectively.