上一篇:二进制网格格式 | 下一篇:可写纹理 | 返回目录
📚 快速导航
目录
📖 简介
在之前的教程中,我们实现了纹理系统,可以加载和使用纹理。但我们一直使用默认采样器,没有控制纹理的采样方式。
本教程将深入探讨纹理采样器 (Texture Samplers):
- 如何控制纹理过滤 (放大/缩小)
- 如何处理纹理边界 (包裹模式)
- 如何提高斜视角纹理质量 (各向异性过滤)
- 如何在材质系统中配置采样器
Texture Sampling Pipeline 纹理采样管线 1. 坐标处理 2. 纹理查找 3. 质量增强 4. 输出 UV Coordinates
纹理坐标
u=0.5, v=0.3 Wrap Mode
包裹模式
处理超出范围 Filtering
过滤
插值计算 Anisotropic
各向异性
斜视角优化 Final Color
最终颜色
为什么重要?
采样器配置的影响:
┌─────────────────────────────────────┐
│ 默认采样器 (Nearest + Repeat) │
│ ┌─────────────────────┐ │
│ │ ████ ████ ████ │ ← 像素化 │
│ │ ████ ████ ████ │ │
│ │ ████ ████ ████ │ │
│ └─────────────────────┘ │
│ • 锯齿严重 │
│ • 远处模糊 │
│ • 质量差 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 优化采样器 (Linear + Mipmap + Aniso)│
│ ┌─────────────────────┐ │
│ │ ▓▓▓▓░░▓▓▓▓░░▓▓▓▓ │ ← 平滑 │
│ │ ▓▓▓▓░░▓▓▓▓░░▓▓▓▓ │ │
│ │ ░░░░▓▓░░░░▓▓░░░░ │ │
│ └─────────────────────┘ │
│ • 边缘平滑 │
│ • 远处清晰 │
│ • 质量高 │
└─────────────────────────────────────┘
正确的采样器配置可以:
✓ 提升视觉质量 10 倍
✓ 消除锯齿和闪烁
✓ 减少 Moire 纹 (摩尔纹)
✓ 优化 GPU 性能
🎯 学习目标
| 目标 | 描述 |
|---|---|
| 理解纹理采样 | 掌握纹理采样的完整流程 |
| 掌握过滤模式 | 理解 Nearest、Linear、Mipmap |
| 掌握包裹模式 | 理解 Repeat、Clamp、Mirror |
| 理解各向异性 | 掌握各向异性过滤的原理和应用 |
| 实现采样器系统 | 在 Vulkan 中创建和管理采样器 |
🔍 什么是纹理采样器
纹理采样过程
从纹理中获取颜色的过程:
纹理采样步骤:
┌────────────────────────────────────┐
│ 1. Fragment Shader 输入 │
│ • UV 坐标: (u, v) │
│ • 例: u=0.5, v=0.3 │
└──────────────┬─────────────────────┘
│
▼
┌────────────────────────────────────┐
│ 2. 包裹模式处理 │
│ • 如果 u < 0 或 u > 1,如何处理? │
│ • Repeat: u = fract(u) │
│ • Clamp: u = clamp(u, 0, 1) │
└──────────────┬─────────────────────┘
│
▼
┌────────────────────────────────────┐
│ 3. 纹理坐标 → 像素坐标 │
│ • u × texture_width │
│ • v × texture_height │
│ • 例: 0.5 × 512 = 256.0 │
└──────────────┬─────────────────────┘
│
▼
┌────────────────────────────────────┐
│ 4. 过滤/插值 │
│ • Nearest: 取最近像素 │
│ • Linear: 双线性插值 4 个像素 │
│ • Mipmap: 选择合适的 LOD 级别 │
└──────────────┬─────────────────────┘
│
▼
┌────────────────────────────────────┐
│ 5. 输出颜色 │
│ • RGBA 值 │
└────────────────────────────────────┘
GLSL 代码示例:
glsl
// Fragment Shader
// 输入
in vec2 in_texcoord; // UV 坐标
// Uniforms
uniform sampler2D u_texture; // 纹理 + 采样器
void main() {
// 采样纹理 (自动应用采样器配置)
vec4 color = texture(u_texture, in_texcoord);
// ^^^^^^^ 采样函数
// ^^^^^^^^^^ 纹理
// ^^^^^^^^^^^^ UV 坐标
out_color = color;
}
采样器的作用
采样器 (Sampler) 是一个配置对象,控制如何从纹理中读取数据:
c
// 采样器配置
typedef struct texture_sampler {
// 过滤模式
texture_filter min_filter; // 缩小过滤 (纹理比屏幕小)
texture_filter mag_filter; // 放大过滤 (纹理比屏幕大)
texture_filter mip_filter; // Mipmap 过滤
// 包裹模式
texture_wrap wrap_u; // U 轴包裹 (水平)
texture_wrap wrap_v; // V 轴包裹 (垂直)
texture_wrap wrap_w; // W 轴包裹 (3D 纹理)
// 高级设置
f32 max_anisotropy; // 各向异性级别 (1.0 ~ 16.0)
vec4 border_color; // 边框颜色 (Clamp to Border 模式)
} texture_sampler;
采样器 vs 纹理:
分离的设计:
┌─────────────────────────────────┐
│ Texture (纹理数据) │
│ • 像素数据 (RGBA) │
│ • 宽度、高度 │
│ • Mipmap 级别 │
│ • GPU 内存 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Sampler (采样器配置) │
│ • 过滤模式 │
│ • 包裹模式 │
│ • 各向异性 │
│ • 无数据存储 │
└─────────────────────────────────┘
优点:
✓ 同一纹理可以用多个采样器
✓ 同一采样器可以用于多个纹理
✓ 灵活配置
示例:
Texture "brick.png"
+ Sampler A (Repeat, Linear) → 地面瓦片
+ Sampler B (Clamp, Nearest) → UI 图标
为什么需要采样器
不同的纹理用途需要不同的采样方式:
| 用途 | 采样器配置 | 原因 |
|---|---|---|
| 地面瓦片 | Repeat + Linear + Mipmap | 无缝重复,平滑过渡 |
| UI 图标 | Clamp + Nearest | 像素完美,无模糊 |
| 天空盒 | Clamp + Linear | 边缘无缝,平滑渐变 |
| 地形纹理 | Repeat + Anisotropic | 斜视角清晰 |
| 字体 | Clamp + Linear | 边缘平滑,无重复 |
🔬 纹理过滤
放大过滤
当纹理比屏幕像素少时 (纹理被拉伸):
放大情况:
┌────────────────────────────────┐
│ 纹理: 4×4 像素 │
│ ┌─┬─┬─┬─┐ │
│ │R│G│B│A│ │
│ ├─┼─┼─┼─┤ │
│ │E│F│G│H│ │
│ ├─┼─┼─┼─┤ │
│ │I│J│K│L│ │
│ ├─┼─┼─┼─┤ │
│ │M│N│O│P│ │
│ └─┴─┴─┴─┘ │
└────────────────────────────────┘
↓ 放大到
┌────────────────────────────────┐
│ 屏幕: 16×16 像素 │
│ • 每个纹理像素对应 4×4 屏幕像素│
└────────────────────────────────┘
Nearest (最近邻过滤):
c
// 伪代码
vec4 sample_nearest(texture, uv) {
int x = (int)(uv.x * texture.width);
int y = (int)(uv.y * texture.height);
return texture.data[y][x]; // 直接返回最近的像素
}
可视化效果:
Nearest 过滤 (放大):
┌────────────────────────────────┐
│ ████████ ████████ ████████ │
│ ████████ ████████ ████████ │
│ ████████ ████████ ████████ │
│ ████████ ████████ ████████ │ ← 块状,像素化
│ │
│ ████████ ████████ ████████ │
│ ████████ ████████ ████████ │
└────────────────────────────────┘
优点: 快速,像素完美
缺点: 锯齿严重,块状
用途: 像素艺术,UI 图标
Linear (线性过滤):
c
// 伪代码
vec4 sample_linear(texture, uv) {
float x = uv.x * texture.width;
float y = uv.y * texture.height;
// 获取周围 4 个像素
int x0 = (int)x;
int y0 = (int)y;
int x1 = x0 + 1;
int y1 = y0 + 1;
vec4 c00 = texture.data[y0][x0]; // 左上
vec4 c10 = texture.data[y0][x1]; // 右上
vec4 c01 = texture.data[y1][x0]; // 左下
vec4 c11 = texture.data[y1][x1]; // 右下
// 双线性插值
float fx = fract(x); // x 的小数部分
float fy = fract(y); // y 的小数部分
vec4 c0 = mix(c00, c10, fx); // 上边插值
vec4 c1 = mix(c01, c11, fx); // 下边插值
return mix(c0, c1, fy); // 垂直插值
}
可视化效果:
Linear 过滤 (放大):
┌────────────────────────────────┐
│ ████▓▓▓▓░░░░▓▓▓▓████ │
│ ████▓▓▓▓░░░░▓▓▓▓████ │
│ ▓▓▓▓▒▒▒▒░░░░▒▒▒▒▓▓▓▓ │ ← 平滑渐变
│ ░░░░░░░░████░░░░░░░░ │
│ │
│ ▓▓▓▓▒▒▒▒░░░░▒▒▒▒▓▓▓▓ │
└────────────────────────────────┘
优点: 平滑,无锯齿
缺点: 稍慢,可能模糊
用途: 大多数 3D 纹理
缩小过滤
当纹理比屏幕像素多时 (纹理被缩小):
缩小情况:
┌────────────────────────────────┐
│ 纹理: 512×512 像素 │
│ (很多细节) │
└────────────────────────────────┘
↓ 缩小到
┌────────────────────────────────┐
│ 屏幕: 64×64 像素 │
│ • 每个屏幕像素对应 8×8 纹理像素│
│ • 需要平均 64 个纹理像素! │
└────────────────────────────────┘
问题:
• Nearest/Linear 只采样 1 或 4 个像素
• 丢失大量细节
• 产生 Aliasing (锯齿) 和闪烁
解决方案: Mipmap
Mipmap
Mipmap 是一组预先缩小的纹理链:
Mipmap 金字塔:
┌────────────────────────────────┐
│ Level 0: 512×512 (原始) │
│ ┌──────────────────────────┐ │
│ │ 原始纹理,最高细节 │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
↓ 缩小 2 倍
┌────────────────────────────────┐
│ Level 1: 256×256 │
│ ┌──────────────┐ │
│ │ 平均每 4 像素 │ │
│ └──────────────┘ │
└────────────────────────────────┘
↓ 缩小 2 倍
┌────────────────────────────────┐
│ Level 2: 128×128 │
│ ┌────────┐ │
│ │ │ │
│ └────────┘ │
└────────────────────────────────┘
↓ ...
┌────────────────────────────────┐
│ Level 9: 1×1 (最小) │
│ ┌┐ │
│ └┘ │
└────────────────────────────────┘
总共 10 个级别 (log2(512) + 1)
额外内存: ~33% (1/4 + 1/16 + 1/64 + ...)
Mipmap 采样:
c
// 根据屏幕像素大小选择 Mipmap 级别
float calculate_mipmap_level(vec2 texcoord, vec2 texcoord_dx, vec2 texcoord_dy) {
// 计算纹理坐标在屏幕上的变化率
float dx = length(texcoord_dx * texture_size);
float dy = length(texcoord_dy * texture_size);
float max_delta = max(dx, dy);
// LOD (Level of Detail) = log2(max_delta)
float lod = log2(max_delta);
return clamp(lod, 0.0, max_mipmap_level);
}
// 示例:
// 远处:max_delta = 8.0 → LOD = 3 (使用 Level 3,更小的纹理)
// 近处:max_delta = 1.0 → LOD = 0 (使用 Level 0,原始纹理)
Mipmap 过滤模式:
| 模式 | 描述 | 质量 | 性能 |
|---|---|---|---|
| Nearest Mipmap Nearest | 选择最近的 Mipmap,最近的像素 | 低 | 最快 |
| Linear Mipmap Nearest | 选择最近的 Mipmap,线性插值 | 中 | 快 |
| Nearest Mipmap Linear | 两个 Mipmap 间插值,最近像素 | 中 | 中 |
| Linear Mipmap Linear (Trilinear) | 两个 Mipmap 间插值 + 像素插值 | 高 | 慢 |
Trilinear 过滤 (三线性过滤):
Trilinear 采样步骤:
1. 计算 LOD (Level of Detail)
例: LOD = 2.3
2. 选择两个 Mipmap 级别
Level 2 (权重 0.7)
Level 3 (权重 0.3)
3. 在 Level 2 中双线性插值
sample_2 = bilinear(level_2, uv)
4. 在 Level 3 中双线性插值
sample_3 = bilinear(level_3, uv)
5. 在两个级别间线性插值
final_color = mix(sample_2, sample_3, 0.3)
总共 8 个纹理采样:
• Level 2: 4 个像素
• Level 3: 4 个像素
🔁 纹理包裹模式
处理 UV 坐标超出 [0, 1] 范围的情况。
Repeat重复
最常用的模式,纹理无限重复:
c
// Repeat 模式
vec2 wrapped_uv;
wrapped_uv.x = fract(uv.x); // 取小数部分
wrapped_uv.y = fract(uv.y);
// 示例:
// uv = (2.3, -0.7)
// wrapped = (0.3, 0.3) // -0.7 → 0.3 (fract 处理负数)
可视化:
Repeat 模式:
UV: -1.0 0.0 1.0 2.0 3.0
│ │ │ │ │
┌────┼──────┼──────┼──────┼──────┤
│🟥🟦│🟥🟦🟦│🟥🟦🟦│🟥🟦🟦│🟥🟦🟦│
│🟦🟥│🟦🟥🟥│🟦🟥🟥│🟦🟥🟥│🟦🟥🟥│
└────┴──────┴──────┴──────┴──────┘
↑ ↑ ↑ ↑ ↑
重复 原始纹理 重复 重复 重复
用途:
• 地面瓦片 (地砖、草地)
• 重复纹理 (墙壁、地板)
• 天空盒 (部分)
Clamp限制
将 UV 限制在 [0, 1] 范围内:
c
// Clamp to Edge 模式
vec2 wrapped_uv;
wrapped_uv.x = clamp(uv.x, 0.0, 1.0);
wrapped_uv.y = clamp(uv.y, 0.0, 1.0);
// 示例:
// uv = (2.3, -0.7)
// wrapped = (1.0, 0.0)
可视化:
Clamp to Edge 模式:
UV: -1.0 0.0 1.0 2.0 3.0
│ │ │ │ │
┌────┼──────┼──────┼──────┼──────┤
│🟥🟥│🟥🟦🟦│🟦🟦🟦│🟦🟦🟦│🟦🟦🟦│
│🟦🟦│🟦🟥🟥│🟥🟥🟥│🟥🟥🟥│🟥🟥🟥│
└────┴──────┴──────┴──────┴──────┘
↑ ↑ ↑ ↑ ↑
边缘像素 原始纹理 边缘像素扩展
用途:
• UI 纹理 (不重复)
• 天空盒 (避免接缝)
• 单次贴图 (海报、标志)
Mirror镜像
镜像重复纹理:
c
// Mirror Repeat 模式
vec2 wrapped_uv;
float u = fract(uv.x);
wrapped_uv.x = ((int)(uv.x) % 2 == 0) ? u : (1.0 - u);
// 类似处理 v
可视化:
Mirror 模式:
UV: -1.0 0.0 1.0 2.0 3.0
│ │ │ │ │
┌────┼──────┼──────┼──────┼──────┤
│🟦🟥│🟥🟦🟦│🟦🟥🟥│🟥🟦🟦│🟦🟥🟥│
│🟥🟦│🟦🟥🟥│🟥🟦🟦│🟦🟥🟥│🟥🟦🟦│
└────┴──────┴──────┴──────┴──────┘
↑ ↑ ↑ ↑ ↑
镜像 原始 镜像 原始 镜像
用途:
• 对称纹理 (减少接缝)
• 水面反射
• 特殊效果
Border边框
超出范围使用指定颜色:
c
// Clamp to Border 模式
vec4 sample_with_border(texture, uv, border_color) {
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
return border_color; // 返回边框颜色
}
return texture_sample(texture, uv);
}
可视化:
Clamp to Border 模式 (border_color = 黑色):
UV: -1.0 0.0 1.0 2.0 3.0
│ │ │ │ │
┌────┼──────┼──────┼──────┼──────┤
│⬛⬛│🟥🟦🟦│⬛⬛⬛│⬛⬛⬛│⬛⬛⬛│
│⬛⬛│🟦🟥🟥│⬛⬛⬛│⬛⬛⬛│⬛⬛⬛│
└────┴──────┴──────┴──────┴──────┘
↑ ↑ ↑ ↑ ↑
边框色 原始纹理 边框色
用途:
• 聚光灯效果
• 阴影贴图 (边界外无阴影)
• 特殊效果
🌀 各向异性过滤
什么是各向异性
问题:斜视角纹理模糊
地面纹理 (斜视角):
┌──────────────────────────────┐
│ 相机视角 │
│ ╱│ │
│ ╱ │ │
│ ╱ │ ← 视线方向 │
│ ╱ │ │
│ ╱ │ │
│ ╱_____│___________________│
│ ╱Ground Texture │
│ ╱ (地面纹理) │
│ ╱ │
└──────────────────────────────┘
屏幕上一个像素对应的纹理区域:
┌────────────────────────────────┐
│ 远处 │
│ ┌─────────────────────┐ ← 细长 │
│ │ │ │
│ └─────────────────────┘ │
│ │
│ 近处 │
│ ┌──┐ ← 正方形 │
│ └──┘ │
└────────────────────────────────┘
问题:
• 标准过滤 (Linear/Trilinear) 假设采样区域是正方形
• 斜视角时采样区域是细长的矩形
• 结果:远处模糊,细节丢失
各向异性过滤 (Anisotropic Filtering):
各向异性采样:
┌────────────────────────────────┐
│ 标准过滤 (4 个采样点) │
│ ┌─────────────────────┐ │
│ │ • • │ │
│ │ ↑ 正方形 │ │
│ │ • • │ │
│ └─────────────────────┘ │
│ → 模糊 │
└────────────────────────────────┘
┌────────────────────────────────┐
│ 各向异性过滤 (16 个采样点) │
│ ┌─────────────────────┐ │
│ │ • • • • • • • • │ │
│ │ ↑ 矩形 │ │
│ │ • • • • • • • • │ │
│ └─────────────────────┘ │
│ → 清晰! │
└────────────────────────────────┘
各向异性级别:
• 1x: 禁用 (标准过滤)
• 2x: 2 个采样 (轻微改善)
• 4x: 4 个采样 (明显改善)
• 8x: 8 个采样 (高质量)
• 16x: 16 个采样 (最高质量)
性能影响
各向异性过滤的成本:
| 级别 | 采样次数 | 性能影响 | 适用场景 |
|---|---|---|---|
| 1x (禁用) | 4 | 0% | 移动设备(低端) |
| 2x | 8 | ~5% | 移动设备(中端) |
| 4x | 16 | ~10% | PC (中低) |
| 8x | 32 | ~15% | PC (中高) |
| 16x | 64 | ~20% | PC (高端) |
现代 GPU 优化:
硬件优化:
• GPU 并行处理多个采样点
• 内存带宽是主要瓶颈,不是计算
• 现代 GPU (2015+) 的 16x 各向异性几乎无性能损失
建议:
• PC/主机:默认 16x
• 移动设备:4x ~ 8x
• 低端移动:2x 或禁用
使用场景
应该使用各向异性的纹理:
需要各向异性:
✓ 地面纹理 (地板、草地、道路)
✓ 墙壁纹理 (砖墙、混凝土)
✓ 远距离表面
✓ 斜视角纹理
不需要各向异性:
✗ UI 纹理 (正对屏幕)
✗ 天空盒 (总是正对)
✗ 粒子纹理 (小且动态)
✗ 法线贴图 (已经是细节纹理)
⚙️ 采样器配置
采样器参数
完整的采样器参数列表:
c
// engine/src/renderer/renderer_types.inl
/**
* @brief 纹理过滤模式
*/
typedef enum texture_filter {
TEXTURE_FILTER_NEAREST = 0, // 最近邻
TEXTURE_FILTER_LINEAR = 1 // 线性
} texture_filter;
/**
* @brief Mipmap 模式
*/
typedef enum texture_mipmap_mode {
TEXTURE_MIPMAP_MODE_NEAREST = 0, // 最近 Mipmap
TEXTURE_MIPMAP_MODE_LINEAR = 1 // 线性 Mipmap (Trilinear)
} texture_mipmap_mode;
/**
* @brief 纹理包裹模式
*/
typedef enum texture_wrap {
TEXTURE_WRAP_REPEAT = 0, // 重复
TEXTURE_WRAP_MIRRORED_REPEAT = 1, // 镜像重复
TEXTURE_WRAP_CLAMP_TO_EDGE = 2, // 限制到边缘
TEXTURE_WRAP_CLAMP_TO_BORDER = 3 // 限制到边框
} texture_wrap;
配置结构
采样器配置结构:
c
/**
* @brief 纹理采样器配置
*/
typedef struct texture_sampler_config {
// 过滤
texture_filter min_filter; // 缩小过滤
texture_filter mag_filter; // 放大过滤
texture_mipmap_mode mipmap_mode; // Mipmap 模式
// 包裹
texture_wrap wrap_u; // U 轴 (水平)
texture_wrap wrap_v; // V 轴 (垂直)
texture_wrap wrap_w; // W 轴 (3D 纹理)
// 高级
f32 max_anisotropy; // 各向异性级别 (1.0 ~ 16.0)
vec4 border_color; // 边框颜色 (RGBA)
// LOD (Level of Detail)
f32 min_lod; // 最小 LOD (默认 0.0)
f32 max_lod; // 最大 LOD (默认 1000.0)
f32 lod_bias; // LOD 偏移 (默认 0.0)
} texture_sampler_config;
预设采样器
常用的预设配置:
c
// engine/src/systems/texture_system.c
/**
* @brief 预设采样器配置
*/
// 默认采样器 (高质量 3D 纹理)
texture_sampler_config default_3d_sampler() {
texture_sampler_config config = {0};
config.min_filter = TEXTURE_FILTER_LINEAR;
config.mag_filter = TEXTURE_FILTER_LINEAR;
config.mipmap_mode = TEXTURE_MIPMAP_MODE_LINEAR; // Trilinear
config.wrap_u = TEXTURE_WRAP_REPEAT;
config.wrap_v = TEXTURE_WRAP_REPEAT;
config.wrap_w = TEXTURE_WRAP_REPEAT;
config.max_anisotropy = 16.0f; // 最高质量
config.min_lod = 0.0f;
config.max_lod = 1000.0f;
return config;
}
// UI 采样器 (像素完美,无重复)
texture_sampler_config ui_sampler() {
texture_sampler_config config = {0};
config.min_filter = TEXTURE_FILTER_LINEAR;
config.mag_filter = TEXTURE_FILTER_LINEAR;
config.mipmap_mode = TEXTURE_MIPMAP_MODE_NEAREST;
config.wrap_u = TEXTURE_WRAP_CLAMP_TO_EDGE;
config.wrap_v = TEXTURE_WRAP_CLAMP_TO_EDGE;
config.wrap_w = TEXTURE_WRAP_CLAMP_TO_EDGE;
config.max_anisotropy = 1.0f; // 禁用
return config;
}
// 像素艺术采样器 (无过滤,清晰像素)
texture_sampler_config pixel_art_sampler() {
texture_sampler_config config = {0};
config.min_filter = TEXTURE_FILTER_NEAREST;
config.mag_filter = TEXTURE_FILTER_NEAREST;
config.mipmap_mode = TEXTURE_MIPMAP_MODE_NEAREST;
config.wrap_u = TEXTURE_WRAP_CLAMP_TO_EDGE;
config.wrap_v = TEXTURE_WRAP_CLAMP_TO_EDGE;
config.max_anisotropy = 1.0f;
return config;
}
// 天空盒采样器
texture_sampler_config skybox_sampler() {
texture_sampler_config config = {0};
config.min_filter = TEXTURE_FILTER_LINEAR;
config.mag_filter = TEXTURE_FILTER_LINEAR;
config.mipmap_mode = TEXTURE_MIPMAP_MODE_LINEAR;
config.wrap_u = TEXTURE_WRAP_CLAMP_TO_EDGE;
config.wrap_v = TEXTURE_WRAP_CLAMP_TO_EDGE;
config.wrap_w = TEXTURE_WRAP_CLAMP_TO_EDGE;
config.max_anisotropy = 1.0f; // 天空盒不需要
return config;
}
🖥️ Vulkan采样器实现
创建采样器
Vulkan 采样器对象创建:
c
// engine/src/renderer/vulkan/vulkan_backend.c
/**
* @brief 创建 Vulkan 采样器
*/
VkSampler create_vulkan_sampler(
vulkan_context* context,
const texture_sampler_config* config
) {
// 1. 填充 Vulkan 创建信息
VkSamplerCreateInfo sampler_info = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
// 过滤模式
sampler_info.magFilter = config->mag_filter == TEXTURE_FILTER_LINEAR
? VK_FILTER_LINEAR
: VK_FILTER_NEAREST;
sampler_info.minFilter = config->min_filter == TEXTURE_FILTER_LINEAR
? VK_FILTER_LINEAR
: VK_FILTER_NEAREST;
// Mipmap 模式
sampler_info.mipmapMode = config->mipmap_mode == TEXTURE_MIPMAP_MODE_LINEAR
? VK_SAMPLER_MIPMAP_MODE_LINEAR
: VK_SAMPLER_MIPMAP_MODE_NEAREST;
// 包裹模式
sampler_info.addressModeU = convert_wrap_mode(config->wrap_u);
sampler_info.addressModeV = convert_wrap_mode(config->wrap_v);
sampler_info.addressModeW = convert_wrap_mode(config->wrap_w);
// 各向异性
if (config->max_anisotropy > 1.0f) {
sampler_info.anisotropyEnable = VK_TRUE;
sampler_info.maxAnisotropy = config->max_anisotropy;
// 限制到设备最大值
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(context->device.physical_device, &props);
sampler_info.maxAnisotropy = KMIN(
sampler_info.maxAnisotropy,
props.limits.maxSamplerAnisotropy
);
} else {
sampler_info.anisotropyEnable = VK_FALSE;
sampler_info.maxAnisotropy = 1.0f;
}
// LOD 设置
sampler_info.minLod = config->min_lod;
sampler_info.maxLod = config->max_lod;
sampler_info.mipLodBias = config->lod_bias;
// 边框颜色
sampler_info.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
// 如果使用 Clamp to Border,可以自定义边框颜色
// 其他设置
sampler_info.compareEnable = VK_FALSE; // 深度比较 (用于阴影贴图)
sampler_info.unnormalizedCoordinates = VK_FALSE; // 使用归一化坐标 [0,1]
// 2. 创建采样器
VkSampler sampler;
VK_CHECK(vkCreateSampler(
context->device.logical_device,
&sampler_info,
context->allocator,
&sampler
));
return sampler;
}
/**
* @brief 转换包裹模式
*/
VkSamplerAddressMode convert_wrap_mode(texture_wrap wrap) {
switch (wrap) {
case TEXTURE_WRAP_REPEAT:
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
case TEXTURE_WRAP_MIRRORED_REPEAT:
return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
case TEXTURE_WRAP_CLAMP_TO_EDGE:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
case TEXTURE_WRAP_CLAMP_TO_BORDER:
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
default:
KWARN("Unknown wrap mode: %d, using REPEAT", wrap);
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
}
}
销毁采样器
c
/**
* @brief 销毁 Vulkan 采样器
*/
void destroy_vulkan_sampler(vulkan_context* context, VkSampler sampler) {
if (sampler) {
vkDestroySampler(context->device.logical_device, sampler, context->allocator);
}
}
采样器池
管理多个采样器:
c
// engine/src/systems/texture_system.c
/**
* @brief 采样器池 (避免重复创建相同配置的采样器)
*/
typedef struct sampler_pool {
texture_sampler_config* configs; // 配置数组
VkSampler* samplers; // Vulkan 采样器数组
u32 count; // 采样器数量
u32 capacity; // 容量
} sampler_pool;
/**
* @brief 获取或创建采样器
*/
VkSampler sampler_pool_get_or_create(
sampler_pool* pool,
const texture_sampler_config* config
) {
// 1. 查找是否已存在相同配置的采样器
for (u32 i = 0; i < pool->count; ++i) {
if (sampler_config_equal(&pool->configs[i], config)) {
return pool->samplers[i]; // 重用
}
}
// 2. 不存在,创建新采样器
if (pool->count >= pool->capacity) {
// 扩容
pool->capacity *= 2;
pool->configs = krealloc(pool->configs, sizeof(texture_sampler_config) * pool->capacity);
pool->samplers = krealloc(pool->samplers, sizeof(VkSampler) * pool->capacity);
}
// 创建采样器
VkSampler sampler = create_vulkan_sampler(context, config);
// 添加到池
pool->configs[pool->count] = *config;
pool->samplers[pool->count] = sampler;
pool->count++;
KDEBUG("Created new sampler (total: %u)", pool->count);
return sampler;
}
/**
* @brief 比较两个采样器配置是否相等
*/
b8 sampler_config_equal(
const texture_sampler_config* a,
const texture_sampler_config* b
) {
return a->min_filter == b->min_filter &&
a->mag_filter == b->mag_filter &&
a->mipmap_mode == b->mipmap_mode &&
a->wrap_u == b->wrap_u &&
a->wrap_v == b->wrap_v &&
a->wrap_w == b->wrap_w &&
KABS(a->max_anisotropy - b->max_anisotropy) < 0.01f &&
KABS(a->min_lod - b->min_lod) < 0.01f &&
KABS(a->max_lod - b->max_lod) < 0.01f &&
KABS(a->lod_bias - b->lod_bias) < 0.01f;
}
🔗 材质系统集成
材质配置扩展
在材质配置中添加采样器:
c
// engine/src/resources/resource_types.h
/**
* @brief 材质贴图配置 (扩展)
*/
typedef struct material_map_config {
char texture_name[256]; // 纹理名称
texture_use use; // 用途 (diffuse/normal/specular)
// 新增:采样器配置
texture_sampler_config sampler; // 采样器配置
} material_map_config;
/**
* @brief 材质配置文件示例 (.kmt)
*/
/*
name=brick_wall
type=material
# Diffuse Map
diffuse_map_name=brick_diffuse.png
diffuse_sampler_filter=linear
diffuse_sampler_wrap=repeat
diffuse_sampler_anisotropy=16.0
# Normal Map
normal_map_name=brick_normal.png
normal_sampler_filter=linear
normal_sampler_wrap=repeat
normal_sampler_anisotropy=16.0
# Specular Map
specular_map_name=brick_specular.png
specular_sampler_filter=linear
specular_sampler_wrap=repeat
specular_sampler_anisotropy=8.0
*/
采样器绑定
将采样器绑定到着色器:
c
// engine/src/systems/material_system.c
/**
* @brief 绑定材质 (包括纹理和采样器)
*/
b8 material_system_apply(material* m, shader* s) {
// 遍历所有贴图
for (u32 i = 0; i < m->map_count; ++i) {
material_map* map = &m->maps[i];
// 获取纹理
texture* tex = texture_system_get(map->texture_name);
if (!tex) continue;
// 获取或创建采样器
VkSampler sampler = sampler_pool_get_or_create(
&texture_system_state->sampler_pool,
&map->sampler_config
);
// 绑定到着色器
shader_bind_texture_sampler(
s,
map->use, // 绑定点 (diffuse=0, normal=1, etc.)
tex->image, // 纹理图像
sampler // 采样器
);
}
return true;
}
Shader集成
在着色器中使用采样器:
glsl
// assets/shaders/Builtin.MaterialShader.frag.glsl
#version 450
// 输入
in vec2 in_texcoord;
in vec3 in_normal;
// 纹理采样器 (组合图像和采样器)
layout(set = 1, binding = 0) uniform sampler2D u_diffuse_map;
layout(set = 1, binding = 1) uniform sampler2D u_normal_map;
layout(set = 1, binding = 2) uniform sampler2D u_specular_map;
// 输出
out vec4 out_color;
void main() {
// 采样 Diffuse (自动使用材质配置的采样器)
vec4 diffuse_color = texture(u_diffuse_map, in_texcoord);
// 采样 Normal
vec3 tangent_normal = texture(u_normal_map, in_texcoord).rgb;
tangent_normal = normalize(tangent_normal * 2.0 - 1.0);
// 采样 Specular
float specular = texture(u_specular_map, in_texcoord).r;
// 组合
vec3 final_color = diffuse_color.rgb * lighting(...);
out_color = vec4(final_color, diffuse_color.a);
}
🎮 实际应用
场景示例:地形纹理
c
// 地形材质配置
material_config terrain_material;
string_copy(terrain_material.name, "terrain_grass");
// Diffuse Map (高质量,无缝重复)
material_map_config diffuse_map;
string_copy(diffuse_map.texture_name, "grass_diffuse.png");
diffuse_map.use = TEXTURE_USE_DIFFUSE;
diffuse_map.sampler.min_filter = TEXTURE_FILTER_LINEAR;
diffuse_map.sampler.mag_filter = TEXTURE_FILTER_LINEAR;
diffuse_map.sampler.mipmap_mode = TEXTURE_MIPMAP_MODE_LINEAR; // Trilinear
diffuse_map.sampler.wrap_u = TEXTURE_WRAP_REPEAT;
diffuse_map.sampler.wrap_v = TEXTURE_WRAP_REPEAT;
diffuse_map.sampler.max_anisotropy = 16.0f; // 地面需要高各向异性
// Normal Map (同样高质量)
material_map_config normal_map;
string_copy(normal_map.texture_name, "grass_normal.png");
normal_map.use = TEXTURE_USE_NORMAL;
normal_map.sampler = diffuse_map.sampler; // 相同配置
terrain_material.maps[0] = diffuse_map;
terrain_material.maps[1] = normal_map;
terrain_material.map_count = 2;
UI 纹理示例:
c
// UI 图标材质
material_config ui_icon;
string_copy(ui_icon.name, "ui_button");
material_map_config icon_map;
string_copy(icon_map.texture_name, "button_icon.png");
icon_map.use = TEXTURE_USE_DIFFUSE;
icon_map.sampler.min_filter = TEXTURE_FILTER_LINEAR;
icon_map.sampler.mag_filter = TEXTURE_FILTER_LINEAR;
icon_map.sampler.mipmap_mode = TEXTURE_MIPMAP_MODE_NEAREST; // UI 不需要 Mipmap
icon_map.sampler.wrap_u = TEXTURE_WRAP_CLAMP_TO_EDGE; // 不重复
icon_map.sampler.wrap_v = TEXTURE_WRAP_CLAMP_TO_EDGE;
icon_map.sampler.max_anisotropy = 1.0f; // 禁用各向异性
ui_icon.maps[0] = icon_map;
ui_icon.map_count = 1;
❓ 常见问题
1. 何时使用 Nearest vs Linear 过滤?
Nearest (最近邻):
- ✓ 像素艺术游戏 (保持像素清晰)
- ✓ UI 图标 (需要精确像素)
- ✓ 低分辨率纹理 (避免模糊)
- ✗ 3D 场景 (会有锯齿)
Linear (线性):
- ✓ 大多数 3D 纹理
- ✓ 照片级真实感
- ✓ 平滑过渡
- ✗ 像素艺术 (会模糊)
示例:
c
// 像素艺术角色
texture_sampler_config pixel_art;
pixel_art.min_filter = TEXTURE_FILTER_NEAREST;
pixel_art.mag_filter = TEXTURE_FILTER_NEAREST;
pixel_art.wrap_u = TEXTURE_WRAP_CLAMP_TO_EDGE;
pixel_art.wrap_v = TEXTURE_WRAP_CLAMP_TO_EDGE;
// 3D 模型纹理
texture_sampler_config realistic;
realistic.min_filter = TEXTURE_FILTER_LINEAR;
realistic.mag_filter = TEXTURE_FILTER_LINEAR;
realistic.mipmap_mode = TEXTURE_MIPMAP_MODE_LINEAR;
realistic.wrap_u = TEXTURE_WRAP_REPEAT;
realistic.wrap_v = TEXTURE_WRAP_REPEAT;
realistic.max_anisotropy = 16.0f;
2. Mipmap 是否必需?
需要 Mipmap 的情况:
- ✓ 3D 场景 (远近距离变化大)
- ✓ 移动相机 (动态 LOD)
- ✓ 地形/大型表面
不需要 Mipmap 的情况:
- ✗ UI 纹理 (固定距离)
- ✗ 2D 游戏 (无深度)
- ✗ 粒子纹理 (小且动态)
- ✗ 字体 (固定大小)
性能影响:
Mipmap 的好处:
• 减少带宽 (访问更小的纹理)
• 减少缓存未命中
• 消除 Aliasing (锯齿)
• 提高帧率 10-30%
Mipmap 的成本:
• 额外内存 ~33% (1/4 + 1/16 + ...)
• 生成时间 (加载时一次性)
结论:大多数情况下应该启用 Mipmap
3. 各向异性过滤在移动设备上值得吗?
移动设备建议:
设备等级 各向异性级别 理由
────────────────────────────────
高端 8x - 16x 性能损失小 (<5%)
中端 4x - 8x 视觉提升明显
低端 2x - 4x 避免过度开销
极低端 1x (禁用) 节省电量
实测数据 (iPhone 11):
• 无各向异性: 60 FPS, 功耗 2.5W
• 4x 各向异性: 59 FPS, 功耗 2.6W (+4%)
• 16x 各向异性: 57 FPS, 功耗 2.8W (+12%)
建议:
• 提供设置选项让用户选择
• 默认 4x-8x (大多数设备可接受)
• 动态调整 (根据帧率)
4. 如何为材质选择正确的包裹模式?
决策树:
纹理会重复吗?
├─ 是 → 使用 Repeat
│ ├─ 需要对称? → Mirror Repeat
│ └─ 无需对称 → Repeat
│
└─ 否 → 使用 Clamp
├─ 需要边框颜色? → Clamp to Border
└─ 边缘拉伸? → Clamp to Edge
示例:
┌──────────────────────────────────┐
│ 地板瓦片 → Repeat │
│ 墙纸 → Mirror Repeat (避免接缝) │
│ UI 图标 → Clamp to Edge │
│ 聚光灯 → Clamp to Border (黑边) │
│ 天空盒 → Clamp to Edge │
└──────────────────────────────────┘
5. LOD bias 是什么,何时使用?
LOD Bias (LOD 偏移):
c
// LOD bias 调整 Mipmap 选择
sampler.lod_bias = 0.5f; // 正值 → 使用更小的 Mipmap (模糊)
sampler.lod_bias = -0.5f; // 负值 → 使用更大的 Mipmap (清晰)
计算:
adjusted_lod = calculated_lod + lod_bias
示例:
• 计算 LOD = 2.0
• lod_bias = -0.5
• adjusted_lod = 1.5 → 使用 Level 1 和 Level 2 插值
视觉效果:
lod_bias = -1.0: 非常清晰 (但可能闪烁)
lod_bias = -0.5: 稍微清晰
lod_bias = 0.0: 默认 (平衡)
lod_bias = +0.5: 稍微模糊
lod_bias = +1.0: 非常模糊 (性能好)
使用场景:
✓ 提高清晰度 (lod_bias = -0.5)
• 主角纹理
• 重要物体
• 近景细节
✓ 提高性能 (lod_bias = +0.5)
• 远景物体
• 背景纹理
• 低端设备
✗ 避免极端值 (|lod_bias| > 1.0)
• 会产生 Aliasing 或过度模糊
📝 练习
练习 1: 实现采样器预设系统
任务: 创建一个采样器预设库,方便重用常见配置。
c
// sampler_presets.h
typedef enum sampler_preset {
SAMPLER_PRESET_DEFAULT_3D,
SAMPLER_PRESET_UI,
SAMPLER_PRESET_PIXEL_ART,
SAMPLER_PRESET_SKYBOX,
SAMPLER_PRESET_TERRAIN,
SAMPLER_PRESET_FONT,
SAMPLER_PRESET_COUNT
} sampler_preset;
/**
* @brief 获取预设采样器配置
*/
texture_sampler_config get_sampler_preset(sampler_preset preset) {
static texture_sampler_config presets[SAMPLER_PRESET_COUNT];
static b8 initialized = false;
if (!initialized) {
// Default 3D
presets[SAMPLER_PRESET_DEFAULT_3D] = (texture_sampler_config){
.min_filter = TEXTURE_FILTER_LINEAR,
.mag_filter = TEXTURE_FILTER_LINEAR,
.mipmap_mode = TEXTURE_MIPMAP_MODE_LINEAR,
.wrap_u = TEXTURE_WRAP_REPEAT,
.wrap_v = TEXTURE_WRAP_REPEAT,
.wrap_w = TEXTURE_WRAP_REPEAT,
.max_anisotropy = 16.0f,
.min_lod = 0.0f,
.max_lod = 1000.0f
};
// UI
presets[SAMPLER_PRESET_UI] = (texture_sampler_config){
.min_filter = TEXTURE_FILTER_LINEAR,
.mag_filter = TEXTURE_FILTER_LINEAR,
.mipmap_mode = TEXTURE_MIPMAP_MODE_NEAREST,
.wrap_u = TEXTURE_WRAP_CLAMP_TO_EDGE,
.wrap_v = TEXTURE_WRAP_CLAMP_TO_EDGE,
.max_anisotropy = 1.0f
};
// Pixel Art
presets[SAMPLER_PRESET_PIXEL_ART] = (texture_sampler_config){
.min_filter = TEXTURE_FILTER_NEAREST,
.mag_filter = TEXTURE_FILTER_NEAREST,
.mipmap_mode = TEXTURE_MIPMAP_MODE_NEAREST,
.wrap_u = TEXTURE_WRAP_CLAMP_TO_EDGE,
.wrap_v = TEXTURE_WRAP_CLAMP_TO_EDGE,
.max_anisotropy = 1.0f
};
// TODO: 实现其他预设
initialized = true;
}
return presets[preset];
}
// 使用示例
material_map_config diffuse_map;
diffuse_map.sampler = get_sampler_preset(SAMPLER_PRESET_DEFAULT_3D);
练习 2: 可视化采样器差异
任务: 创建工具对比不同采样器配置的效果。
c
// sampler_comparison_tool.c
/**
* @brief 并排显示不同采样器效果
*/
void render_sampler_comparison() {
// 同一纹理,4 种采样器
texture* tex = texture_system_get("test_texture");
texture_sampler_config configs[4];
const char* labels[4];
// Nearest
configs[0] = (texture_sampler_config){
.min_filter = TEXTURE_FILTER_NEAREST,
.mag_filter = TEXTURE_FILTER_NEAREST,
.wrap_u = TEXTURE_WRAP_REPEAT
};
labels[0] = "Nearest";
// Linear
configs[1] = (texture_sampler_config){
.min_filter = TEXTURE_FILTER_LINEAR,
.mag_filter = TEXTURE_FILTER_LINEAR,
.wrap_u = TEXTURE_WRAP_REPEAT
};
labels[1] = "Linear";
// Linear + Mipmap
configs[2] = configs[1];
configs[2].mipmap_mode = TEXTURE_MIPMAP_MODE_LINEAR;
labels[2] = "Trilinear";
// Linear + Mipmap + Aniso
configs[3] = configs[2];
configs[3].max_anisotropy = 16.0f;
labels[3] = "Trilinear + 16x Aniso";
// 渲染 4 个并排的平面
for (u32 i = 0; i < 4; ++i) {
VkSampler sampler = create_vulkan_sampler(&configs[i]);
// 渲染平面
render_quad(
(vec3){i * 2.0f - 3.0f, 0, 0}, // 位置
(vec2){1.5f, 1.5f}, // 大小
tex,
sampler
);
// 绘制标签
render_text(labels[i], (vec2){i * 0.25f, 0.9f});
}
}
预期结果:
屏幕输出:
┌──────────┬──────────┬──────────┬──────────┐
│ Nearest │ Linear │ Trilinear│Tri+Aniso │
│ │ │ │ │
│ ████████ │ ▓▓▓▓▓▓▓▓ │ ▒▒▒▒▒▒▒▒ │ ░░░░░░░░ │
│ ████████ │ ▓▓▓▓▓▓▓▓ │ ▒▒▒▒▒▒▒▒ │ ░░░░░░░░ │
│ (块状) │ (平滑) │ (远处清晰│ (最清晰) │
└──────────┴──────────┴──────────┴──────────┘
对比说明:
• Nearest: 像素化,锯齿严重
• Linear: 平滑,但远处模糊
• Trilinear: 远处也清晰,但斜视角模糊
• Trilinear + Aniso: 斜视角也清晰,最佳质量
练习 3: 自动 Mipmap 生成
任务: 实现从原始纹理自动生成 Mipmap 链。
c
// mipmap_generator.c
/**
* @brief 生成完整的 Mipmap 链
*/
void generate_mipmaps(texture* tex) {
u32 width = tex->width;
u32 height = tex->height;
// 计算 Mipmap 级别数
u32 mip_levels = (u32)floor(log2(KMAX(width, height))) + 1;
KINFO("Generating %u mipmap levels for %s (%ux%u)",
mip_levels, tex->name, width, height);
// 生成每个级别
for (u32 level = 1; level < mip_levels; ++level) {
u32 src_width = KMAX(width >> (level - 1), 1);
u32 src_height = KMAX(height >> (level - 1), 1);
u32 dst_width = KMAX(width >> level, 1);
u32 dst_height = KMAX(height >> level, 1);
KDEBUG("Level %u: %ux%u → %ux%u",
level, src_width, src_height, dst_width, dst_height);
// 获取源级别数据
u8* src_data = tex->mip_levels[level - 1];
// 分配目标级别
u8* dst_data = kallocate(dst_width * dst_height * 4, MEMORY_TAG_TEXTURE);
// 下采样 (Box Filter - 简单平均)
for (u32 y = 0; y < dst_height; ++y) {
for (u32 x = 0; x < dst_width; ++x) {
// 平均 2x2 像素块
u32 src_x = x * 2;
u32 src_y = y * 2;
vec4 color = {0};
for (u32 dy = 0; dy < 2; ++dy) {
for (u32 dx = 0; dx < 2; ++dx) {
u32 sx = KMIN(src_x + dx, src_width - 1);
u32 sy = KMIN(src_y + dy, src_height - 1);
u32 src_index = (sy * src_width + sx) * 4;
color.r += src_data[src_index + 0];
color.g += src_data[src_index + 1];
color.b += src_data[src_index + 2];
color.a += src_data[src_index + 3];
}
}
// 平均
color.r /= 4.0f;
color.g /= 4.0f;
color.b /= 4.0f;
color.a /= 4.0f;
// 写入目标
u32 dst_index = (y * dst_width + x) * 4;
dst_data[dst_index + 0] = (u8)color.r;
dst_data[dst_index + 1] = (u8)color.g;
dst_data[dst_index + 2] = (u8)color.b;
dst_data[dst_index + 3] = (u8)color.a;
}
}
// 保存 Mipmap 级别
tex->mip_levels[level] = dst_data;
}
tex->mip_level_count = mip_levels;
// 上传到 GPU
upload_mipmaps_to_gpu(tex);
}
高级: Lanczos 过滤器 (更高质量)
c
// 使用 Lanczos 重采样 (保留更多细节)
void generate_mipmaps_lanczos(texture* tex) {
// Lanczos 窗口大小
const int a = 3; // Lanczos-3
// ... (Lanczos 实现,比 Box Filter 复杂但质量更高)
}
恭喜!你已经掌握了纹理采样器系统! 🎉
Tutorial written by 上手实验室