Anisotropic Filtering Meaning and How It Works

On a floor seen at a low angle, one screen pixel can cover a strip of texture many times longer than it is wide. Ordinary mipmapping blurs that strip into grey mush. Anisotropic sampling, better known as anisotropic filtering (AF), fixes this by taking several texture probes along the strip’s long axis and blending them. It costs little on modern GPUs, and once your mipmaps exist you can turn it on with one or two lines of code.

TopicTexture filtering
LevelBeginner to intermediate
APIs coveredWebGL, WebGPU, Vulkan, Unity
DemoFree p5.js sketch

Key takeaways

  • AF samples along the long axis of each pixel’s elliptical texture footprint, so floors and roads stay sharp at grazing angles.
  • 2x, 4x, 8x and 16x set the most probes the GPU may use. Pixels that face the camera still get one probe.
  • On current GPUs the frame cost is small. 8x vs 16x is mostly a choice for integrated graphics.
  • AF does nothing without mipmaps and linear filtering, which explains most “no effect” bug reports.
  • WebGPU rejects any sampler with maxAnisotropy above 1 unless all three filters are “linear”.

Fact-checked

We checked these key facts against the sources named, on November 14, 2026:

  • Most hardware and APIs top out at 16 (source: bugs.webkit.org)
  • A 1024 by 1024 texture has levels at 512, 256, 128 and so on, down to 1 by 1 (source: registry.khronos.org)
  • Program Settings let you pick one game’s executable and set Anisotropic filtering to Off, 2x, 4x, 8x or 16x for that game only (source: nvidia.com)
  • Resize it to 1024 by 1024, or use WebGL 2. :::tip Studio tip Always clamp your requested anisotropy with Math.min(requested, max) instead of hard-coding 16 (source: developer.mozilla.org)
  • Chrome’s Dawn only turns on Vulkan anisotropy when the device reports samplerAnisotropy as VK_TRUE and the requested maxAnisotropy is above 1, and otherwise leaves it off (source: dawn.googlesource.com)

Anisotropic sampling in one picture

Anisotropic sampling filters a texture differently in each direction. It matches the stretched shape that a tilted surface projects onto the screen. Picture a checkerboard floor running to the horizon. With plain trilinear filtering, the far squares melt into grey. With 16x AF, the rows stay crisp until they reach the pixel grid’s limit.

The live sketch at the end of this page draws that floor. You can switch between nearest, bilinear, trilinear and AF at 2x, 4x, 8x and 16x, and drag to change the camera pitch. Lower the pitch and the gap between the modes grows, because the footprint stretches further.

What anisotropy means in simple terms

Something is anisotropic when its properties depend on direction. Wood is a good everyday example. It splits easily along the grain and resists splitting across it. In graphics, the “property” is how much texture one pixel covers. Look at a wall head-on and each pixel covers about the same distance left-right as up-down. Look along a floor and each pixel covers far more texture in depth than across.

The term “texture anisotropy” comes up in two other places. Materials scientists use it for grain orientation in metals and rocks. Shader artists use “anisotropic texture” for surfaces like brushed aluminium, where highlights stretch in one direction. This article covers the filtering meaning, which is what game settings and sampler APIs refer to.

Anisotropic vs isotropic

An isotropic filter treats every direction the same way. Bilinear and trilinear filtering are isotropic: they assume a pixel’s footprint is a square or circle and blur equally in all directions. An anisotropic filter lets the footprint be long and thin, and spends its samples along the long side.

Property Isotropic filtering Anisotropic filtering
Footprint shape assumed Square or circle Ellipse with any orientation
Blur direction Equal in all directions Mostly along the long axis
Examples Bilinear, trilinear AF 2x to 16x
Best on Surfaces facing the camera Floors, roads, terrain, angled walls
Weakness Blurs or shimmers at grazing angles More texture fetches on oblique pixels

Why textures go blurry at an angle

Textures blur at an angle because mipmapping chooses one level of detail per pixel, and that level is sized for a round footprint. On an oblique surface the footprint is an ellipse. Any single round size is either too big for the short axis, which blurs, or too small for the long axis, which shimmers.

Mipmaps and the one-texel assumption

Mipmapping stores a texture as a chain of smaller copies. Each level is half the width and height of the one before. A 1024 by 1024 texture has levels at 512, 256, 128 and so on, down to 1 by 1. When a pixel covers four texels across, the GPU reads from the level where those four texels have been averaged into one. That removes the sparkle and moiré you would get from sampling a single texel out of many.

The catch is the level choice. The GPU estimates how fast the texture coordinates change from one pixel to the next, both horizontally and vertically. These are the screen-space derivatives. It then picks the level that makes the larger of the two rates about one texel per pixel. That works well when both rates are similar, and badly when one is six times the other.

The footprint ellipse

Project a round pixel onto a tilted plane and you get an ellipse. The minor axis, its short side, runs across the floor. The major axis, its long side, runs toward the horizon. The ratio between them is the anisotropy of that pixel.

