纹理采样与 GPU 内存优化:mipmap、压缩格式与寻址

纹理是 GPU 内存消耗的最大头。本文讲解纹理采样流程与过滤(最近邻/双线性/三线性)、mipmap 的原理与生成、各向异性过滤的工作机制,深入对比 BC/ETC/ASTC 等 GPU 纹理压缩格式,并介绍纹理数组、纹理图集、GPU 内存预算、流式加载与调试工具,帮助开发者把显存花在刀刃上。

纹理(Texture)既是渲染真实感的素材来源,也是 GPU 显存的最大消耗者——一个 4K 未压缩 RGBA 纹理就占用 32 MB,一个现代 3A 游戏的全部纹理资产往往达到 20~40 GB。理解纹理从采样到显存的完整链路,是图形性能优化的必修课。本文沿着"采样流程 → 过滤 → mipmap → 各向异性 → 压缩 → 内存管理"的顺序,构建完整的纹理知识体系。


一、纹理采样流程与过滤

一句话:纹理采样是把归一化 UV 坐标映射到 texel 并读取颜色的过程,过滤策略决定放大/缩小时的画质与开销。

1.1 采样管线

纹理采样在 GPU 上由专用的 Texture Unit(纹理单元)完成,与着色器流水线并行。一个采样操作涉及:

  1. UV 坐标计算:顶点属性经透视校正插值得到逐片段 UV。
  2. 寻址模式:UV 超出 [0,1] 时按寻址模式处理——Repeat(重复)、Clamp(钳制)、Mirror(镜像)、Border(边界色)。
  3. Mipmap 选择:根据屏幕空间的纹理足迹选择 LOD。
  4. 过滤:在所选 mip 层内/层间插值。
  5. 返回颜色:经过 sRGB 解码(若纹理标记为 sRGB 格式)后传入着色器。
// GLSL:显式控制采样过程
uniform sampler2D uTexture;
uniform float uMipBias;

vec4 sampleWithControl(vec2 uv) {
    // 显式 LOD 采样:texelFetch 直达指定 mip 层,不做过滤
    vec4 t0 = texelFetch(uTexture, ivec2(uv * vec2(textureSize(uTexture, 0))), 0);

    // 显式 LOD + 双线性过滤
    vec4 t1 = textureLod(uTexture, uv, 2.0);

    // 各向异性采样
    vec4 t2 = textureGrad(uTexture, uv, dFdx(uv), dFdy(uv));
    return t1;
}

1.2 放大与缩小过滤

  • 放大过滤(Magnification):UV 覆盖小于一个 texel,即一个屏幕像素对应多个 texel 采样点。最近邻(Nearest)产生像素块,双线性(Bilinear)在 2×2 texel 间插值,视觉平滑。
  • 缩小过滤(Minification):一个 texel 被多个屏幕像素共享,若直接最近邻会产生严重闪烁与摩尔纹——这就是必须引入 mipmap 的场景。
过滤模式采样数画质开销典型用途
Nearest1像素化、闪烁极低像素风、体素、阴影图
Bilinear4平滑、缩小仍闪烁低UI、放大为主
Trilinear8层间也平滑中通用 3D 场景
Anisotropic 2x~16x8~128倾斜视角锐利高地面、道路等掠射面

二、mipmap 原理

一句话:mipmap 是一系列预生成的逐级减半分辨率副本,采样时按屏幕覆盖率自动选择合适层级,同时消除摩尔纹与闪烁、加速采样。

2.1 为什么需要 mipmap

当一个远处地面上的纹理缩小到每 texel 只占不到一个屏幕像素时,直接点采样会触发时间混叠:相邻帧的采样点落位不同,导致高频细节随机闪烁(shimmering),摩尔纹(Moiré pattern)随之出现。mipmap 将纹理预滤波成低分辨率版本,采样时根据屏幕覆盖率选择"恰好让一个 mip texel 约等于一个屏幕像素"的层级,从根源上滤掉高于屏幕频率的细节。

2.2 LOD 计算

