TA百人计划 图形 1.3 纹理的秘密

纹理三问

纹理是什么

  • 宏观角度:一张图片(不准确)
  • 微观角度:一种可供着色器读写的结构化存储形式

简而言之,纹理是个容器。

以二维纹理为例,二维纹理以宽高以及想要存储的信息(RGBA 等)构造的一个三维数组 [i,j,k],其中 ij 类比数组或者矩阵,代表像素点,k 代表我们想要存储的 RGBA 或者其他信息。

由二维纹理可推得:

  • 一维纹理就是一个 i || j 其中一个为 0 的二维数组;
  • 三维纹理则是可以看做由二维向量层层叠加的四维数组;

图片是量化颜色信息并储存的容器纹理也是储存信息的容器,但纹理又不同于图片仅可储存颜色,它还可以存高度,法线,光照信息等
因为纹理是容器,所以我们也可以用代码自己敲一个函数,然后将结果信息存储到纹理中。

为什么存在纹理

纹理最初的目的即是用一张图片来控制模型的外观。利用纹理映射技术,我们可以把图片像是黏贴在模型的表面,逐纹素的控制模型的颜色。

通过纹理的使用,我们虽然牺牲了几何细节,但是降低了建模所需的工作量和存储空间,提高了读取速度。

如果一个模型我们要把所有的细节都表现出来,那无疑是一个巨大工作量,对于简单的模型还好说,但是当我们把工作转换到人体模型上面,人体结构的复杂,外加服装可能各式各样,花式不同,这无疑增大了建模师的工作量。

但是我们如果将一个纹理贴到这些模型上,能保证模拟物体表面的技术没有太大变化,在省略了很多细节之后,也能大致还原原来的表现形式。

纹理如何工作

下文继续

纹理管线

投影函数(非P矩阵的那个)

获取要渲染的位置,将其从模型空间投影到纹理坐标空间中,转化成纹理坐标(UV 坐标)。该过程通常在建模的展 UV 过程中使用,最终将结果存到顶点数据中。

通讯函数

使用通讯函数,可以对 UV 进行灵活的拓展,如平移,缩放,旋转等;

纹理采样

我们可以通过灵活拓展后的纹理坐标去获取纹理值,该过程称为纹理采样

  • 着色器中纹理通常以 SamplerVariable(采样变量)的形式存在,它是一种 uniform 型变量,在处理不同片元时,它是已知不变的;
  • 一个 Sample 与一个 Texture 对应,是一种特殊变量。如果是二维纹理时,则为 Sampler2D
  • 当我们使用 Texture2D 或者类似方式来访问纹理时,只要像素着色器不是直接用顶点着色器传过来,没有经过修改的数值,而是需要计算的,就会产生依赖纹理读取,这会影响性能。所以我们平时把 UV 相关的计算置于顶点着色器,而非像素着色器的原因之一;
  • 大部分的实时渲染中,用到的都是图像纹理,都是用 lookup 函数来索引值,但是也有程序纹理,程序纹理中涉及到的不是内存查找,而是函数计算,获取到的结果通常是 RGBA 四个值(也可以是别的如粗糙度的值);

如上图,模型空间中的位置是 (-2.3,7.1,88.2) 通过投影函数的变换后得到 UV 值 (0.32,0.29),我们假设这张贴图的大小是256*256,那通过与uv坐标相乘得到 (81.92,74.24) 的值,后续具体的则需要根据纹理采样的设定所决定。纹理的采样设置同时包含在纹理对象里面的,这里面有一些常见且重要的设置;

Wrap Mode

决定 UV 值在 [0,1] 以外的表现,OpenGL 使用“包装模式”,DirectX 使用“纹理寻址”模式

Filter Mode

它决定了当纹理由于变换而产生拉伸时,要采用哪一种滤波(这里指图像处理中的滤波)模式来调整其表现,即使纹理大小完全相同的情况下,也有可能因为没有对齐或者旋转,或角度问题导致一个像素覆盖四个相邻像素的情形,因此还是需要一定程度过滤。

简单来讲,纹理过滤就是用来描述在不同形状、大小、角度和缩放比的情况下如何应用纹理。

