纹理映射(Texture Mapping),又称纹理贴图,是将纹理空间中的纹理像素映射到屏幕空间中的像素的过程。简单来说,就是把一幅图像贴到三维物体的表面上来增强真实感,可以和光照计算、图像混合等技术结合起来形成许多非常漂亮的效果。
1. 纹理映射
上一篇博文中,漫反射的计算公式通过
m
d
i
f
f
u
s
e
m_{diffuse}
mdiffuse表示漫反射颜色,对于纯色物体,可指定一个颜色值进行漫反射计算,但在绝大多数渲染工作中,所渲染物体表面都是由复杂的图案构成,单一颜色值无法表达更加复杂的模型表面细节。我们当然可以通过增大顶点数量来以不同的顶点色来实现,但这样无疑增大了开销,在实际的工程应用中通常是通过纹理在模型上设置不同的漫反射颜色
m
d
i
f
f
u
s
e
m_{diffuse}
mdiffuse从而增强渲染的真实感。
c
d
i
f
f
u
s
e
=
(
c
l
i
g
h
t
⋅
m
d
i
f
f
u
s
e
)
m
a
x
(
0
,
n
⋅
l
)
c_{diffuse}=(c_{light}·m_{diffuse})max(0,n·l)
cdiffuse=(clight⋅mdiffuse)max(0,n⋅l)
实现纹理贴图的基本工作流程是首先在建模软件中对三维模型进行纹理展开操作,建立三维模型与纹理空间的坐标映射关系,并得到一张纹理参考图,然后依据参考图绘制模型的表现细节(也可通过某些软件在三维模型上绘制),如下图中建立了模型与纹理图像的映射关系,就可以将图像“贴”在物体表面了。这一过程通常是由美工来完成。
纹理映射关系由纹理空间的坐标来描述,纹理空间是一个二维空间,其坐标通常用(u,v)来表示,u是横向坐标,v是纵向坐标,且无论纹理图像的分辨率为多少,u、v的取值范围都被归一化到[0,1]内。
2. 重心坐标系
对于三角形顶点的纹理坐标可以直接获得,但想要在内部做平滑的过度则需要通过插值来得到,例如纹理坐标、颜色法线向量等等。三角形平面中的任何点都可以表示为顶点的加权平均值,这些加权被称为重心坐标(Barycentric Coordinates)重心坐标系的定义如下:
给定一个三角形的三个顶点 A ( x A , y A ) A(x_A,y_A) A(xA,yA), B ( x B , y B ) B(x_B,y_B) B(xB,yB), C ( x C , y C ) C(x_C,y_C) C(xC,yC),则三角形所在平面内的任意一点 P ( x P , y P ) P(x_P,y_P) P(xP,yP)可表示为 A , B , C A,B,C A,B,C坐标的线性组合
P = α A + β B + γ C P=αA+βB+γC P=αA+βB+γC其中 α + β + γ = 1 α+β+γ=1 α+β+γ=1
这样构建的坐标系被称为重心坐标系,此时称 ( α , β , γ ) (α,β,γ) (α,β,γ)为点 P P P在重心坐标系下的坐标,简称重心坐标。
对于重心坐标系,若点在三角形内部,则重心坐标均为正;若点在三角外部,则重心坐标存在负值;若点是三角形的顶点,则重心坐标分别为
(
1
,
0
,
0
)
≡
A
(1,0,0)≡A
(1,0,0)≡A,
(
0
,
1
,
0
)
≡
B
(0,1,0)≡B
(0,1,0)≡B,
(
0
,
0
,
1
)
≡
C
(0,0,1)≡C
(0,0,1)≡C;三角形重心位置的重心坐标为
(
1
3
,
1
3
,
1
3
)
(\frac{1}{3},\frac{1}{3},\frac{1}{3})
(31,31,31)。
重心坐标系的推导及详细介绍可以参考这篇知乎。
重心坐标问题:投影变换后不能保证重心坐标不变,这意味着应该在投影前对空间三角形进行插值操作。
3. 纹理映射的问题
纹理实际上就是一张图像,它有自身的分辨率,纹理图像也由像素组成,每个像素均有自己的下标,纹理上的像素我们常称其为纹素(texel);所渲染的物体最终被显示的屏幕上,屏幕也有自身的分辨率,屏幕的像素称之为像素(pixel)。所渲染到屏幕上的像素均对应三角形内部的点,这些点通过uv坐标与纹理图像建立映射关系,由于屏幕分辨率和纹理分辨率的差异,在纹理映射时会产生一些问题。
3.1 纹理分辨率过小
纹理分辨率较小的问题容易理解,假如把一张分辨率为100×100的图像应用到分辨率为500×500的区域内必然会导致失真。
最近邻法: 例如我们屏幕像素(50, 50) 对应到纹理像素(5,5),屏幕像素(51, 50) 对应到纹理像素(5.1,5),屏幕像素(52, 50) 对应到纹理像素(5.2,5),然后对于浮点数我们会四舍五入成整数,那么屏幕像素(50, 50),(51, 50),(52, 50)对应的纹理像素都是(5,5),也就是说当我们纹理太小的时候,我们多个屏幕像素会对应到一个相同的纹理像素上,所以产生了模糊或者锯齿。
对于纹理分辨率过小而造成的走样,可以通过插值部分解决。
双线性插值(Bilinear)
假设上图是一张纹理图的,红点位置是纹理采样位置,依据上述的“最近邻”的采样方法,红点位置采样的结果为 u 11 u_{11} u11像素的颜色值。而双线性插值是通过对周围邻域像素进行插值得到采样位置的像素颜色,这样相当于在纹理采样前对纹理图像进行平滑操作,从而达到反走样的效果,关于反走样可参考之前的笔记。
在说明双线性插值前,需要先了解一下单线性插值,已知点
A
,
B
A,B
A,B构成的直线,
A
,
B
A,B
A,B分别是两个端点,点
C
C
C在直线上,且在
A
,
B
A,B
A,B之间,根据长度我们设
A
C
A
B
=
x
,
x
∈
[
0
,
1
]
\frac{AC}{AB}=x,x∈[0,1]
ABAC=x,x∈[0,1],当已知
A
.
B
A.B
A.B的属性(例如颜色、法线)时,
C
C
C点对应的属性即为
C
=
l
e
r
p
(
x
,
A
,
B
)
=
A
+
x
(
B
−
A
)
C=lerp(x,A,B)=A+x(B-A)
C=lerp(x,A,B)=A+x(B−A)
双线性插值是对单线性插值进行了扩展,即在水平、竖直两个方向进行插值。双线性插值的第一步是要找出采样点周围的四个像素 u 00 , u 10 , u 01 , u 11 u_{00},u_{10},u_{01},u_{11} u00,u10,u01,u11,采样点与 u 00 u_{00} u00的距离在水平、竖直的分量 s , t s,t s,t,
首先在水平方向,先插值计算出 u 0 , u 1 u_0,u_1 u0,u1处的颜色值。
u
0
=
l
e
r
p
(
s
,
u
00
,
u
10
)
u_0=lerp(s,u_{00},u_{10})
u0=lerp(s,u00,u10)
u
1
=
l
e
r
p
(
s
,
u
01
,
u
11
)
u_1=lerp(s,u_{01},u_{11})
u1=lerp(s,u01,u11)
然后通过
u
0
,
u
1
u_0,u_1
u0,u1在竖直方向插值计算出采样点的颜色值。
f
(
x
,
y
)
=
l
e
r
p
(
t
,
u
0
,
u
1
)
f(x,y)=lerp(t,u_0,u_1)
f(x,y)=lerp(t,u0,u1)
双线性插值考虑了采样位置周围的四个像素点总共进行了3次线性插值计算,能够较好的缓解走样现象,还有一种方法叫做双三次插值(Bicubic),其是利用周围16个点进行计算,对比效果如下图所示。双三次插值的效果更加显著,但也意味着更高的开销,具体可参考这篇博文。
3.2 纹理分辨率过大
纹理分辨率过小会带来走样问题,实际上纹理过大产生的走样问题更加严重。对于一张网格纹理,期望渲染出来的结果如左所示,但直接采样得到的结果实际上是右图的效果(远处摩尔纹,近处锯齿)。
造成此现象的原因大多数是由于透视投影,地板上铺满了重复的方格贴图,根据近大远小,远处的一张完整的贴图可能在屏幕空间中仅仅是几个像素的大小,那么必然屏幕空间的一个像素对应了纹理贴图上的一片范围的点,这其实就是纹理过大所导致的,直观来说想用一个点采样的结果代替纹理空间一片范围的颜色信息,必然会导致严重失真!(从信号的角度来说就是,采样频率过低无法还原信号原貌)。下图中蓝色表示屏幕像素,网格代表纹理坐标空间。从图中可以看出,当采样率较高时(相机近处),屏幕中的一个像素对应纹理图像中的一个像素,而随着采样率的降低(相机远处),一个屏幕像素对应的纹理空间区域逐渐增大,从而造成走样,这种现象被称为屏幕像素在纹理空间的footprint。。
参考之前博客中反走样部分,对于这类问题,当然可通过超采样解决,但这种方法会带来巨量的性能开销,通过Mipmap、Ripmap或其他算法进行改进。
点查询(Point Query)和范围查询(Range Query)
我们前面讲到双线性插值其实就属于一种点查询方式:我们得知纹理上的任意一点,通过点查询得知其对应的颜色值。
而对于摩尔纹,则需要用范围查询,即我们知道一定范围的纹理像素,要查询出它的平均值(当然应对不同的情况我们也可查询一个范围内的最大值或最小值)。那么当我们得知一个范围后,如果能立刻得知它的平均值,就可以在不增加运算量的情况下,解决摩尔纹的问题了。
3.2.1 Mipmap
Mipmap就是一种可以帮我们实现范围查询的方法,它速度快,但并不是特别的准确,结果是一个近似值,此外它只能做正方形的范围查询。Mipmap的本质,其实就是一张纹理生成一系列的纹理,如下图:
如下图所示,远近两个圆圈内的像素点对应的footprint区域一定大小不同,远处圆圈里的footprint必然比近处的要大,因此必须要准备不同level的区域查询才可以,而这正是Mipmap。
我们假设原本的纹理是 n × n n×n n×n大小的(纹理大小也就是纹理像素的数量),原始的纹理图像为第0层(level 0)。然后我们用它增加更多层的纹理,每一层的分辨率都是上一层的一半,那么总共就会有 log 2 n \log_2n log2n 层。这样我们只需要在使用前先生成好mipmap,然后使用时直接使用它做查询,就可以节省下使用时很多的计算时间。
接下来需要利用屏幕像素的相邻像素点估算footprint大小再确定level D。如上图所示,在屏幕空间中取当前像素点的右方和上方的两个相邻像素点(4个全取也可以),分别查询得到这3个点对应在纹理空间的坐标,计算出当前像素点与右方像素点和上方像素点在纹理空间的距离,二者取最大值,那么level D就是这个距离的 log 2 \log_2 log2的值 ( D = log 2 L D = \log_2L D=log2L)。
这里D值计算出来可能不是一个整数,有两种解决方法。
采用四舍五入的方法选择最近的level
采用三线性插值的方法(下面level自身双线性插值,上面level自身双线性插值,两者之间再次线性插值)
将D值四舍五入获取整数的方式会产生明显的“分界”,如下图所示。
三线性插值是指首先用双线性插值求出D层和D+1层的值,然后再线性插值求最终的颜色值(x范围0-1),这样等于在双线性插值的基础上再做了一趟线性插值,所以我们称之为三线性插值。这样就可以使得mipmap层与层之间的颜色变化是连续的。
通过Mipmap,可以部分解决摩尔纹的问题,但有些情况在远处会产生过曝现象,如下图所示
这是因为Mipmap默认的都是正方形区域的Range Query,而实际情况屏幕像素,如下图所示。
3.2.2 Rapmap
Ripmap,我们也可称之为各向异性过滤,它和mipmap的不同之处就是它可以支持长方形的查询,生成出来的纹理如下: