纹理映射
需要预先学习:
1. 插值
在渲染管线中,光栅化阶段(见 着色与渲染管线)需要为三角形内部的每个片元确定颜色、法线、纹理坐标等属性,而顶点只提供了属性在三个顶点处的值。三角形内部的属性要靠插值(interpolation)推算:已知离散的顶点值,估计连续区域内的值。
1.1 线性插值
线性插值(linear interpolation)是最基本的插值方式:在两个已知值之间按比例取中间值。一维情况下,在 、 之间按参数 插值:
得到 , 得到 , 得到中点。这个式子对二维点、三维向量同样成立,逐分量计算即可。
1.2 重心坐标
三角形内部任意一点的位置可以用重心坐标(barycentric coordinates)表示:用三个非负权重 描述点相对三个顶点的占比:
其中 、、 是三角形三个顶点。点 在三角形内部,当且仅当 。
重心坐标有直观的几何意义——面积比: 等于点 与边 围成的小三角形面积占整个三角形的比例,其余两个同理:
实际计算时用向量叉积求面积,或者直接解线性方程组。值得一提的关联:光栅化 中判断"点是否在三角形内"的叉积法,其实就是在判断三个权重是否同号——三个叉积的符号统一,等价于 都非负。
有了重心坐标,三角形内部任意属性都可以插值。已知三个顶点处的属性值 、、,点 处的属性为:
这就是属性插值。光栅化阶段用它插值颜色、法线,纹理映射中的 UV 坐标也靠它插值。
注:屏幕空间的重心坐标不能直接套用。透视投影下,屏幕上的插值比例与三维空间中的插值比例不一致,直接插值会产生透视扭曲。
2. 纹理映射(Texture Mapping)
回顾 着色与渲染管线 中的 Blinn-Phong 模型:漫反射系数 是一个常数,整个物体只有一个颜色,无法表现斑马条纹、木纹这类表面细节。纹理映射(texture mapping)解决这个问题——把一张图片(纹理,texture)贴到三维表面上,让着色器从纹理中查询每个点的颜色,用它作为漫反射系数。
2.1 纹理与纹理坐标
纹理是一张二维图片,由纹素(texel,texture element,纹理的基本单元,对应屏幕上的像素)组成。要把纹理贴到表面,每个顶点需要指定一个纹理坐标(texture coordinate,也称 UV 坐标),记作 ,取值范围约定为 :
| 坐标 | 含义 |
|---|---|
| 纹理横向位置,0 到 1 从左到右 | |
| 纹理纵向位置,0 到 1 从下到上 |
对应纹理左下角, 对应右上角。
注: 轴方向在不同 API 中可能有差异(有的以左上角为原点),具体以所用图形 API 的约定为准。
2.2 纹理映射的实现流程
纹理映射贯穿渲染管线的几个阶段:
- 建模阶段:在顶点上指定 UV 坐标,相当于把模型表面"展开"后对应到纹理图片上;
- 光栅化阶段:三角形内部每个片元的 UV 由三个顶点的 UV 用重心坐标插值得到;
- 片元处理阶段:用片元的 UV 到纹理中采样(sample)颜色,得到该点的漫反射系数 ,代入着色模型计算颜色。
第三步的采样本质是查表:UV 落在哪个纹素附近,就取哪个纹素的颜色。
// 伪代码:片元着色中用纹理颜色替代常数漫反射系数
Vec3 fragment_shading(const Vec2& uv, ...)
{
Vec3 k_d = texture.sample(uv); // 从纹理采样漫反射颜色
return blinn_phong(k_d, ...); // 代入 Blinn-Phong 计算
}
2.3 纹理采样与双线性插值
UV 是连续值,很少恰好落在某个纹素中心。最简单的最近邻采样(nearest neighbor)直接取距离最近的纹素,速度快但画面会出现明显的块状锯齿,纹理被放大时尤其严重。
双线性插值(bilinear interpolation)改善这种情况:取 UV 周围 2×2 的四个纹素,先在水平方向做两次线性插值,再在竖直方向做一次,得到平滑的过渡:
设 u 落在纹素列 u0 与 u1 之间,v 落在纹素行 v0 与 v1 之间
水平方向: c0 = lerp(tex(u0, v0), tex(u1, v0), u - u0)
c1 = lerp(tex(u0, v1), tex(u1, v1), u - u0)
竖直方向:color = lerp(c0, c1, v - v0)
2.4 MipMap
纹理缩小(minification)时,多个纹素挤进一个像素,密集细节在采样下出现摩尔纹(见 光栅化 的走样),时间上还会表现为闪烁——不同帧的采样位置不同,画面抖动。问题根源与 光栅化 中的走样相同:采样频率不足。
MipMap(multum in parvo,拉丁语"许多东西放在一个小地方")是预生成的纹理金字塔:原始纹理作为第 0 级,每一级把边长缩小一半(面积缩小到四分之一),直到 1×1,级别数约为 ( 为最长边):
| 级别 | 分辨率 | 适用场景 |
|---|---|---|
| level 0 | 原始纹理 | 像素覆盖的纹素很少(近处) |
| level 1 | 边长减半 | 稍远 |
| level 2 | 边长再减半 | 更远 |
核心思想:根据像素覆盖的纹理区域大小选择合适分辨率的级别——远处一个像素覆盖很多纹素,就去低分辨率级别采样,等效于预先完成了低通滤波(呼应 光栅化 中"先模糊再采样"的反走样思路)。
级别选择:用屏幕相邻像素的 UV 差分,估计当前像素在纹理空间覆盖区域的大小。设 UV 对屏幕坐标的偏导数为 、、、,像素覆盖区域近似为边长 的正方形:
再取对数得到级别 。这些偏导数 GPU 硬件可以直接计算。
三线性插值(trilinear interpolation): 通常不是整数,先在 、 两级内分别做双线性插值,再按小数部分对两个结果做一次线性插值,得到平滑的过渡,这就是三线性过滤(trilinear filtering)。
存储代价:所有级别加起来比原始纹理多约 的存储量()。
2.5 各向异性过滤
MipMap 假设像素在纹理空间覆盖的是正方形区域,用边长 一个数描述。但斜着看表面时(比如俯视地面),像素覆盖的纹理区域其实是细长的长方形——沿某个方向拉伸得很厉害,正方形假设失效,MipMap 只能取较模糊的级别,远处纹理依然发糊。
各向异性过滤(anisotropic filtering)针对这种情况:不再按单一的正方形采样,而是沿拉伸方向多次采样(次数称各向异性程度,常见 2x、4x、8x、16x)后取平均,让斜向表面也保持清晰。代价是采样次数增加、性能开销变大,是目前 GPU 上最常用的高质量纹理过滤方式。