每个 mip 层的尺寸为 max(1, floor(baseSize / 2^level))。硬件按屏幕空间 UV 的导数计算 LOD:

LOD = log2( max( |du/dx| * width, |dv/dy| * height ) )

du/dx、dv/dy 是 UV 沿屏幕 x/y 方向的偏导数(屏幕空间覆盖率),GPU 的纹理单元自动计算;开发者也可用 textureLod 显式指定,或用 textureGrad 传入自定义导数(如反射方向的导数,用于环境贴图)。

// C++/Vulkan:生成完整 mipmap 链
void generateMipmaps(VkCommandBuffer cmd, VkImage image,
                     uint32_t width, uint32_t height, uint32_t levels) {
    VkImageMemoryBarrier barrier = {
        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        .image = image,
        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1},
    };
    for (uint32_t i = 1; i < levels; i++) {
        // 将上一级从 shader read 转为 blit src,将本级从 undefined 转为 blit dst
        barrier.subresourceRange.baseMipLevel = i - 1;
        barrier.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
        barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
        vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
                             VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
                             0, nullptr, 0, nullptr, 1, &barrier);

        VkImageBlit blit = {};
        blit.srcOffsets[1] = { (int32_t)std::max(1u, width >> (i - 1)), (int32_t)std::max(1u, height >> (i - 1)), 1 };
        blit.dstOffsets[1] = { (int32_t)std::max(1u, width >> i), (int32_t)std::max(1u, height >> i), 1 };
        blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        blit.srcSubresource.mipLevel = i - 1;
        blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        blit.dstSubresource.mipLevel = i;
        vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                       image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
    }
    // 末尾将最后一层转为 shader read
}

2.3 Mip Bias 与纹理锐度

开发者可设置 minLod/maxLod/mipLodBias 微调层选择:mip bias +1 会让整体采样更糊(常用于远处物体统一降档),-0.5~-1 则让纹理更锐利(代价是采样更小的 mip、增加闪烁)。高分辨率贴图(如 4096²)配合偏高的 bias 可显著减少显存带宽。


三、各向异性过滤

一句话:双线性/三线性假定屏幕上的纹理足迹是"正方形",遇到掠射角地面会糊成一片;各向异性过滤按足迹的实际椭圆形状采样,让倾斜表面依然锐利。

当纹理表面与屏幕近乎平行时(如远处地面),UV 在屏幕上的变化率在某个方向远大于另一方向,纹理足迹是一个极扁的椭圆。三线性只会在这个椭圆里选一个近似正方形的 mip,结果是对角线方向被严重模糊。各向异性过滤(Anisotropic Filtering, AF)沿着足迹最长轴方向采样多个点,再与正交方向做双线性,逼近"任意方向都锐利"。

// C++/Vulkan:创建各向异性采样器
VkSamplerCreateInfo samplerInfo = {};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = VK_FILTER_LINEAR;
samplerInfo.minFilter = VK_FILTER_LINEAR;
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;   // 三线性
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.maxAnisotropy = 8.0f;   // 1/2/4/8/16,8 是画质/性能甜点
// 启用各向异性需查询设备特性:VkPhysicalDeviceFeatures.samplerAnisotropy
samplerInfo.anisotropyEnable = VK_TRUE;

采样开销随各向异性倍率上升,但现代 GPU 硬件优化后,4x/8x 的额外开销通常可忽略。注意:各向异性过滤只作用于缩小过滤,且对阴影贴图、SSAO 这类比较采样无意义,应单独使用无各向异性的采样器。


四、纹理压缩格式:BC/ETC/ASTC

一句话:GPU 纹理压缩把 4×4 texel 块编码为固定位数(如 BC1 为 64 位、BC7/ASTC 为 128 位),显存占用直降 4~8 倍,且支持硬件在采样时即时解压,无需 CPU 预解压。

4.1 压缩原理

GPU 压缩格式(Block Compression)与 PNG/JPEG 不同:它以固定大小块为单位,每块独立编码,GPU 采样时按块索引直接读取并解码,无随机访问代价。以 BC1 为例:每 4×4 texel 块存 2 个 16 位端点色(RGB565),中间 6 色由线性插值得到,每个 texel 用 2 位索引选色——共 64 位/块,即 4 位/texel(RGBA8888 的 1/8)。

4.2 主流格式对比

格式位率平台特点适用场景
BC1 (DXT1)4 bppDX/PC1 bit alpha,RGB漫反射、法线
BC3 (DXT5)8 bppDX/PC完整 alpha(插值)带透明纹理
BC4 / BC54 / 8 bppDX/PC单/双通道高度图、法线
BC6H8 bppDX/PCHDR 半浮点HDR 天空盒、光照图
BC78 bppDX/PC高质量 RGBA、8 种模式高质量美术资产
ETC24/8 bppAndroid/GLES兼容性最佳Android 通用
ASTC 4x4~12x120.89~8 bppVulkan/GLES 3.1+块大小可变、HDR 支持跨平台、WebGPU

ASTC(Adaptive Scalable Texture Compression) 是移动与 Web 端的事实标准:块尺寸可从 4×4 到 12×12 自由选择(4×4 最清晰、12×12 最省),支持 HDR(ASTC HDR)与 3D 纹理,压缩率/质量比远超 ETC2。缺点是硬件解压器功耗略高,且低端机加载时间较长。

4.3 法线贴图压缩要点

法线贴图直接压成 RGB(BC1/BC7)会损失精度导致高光闪烁。最佳实践:

  • 两通道方案:只存 X、Y,重建 Z = sqrt(1 - x² - y²),用 BC5(双通道,各 8 位)压缩,误差极小。
  • 八面体编码(Octahedral):将单位向量编码到 2D 再压 BC5/BC7,常用于 G-Buffer 法线。
// GLSL:BC5 两通道法线重建
vec3 decodeNormal(vec2 rg) {
    vec3 n;
    n.xy = rg * 2.0 - 1.0;
    n.z = sqrt(clamp(1.0 - dot(n.xy, n.xy), 0.0, 1.0));
    return normalize(n);
}

4.4 压缩工具链

  • 桌面:DirectXTex texconv(BC1~BC7)、NVTT、Compressonator(AMD,支持 BC/ASTC 全格式)。
  • 移动/Web:etc2comp(ETC2)、astc-encoder(Arm,官方 ASTC 编码器,支持高质量慢速档)。
  • WebGPU 现状:"texture-compression-bc"、"texture-compression-astc"、"texture-compression-etc2" 三个扩展对应桌面与移动平台,编码需在离线完成,运行时仅解码。

五、纹理数组与纹理图集

一句话:纹理数组(Texture Array)用同一采样器绑定多张同尺寸纹理,消除切换纹理时的状态切换与 Draw Call 开销;图集(Atlas)则把不同纹理打包进一张图,以寻址换来更少绑定。

5.1 Texture Array

Vulkan 的 VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT 创建多层 2D 纹理,采样器通过 layer 维度选择层:

// WGSL:采样纹理数组
@group(0) @binding(0) var texArray: texture_2d_array<f32>;
@group(0) @binding(1) var samp: sampler;

@fragment
fn main(@location(0) uv: vec2f, @location(1) layer: f32) -> @location(0) vec4f {
    let color = textureSample(texArray, samp, uv, i32(layer));
    return color;
}

优势:一张纹理数组只需一次 vkCmdBindDescriptorSets,大量物体共享一个采样器;配合实例化渲染(Instanced Rendering),每实例传一个 layer 索引,即可实现"一次 Draw Call 渲染多种不同贴图物体"。

5.2 Texture Atlas 与 Mip Bleeding

图集把多张小图打包进大图,减少绑定切换。但 mipmap 会在图集边界采样到"邻居"内容,产生 Mip Bleeding。解决方案:

  • 各小图之间留出 padding 边界(4~8 texel),并在边界重复内容;
  • 使用 Texture Array 替代图集(更现代,各层独立 mip,无 bleeding);
  • 图集配合 textureGrad 手动限界采样(复杂,不推荐)。

5.3 虚拟纹理(Virtual / MegaTexture)

大型开放世界将整块地表纹理划分成 1024²~2048² 的页(Page),维护一张 Page Table(间接寻址),运行时按需从磁盘/压缩流中加载页到 GPU 缓存。这就是 虚拟纹理 / MegaTexture 技术(id Software 提出,现代引擎的纹理流送方案,如 UE5 的 Virtual Texture)。采样通过 Page Table 二次寻址,命中率是性能关键。


六、GPU 内存预算与流送

一句话:显存不是无限的——制定内存预算、按 LOD/距离流送纹理、复用资源,是大型项目不 OOM 的底线工程。

6.1 内存预算模型

一张 W×H 的 BC7 纹理完整 mip 链占内存约为 8/8 × W×H × 4/3 ≈ 1.33 × W×H 字节(mip 链总和约为基础层 1.33 倍)。粗估公式:base_bpp × W × H × 4 / 3。

以 4 GB 显存的中端 GPU 为参考预算:

资产类型数量单资产预算占比
角色/场景 PBR 贴图300 套BC7 2048² ≈ 5.5 MB~2 GB
环境贴图 + IBL60 套BC6H 1024² ≈ 1.3 MB~80 MB
阴影/后处理 RT—动态分配~300 MB
顶点/索引缓冲—压紧格式~400 MB

6.2 纹理流送(Streaming)

流送系统按相机距离与屏幕占比决定每张纹理的驻留 mip 级别:

  • 常驻(0 级 + 备用):UI、最近场景、角色皮肤——最优先级。
  • 需求加载:进入视野范围内触发后台加载,加载完成前先用低 mip 占位。
  • LRU 驱逐:长期不可见资产按最近最少使用淘汰,必要时丢弃整个纹理。
// C++:流送优先级计算
float texturePriority(float distance, float screenSize, float minDist, float maxDist) {
    float t = clamp((distance - minDist) / (maxDist - minDist), 0.0f, 1.0f);
    // 屏幕占比越大、距离越近,优先级越高
    return screenSize * (1.0f - t);
}

6.3 Vulkan 显存分配实践

  • 使用 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 的显存承载纹理;上传走暂存缓冲(Staging Buffer),避免每次更新走 PCIe。
  • 稀疏纹理(Sparse Texture):VK_IMAGE_CREATE_SPARSE_BINDING_BIT 允许纹理不同 mip/层绑定到不同显存页,是虚拟纹理与流送的底层支撑。
  • 减少采样带宽:小格式(R8 → BC1)、低 mip bias、压缩采样器、避免在着色器中重复采样同一纹理。

七、纹理调试工具

一句话:纹理问题(花屏、闪烁、糊、OOM)需要专业的 GPU 工具观察显存与采样行为,而不是猜。

工具平台能力
RenderDoc多平台逐资源查看纹理、mip 链、反汇编采样指令
Nsight GraphicsNVIDIAGPU 显存占用、纹理采样性能分析、带宽热点
AMD RGPAMD纹理带宽、L2 命中率、压缩率统计
Mali Offline Compiler / Adreno GPU Profiler移动纹理单元利用率、带宽瓶颈定位
Chrome DevTools / WebGPU InspectorWeb浏览器端纹理上传与 GPU 内存可视化

常见纹理故障定位速查:

  • 花屏/彩色噪点:格式不匹配(数据格式与 shader 解释不一致)、sRGB 标记错误。
  • 远处闪烁/摩尔纹:缺 mipmap 或 mip 生成错误,检查 minFilter = VK_FILTER_LINEAR 与完整 mip 链。
  • 低模表面过糊:AF 未开、mip bias 过大。
  • 显存 OOM/闪烁:流送预算超限,优先压缩格式与低 mip 占位。

八、sRGB 与色彩管理

一句话:纹理像素的存储值与物理亮度是曲线关系(sRGB 编码);采样后必须先解码到线性再做光照计算,否则 PBR 光照会整体偏暗偏灰。