Here is a worked example. A pixel on a road covers 1 texel across and 6 texels along. That is a 6:1 ratio.

  • Mip level picked for the long axis (what trilinear does): log2(6) ≈ 2.58. The GPU reads from a level averaged over roughly 6 by 6 texels, so the road’s lane markings blur sideways even though the pixel only spans 1 texel across.
  • Mip level picked for the short axis: level 0. Now one sample stands in for 6 texels along the road. Detail along that axis aliases, so you see crawling and shimmering as the camera moves.
  • Anisotropic answer: take about 6 samples along the long axis at level 0 and average them. Each sample covers about 1 by 1 texel, and together they cover the whole strip.

That third option is anisotropic sampling.

How the GPU actually does anisotropic sampling

Hardware AF fits an ellipse to the pixel’s footprint and divides the major axis by the minor axis. It uses that ratio to decide how many probes to take, places them along the major axis, and averages the results. Each probe is an ordinary bilinear or trilinear lookup, taken from a mip level sized for the short axis.

GPU patent literature, for example US patent 7,372,468, describes the same structure. The footprint is approximated as an ellipse, the number of samples follows from the anisotropic ratio, and isotropic taps are spaced along the major axis. Vendors differ in how they estimate the axes and weight the taps, which is why AF looks slightly different on NVIDIA, AMD, Intel and Apple GPUs.

The reference formula in the original EXT_texture_filter_anisotropic specification is easy to follow:

  1. Measure the footprint’s two axis lengths in texels, Pmax and Pmin.
  2. Compute the probe count N = min(ceil(Pmax / Pmin), maxAnisotropy).
  3. Pick the mip level from log2(Pmax / N) instead of log2(Pmax).
  4. Take N probes along the major axis at that level and average them.

In the 6:1 road example with 16x allowed, N is 6 and the level is log2(1) = 0. At 4x, N is capped at 4 and the level is log2(1.5) ≈ 0.58, which is a little softer. With plain trilinear, the level is 2.58, which is the blur you saw before.

What 2x, 4x, 8x and 16x really cap

The number in a setting like “8x” is a ceiling on the anisotropy ratio the GPU will honour. It is not a fixed sample count for every pixel. At 8x, a pixel with a 3:1 footprint gets about 3 probes. A 12:1 pixel gets 8, and the leftover stretch shows up as a little blur.

Most hardware and APIs top out at 16. Direct3D 11 and 12 accept MaxAnisotropy values from 1 to 16. Metal’s sampler range is 1 to 16. The WebGPU specification says most implementations support 1 to 16 inclusive. Vulkan reports each device’s own limit in maxSamplerAnisotropy.

Why the cost only appears on oblique pixels

Pixels that face the camera have a ratio near 1:1, so they get a single probe whatever the setting. Extra fetches only happen where the surface is steeply tilted, such as the lower half of a frame showing a road or a floor. That is the main reason AF is cheaper than its “16 times the samples” reputation suggests. Texture caches also help, because the probes along one axis tend to land on neighbouring texels the GPU has just fetched.

Nearest vs bilinear vs trilinear vs anisotropic

Each filtering mode fixes the artefact left by the one before it. Nearest picks one texel and aliases. Bilinear smooths magnification but still aliases when the texture shrinks. Trilinear uses mipmaps and blends between levels but blurs oblique surfaces. Anisotropic filtering keeps oblique surfaces sharp.

Mode Samples per lookup (conceptual) Uses mipmaps Fixes Leaves Typical cost
Nearest (point) 1 texel No, or nearest level Nothing; keeps hard pixel edges Blocky close-ups, heavy shimmer at distance Lowest
Bilinear 4 texels No, or one level Blocky magnification Aliasing and moiré when minified Very low
Bilinear + mipmaps 4 texels from one level Yes Most distant shimmer Visible bands where mip levels switch Very low
Trilinear 8 texels from two levels Yes Mip banding Blur on floors and roads at grazing angles Low
Anisotropic (Nx) Up to N trilinear probes, so up to 8N texels Yes, required Oblique blur and shimmer Very extreme angles past the cap Low to moderate, oblique pixels only

The sample counts describe how the filter is defined. Real GPUs take shortcuts. Driver options such as NVIDIA’s “Trilinear optimization” and “Anisotropic sample optimization” exist precisely to trade some of those texels for speed.

Trilinear vs anisotropic: which one fixes what

Trilinear filtering fixes problems between mip levels. Anisotropic filtering fixes problems inside one pixel’s footprint. These are separate jobs, so the modes stack rather than compete. In most APIs an anisotropic sampler is a trilinear sampler with extra probes, and WebGPU makes that explicit by requiring all-linear filters. If you see bands where the floor’s sharpness changes, add trilinear. If the floor is smooth but soft, add AF.

Studio tip

Turn AF on for floors, roads, terrain and text on angled planes. Skip it for screen-aligned UI, pixel-art textures sampled with “nearest”, and lookup or data textures such as noise tables or colour ramps, where blending neighbouring values would corrupt the data.

Does anisotropic filtering affect FPS?

Yes, slightly. On a modern dedicated GPU the cost is usually a few frames per second at most, and often too small to measure. Intel’s gaming guide describes AF as a large visual improvement for a relatively small performance hit, especially compared with texture or shadow quality. It also says the difference varies by game and PC.

