(个人笔记,由于刚开始学习再加上英语不太好,所以有的地理解的可能不太对,望指正)
Chapter 6 Texturing
Texturing Pipeline
纹理管线的开始是确定一个空间中的点,通常采用模型坐标,因为它可以随着模型的移动而移动。之后通过一个投影函数(projector function)将该点转换成纹理坐标,这一步称为mapping。在使用纹理坐标来访问纹理前,需要使用匹配函数(corresponder function)将纹理坐标转换到纹理空间中,得到的值可以用来获取纹理值,获取的值可能再被一次值转换(value transform)之后可以对模型表面进行修改。
将墙壁纹理设置到墙上的过程:
The projector function
投影函数一般讲三维坐标转换成纹理坐标,常用的方法包括球投影,圆柱投影和平面投影
投影函数也可以提供一些其他的参数,例如表面法线可以用来选择使用六个平面投影方向中的哪一个。
投影函数也可以跟投影完全没有关系,而是作为一种隐式的表面生成和细分的一部分。例如参数曲线表面天然包括一系列的(u, v)值在它的定义中。纹理坐标也可以通过其他的参数来确定,比如说视角方向,表面温度等,投影函数只是为了获取纹理坐标,通过顶点坐标进行计算只是其中一种方式。
在模型制作过程中,可能会使用多种不同的投影函数。
在实践中,投影函数也不仅仅是使用在模型制作阶段,处于某些目的也可以在顶点着色器或者是片元着色器中得到计算。在环境影射当中,会有专用的投影函数来计算每个像素值的纹理坐标。
球投影是将顶点投射到一个假想的球上。
圆柱投影的u坐标跟球投影一样,v坐标代表到圆柱中心的距离,通常用语一些有天然轴的物体。
平面投影是使用正交投影将物体投射到一个平面上。
对于一维纹理也有它的作用,例如用于地表模型,颜色代表它的海拔高度;渲染雨时也可以使用一维纹理来将线进行处理。
The corresponder function
匹配函数将纹理坐标转换到纹理空间中。它增加了使用纹理过程中的灵活性,例如使用匹配函数选择纹理的一部分进行使用。
另一种类型的匹配是在顶点或者是片元着色器中使用矩阵变换,它提供了纹理的在表面应用的移动旋转等方式。需要注意的是纹理变换的顺序是与一般预期相反的,因为纹理变换实际影响的是图像在哪里被看到,纹理本身是不被变幻的,变幻的是纹理所在的空间。
第三种类型的匹配函数控制图像应用的方式,也就是当纹理坐标超出[0, 1]的范围时如何进行映射,常规方法有:
wrap(DX),repeat(OpenGL):忽略纹理坐标的整数部分
mirror:[0, 1]区间与[1, 2]区间是相反的
clamp:使用图像的边作为超出范围坐标的值
border:使用一个给定值
在不同的轴向也可以使用不同的方式。
重复是一种高效的添加视觉细节的方式,但是3次以上的重复会使结果看起来不真实,一种解决方式是结合一个不重复的纹理一起使用,另一种解决方式是使用随机再结合纹理模式。
Texture values
除了直接使用使用获取的图像纹理值,也可以使用程序纹理计算一个结果进行使用。获取的纹理值也可以进行转换之后再使用。
Image texturing
依赖纹理读取(dependent texture read)有两个含义。在移动设备中,当获取纹理值时,不使用原始未经过任何修改的纹理坐标时都会发生依赖纹理读取,在一些旧设备上不发生依赖纹理读取时效率会高很多。另一个含义是在早期GPU中,依赖纹理读取指的是当一个纹理坐标依赖与一些之前的纹理值,例如纹理改变了表面的坐标,进而影响的cube map的采样结果。
降低纹理采样结果的走样是希望得到的结果,对于输入进行滤波和对于输出进行滤波是有区别的,只要输入和输出是线性相关的,那么对于单个纹理值进行滤波就等同于对于最终颜色进行滤波,但是对于其他非线性相关的值例如法线,标准滤波方式就会产生走样。
Magnification
最常用的放大滤波方式是nearest neighbor(使用box filter)和双线性插值,除此之外还有三线性插值。
nearest neighbor滤波方式最大的缺点就是像素化。
双线性差值图像表示:
计算目标 ( u ′ , v ′ ) (u', v') (u′,v′)的插值结果的方法分为两步:首先对 t ( x , y ) 和 t ( x + 1 , y ) 以 及 t ( x , y + 1 ) 和 t ( x + 1 , y + 1 ) t(x, y)和t(x + 1, y)以及t(x, y + 1)和t(x + 1, y + 1) t(x,y)和t(x+1,y)以及t(x,y+1)和t(x+1,y+1)在水平方向进行插值得到两个值 ( 1 − u ′ ) t ( x , y ) + u ′ t ( x + 1 , y ) (1 - u')t(x, y) + u't(x + 1, y) (1−u′)t(x,y)+u′t(x+1,y)和 ( 1 − u ′ ) t ( x , y + 1 ) + u ′ t ( x + 1 , y + 1 ) (1 - u')t(x, y + 1) + u't(x + 1, y + 1) (1−u′)t(x,y+1)+u′t(x+1,y+1),之后再对这两个结果在垂直方向进行插值可以得到:
b ( p u , p v ) = ( 1 − v ′ ) ( ( 1 − u ′ ) t ( x , y ) + u ′ t ( x + 1 , y ) ) + v ′ ( ( 1 − u ′ ) t ( x , y + 1 ) + u ′ t ( x + 1 , y + 1 ) ) = ( 1 − u ′ ) ( 1 − v ′ ) t ( x , y ) + u ′ ( 1 − v ′ ) t ( x + 1 , y ) + ( 1 − u ′ ) v ′ t ( x , y + 1 ) + u ′ v ′ t ( x + , y + 1 ) b(p_u, p_v) = (1 - v')((1 - u')t(x, y) + u't(x + 1, y)) + v'((1 - u')t(x, y + 1) + u't(x + 1, y + 1)) \\ = (1 - u')(1 - v')t(x, y) + u'(1 - v')t(x + 1, y) + (1 - u')v't(x, y + 1) + u'v't(x + , y + 1) b(pu,pv)=(1−v′)((1−u′)t(x,y)+u′t(x+1,y))+v′((1−u′)t(x,y+1)+u′t(x+1,y+1))=(1−u′)(1−v′)t(x,y)+u′(1−v′)t(x+1,y)+(1−u′)v′t(x,y+1)+u′v′t(x+,y+1)
解决方法产生的模糊的一种方式是使用细节纹理(detail texture)。
一般来讲高阶滤波方式可以表示为线型滤波的一种重复表示,在可以利用GPU针对于线性插值的硬件来通过查找进行高阶滤波的计算。
也可以使用平滑曲线进行插值计算,两种常用的曲线是smoothstep()和quintic(),它们的方程式是:
s m o o t h s t e p : s ( x ) = x 2 ( 3 − 2 x ) q u i n t i c : q ( x ) = x 3 ( 6 x 2 − 15 x + 10 ) 其 中 : s ′ ( 0 ) = s ′ ( 1 ) = 0 , q ′ ′ ( 0 ) = q ′ ′ ( 1 ) = 0 smoothstep: s(x) = x^2(3 - 2x) \\ quintic: q(x) = x^3(6x^2 - 15x + 10) \\ 其中: s'(0) = s'(1) = 0, q''(0) = q''(1) = 0 smoothstep:s(x)=x2(3−2x)quintic:q(x)=x3(6x2−15x+10)其中:s′(0)=s′(1)=0,q′′(0)=q′′(1)=0
图像:
使用这种方式计算 ( u ′ , v ′ ) (u', v') (u′,v′)的方法是:首先乘以纹理的尺寸再加0.5,取小数部分保存在 u ′ 和 v ′ u'和v' u′和v′中,之后经过变换 ( t u , t v ) = ( q ( u ′ ) , q ( v ) ′ ) (t_u, t_v) = (q(u'), q(v)') (tu,tv)=(q(u′),q(v)′),变换后的坐标仍位于[0, 1]范围内,再减去0.5并还原之前的整数部分,最后得到的横纵方向的两个值分别除以纹理的宽和高,得到最终结果。
这种方法对于每个纹素都会产生一个高原值(plateaus),这意味着如果在RGB空间内这种方式的插值结果会得到一个平滑的但仍然有楼梯状的结果,如图所示:
Minification
几种缩小滤波的效果图:
纹理的信号频率决定了纹素在屏幕上的距离有多近,根据Nyquist限制,为了避免走样需要保证纹理的信号频率不能大于采样频率的一半。所以为了达到反走样的目的,可以提高采样频率或者是降低纹理频率,第五章介绍的增加采样频率的方法对于采样频率的提升是有限的,因此需要其他的方法来缩小纹理。
方法的基本思想就是对纹理的预处理并创建一系列数据来帮助获取一个像素上多个纹素的快速近似效果。
Mipmapping
在真正的渲染之前,原始纹理会被扩充附带上多个该纹理的不同缩小版本。
创建高质量的mipmap的两个关键点是合适的滤波和γ校正。
常用的创建mipmap的方式是将每个2*2的纹素集合求平均来得到下一级的mipmap纹素值,仅仅使用box-filter一般会得到不太好的结果,因为它会模糊掉低的频率而保留了高频部分从而导致走样。比较好的滤波方式有Gaussain,Lanczos和Kaiser等。
对于非线性空间的纹理(颜色等),需要进行γ校正将其从sRGB转换到一个线性空间中。目前很多API对于sRGB都有支持并产生正确的mipmap。
当一个像素投影到一个纹理上后,可能会包含多个纹素。直接使用像素点的边界是不严格精确地,但是可以用来作简化的表示:
如何使用mipmap的关键是计算一个d值(OpenGL中称为 γ \gamma γ,同时也被称为纹理细节层次(texture level of detail))。通常有两种方式计算:第一种是使用像素点形成的四边形较长边来近似像素的覆盖范围;第二种是测量 ∂ u / ∂ x , ∂ u / ∂ y , ∂ v / ∂ x 和 ∂ v / ∂ y \partial u/\partial x,\partial u/\partial y,\partial v/\partial x和\partial v/\partial y ∂u/∂x,∂u/∂y,∂v/∂x和∂v/∂y中的最大绝对值。每个微分值代表了纹理坐标针对于屏幕轴的变化量,(然后呢?)。
OpenGL中 γ \gamma γ的计算方式如下:
γ
=
l
o
g
2
ρ
+
l
o
d
b
i
a
s
\gamma = log_2\rho + lod_{bias}
γ=log2ρ+lodbias
其中
ρ
\rho
ρ为为图像和纹理映射多边形大小比例因子
得到d值之后,将其作为mipmap金字塔的层级坐标,d是一个浮点数,可以用来在两个mipmap层级之间进行插值。同时也可以添加一个LOD bias用来对d进行偏移,如果图像有点模糊该值可以为负值,如果是合成的有走样的图片可以用正值。
mipmap的优点是预处理了影响某个像素值的纹素的插值结果,使滤波操作对于多大程度的缩小都可以在固定的时间完成。缺点是可能会过度的模糊。
Summed-Area Table
使用SAT首先要创建一个与纹理尺寸相同的数组,但是每个元素都会包含精度更高的颜色值。每个位置的元素都包含了由该点和(0, 0)点构成的矩形的所有纹素的和,在纹理生成时,像素点投射到纹理上形成一个矩形,通过SAT获取到该矩形的平均颜色值作为纹理颜色值返回给该像素。
计算公式如下:
c = s [ x u r , y u r ] − s [ x u r , y l l ] − s [ x l l , y u r ] + s [ x l l , y l l ] ( x u r − x l l ) ( y u r − y l l ) c = \frac{s[x_{ur}, y_{ur}] - s[x_{ur}, y_{ll}] - s[x_{ll}, y_{ur}] + s[x_{ll}, y_{ll}]}{(x_{ur} - x_{ll})(y_{ur} - y_{ll})} c=(xur−xll)(yur−yll)s[xur,yur]−s[xur,yll]−s[xll,yur]+s[xll,yll]
SAT的缺点是对于对角线方向的线会产生模糊,因为相当于获取了很多并不在像素周围点计算平均值。
Unconstrained Anisotropic Filtering
这个方法使用了mipmap,像素投射到纹理上,形成一个四边形,首先使用短边来确定d,之后用长边创建一条各向异性线位与两条长边中间,如果异向性位与1:1到2:1之间那么沿该方向取样两个。
该方法解决了SAT沿对角线方向模糊的缺陷。
Volume Textures
Cube Maps
Cube map使用六个方形纹理形成的正方体来进行纹理采样,纹理值通过从正方体中心发射出的射线进行查找,射线与正方体相交于一点,该点坐标的绝对值最大轴方向用来选择在哪个面上进行采样,其他两个轴向的坐标根据最大坐标归一化之后作为纹理坐标。
Texture Representation
为了降低渲染过程中更换纹理产生的消耗,常用的解决方法有:纹理图集(texture atlas),纹理数组(texture array)和无约束纹理(bindless texture)。
纹理图集是将多个图片合并到一张纹理当中。在使用纹理图集的时候要注意mipmap的生成和访问问题,因为高层mipmap可能包含了一些不相关的图片(原文中标记了一些解决的论文)。使用纹理图集有两个问题,一个是纹理的wrap和mirror模式会影响整个图集纹理而不是每一个子纹理,另一个是对图集生成mipmap时,一个子纹理可能会渗透进另一个子纹理。不过这个可以通过先生成mipmap再生成图集并使用2的次幂大小的纹理来避免。
另一个解决纹理图集的上述问题的方式是使用纹理数组,所有纹理数组中的子图都需要有相同的维度,格式,mipmap层级和MSAA设置。纹理数组只需要创建一次,就可以在着色器中使用索引进行访问。
为了解决状态变更导致的损耗,可以使用无约束纹理。它解决了将一般步骤下需要将纹理绑定到纹理绑定点然后使用,然而绑定点数量是有限的问题。每个纹理被关联到一个指向对应数据结构的指针,也称为一个句柄,句柄可以被多种不同方式访问到包括uniform,变量或者是ssbo等,同时也降低了绑定时的消耗,不过需要程序保证纹理是存在GPU中的。
Texture Compression
硬件上使用的压缩方法之一是DXTC(dx10 之后称为BC-Block Compression),opengl实际上也是使用的该方法。它的优点之一是创建固定大小的压缩图片,互相独立的编码块和简单的解码方式。
DXTC/BC有多种模式,他们共有一些属性:编码是以4*4纹素块为单位进行的也称为tile,每一个块单独编码。编码是基于插值运算的,每一个编码对象,都会存储两个参考值,块中的每个纹素都会存储一个插值系数,通过该系数在两个参考值之间进行插值运算,原理如图所示:
多种模式的区别:
这种方法的主要缺点是有损耗的,原始的颜色值是不能够取得的,只能通过参考值进行计算,如果一个块中的颜色很多,那么就会有损耗。
BC1-BC5的另一个问题是所有颜色都必须是位与同一条线上的,不能够是离散的。
OpenGL ES中使用了Ericsson texture compression(ETC)的压缩方式。具体原理:http://kc123kc.github.io/2016/03/20/ETC1-Introduce/
OpenGL ES3.0 之后添加了ETC2,ETC2包含了alpha通道。
Ericsson alpha compression(EAC)可以压缩只包含一个分量的图片,它的处理方式类似于ETC,压缩结果是每个纹素包含4位。一般跟ETC2结合,使用两个EAC通道来保存法向量,一般来说法向量都只保存x,y轴数值,z轴通过计算可以得出。
PVRTC通过硬件powerVR使用,一般用于iphone或者ipad上,也是4*4纹素组成一个块,可以将每个纹素压缩至2个bit或者4个bit。边界处会损耗比较大。
ASTC在ios 8之后好像有支持,它的块大小可能从44到1212,块大小越大质量越低,压缩率越高,它将每个块压缩至128个bit。
可以使用直方图均匀化对纹理进行处理,它将图像中的颜色值扩充到整个颜色域之内,提高了对比度,可以减少压缩后banding artifacts。
也可以通过将颜色压缩到不同的颜色空间中来提高压缩的速度,比如从RGB到YCoCg,两个空间的转换都是线性的:
( Y C o C g ) = ( 1 / 4 1 / 2 1 / 4 1 / 2 0 − 1 / 2 − 1 / 4 1 / 2 − 1 / 4 ) ( R G B ) \begin{pmatrix} Y \\ C_o \\ C_g \end{pmatrix}= \begin{pmatrix} 1/4 & 1/2 & 1/4 \\ 1/2 & 0 & -1/2 \\ -1/4 & 1/2 & -1/4 \end{pmatrix} \begin{pmatrix} R \\ G \\ B \end{pmatrix} ⎝⎛YCoCg⎠⎞=⎝⎛1/41/2−1/41/201/21/4−1/2−1/4⎠⎞⎝⎛RGB⎠⎞
逆操作也很容易:
G = ( Y + C g ) t = ( Y − C g ) R = t + C o B = t − C o G = (Y + C_g) \\ t = (Y - C_g) \\ R = t + C_o \\ B = t - C_o G=(Y+Cg)t=(Y−Cg)R=t+CoB=t−Co
另一种转换方式:
{ C 0 = R − B t = B = ( C o > > 1 ) C g = G − t Y = t + ( C g > > 1 ) ≡ { t = Y − ( C g > > 1 ) G = C g + t B = t − ( C o > > 1 ) R = B + C o \begin{cases} C_0 = R - B \\ t = B = (C_o >> 1) \\ C_g = G - t \\ Y = t + (C_g >> 1) \end{cases} \equiv \begin{cases} t = Y - (C_g >> 1) \\ G = C_g + t \\ B = t - (C_o >> 1) \\ R = B + C_o \end{cases} ⎩⎪⎪⎪⎨⎪⎪⎪⎧C0=R−Bt=B=(Co>>1)Cg=G−tY=t+(Cg>>1)≡⎩⎪⎪⎪⎨⎪⎪⎪⎧t=Y−(Cg>>1)G=Cg+tB=t−(Co>>1)R=B+Co
YCoCg变换通常用于图像压缩,The YCoCg transform and other luminance-chrominance transforms are often used for image compression, where the chrominance components are averaged over 2 × 2 pixels。(这句话啥意思)
Procedural Texturing
程序纹理一般用于离线渲染,图像纹理一般用于实时渲染。体积纹理是程序纹理的一种应用,一种常见的方法是使用一个或多个噪声函数,一般来说三维栅格点都会提前计算。
程序纹理的另一种用途是物理模拟或者其他类型的交互过程比如说水波。
对于二维程序纹理参数化会比图像纹理更加困难,对于图像纹理拉伸和接缝的问题可以通过手动处理解决。对于反走样的处理来说既困难又容易,一方面来讲程序纹理是没有mipmap的,需要程序处理,另一方面程序对于程序纹理的内容是了解的,所以比较容易通过裁剪来避免走样,特别是针对于通过噪声函数生成的纹理,因为噪声的频率是已知的,所以任何引起走样的频率都可以被舍弃。
Texture Animation
Material Mapping
Alpha Mapping
alpha映射的一个用途是贴花,如图所示:
另一种用途可以作为cutouts
为了减少大量半透物体重叠导致的排序苦难的问题,有几个解决方法。首先是使用alpha测试,也就是丢弃一些alpha值过低的片元。另一种是每个模型使用两个通道,一个通道渲染实体部分,会写入深度缓存,另一个渲染透明部分不写入深度缓存。
alpha测试会有几个问题,也就是过度放大和缩小。当使用mipmap时,高级的mipmap会平均周围纹素的alpha值,导致mipmap大量纹素不能通过alpha测试,如图所示:
一种解决方式(Computing Alpha Mipmaps)是使用:
c k = 1 n k ∑ i ( α ( k , i ) > α t ) c_k = \frac{1}{n_k}\sum_i(\alpha(k, i) > \alpha_t) ck=nk1i∑(α(k,i)>αt)
其中 n k n_k nk是第k级mipmap的纹素数目, α ( k , i ) \alpha(k,i) α(k,i)是该级mipmap位与i处的纹素alpha值, α t \alpha_t αt表示原alpha阈值,如果 α ( k , i ) > α t \alpha(k, i) > \alpha_t α(k,i)>αt满足则为1否则为0。对于每一级的mipmap都会计算一个新的 α k \alpha_k αk来取代 α t \alpha_t αt,使 c k c_k ck尽量接近0,可以使用二分查找。
第二种解决方式(Anti-aliased Alpha Test: The Esoteric Alpha to Coverage)是在着色器中随着mipmap级别的增长,将alpha值进行一定比例的提高。
第三种解决方式(Hashed Alpha Testing)是使用一个随机函数来进行片元的丢弃:
if (texture.a < random(0)) discard;
为了避免一些时间和空间高频噪声可以使用一个hash方法:
float hash2D(x, y){
return fract(1.0e4 * sin(17.0 * x + 0.1 * y) * (0.1 + abs(sin(13.0 * y + x))));
}
float hash3D(x, y, z){
return hash2D(hash2D(x, y), z);
}
hash函数的输入模型空间坐标除以模型空间坐标对于最大屏幕空间的导数(这是个什么鬼。。),之后进行clamping
alpha to coverage将透明度转换成多有少个子像素覆盖了像素。
https://www.zhihu.com/question/25822656
(Anti-aliased Alpha Test: The Esoteric Alpha to Coverage)一文中使用了fwidth使alpha to coverage产生锐利的边界。
在对颜色进行插值时,需要首先将alpha应用到颜色值上之后再进行插值,否则得不到正确的结果。在生成mipmap时也要用同样的方式进行。
Bump Mapping
对于bump mapping来说,法线必须根据一些框架(frame)来改变方向,所以对于每一个顶点都添加了一个tangent frame,也称为切线空间基(tangent-space basis)。
法向量,切向量和副法线向量组成一个矩阵
( t x t y t z 0 b x b y b z 0 n x n y n z 0 0 0 0 0 ) \begin{pmatrix} t_x & t_y & t_z & 0 \\ b_x & b_y & b_z & 0 \\ n_x & n_y & n_z & 0 \\ 0 & 0 & 0 & 0 \end{pmatrix} ⎝⎜⎜⎛txbxnx0tybyny0tzbznz00000⎠⎟⎟⎞
该矩阵可以将光线方向从世界坐标转换到切线坐标。
Blinn’s Methods
Blinn的第一种方法是在每个纹素中存储两个值,这两个值分别乘以贴图的uv轴向,想乘后得到的两个向量用来偏移顶点本来的法向量。第二种方式是高度图。
Normal Map
法线一般保存切线空间中的值。
法线的滤波一般比颜色的滤波要复杂很多,通常来说法线与着色后的颜色之间不是线性关系。
Lambertian表面是一种理想表面,在任何观测点都能够看到相同的亮度的表面,因此在这种情况下法线贴图对于着色的影响几乎是线性的。
在非Lambertian情况下一般将滤波的输入进行分组而不是单独的处理。
法线贴图也可以通过高度图获取:
h x ( x , y ) = h ( x + 1 , y ) − h ( x − 1 , y ) 2 h y ( x , y ) = h ( x , y + 1 ) − h ( x , y − 1 ) 2 n ( x , y ) = ( − h x ( x , y ) , − h y ( x , y ) , 1 ) h_x(x, y) = \frac{h(x + 1, y) - h(x - 1, y)}{2} \\ h_y(x, y) = \frac{h(x, y + 1) - h(x, y - 1)}{2} \\ n(x, y) = (-h_x(x, y), -h_y(x, y), 1) hx(x,y)=2h(x+1,y)−h(x−1,y)hy(x,y)=2h(x,y+1)−h(x,y−1)n(x,y)=(−hx(x,y),−hy(x,y),1)
得到的值是非归一化的。
水平映射(Smooth Horizon Mapping)可以使法线贴图在自身产生阴影。
Parallax Mapping
法线贴图只改变了表面的法向量,不会使顶点随着观察点的变化而发生位置上的改变。视差映射解决了这个问题,可以使物体的位置随着观察位置的改变而变化,它的主要思路是计算某个像素由于高度影响之后哪一部分将会被看到。
具体原理如图:
计算公式如下:
P a d j = p + h ∙ v x y v z P_{adj} = p + \frac{h \bullet v_{xy}}{v_z} Padj=p+vzh∙vxy
注意需要在切线空间中进行计算。由于周围的纹素一般有相同的高度,所以使用原始位置的高度进行计算是合理的,但是这种方式随着观察角度越来越接近水平而会产生较大的误差。所以可以令 v z = 1 v_z = 1 vz=1,如图所示:
Parallax Occlusion Mapping
视差映射的前提是假定某个像素的高度与其周围的像素高度是相同的,但是不一定都能满足这个前提,真正需要的是视线与高度图相交的第一个点。