8.1 为什么纹理要区分 sRGB 与线性

显示设备对电压的响应是近幂函数(Gamma ≈ 2.2),因此图像存储时通常做sRGB 编码(encoded = pow(linear, 1/2.2) 的近似),让 8 位整数在暗部也分布足够多的阶数,避免暗部色带(Banding)。代价是:直接把这些值当线性亮度参与光照数学(乘法、求和、BRDF),结果会系统性偏暗。正确流程:

纹理读取(sRGB 编码) → GPU 硬件/着色器解码为线性 → 光照/混合(线性)
→ 色调映射 → sRGB 编码 → 显示

现代 API 的做法是给颜色纹理标记 sRGB 格式(VK_FORMAT_R8G8B8A8_SRGB、SRGB8_ALPHA8_ASTC*),GPU 采样时自动解码,写回时自动编码,着色器代码完全无感。

// GLSL:手动 sRGB 解码(若格式未标记 sRGB)
vec3 srgbToLinear(vec3 c) {
    return mix(pow((c + 0.055) / 1.055, vec3(2.4)),
               c / 12.92, lessThanEqual(c, vec3(0.04045)));
}
vec3 linearToSrgb(vec3 c) {
    return mix(1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055,
               12.92 * c, lessThanEqual(c, vec3(0.0031308)));
}

8.2 哪些纹理要 sRGB,哪些要线性

纹理类型色彩空间原因
漫反射 AlbedosRGB颜色数据,需要正确解码
法线贴图线性向量数据,解码会破坏方向
粗糙度/金属度线性标量材质参数
高度/位移图线性几何位移数据
HDR 环境贴图线性(半浮点)物理亮度
阴影/深度图线性深度比较语义

一句话:只有"描述颜色"的纹理用 sRGB,所有描述"物理量"(法线、粗糙度、高度、深度)的纹理必须线性。标错会直接导致 PBR 材质偏色——这是新手最常踩的坑。

8.3 压缩格式与 sRGB 的交互

  • sRGB 解码由 GPU 硬件在解压之后执行:BC7_sRGB、ASTC_4x4_sRGB 等格式既压缩又编码,采样一次完成解码+解压,无额外成本。
  • 压缩编码器(BC7/ASTC)在 sRGB 空间压缩更高效(视觉权重集中于暗部),工具链应选择匹配的 sRGB 格式。
  • 采样 sRGB 纹理做 texelFetch(直达 texel 跳过过滤)时,不会自动解码,需要手动 srgbToLinear——这是 mip 生成、后处理读颜色时的常见遗漏。

总结

纹理系统是"画质 × 显存 × 带宽"三角的平衡木。采样过滤决定画质下限,mipmap 决定缩小场景的稳定性,各向异性过滤决定掠射角锐度,块压缩(BC/ASTC)直接决定显存与带宽预算,纹理数组/虚拟纹理决定 GPU 驱动效率与流送能力。

决策点推荐实践收益
过滤策略Trilinear + 8x AF通用场景画质甜点
压缩格式桌面 BC7 / BC6H,跨平台 ASTC显存降 4~8 倍
法线贴图BC5 双通道 + 重建 Z精度损失最小
绑定优化Texture Array + 实例化Draw Call 大降
大世界虚拟纹理 + LRU 流送内存预算可控
调试RenderDoc + Nsight 联合快速定位故障

实践建议:先为项目建立一张"格式 × 平台"矩阵表,明确每个平台的主用压缩格式;再为每个资产管线接入离线压缩工具;最后用 GPU 分析工具验证每帧纹理带宽与显存峰值,将流送阈值调至预算内。纹理优化的收益通常是立竿见影的——往往一次格式切换就能把带宽开销砍半。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机图形学」更多文章

  1. 光线追踪与混合渲染:BVH、路径追踪与 RTX
  2. 骨骼动画与蒙皮:骨骼层级、动画混合与 GPU 蒙皮
  3. 大地形与开放世界渲染:LOD、分块与实例化