纹理(Texture)既是渲染真实感的素材来源,也是 GPU 显存的最大消耗者——一个 4K 未压缩 RGBA 纹理就占用 32 MB,一个现代 3A 游戏的全部纹理资产往往达到 20~40 GB。理解纹理从采样到显存的完整链路,是图形性能优化的必修课。本文沿着"采样流程 → 过滤 → mipmap → 各向异性 → 压缩 → 内存管理"的顺序,构建完整的纹理知识体系。
一、纹理采样流程与过滤
一句话:纹理采样是把归一化 UV 坐标映射到 texel 并读取颜色的过程,过滤策略决定放大/缩小时的画质与开销。
1.1 采样管线
纹理采样在 GPU 上由专用的 Texture Unit(纹理单元)完成,与着色器流水线并行。一个采样操作涉及:
- UV 坐标计算:顶点属性经透视校正插值得到逐片段 UV。
- 寻址模式:UV 超出 [0,1] 时按寻址模式处理——Repeat(重复)、Clamp(钳制)、Mirror(镜像)、Border(边界色)。
- Mipmap 选择:根据屏幕空间的纹理足迹选择 LOD。
- 过滤:在所选 mip 层内/层间插值。
- 返回颜色:经过 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 的场景。
| 过滤模式 | 采样数 | 画质 | 开销 | 典型用途 |
|---|---|---|---|---|
| Nearest | 1 | 像素化、闪烁 | 极低 | 像素风、体素、阴影图 |
| Bilinear | 4 | 平滑、缩小仍闪烁 | 低 | UI、放大为主 |
| Trilinear | 8 | 层间也平滑 | 中 | 通用 3D 场景 |
| Anisotropic 2x~16x | 8~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 bpp | DX/PC | 1 bit alpha,RGB | 漫反射、法线 |
| BC3 (DXT5) | 8 bpp | DX/PC | 完整 alpha(插值) | 带透明纹理 |
| BC4 / BC5 | 4 / 8 bpp | DX/PC | 单/双通道 | 高度图、法线 |
| BC6H | 8 bpp | DX/PC | HDR 半浮点 | HDR 天空盒、光照图 |
| BC7 | 8 bpp | DX/PC | 高质量 RGBA、8 种模式 | 高质量美术资产 |
| ETC2 | 4/8 bpp | Android/GLES | 兼容性最佳 | Android 通用 |
| ASTC 4x4~12x12 | 0.89~8 bpp | Vulkan/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 |
| 环境贴图 + IBL | 60 套 | 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 Graphics | NVIDIA | GPU 显存占用、纹理采样性能分析、带宽热点 |
| AMD RGP | AMD | 纹理带宽、L2 命中率、压缩率统计 |
| Mali Offline Compiler / Adreno GPU Profiler | 移动 | 纹理单元利用率、带宽瓶颈定位 |
| Chrome DevTools / WebGPU Inspector | Web | 浏览器端纹理上传与 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,哪些要线性
| 纹理类型 | 色彩空间 | 原因 |
|---|---|---|
| 漫反射 Albedo | sRGB | 颜色数据,需要正确解码 |
| 法线贴图 | 线性 | 向量数据,解码会破坏方向 |
| 粗糙度/金属度 | 线性 | 标量材质参数 |
| 高度/位移图 | 线性 | 几何位移数据 |
| 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 分析工具验证每帧纹理带宽与显存峰值,将流送阈值调至预算内。纹理优化的收益通常是立竿见影的——往往一次格式切换就能把带宽开销砍半。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。