DEV Community

GameDevToolLab
GameDevToolLab

Posted on

Unity Mipmaps Beyond Smaller Textures: Temporal Stability, Streaming, and Semantic Mips

Introduction

Mipmaps are usually introduced as smaller copies of a texture. That is correct, but it does not explain why a no-mipmap screenshot looks sharp while motion shimmers, why foliage disappears, why normal-mapped highlights flash, or why an atlas bleeds only in lower levels.

A more useful model treats a mip chain as three things:

  1. A tool for temporal image stability.
  2. A mechanism for allocating bandwidth and resident memory.
  3. A hierarchy whose levels can preserve different kinds of meaning.

This article uses Unity 6.5 (6000.5) terminology. Inspector names can differ in Unity 2022/2023 LTS, other Unity 6 versions, and between URP and HDRP package versions.

Mipmaps Match Sampling Density, Not Distance

Imagine a 4096 x 4096 texture on a floor viewed at a shallow angle. One screen pixel may cover hundreds of texels. Sampling only mip 0 selects a tiny subset of that footprint, so a small camera movement selects a different subset and produces crawling detail, moire, and flicker.

A mipmap selects a texel density closer to the projected footprint. Distance matters, but it is not the rule. Projected size, UV scale, surface angle, texture resolution, projection, bias, and anisotropy all affect the result.

For ordinary implicit sampling, the GPU estimates UV change across neighboring fragments. In simplified form:

rho = max(length(ddx(texelPosition)), length(ddy(texelPosition)))
lod = log2(rho)
Enter fullscreen mode Exit fullscreen mode

An LOD near 0 points to mip 0; near 1 points to mip 1. Hardware is more sophisticated, especially with anisotropy, but the key model is: screen-space UV derivatives drive mip selection.

Bilinear, trilinear, and anisotropic filtering solve different problems

  • Bilinear: blends texels inside one mip.
  • Trilinear: blends two adjacent mips.
  • Anisotropic: handles elongated footprints on oblique surfaces.

Trilinear filtering does not replace anisotropic filtering. A road may need anisotropic sampling even when mip transitions are smooth.

Ideal LOD and resident quality are separate

The sampler may want mip 0 while Streaming currently has only mip 2 and coarser levels resident. This explains why a surface can be blurry after a camera cut and sharpen later. Always separate the ideal sample LOD from the highest-resolution mip actually available in GPU memory.

The 33% Cost Hides a More Useful Fact

For a square texture, the complete chain is approximately:

Mip 0: 4096 x 4096
Mip 1: 2048 x 2048
Mip 2: 1024 x 1024
...
Mip 12: 1 x 1
Enter fullscreen mode Exit fullscreen mode

Each level contains one quarter as many texels as the previous one:

1 + 1/4 + 1/16 + 1/64 + ... = 4/3
Enter fullscreen mode Exit fullscreen mode

So a full mip chain adds about 33% more texels than mip 0 alone. The reverse view is more useful for budgeting:

Range Approximate share of the full chain
Mip 0 75.00%
Mip 1 18.75%
Mip 2 4.69%
Mip 3 1.17%
Mip 4 and below combined 0.39%

Mip 0 alone is roughly three quarters of the chain. Dropping the maximum resolution by one level does not halve the texel count; it reduces the remaining chain to about one quarter. Two levels reduce it to about one sixteenth.

Compression blocks, minimum mip storage, alignment, and platform details change exact byte counts, but this approximation explains why Mipmap Limits and Mipmap Streaming can have such large effects.

Why “No Mipmaps” Can Win a Screenshot Comparison

Disabling mipmaps can sharpen one frame while making motion unstable. Test fine grids, gravel, leaves, thin text, and repeating patterns with a slow, repeatable camera pan. Temporarily disabling TAA can help isolate texture aliasing.

Watch for crawling patterns, distant moire, flashing highlights, oblique-surface noise, and discontinuities between render scales. Mipmaps are spatial prefilters, but in practice they are also temporal noise reduction. Judge negative mip bias the same way: in motion, not from one sharp frame.

Configure Import Settings by Meaning, Not File Extension

Two PNG files can require opposite settings. Decide from what the shader expects the texels to mean.