Published benchmarks agree. In an older PC Gamer graphics-options test using BioShock Infinite, going from bilinear filtering to 16x AF cost about 6 FPS on average. That hardware is now several generations old, and texture throughput has grown since, so read the number as an upper bound for a typical modern card.

The cost goes up in three situations:

  • Integrated or low-end GPUs. They share memory bandwidth with the CPU, and AF’s extra fetches compete for it.
  • Bandwidth-limited scenes. Large uncompressed textures, many texture layers per material, or high resolutions put more pressure on memory.
  • Scenes made mostly of oblique surfaces. Racing games, flight sims and open terrain push more pixels into the multi-probe path.

8x vs 16x: can you see the difference?

Usually only barely, and only on surfaces at very steep angles. A pixel needs a footprint ratio above 8:1 before 16x does anything that 8x doesn’t. Those pixels sit close to the horizon, on long roads, runways and floor tiles in the distance. Intel’s guide says the step from 8x to 16x can be slight.

So is 16x worth it over 8x, or is that placebo? It is not placebo. The difference is real but confined to a thin band of the image. Some players say 16x has basically no hit on modern cards, and others call 8x the sweet spot. Both are partly right. On a discrete GPU from the last several years, 16x costs so little that there is no reason to drop it. On integrated graphics or a handheld PC, 8x keeps nearly all the benefit and saves a little bandwidth. Intel suggests trying lower values such as 4x if a PC struggles at 16x.

The recommendations below weigh three things: vendor guidance, published frame-rate tests, and the math of how the cap limits probes.

Level When it makes sense Visible difference vs the previous level Performance note
1x (off) Pixel-art games with nearest filtering, extreme low-end hardware Not applicable; baseline No extra cost
2x Very weak integrated GPUs Large: mid-distance floors sharpen Tiny
4x Integrated GPUs, handheld PCs, competitive players chasing every frame Clear on roads and terrain Small
8x Laptops, older discrete cards Moderate, mostly in the far distance Small
16x Any modern discrete GPU Slight, near the horizon only Usually negligible

Studio tip

If you’re short on frames in a game, lower anti-aliasing, shadows or volumetrics before you touch AF. If 16x feels expensive on an integrated GPU, try 8x.

In-game setting vs driver override (NVIDIA Manage 3D Settings)

Use the in-game setting first. The game knows which textures should be filtered and which shouldn’t, and a driver override applies to all of them. Overrides are useful for older or console-port games that have no AF slider or a broken one.

In the NVIDIA Control Panel, the controls sit under Manage 3D Settings, which has two tabs:

  1. Global Settings apply to every application. Leave Anisotropic filtering at “Application-controlled” here unless you want it forced everywhere.
  2. Program Settings let you pick one game’s executable and set Anisotropic filtering to Off, 2x, 4x, 8x or 16x for that game only.
  3. Related options under the same page include “Texture filtering – Quality”, “Texture filtering – Anisotropic sample optimization” and “Texture filtering – Trilinear optimization”. These trade filter accuracy for speed. Leave them at their defaults unless you are troubleshooting.

Should you turn off the in-game AF and use the driver’s instead? Only if the game’s option is missing, capped low or visibly not working. Forcing AF from the driver can blur textures the developer meant to keep crisp, and overrides don’t take effect in every game. After any change, check a long floor or road in the game itself and see whether it got sharper. AMD Software and Intel’s graphics control apps offer the same kind of per-game override.

Older guides describe Control Panel steps for Windows 8 Store apps and early Windows 10 GPU-preference changes. Those menus have since changed, so trust what your current driver shows.

Valorant and other games: where to find it

In Valorant, Anisotropic Filtering lives under Settings, then Video, then Graphics Quality, with options from 1x to 16x. PC Gamer’s recommended Valorant presets used 1x on its Low profile and 8x on Medium. Competitive players differ on the value, and guides suggest anything from 4x to 16x. On any recent discrete card, 16x costs so little that 8x and 16x play the same.

Most other PC games put the option in the graphics or video menu, labelled “Anisotropic Filtering”, “Texture Filtering” or simply “AF”. Minecraft is a special case. Its pixel-art textures rely on nearest-neighbour magnification, and Java Edition exposes a Mipmap Levels slider in Video Settings instead of an AF slider. That’s a reminder that the setting only matters where mipmaps and linear filtering are in play.

Turn it on in code: WebGL, WebGPU and three.js

On the web, you set AF per texture in WebGL, per sampler in WebGPU, and per texture in three.js. All three need a mipmapped texture and linear filtering, and all three should clamp the value to what the device supports.

Quick steps: enable AF on any GPU API

  1. Upload the texture and generate its full mip chain.
  2. Set min filter to linear-mipmap-linear and mag filter to linear.
  3. Query the device’s maximum anisotropy, if the API exposes it.
  4. Set the anisotropy to the smaller of your target and that maximum.
  5. Check a tilted floor at a grazing angle to confirm it got sharper.

WebGL 1/2: EXT_texture_filter_anisotropic