对于大部分可交互的图形应用上来说,纹理过滤是在专用硬件上,通过一系列技术来完成,但也可以在软件中完成或者软硬两者共同完成。

放大过滤纹理常见的三种方式:最近邻,双线性插值,立方卷积分别对应Unity中的Point,Bilinear,Trilinear

  • 最近邻:效果是像素化,在放大时,每个像素读取最邻近的纹素,可能会出现多个像素读取同一个纹素从而产生块状表现。虽然表现很差,但是消耗少;
  • 双线性插值:对于每一个像素点都找到相邻的四个像素点在二维空间上进行的线性插值,得到像素的混合值;

如上图,我们设 P(u,v)=(81.92,74.24)
样本位置-像素中点 (0.5,0.5)=(81.42,73.74)
最接近的四个像素点范围是 (81,73)\sim (82,74)
相对于该四个像素中心形成的坐标系位置 (u’,v’)=(0.42,0.74)
最后插值颜色=(1-0.42)×(1-0.74)t(x,y)+0.42(1-0.74)t(x+1,y)+(1-0.42)0.74t(x,y+1)+0.42×0.74×t(x+1,y+1)

  • 立方卷积插值:不仅考虑到周围四个直接相邻像素点的影响,还考虑它们变化率的影响

  • 卷积插值公式:假设 (x+u,y+u) 点就是 (x,y) 对应在目标图像的位置,双立方插值就是通过bicubic 基函数得到目标像素点周围的16个相邻像素目标像素点P的影响因子。

卷积插值公式:假设 (x+u,y+u) 点就是 (x,y) 对应在目标图像的位置,双立方插值就是通过 bicubic 基函数得到目标像素点周围的 16 个相邻像素目标像素点P的影响因子。

  • Qu’ilez 的光滑曲线插值:类似于双线性插值,它是在纹理坐标带入到双线性插值过程前,再额外做了一步处理

举个例子:我们还是取点P(u,v)=(81.92,74.24)
u’=81.92256+0.5=20972.02,
v’=74.24256+0.5=19005.94
u’=0.02,v’=0.94
u’=\dfrac{(s(u’)-0.5+20972)}{256}
v’=\dfrac{(s(v’)-0.5+19005)}{256}
新的 u’v’ 当做新纹理坐标代入双线性插值过程中求值

效果对比:

image-20221017150457331

  • 缩小的最近邻与双线性插值:会出现颜色丢失与闪烁的问题

Mipmap(多级渐远纹理技术)

它将原纹理提前用滤波处理来得到很多更小的图像,形成图像金字塔,每一层都对上一层图像降采样。这整组图片即为 Mipmap。

  • 由于每一级都是上一级的 1/4,所以整套纹理比原来的纹理多了 1/3 的内存;
  • Mipmap 核心在于如何选择正确的 level,以满足所说的采样定理,做法是使用像素单元格所形成的四边形的最长边近似像素覆盖的范围,四边形两边可通过偏导求得;
  • GPU 并非逐像素执行,而是将其分成 2*2 的一组,分块执行,主要原因之一就在于求 ddx 和 ddy 以及法线,求取level的思路参考代码部分;

float dx = ddx(i, uv);
float dy = ddy(i, uv);
float lod = 0.5 * log2(max(dot(dx, dx), dot(dy, dy)));