Texture meaning sRGB Mipmaps Main concern
Base color / ordinary emissive color Usually on Usually on in 3D Alpha coverage; HDR/data exceptions
Normal map Import as Normal Map Usually on Average direction loses normal variance
Roughness / metallic / AO Off Often on Numerical meaning after filtering
Material ID / category Off Use cautiously Averaging can invent invalid IDs
Fixed-size UI Usually on Often unnecessary Reconsider for scaling, rotation, world space
Pixel art Pipeline-dependent Usually off or custom Point sampling and integer scaling
Lookup/data texture Off Usually off Automatic averaging may corrupt data

sRGB is semantic

Base color is normally color. Roughness, metallic, AO, height, vectors, coefficients, LUT values, and masks are normally linear data. HDR textures and emissive maps that store numerical intensity are common exceptions. Because mip generation is filtering, the wrong color space changes the meaning of lower levels.

Box, Kaiser, and negative bias

Unity's Box filter is smoother; Kaiser retains more sharpness and potentially more shimmer. Negative bias also selects higher-resolution levels, increasing aliasing, bandwidth, cache pressure, and streaming demand. Before using it globally, check anisotropic filtering, UV density, source resolution, render scale, and the active upscaler.

Failure Case 1: Foliage and Fences Disappear

An alpha-tested shader may contain:

clip(alpha - 0.5);
Enter fullscreen mode Exit fullscreen mode

A thin leaf may be alpha 1 against alpha 0. Downsampling creates boundary values such as 0.4, which fail a 0.5 cutoff. The average is not mathematically wrong; the area that survives the threshold, or coverage, changed between mip levels.

Unity's Preserve Coverage adjusts lower levels to keep coverage more stable. Match its cutoff to the shader and check the base pass, shadow-caster pass, Shader Graph threshold, LOD materials, and any Alpha To Coverage path. If the object remains visible but its shadow thins, inspect the shadow pass first.

Replicate Border is different: it preserves image-edge values for cases such as light cookies. It does not replace Preserve Coverage.

Failure Case 2: Normal Maps Still Produce Flashing Highlights

Mipmapping averages normal directions, but an average normal does not describe their distribution. Equal left- and right-facing normals may average forward; that does not make the region smooth. It means variance was discarded. Keeping the original smoothness can then create a sharp, unstable specular lobe around the average direction.

Treat normal filtering and specular stability separately:

  • import the normal map correctly and give it mipmaps;
  • keep roughness/smoothness linear;
  • feed lost normal variance into roughness when possible;
  • consider HDRP's Geometric Specular Anti-Aliasing;
  • in custom shaders, reduce smoothness from normal variation or LOD.

MSAA mainly addresses polygon edges. TAA can hide some instability, but unstable input often trades shimmer for blur or ghosting. Stabilize the material before temporal reconstruction.

Failure Case 3: Packed Masks Share One Mip Policy

Packing metallic, AO, roughness, and other masks into RGBA reduces samples and memory, but all channels still share mip generation, filtering, wrap mode, sRGB state, compression, Mipmap Limit Group, streaming priority, and bias.

Mip filtering operates on channel values, while compression error is not guaranteed to be independent per channel. More importantly, sampling and residency policy are selected at texture granularity. Pack values because they can share that policy, not merely because a channel is empty.

Data Desired reduction behavior
AO Smooth aggregation may be acceptable
Roughness Specular energy matters, not only arithmetic mean
Binary mask Preserve area, majority, or max depending on meaning
Material ID Never invent an intermediate category
Signed direction May require renormalization or reinterpretation

For IDs and categories, consider a separate non-mipmapped texture, point sampling, custom majority/max mips, or a buffer/spatial structure.

Failure Case 4: Atlas Bleeding Gets Worse at Lower Mips

Four pixels of mip-0 padding become two in mip 1, one in mip 2, and half a pixel in mip 3:

effective padding at mip L ~= mip 0 padding / 2^L
Enter fullscreen mode Exit fullscreen mode

Design padding for the lowest level you expect to use. Dilate island colors, increase padding, keep UVs inside their islands, and distinguish texture-edge problems from neighbors inside the atlas.

A Texture2DArray isolates layers and avoids much atlas bleeding, but Unity's Mipmap Streaming does not support array textures. Balance visual stability, batching, streaming, and platform support.

Hand-authored lower mips can go further: detailed sign text can become a bold pictogram at distance. That is the basis of semantic mipmaps.

Mipmap Limits and Streaming as a Quality Scheduler

A Mipmap Limit caps allowed quality; Mipmap Streaming decides which allowed levels are resident. Group textures by visual importance, such as CharacterHero, CharacterCrowd, WorldNear, WorldFar, and Cinematic. One removed top level has a large effect because mip 0 was about 75% of the chain.

Priority is relative, not a reservation for mip 0. A budget shortage can prevent a requested level from loading, so never wait forever. This is a small cutscene-oriented example; a general preloader also needs ownership, cancellation, limit handling, camera-change handling, destruction safety, and budget monitoring.

using System.Collections;
using UnityEngine;

public sealed class MipmapPreloader : MonoBehaviour
{
    [SerializeField] private Texture2D[] textures = { };
    [SerializeField, Min(0)] private int requestedLevel;
    [SerializeField, Min(0.1f)] private float timeoutSeconds = 3f;

    public IEnumerator Preload()
    {
        foreach (Texture2D texture in textures)
            if (CanStream(texture))
                texture.requestedMipmapLevel = Mathf.Clamp(
                    requestedLevel, 0, texture.mipmapCount - 1);

        float deadline = Time.realtimeSinceStartup + timeoutSeconds;
        while (Time.realtimeSinceStartup < deadline)
        {
            bool complete = true;
            foreach (Texture2D texture in textures)
                if (CanStream(texture))
                    complete &= texture.IsRequestedMipmapLevelLoaded();

            if (complete)
                yield break;

            yield return null;
        }

        Debug.LogWarning("Mip preload timed out; continuing with fallback quality.");
    }

    public void Release()
    {
        foreach (Texture2D texture in textures)
            if (CanStream(texture))
                texture.ClearRequestedMipmapLevel();
    }

    private void OnDisable() => Release();

    private static bool CanStream(Texture2D texture) =>
        texture != null && texture.streamingMipmaps;
}
Enter fullscreen mode Exit fullscreen mode

Define the timeout fallback: continue at lower quality, extend a fade, or reduce the request. Release control after the cutscene. Centralize ownership because one caller's ClearRequestedMipmapLevel can clear another caller's expectation.

Unity estimates required levels from meshes, UVs, cameras, and standard material conventions. Array textures, cubemap arrays, and 3D textures are unsupported by Mipmap Streaming; custom drawing and nonstandard UV transforms also need verification.

Success is not “Streaming is enabled.” It is that required-mip estimation and the memory budget remain valid during real camera motion on target hardware.

Visualize the Approximate Mip Level

Subjective reports such as “slightly blurry on one device” are easier to investigate when approximate LOD is visible.

float ApproximateMipLevel(float2 uv, float4 texelSize)
{
    // texelSize = (1/width, 1/height, width, height)
    float2 texelPosition = uv * texelSize.zw;
    float2 dx = ddx(texelPosition);
    float2 dy = ddy(texelPosition);
    float footprint2 = max(dot(dx, dx), dot(dy, dy));
    return max(0.5 * log2(max(footprint2, 1e-8)), 0.0);
}
Enter fullscreen mode Exit fullscreen mode

Color floor(lod) bands to reveal UV-density discontinuities, material bias differences, oblique surfaces, dynamic-resolution changes, and the mip where an atlas starts bleeding.

This is diagnostic, not an exact sampler result. Real sampling also accounts for anisotropy, bias, and platform details. Calculate derivatives before divergent per-fragment branches whenever possible.

Technique 1: Semantic Mipmaps

A mip level can carry a representation designed for its scale rather than an automatic reduction. I will call this a semantic mipmap.

  • replace a shop name with its icon at distance;
  • remove minor roads from lower map levels;
  • thicken selection or emissive marks in coarse mips;
  • fade a detail normal toward neutral;
  • reduce a binary mask by majority or max instead of average.

This is texture LOD: the representation changes, not only its resolution.

Trilinear filtering blends adjacent levels. If mip 2 is text and mip 3 is an icon, the transition contains both. Design blendable levels, transition over several mips, use explicit LOD, or blend separate textures.