WebGL exposes AF only through the EXT_texture_filter_anisotropic extension, in both WebGL 1 and WebGL 2. The Khronos Group never moved it into WebGL 2’s core. You query the device maximum with getParameter on MAX_TEXTURE_MAX_ANISOTROPY_EXT (0x84FF), and set the value per texture with texParameterf on TEXTURE_MAX_ANISOTROPY_EXT (0x84FE). Very old browsers used MOZ_ and WEBKIT_ prefixed names, so the fallback chain below costs nothing to include.

const gl = canvas.getContext('webgl2'); // works the same with 'webgl'

const ext =
  gl.getExtension('EXT_texture_filter_anisotropic') ||
  gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
  gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');

const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D); // AF needs a mip chain
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

if (ext) {
  const max = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT); // 0x84FF
  gl.texParameterf(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(16, max)); // 0x84FE
}

In WebGL 1, non-power-of-two textures can’t have mipmaps, so a 1000 by 750 photo gets no benefit from AF. Resize it to 1024 by 1024, or use WebGL 2.

Studio tip

Always clamp your requested anisotropy with Math.min(requested, max) instead of hard-coding 16. Some mobile GPUs report a maximum of 4 or 8, and an unclamped value is either rejected or silently clamped, depending on the API.

WebGPU: maxAnisotropy and the all-linear rule

WebGPU sets anisotropy on the sampler, through the maxAnisotropy field of GPUSamplerDescriptor. The field defaults to 1, which means off. The specification adds a strict rule: if maxAnisotropy is greater than 1, magFilter, minFilter and mipmapFilter must all be “linear”, or sampler creation fails validation. The value must also be at least 1.

const sampler = device.createSampler({
  addressModeU: 'repeat',
  addressModeV: 'repeat',
  magFilter: 'linear',     // all three must be 'linear'
  minFilter: 'linear',     // when maxAnisotropy > 1,
  mipmapFilter: 'linear',  // or validation fails
  maxAnisotropy: 16,       // clamped to the platform maximum
});

WebGPU has no call that reports the maximum. Values above the platform limit are clamped instead. MDN and the specification note that most implementations support 1 to 16. Browser engines handle the clamp themselves. WebKit’s Metal backend applies min(value, 16) to match Metal’s 1 to 16 range; early builds treated out-of-range values as errors before switching to clamping. Chrome’s Dawn only turns on Vulkan anisotropy when the device reports samplerAnisotropy as VK_TRUE and the requested maxAnisotropy is above 1, and otherwise leaves it off.

WebGPU also has no built-in generateMipmap. You have to create the texture with a mipLevelCount above 1 and fill each level yourself, usually with a small render or compute pass. If you skip that step, a perfectly valid anisotropic sampler has nothing to sample from.

Did you know?

According to the WebGPU specification, a sampler with maxAnisotropy above 1 is invalid unless its mag, min and mipmap filters are all “linear”. No other major API rejects the combination outright.

three.js: texture.anisotropy

three.js sets AF per texture through texture.anisotropy, which defaults to 1. The renderer reports the device limit through renderer.capabilities.getMaxAnisotropy(), which reads the GPU’s MAX_TEXTURE_MAX_ANISOTROPY_EXT value. That call replaced the older renderer.getMaxAnisotropy() you’ll see in outdated examples.

import * as THREE from 'three';

const renderer = new THREE.WebGLRenderer({ antialias: true });
const tex = new THREE.TextureLoader().load('textures/floor.jpg');

tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = renderer.capabilities.getMaxAnisotropy(); // usually 16 on desktop
// Keep the defaults: generateMipmaps = true, minFilter = LinearMipmapLinearFilter

If you change anisotropy after the texture has been rendered once, set tex.needsUpdate = true so three.js re-applies the sampler state. Setting minFilter to THREE.LinearFilter or THREE.NearestFilter turns off mipmap sampling, and AF goes with it.

Native and engine setups: OpenGL 4.6, Vulkan, Unity

Native APIs follow the same pattern with stricter rules. OpenGL 4.6 has AF in core. Vulkan treats it as an optional device feature you must request. Unity layers a project-wide quality switch over a per-texture level.

API / engine Where to set it How to query the max Gotcha
WebGL 1 / WebGL 2 texParameterf with TEXTURE_MAX_ANISOTROPY_EXT, per texture getParameter(MAX_TEXTURE_MAX_ANISOTROPY_EXT) Extension may be missing; WebGL 1 can’t mipmap non-power-of-two textures
WebGPU GPUSamplerDescriptor.maxAnisotropy, per sampler None; value is clamped All three filters must be “linear”; no built-in mip generation
three.js texture.anisotropy renderer.capabilities.getMaxAnisotropy() Needs needsUpdate after first render; non-mipmap minFilter disables it
OpenGL 4.6 glTexParameterf or glSamplerParameterf with GL_TEXTURE_MAX_ANISOTROPY glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY) Older contexts need the _EXT or ARB extension token
Vulkan VkSamplerCreateInfo anisotropyEnable and maxAnisotropy VkPhysicalDeviceLimits::maxSamplerAnisotropy samplerAnisotropy feature must be enabled at device creation
Direct3D 11 / 12 Sampler filter set to anisotropic, MaxAnisotropy 1 to 16 Fixed range of 1 to 16 Anisotropic filter mode sets min, mag and mip together
Metal Sampler descriptor maxAnisotropy Fixed range of 1 to 16 Values outside the range are not valid
Unity Texture.anisoLevel plus Quality Settings Engine handles it “Forced On” raises levels 1 to 9 up to 9