float3 albedo = tex2Dlod(_MainTex, float4(i, uv, 0, lod)).rgb;
  • 片元着色器中可以用 ddx 和 ddy 两个 API 求偏导数;

  • 因为我们要应用到具体 0 级,1 级,2 级,3 级…所以静态来看块与块之间过渡不平滑,动态来看可能会出现忽然的变化 level 前后不一致;

  • 三线性插值的出现就是为了解决上述问题,做法类似于双线性插值,它会对最接近的两层 Mipmap level 进行双线性过滤,然后再对其结果再进行一次双线性插值;

  • Mipmap 优点:不需要实时的累加所有影响这个像素的纹素,只需访问经过预处理的 Mipmap 图集,无论最后计算所得 level 为多少,但花费时间一样。虽然会增加包的体积的 1/3 但是消耗带宽较少,只需传输较小的想要的那张图即可,不需要都传;

  • Mipmap 缺点:过度模糊,因为我们一直假设这个 Texture 在投射屏幕上是各向同性的,如果一个像素单元格在 U 上覆盖了大量的纹理,但是在 V 上覆盖了少量的纹理,也就是说在 MipMap 情况下,那些被平均过的纹理就会被限制到这些正方形中,于是就产生了各向异性过滤处理部分问题

  • 各向异性过滤:指代一种思想,是各个方法的总称,严格说不指代某种具体方法;

  • Ripmap:它不仅仅进行了正方形的处理,是进行了各种比例的矩形预处理,以解决部分局限于正方形的问题,但也因此会生成许多额外的图,导致内存开销增大,那为什么说是部分解决?当我们沿着纹理对角线观察的时候,会出现一个很大斜角的矩形,此时即便是使用一个矩形来近似,也并不合适,因为包含了太多无关元素。

    RipMap 确实能比 Mipmap 带来更准确地结果,但是斜着的区域,同样会出现过度模糊的问题

  • 积分图 Summed-Area Table:创建一个和纹理大小相同,但是储存颜色精度更高的数组,在数组的每个位置,以左上为元素的原点,每一个位置计算并储存这个位置和纹素原点所形成的的矩形所对应的所有的纹素的总和。


    average=\dfrac{LR-UR-LL+UL}{w*h}

举个例子:3 行 4 列,就是以左上角为起点,前三行和前四列形成的矩形的所有元素的加和值为 19
3 行 2 列,就是 2+3+3+0+1+3=12

为什么在 Unity/UE4 里,开启各向异性过滤后,纹理内存并不是同理论般的 3 倍,而是 1/3?

各向异性过滤是总称,Ripmap 只是其中一种,如果任何一张纹理使用 RipMap 都会扩大纹理内存 3 倍,在实际项目中是用不起的。
对于现在的优化方法,重用 Mipmap,将屏幕像素反向投影到纹理空间,根据方块的最短边确定 level,较长的边则创建一条各向异性的线穿过方块中心,按照过滤等级的高低,沿着这条线进行多次的采样并合成,得到最终采样的结果。
总结下:因为其实它整套仍是用的还是 Mipmap 的算法;

优化与纹理应用

纹理图集/数组

  • 纹理图集/数组的最主要目的是,避免渲染时频繁改变纹理所带来的消耗
  • 纹理图集:如上图是把不同的贴图整合成一张大的贴图;
  • 纹理数组:数组里存储纹理;

纹理压缩

减少了资源在CPU中进行解压缩的过程;减小了包体大小,减少了数据量级,减轻了带宽计算的压力;内存的使用效率更高

立方体贴图 CubeMap

使用三维纹理坐标对立方体映射进行采样

根据 OpenGL,+x 代表右,-x 代表左;+y 代表上,-y 代表下;+z 代表后,-z 代表前

假设这个矢量是(-3.2,5.1,-8.4),那我们就用 -z 面,坐标 (-3.2/8.4,5.1/8.4), 再转化为 [0,1] 的范围,所以最后用来索引纹理的坐标就是 ((-3.2/8.4+1)/2, (5.1/8.4+1)/2)

天空盒常用,大环境下的以假乱真,是环境映射的一种实现方法。

  • 优点:实现简单快速,效果好;
  • 缺点:场景中引入新的物体/光源/物体发生偏移时,需重新生成新的立方体纹理(所以建议可以在静态场景的例如解密或恐怖游戏中使用);

凹凸贴图/位移贴图

凹凸贴图通过改变几何体表面各点法线存储相对高度,使原本平淡无奇的东西看起来错落有致,实际上并未修改顶点高度,只是定义一个虚拟高度;
位移贴图效果虽类似凹凸贴图,但是确实实实在在的改变了顶点的位置(对比两个阴影就能看出),想要位移贴图效果好,就必须在建模时尽可能多的加面,否则会显得棱角分明,而加了面后会增加内存负担;

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