Filter and wrap modes are semantic too. A sign may use Trilinear and Clamp; an ID hierarchy may require Point or explicit LOD. Point filtering stops interpolation but cannot repair a mip generated with the wrong reduction rule.

Texture2D.SetPixelData can write every level. The critical detail is Apply(updateMipmaps: false); otherwise Unity regenerates the chain from mip 0.

using System;
using System.Collections.Generic;
using UnityEngine;

public static class SemanticMipTextureBuilder
{
    public static Texture2D Create(
        int width,
        int height,
        IReadOnlyList<Color32[]> mips,
        bool linear,
        FilterMode filterMode,
        TextureWrapMode wrapMode)
    {
        if (width <= 0)
            throw new ArgumentOutOfRangeException(nameof(width));
        if (height <= 0)
            throw new ArgumentOutOfRangeException(nameof(height));

        int expected = Mathf.FloorToInt(
            Mathf.Log(Mathf.Max(width, height), 2f)) + 1;

        if (mips == null || mips.Count != expected)
            throw new ArgumentException($"Exactly {expected} mip levels are required.");

        int mipWidth = width;
        int mipHeight = height;
        for (int level = 0; level < expected; level++)
        {
            if (mips[level] == null || mips[level].Length != mipWidth * mipHeight)
                throw new ArgumentException($"Mip {level} has an invalid pixel count.");

            mipWidth = Mathf.Max(1, mipWidth >> 1);
            mipHeight = Mathf.Max(1, mipHeight >> 1);
        }

        var texture = new Texture2D(
            width, height, TextureFormat.RGBA32,
            mipChain: true, linear: linear)
        {
            filterMode = filterMode,
            wrapMode = wrapMode
        };

        for (int level = 0; level < expected; level++)
            texture.SetPixelData(mips[level], level);

        texture.Apply(updateMipmaps: false, makeNoLongerReadable: false);
        return texture;
    }
}
Enter fullscreen mode Exit fullscreen mode

For shipped assets, an editor or external pipeline that generates levels before platform compression is often easier to manage. Use makeNoLongerReadable: true when the CPU no longer needs the texture.

Technique 2: Use the Mip Chain as a Hierarchical Data Structure

A mip chain repeatedly aggregates each 2 x 2 region. The reduction need not be an average:

  • Average: luminance, exposure, bloom;
  • Min / Max: regional extrema;
  • Depth pyramid: occlusion and screen-space effects;
  • Occupancy: reject empty coarse regions before reading detail;
  • Variance: preserve mean and dispersion.

For custom reduction, create a mipmapped RenderTexture, disable autoGenerateMips, and dispatch a compute shader from level 0 to 1, then 1 to 2. GenerateMips performs ordinary generation; it does not express max, majority vote, or arbitrary aggregation.

This is only an outline. Enable enableRandomWrite before creation, verify GraphicsFormat random-write support, bind the destination mip with ComputeShader.SetTexture(..., mipLevel), define ordering and synchronization, and test every target graphics API.

Mip 0: source 16 x 16
  -> max over each 2 x 2 block
Mip 1: 8 x 8 -> Mip 2: 4 x 4 -> Mip 3: 2 x 2 -> Mip 4: 1 x 1
Enter fullscreen mode Exit fullscreen mode

A coarse occupancy level can terminate a search before fine reads. For depth pyramids, min/max meaning reverses between conventional Z and reversed Z.

Technique 3: Automate Import Rules by Semantic Suffix

Many mip failures are configuration drift. Avoid an ambiguous _mask suffix and split conventions by reduction behavior:

  • _orm: continuous linear data with mipmaps;
  • _cutout: alpha-tested color with Preserve Coverage;
  • _coverage: linear coverage data with Preserve Coverage;
  • _binary / _id: discrete values, no automatic mipmaps, point filtering;
  • separate suffixes for majority, max, or authored semantic mips.
using UnityEditor;
using UnityEngine;

public sealed class TextureImportConvention : AssetPostprocessor
{
    public override uint GetVersion() => 2;

    private void OnPreprocessTexture()
    {
        var importer = (TextureImporter)assetImporter;
        string path = assetPath.Replace('\\', '/').ToLowerInvariant();

        if (path.Contains("/textures/world/"))
        {
            importer.mipmapEnabled = true;
            importer.filterMode = FilterMode.Trilinear;
            importer.streamingMipmaps = true;
        }

        if (HasSuffix(path, "_orm"))
        {
            importer.sRGBTexture = false;
            importer.mipmapEnabled = true;
            importer.filterMode = FilterMode.Trilinear;
        }

        if (HasSuffix(path, "_cutout") || HasSuffix(path, "_coverage"))
        {
            importer.mipmapEnabled = true;
            importer.filterMode = FilterMode.Trilinear;
            importer.mipMapsPreserveCoverage = true;
            importer.alphaTestReferenceValue = 0.5f;

            if (HasSuffix(path, "_coverage"))
                importer.sRGBTexture = false;
        }

        if (HasSuffix(path, "_binary") || HasSuffix(path, "_id"))
        {
            importer.sRGBTexture = false;
            importer.mipmapEnabled = false;
            importer.streamingMipmaps = false;
            importer.filterMode = FilterMode.Point;
        }
    }

    private static bool HasSuffix(string path, string suffix) =>
        path.EndsWith($"{suffix}.png") || path.EndsWith($"{suffix}.tga");
}
Enter fullscreen mode Exit fullscreen mode

Place this in an Editor folder or editor-only assembly. Keep the cutoff synchronized with the shader. Separate mandatory rules, first-import defaults, and convention warnings. Increment GetVersion() when behavior changes.

Dynamic Resolution, Upscalers, and Mip Bias

Lower internal resolution changes UV derivatives and can select coarser mips. A strong global negative bias may sharpen a still image while increasing shimmer, bandwidth, and streaming pressure.

Check the active pipeline and upscaler first. HDRP provides Use Mip Bias for Dynamic Resolution, and its DLSS integration can apply automatic correction. URP/HDRP, DLSS/FSR/STP, package versions, and custom shaders do not apply bias in the same place, so avoid correcting it twice at pipeline, texture, and shader levels.

Compare stationary detail, slow-pan shimmer, GPU time, streaming-budget pressure, and render-scale transitions on one camera path. Output sharpening and sampler bias are different: one modifies the reconstructed image; the other introduces higher-frequency texture input.

A Practical Decision Flow

  1. Will it shrink or rotate on screen? Use mipmaps for most 3D textures; reconsider UI that scales, rotates, or enters world space.
  2. Color or data? Use sRGB for ordinary color, linear for numerical data, and the Normal Map importer for normals.
  3. Alpha tested? Match Preserve Coverage with every shader pass that clips.
  4. Viewed obliquely? Try anisotropic filtering before negative bias.
  5. Atlased? Derive padding and dilation from the lowest required mip.
  6. Memory constrained? Prefer Mipmap Limit Groups and Streaming over disabling mipmaps globally.
  7. Does averaging preserve meaning? Use ordinary generation for color-like signals, Preserve Coverage for cutouts, custom min/max/majority reduction for special data, and semantic mips when the representation should change.

Common Mipmap Myths

Myth Better model
Distance alone selects the mip Screen-space UV derivatives select the ideal LOD
No mipmaps means higher quality It may sharpen a still frame while increasing temporal aliasing
Trilinear fixes an oblique floor Anisotropic footprints need anisotropic filtering
A mip chain nearly doubles memory The theoretical increase is about 33%; mip 0 is about 75% of the chain
High priority guarantees mip 0 Requests can lose to the memory budget
Normal mipmaps guarantee stable highlights Average direction does not preserve normal variance
Any empty packed channel is free All channels share filtering, compression, and residency policy
Atlas padding is a mip-0 decision Effective padding halves at every level
Mips must be automatic reductions Levels can contain semantic replacements or custom aggregates

Conclusion

Mipmaps connect image stability, bandwidth, resident memory, material semantics, and scalable representation. The central question is:

When this texture becomes smaller on screen, what information must survive?

Color may need an average; foliage needs coverage; normals need variance; IDs need category integrity; signs need readability; hierarchical depth needs min or max. Treat every level as a deliberate representation, and mipmaps become a design tool rather than an import checkbox.

References

Top comments (0)