OpenGL: from extension to core in 4.6

OpenGL only got anisotropic filtering into its core specification with version 4.6, released in July 2017. For years before that it was an extension, GL_EXT_texture_filter_anisotropic, that nearly every driver shipped anyway. OpenGL 4.6 promoted GL_ARB_texture_filter_anisotropic into core. In the Khronos announcement, Piers Daniell, chair of the OpenGL Working Group, said the release resolved earlier intellectual-property roadblocks to adding AF and polygon offset clamping. Phoronix reported at launch that Mesa didn’t yet support the extension. NVIDIA shipped beta 4.6 drivers on release day.

GLfloat maxAniso = 1.0f;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &maxAniso); /* 0x84FF */ glBindTexture(GL_TEXTURE_2D, tex);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, fminf(16.0f, maxAniso)); /* 0x84FE */

The core tokens use the same numeric values as the old _EXT tokens. That means code written for a 3.3 context with the extension and code written for 4.6 core differ only in the token names. If you use sampler objects, set the same parameter with glSamplerParameterf. A sampler object bound to a texture unit overrides the texture’s own parameters.

Did you know?

According to Phoronix’s OpenGL 4.6 coverage, patents had kept anisotropic filtering out of the OpenGL core specification for years, even though nearly every desktop driver already supported it as an extension.

Vulkan: samplerAnisotropy is optional

In Vulkan, anisotropic filtering is an optional device feature. The specification says anisotropyEnable in VkSamplerCreateInfo must be VK_FALSE unless you enabled the samplerAnisotropy feature when you created the logical device. When it is on, maxAnisotropy must sit between 1.0 and VkPhysicalDeviceLimits::maxSamplerAnisotropy inclusive. Vulkan does not clamp for you.

VkPhysicalDeviceFeatures supported;
vkGetPhysicalDeviceFeatures(physicalDevice, &supported);
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(physicalDevice, &props);

/* At device creation: request the feature only if the GPU has it */
VkPhysicalDeviceFeatures enabled = {0};
enabled.samplerAnisotropy = supported.samplerAnisotropy;
/* deviceCreateInfo.pEnabledFeatures = &enabled; */

VkSamplerCreateInfo info = {0};
info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
info.magFilter = VK_FILTER_LINEAR;
info.minFilter = VK_FILTER_LINEAR;
info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
info.addressModeU = info.addressModeV = info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
info.anisotropyEnable = enabled.samplerAnisotropy;
info.maxAnisotropy = enabled.samplerAnisotropy
    ? fminf(16.0f, props.limits.maxSamplerAnisotropy) : 1.0f;
info.minLod = 0.0f;
info.maxLod = VK_LOD_CLAMP_NONE;
info.unnormalizedCoordinates = VK_FALSE;
vkCreateSampler(device, &info, NULL, &sampler);

Three combinations are not allowed. AF can’t be used with unnormalizedCoordinates set to VK_TRUE, with VK_FILTER_CUBIC_EXT as the min or mag filter, or with a sampler that uses Y′CBCR conversion, the path used for video frames and camera feeds. If your validation layers flag a sampler that “obviously” should work, check those three first. This is the same check Dawn runs before it passes WebGPU’s maxAnisotropy down to Vulkan.

Unity: anisoLevel and Quality Settings

Unity controls AF in two places. Each texture has an anisoLevel, and the project’s Quality Settings have an Anisotropic Textures mode: Disabled, Per Texture or Forced On. Under Per Texture, the level you set is the level you get. Under Forced On, Unity’s Texture.anisoLevel documentation for Unity 6.2 says values from 1 to 9 are raised to 9, a value of 0 applies no AF at all, and values above 9 are clamped into the 9 to 16 range.

using UnityEngine;

public class FloorTextureSetup : MonoBehaviour
{
    public Texture2D floorTexture; // import with Generate Mipmaps enabled void Start()
    {
        QualitySettings.anisotropicFiltering = AnisotropicFiltering.Enable; // "Per Texture"
        floorTexture.filterMode = FilterMode.Trilinear;
        floorTexture.anisoLevel = 8;
    }
}

So under Forced On, a texture you set to level 3 quietly becomes 9. Level 0 is the only way to opt a texture out, which matters for UI atlases and data textures. The same page notes that on Metal, Vulkan and OpenGL, mipmap filtering and the AF level can be controlled independently. On other graphics APIs, enabling AF also turns on trilinear filtering.

Why anisotropy “does nothing”: a troubleshooting checklist

When AF seems to have no effect, the cause is almost always upstream of the setting. The texture has no mipmaps, the filter is wrong, or the blur comes from somewhere AF can’t reach. Work through this list in order.

  1. No mipmaps. AF chooses which mip level each probe reads. With only level 0 there is nothing to choose, and the floor shimmers the same as before. Generate mipmaps, and in WebGL 1 make sure the texture is a power of two.
  2. Nearest or non-mipmap filtering. A minFilter of NEAREST, LINEAR, NearestFilter or LinearFilter bypasses the mip chain. Use linear-mipmap-linear.
  3. The value never reached the GPU. In three.js, a change made after first render needs texture.needsUpdate = true. In raw WebGL, the extension object might be null, so the parameter call never ran.
  4. Unclamped or invalid values. Vulkan rejects maxAnisotropy above maxSamplerAnisotropy. WebGPU rejects anything below 1. Clamp before you set.
  5. The WebGPU validation error. “Sampler is invalid” with maxAnisotropy above 1 almost always means one of the three filters is still “nearest”.
  6. Render targets and XR layers. Textures produced by render passes, including many post-processing chains and some WebXR layer setups, have no mip chain unless you build one, so AF has nothing to work with. People ask whether texture anisotropy has no effect in WebXR. Usually the texture itself is fine, and the problem is a missing mip chain or the compositor resampling the final image.
  7. The blur comes from the mip chain itself. Mip levels made with a soft downsampling filter, or a source texture too small for the screen, look blurry at every AF level. AF can’t add detail the texture doesn’t have.
  8. Explicit LOD sampling. GLSL textureLod and WGSL textureSampleLevel give the GPU a level but no footprint, so there is nothing to stretch. Use texture or textureGrad when you want hardware AF.

Three community reports fit this list closely:

  • “I set renderer.capabilities.getMaxAnisotropy() and see no change at all.” Check items 2 and 3. Then look at a plane tilted almost edge-on. On a plane facing the camera, AF correctly does nothing.
  • “My plane with an image texture gets blurry when I rotate it.” That is the trilinear blur AF exists to fix. If it stays after you set anisotropy, the photo is probably non-power-of-two in a WebGL 1 context, or minFilter was changed.
  • “Blurred textures at a distance, setting anisotropy levels doesn’t affect it at all.” Distance alone makes things smaller in both directions, and that isotropic blur is mipmapping doing its job. AF only sharpens the stretched part. For more detail in the distance you need a higher-resolution texture, a sharpening pass or a small negative LOD bias, and the last two trade sharpness for shimmer.

Filtering is a per-fragment texture operation, independent of depth testing and blending. Changing depth or blend state won’t affect it, and AF settings won’t fix z-fighting that looks like texture noise.

Heads up

A negative LOD bias makes textures look sharper in stills, but it brings back the shimmer that mipmaps remove. Test it in motion, at the resolution you ship.

Going further: shader-side anisotropic sampling

You can shape anisotropic sampling yourself in a shader. Pass custom derivatives with textureGrad, or write your own multi-tap filter along any axis you choose. That is useful for procedural textures, stylised blur and experiments where the hardware’s footprint guess is wrong.

textureGrad and custom footprints

GLSL’s textureGrad(sampler, uv, dPdx, dPdy) lets you supply the screen-space derivatives that texture() normally computes for you. Those derivatives define the footprint the GPU assumes, which drives both mip selection and, when the sampler has AF enabled, the hardware’s anisotropic probes. Shrink them to sharpen. Grow them, or grow just one, to blur in a chosen direction.

\#version 300 es
precision highp float;

uniform sampler2D uTex;
uniform float uSharpness; // 1.0 = normal, 0.5 = sharper, 2.0 = softer
in vec2 vUv;
out vec4 fragColor;

void main() {
  vec2 dx = dFdx(vUv) * uSharpness;
  vec2 dy = dFdy(vUv) * uSharpness;
  fragColor = textureGrad(uTex, vUv, dx, dy);
}

WGSL has the same tool in textureSampleGrad. Custom gradients also fix a classic seam bug. When UVs jump inside a fragment quad, for example with fract() tiling or atlas lookups, the automatic derivatives spike and the GPU picks the smallest mip level along the seam. Computing derivatives from the unwrapped coordinates and passing them in removes that line.

For creative work, you can run the hardware formula by hand and point the axis wherever you like. This GLSL ES 3.0 function copies the EXT reference algorithm with textureLod probes. textureLod doesn’t trigger hardware AF, so the result is purely yours:

const int MAX_TAPS = 16;

vec4 manualAniso(sampler2D tex, vec2 uv, float maxAniso) {
  vec2 size = vec2(textureSize(tex, 0));
  vec2 du = dFdx(uv), dv = dFdy(uv);
  float px = length(du * size), py = length(dv * size);  // footprint axes in texels
  float pMax = max(px, py), pMin = max(min(px, py), 1e-4);
  float n = min(ceil(pMax / pMin), maxAniso);           // probes along the long axis
  float lod = log2(max(pMax / n, 1.0));                 // level sized for the short axis
  vec2 axis = px > py ? du : dv;                         // long axis in UV units
  vec4 sum = vec4(0.0);
  for (int i = 0; i < MAX_TAPS; i++) {
    if (float(i) >= n) break;
    float t = (float(i) + 0.5) / n - 0.5;
    sum += textureLod(tex, uv + axis * t, lod);
  }
  return sum / n;
}

Replace axis with a flow-field direction, such as one from a Perlin noise field, and the same loop gives directional streaks and painterly smears. These look nothing like hardware AF, and that is the point.

Stochastic texture filtering

Stochastic texture filtering swaps many fixed taps for one randomly placed tap per pixel per frame, and lets temporal accumulation or denoising average the results. The 2023 paper “Filtering After Shading with Stochastic Texture Filtering”, by Matt Pharr, Bartlomiej Wronski, Marco Salvi and Marcos Fajardo (arXiv 2305.05810), includes an HLSL listing for a stochastic anisotropic LOD computation used alongside screen-space jittering. The approach suits renderers that already rely on temporal upscaling. It also lets you filter after shading, which keeps nonlinear operations such as normal-map lighting correct. The trade-off is noise in any frame that isn’t accumulated.

One naming note before you search further. In offline and path-traced rendering, “anisotropic sampling” also means importance-sampling an anisotropic BRDF, such as the GGX model with separate roughness along tangent and bitangent, used for brushed metal and hair. That is a light-transport topic, unrelated to the texture filtering described here. Searches for “anisotropic GGX sampling” will take you to the right papers.

FAQ

Does anisotropic filtering affect performance?

Slightly. Intel describes the hit as relatively small, and an older PC Gamer BioShock Infinite test measured about 6 FPS between bilinear and 16x. The cost lands only on pixels showing steeply angled surfaces, and it is higher on integrated GPUs that share memory bandwidth.

What is anisotropic vs isotropic?

Isotropic means the same in every direction, and anisotropic means direction-dependent. Isotropic texture filters such as bilinear and trilinear blur equally in all directions. Anisotropic filtering blurs mainly along the stretched direction of a tilted surface, which keeps detail across it.

What is the difference between 8x and 16x anisotropic filtering?

16x lets the GPU take up to 16 probes per pixel instead of 8. That only changes pixels whose footprint is stretched more than 8:1, usually surfaces near the horizon. The visual gain is slight, and on modern discrete GPUs so is the cost.

What is anisotropy in simple terms?

Anisotropy means a property changes with direction. Wood splits along the grain more easily than across it. A floor seen at an angle covers far more texture in depth than in width per pixel. Both are anisotropic.

Should anisotropic filtering be on or off for FPS?

On, in almost every case. AF is one of the cheapest settings for how much it improves the image. If you need frames, lower anti-aliasing, shadows or volumetric effects first, and on integrated graphics drop AF to 8x or 4x before turning it off.

Trilinear vs anisotropic filtering: which is better?

Anisotropic filtering looks better on angled surfaces, and in most APIs it builds on trilinear filtering rather than replacing it. Trilinear removes banding between mip levels. AF removes the blur trilinear leaves on floors and roads. Use both.

Is anisotropic filtering the same as anti-aliasing?

No. Anti-aliasing smooths jagged geometry edges and shimmer caused by the pixel grid. Anisotropic filtering improves how textures are sampled inside surfaces. They fix different artefacts and are set separately.

How do I enable anisotropic filtering in three.js or WebGL?

In three.js, set texture.anisotropy = renderer.capabilities.getMaxAnisotropy(). In WebGL, get the EXT_texture_filter_anisotropic extension, read MAX_TEXTURE_MAX_ANISOTROPY_EXT, and call texParameterf with TEXTURE_MAX_ANISOTROPY_EXT on a mipmapped texture using LINEAR_MIPMAP_LINEAR.

Why does texture.anisotropy have no effect?

Usually the texture has no mipmaps, its minFilter skips mipmaps, or the change was made after first render without needsUpdate. It’s also possible that the surface isn’t tilted enough to need AF. Blur that remains on a well-configured texture comes from the texture’s resolution or its mip chain, and AF can’t add that detail back.

Try it live

This sketch draws a tilted checkerboard floor and filters it in software, using the same probe math GPUs use. The bilinear mode is approximated as a fixed one-texel blur with no mip levels. Press 1 to 7 to switch modes, drag up and down to change the camera pitch, and press space to make the floor drift so you can see shimmer. Try a low pitch and compare mode 3 (trilinear) with mode 7 (16x). Then step down to 2x and watch the blur creep back near the horizon.

// Tilted checkerboard floor: compare texture filtering modes.
// Keys 1-7 pick a mode. Drag up/down to change camera pitch. Space toggles drift.

const W = 240, H = 180;   // internal resolution, scaled up to the canvas
let buf;
let mode = 6;             // start on AF 16x
let pitch = 0.25;         // camera pitch in radians (small = more oblique)
let drift = 0;
let moving = false;
const names = ['Nearest', 'Bilinear (no mipmaps)', 'Trilinear',
               'AF 2x', 'AF 4x', 'AF 8x', 'AF 16x'];
const caps = [0, 0, 1, 2, 4, 8, 16];

function setup() {
  createCanvas(720, 540);
  pixelDensity(1);
  buf = createGraphics(W, H);
  buf.pixelDensity(1);
  noSmooth();
  textFont('monospace');
}

// Project a screen pixel onto the floor and return texture coords [u, v]
function floorUV(px, py) {
  const sx = (px - W / 2) / H;
  const sy = (H / 2 - py) / H;
  const dy = -sin(pitch) + sy * cos(pitch);  // ray direction, height part
  const dz = cos(pitch) + sy * sin(pitch);   // ray direction, depth part
  if (dy >= -0.0005) return null;            // ray misses the floor (sky)
  const t = 1.0 / -dy;                       // camera sits 1 unit above the floor
  return [sx * t * 4, dz * t * 4 + drift];   // 4 checker squares per unit
}

// Box-filtered square wave: +1 / -1 alternating each unit, averaged over width w
function sq(p, w) {
  return 2 * (abs(fract((p - 0.5 * w) / 2) - 0.5) -
              abs(fract((p + 0.5 * w) / 2) - 0.5)) / w;
}

// Checkerboard averaged over a square footprint of width w (an isotropic probe)
function checker(u, v, w) {
  return 0.5 - 0.5 * sq(u, w) * sq(v, w);
}

// Return brightness 0..1 for a pixel, or -1 for sky
function shade(px, py) {
  const c = floorUV(px, py);
  if (!c) return -1;
  if (mode === 0) return (floor(c[0]) + floor(c[1])) & 1; // one point sample
  if (mode === 1) return checker(c[0], c[1], 0.15);       // fixed small blur, no mips
  const a = floorUV(px + 1, py), b = floorUV(px, py + 1);
  if (!a || !b) return 0.5;
  const ax = a[0] - c[0], ay = a[1] - c[1];               // footprint axis 1
  const bx = b[0] - c[0], by = b[1] - c[1];               // footprint axis 2
  const la = Math.hypot(ax, ay), lb = Math.hypot(bx, by);
  const major = max(la, lb);
  const minor = max(min(la, lb), 1e-6);
  if (mode === 2) return checker(c[0], c[1], major);      // trilinear: sized to long axis
  const n = min(ceil(major / minor), caps[mode]);         // probes, capped by AF level
  const w = max(major / n, minor);                        // each probe covers less
  const mx = la > lb ? ax : bx, my = la > lb ? ay : by;   // long axis direction
  let sum = 0;
  for (let i = 0; i < n; i++) {
    const k = (i + 0.5) / n - 0.5;                        // spread probes along the axis
    sum += checker(c[0] + mx * k, c[1] + my * k, w);
  }
  return sum / n;
}

function draw() {
  if (moving) drift += 0.02;
  buf.loadPixels();
  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      const s = shade(x, y);
      const i = 4 * (y * W + x);
      if (s < 0) {
        buf.pixels[i] = 40; buf.pixels[i + 1] = 60; buf.pixels[i + 2] = 90;
      } else {
        const g = 30 + s * 200;
        buf.pixels[i] = g; buf.pixels[i + 1] = g; buf.pixels[i + 2] = g;
      }
      buf.pixels[i + 3] = 255;
    }
  }
  buf.updatePixels();
  image(buf, 0, 0, width, height);

  // Label bar
  noStroke();
  fill(0, 170);
  rect(0, 0, width, 52);
  fill(255);
  textSize(14);
  text('Mode ' + (mode + 1) + ': ' + names[mode] +
       '   pitch ' + nf(degrees(pitch), 1, 1) + ' deg', 12, 20);
  text('Keys 1-7: mode   drag up/down: pitch   space: drift', 12, 40);
  if (!moving) noLoop(); // redraw only when something changes
}

function keyPressed() {
  if (key >= '1' && key <= '7') mode = int(key) - 1;
  if (key === ' ') moving = !moving;
  loop();
  return false;
}

function mouseDragged() {
  pitch = constrain(map(mouseY, 0, height, 0.05, 1.2), 0.05, 1.2);
  loop();
}

To take it further, port shade() to a fragment shader. Then swap the probe axis for a direction of your own and see what kinds of directional blur you can get.

On this article

By Demos Lab Editorial Team

  • Facts were checked against the sources below before publication.
  • Published
Sources (8)
  1. GPUDevice: createSampler() method - MDN (developer.mozilla.org)
  2. GPUSamplerDescriptor - WebGPU types (gpuweb.github.io)
  3. WebKit bug 238063: WebGPU sampler maxAnisotropy clamping (bugs.webkit.org)
  4. EXT_texture_filter_anisotropic - MDN (developer.mozilla.org)
  5. How to increase maxAnisotropy - three.js forum (discourse.threejs.org)
  6. OpenGL 4.6 Released With Vulkan/SPIR-V Ingestion, Parallel Shader Compiles & Finally AF - Phoronix (phoronix.com)
  7. OpenGL Celebrates 25th Anniversary With 4.6 Release - CGW (cgw.com)
  8. VkSamplerCreateInfo - Khronos Vulkan Registry (registry.khronos.org)

Written by Demos Lab Editorial Team

Guides, tutorials and sketches from the Demos Lab team.

All posts