实时渲染基础(4)纹理(Texture)

纹理映射(Texture Mapping)

纹理(Texture) 相当于着色物体的"皮肤",负责提供基础颜色,而为了方便表示纹理(可以想象下,一个3D物体的皮肤其实是可以展开成一张平面),往往使用一个二维颜色数组去表示纹理。

于是纹理平面就有了自己的坐标系(纹理空间),通常用u、v表示坐标轴。

在之前的着色(Shading)计算中,光照计算只是计算了物体受到多少光照的影响(可以粗略理解成亮度,因为有些光本身也会提供颜色),而纹理则可以给这些物体提供基础颜色。

为了使用纹理,对于每个三角形顶点vertex属性额外需要存储u、v坐标以便映射到纹理空间(由于三角形也是一个平面,因此非常方便映射到平面的纹理空间),三角形内的点则只需要根据三角重心坐标插值也能算出对应的u、v坐标。这样,三角形每个像素点就可以找到纹理中对应位置的颜色。

在某些情形下,我们可能需要将空间中的曲面(一般是指凸多面体)映射到纹理平面,即 \(f:\R ^3 \rightarrow \R ^2\)​ ; \((x,y,z)\rightarrow(u,v)\)​ ,那么就需要借助 球形贴图(Spherical Map) 或者 立方体贴图(Cube Map)

球形贴图(Spherical Map)

所谓球形贴图,就是以球的形式映射贴图。

例如一个球面展开后,会得到这样一张贴图(但是球形贴图看起来会有一种扭曲现象,表现不直观):

球形贴图映射的思路:

  1. 假设一个单位球包围了物体中心,当物体中心看向物体表面某个位置 \((x,y,z)\) 时,从中心朝这个位置发出一条射线,此时射线会与单位球相交于某点。
  2. 根据射线与单位球体的交点坐标 \((x_o,y_o,z_o)\)​,推算出交点所在的偏航角和俯仰角 \((yaw,pitch)\)​,然后来映射成在球形贴图对应的 \((u,v)\)​​​ 坐标点。

此处的物体一般是指凸多面体网格,而渲染中的曲面(包括球面)实际上也是又若干个三角面组成,因此也算是多面体网格。

立方体贴图(Cube Map)

立方体贴图,通过使用构成立方体六个表面的六张贴图存储周围环境影像:

立方体贴图映射的思路:

  1. 假设一个单位立方体包围了物体中心,当物体中心看向物体表面某个位置 \((x,y,z)\) 时,从中心朝这个位置发出一条射线,此时射线会与单位立方体相交于某点。
  2. 根据视线与单位立方体的交点坐标 \((x_o,y_o,z_o)\),在\(x_o\)\(y_o\)\(z_o\)中取绝对值最大的那个分量,根据它的符号来判定来确定要映射在哪一个面。

  1. 确定映射在第几个面后,剩余另外两个分量便是来映射成在第几张立方体贴图中的 \((u,v)\) 坐标点。

纹理走样问题

纹理映射的一个问题是,当纹理颜色变化多且高分辨率(高频信息多),而渲染目标物体的像素量少(采样频率过低)时,很容易出现锯齿现象和摩尔纹现象。

换句话说,当屏幕一个像素点(如对应下图的斜四边形)实际对应纹理很大片的纹素区域时,只通过像素点中心采样得到的纹理颜色结果不能准确表示整片区域的纹理颜色。

使用超采样的方法可以避免走样问题,然而需要付出极大开销:

Mipmap

Mipmap技术则可以让纹理采样进行快速的近似正方形范围查询,它需要预先提供多层分辨率各不同(每层纹理长宽都比上层缩小一半)的纹理(用D表示它们的层级)

注:Mipmap技术的纹理额外空间开销为原分辨率纹理的1/3

这样当屏幕像素点对应纹理中较大片纹素区域时,可以选择相应高层级的纹理(低分辨率纹理),这样就能近似的对应低分辨率纹理中的一个纹素而非对应高分辨率纹理的一大片纹素区域,从而也就能近似表示这片区域的纹理颜色。

选择层级的方式则是计算屏幕像素点对应在纹理坐标中的最大微分(du、dv中取变化最大的),从而得到近似正方形的边长L,最后通过log2函数可以得出层级D,这样就可以选择合适的Mipmap层级。

为了避免着色三角形时而远时而近,导致频繁切换Mipmap层级产生突兀的变化,实际上还需要使用了层级之间的插值(根据D值),这样就可实现两个层级之间切换的平滑过渡。

各向异性过滤(Ripmap)

Mipmap的一个缺点是,它只适合近似正方形范围查询,当一个屏幕像素点对应的纹素范围是别的形状(尤其长条形状)时,很容易导致近似正方形覆盖的区域比实际覆盖区域大得多,从而造成过度模糊现象(例如下图中远处的格子已经模糊地看不见黑色区域)

各向异性过滤的原理:在Mipmap的基础上提供横向伸缩和纵向伸缩的纹理层级(以适应横着和竖着的长条形状);但是这只能减少上述过度模糊现象的发生,因为实际渲染中还有斜向的长条形状或者其它难以近似的形状。

注:各向异性技术的纹理额外空间开销为原分辨率纹理的3倍

此外,还有一种少见的各向异性过滤方法:EWA过滤,大概原理就是任意一个不规则形状都可以拆解成不同的圆形,虽然通过多次查询的效果很好,但是性能代价非常高。

纹理应用技术(Texture Application)


纹理通常被认为是物体的“皮肤”,因为本质上它是一个二维数组,存储的元素是颜色。但是随着纹理技术的发展,纹理衍生成了一个广泛的意义:存储某种数据的N维数组。下面各种技术便是将纹理应用到不同方面的体现。

天空盒(SkyBox)


现实世界中,有些物体非常远(例如远处的山、树林、天空),无论观察者怎么移动,这个物体的大小是几乎没有什么变化的。这些物体往往是做成场景的背景图,一般被称为 天空盒(SkyBox)

天空盒渲染原理:

  1. 先准备6个面的天空背景制成 Cube Map。
  2. 将一个不会旋转、大小随意的立方体物体始终罩在摄像机的周围(让摄像机始终位于这个立方体的中心位置)。
  3. 每一帧摄像机渲染该立方体时,shading 应采用立方体贴图映射方式来采样 Cube Map。

渲染该立方体时应当选择不写入深度方式(天空盒在渲染时应该为最远的深度,换句话说不应该遮挡任何其他物体)+取消背面消隐(因为在渲染立方体内部表面)。

优化:

  • 天空盒渲染应该放在最后的渲染顺序,可以减少很多像素overdraw开销(放在最后的渲染顺序可以直接丢弃大量像素,而不必对这些迟早要丢弃的像素进行着色)

环境映射(Environment Mapping)


现实世界的环境光是非常复杂的,存在大量经过多次反射后到达着色物体后再到达人眼的间接光(例如光照在窗户上,窗户再反射到杯子上,杯子再反射回人眼),这个过程中会把反射经过的物体颜色按一定权重混合在一起(因此看到的杯子混合了窗户的颜色),最后在着色物体形成近似“镜面反射”的效果(本质上就是接受了复杂环境光的结果)。

为了实现接受环境光的效果,我们可以用一个贴图来存放环境光信息,即 环境贴图(Environment Map)反射贴图(Reflection Map),在给物体着色的时候不仅采样普通纹理,也采样环境贴图,按一定比例混合这两者的颜色(例如物体材质越光滑,镜面反射效果越强)。

对于360°方向均需要镜面反射现象的物体(如金属球),应采用立方体贴图方式映射;对于单一方向需要反射现象的物体(如一面镜子),可以使用普通方式映射。

如何生成立方体贴图方式的环境贴图(一个最简单的方式):

  1. 设置6个摄像机位于物体中心,每帧分别朝六个方向渲染得到的图像输出到立方体贴图里对应的面上。

优化:

  • 一般的环境贴图并不需要高精度的环境光信息,因此可以使用分辨率更低的环境贴图(这样生成环境贴图的开销也会减少不少)。

光照贴图(Light Map)

在平时的渲染中,高质量的光照(例如光线追踪, 辐射度, AO,阴影等算法)计算量无疑是庞大的,实时计算这些光照性能开销巨大,但是光照贴图给我们提供了一个预计算的方法:

  1. 烘培(Bake):预先计算这些复杂的光照,并保存进一个专门存放光照计算结果的光照贴图(Light Map)。
  2. 当物体在着色的时候,不仅采样正常纹理+Light Map,便可以让物体拥有高质量的着色。

在Unity中,如果我们把有网格组件的GameObject勾上static属性时,该物体会被认为是参与烘培的物体,从而为它预计算出光照贴图(也包括重新计算其它static物体的光照贴图)

好处:

  • 烘培光照,可以减少大量运行时的开销。

代价:

  • 光照是静态的,不能动态变化(预计算特点),因此只适用于静态物体。
  • 烘培(即预计算光照)的用时是漫长的,需要开发人员的耐心 😃
  • 若场景同时包含静态物体和动态物体,需要考虑静态光照与对动态光照的结合算法。

环境光遮蔽贴图(Ambient Occlusion Texture Map)


环境光遮蔽(Ambient Occlusion)本质上也属于环境光的一部分计算,只不过是表示遮蔽的部分(而非光照的部分)。这种屏蔽的来源是来物体和物体(包括局部物体)相交或靠近的时候遮挡周围漫反射光线的效果,可以解决或改善场景中缝隙、褶皱与墙角、角线以及细小物体等的表现不清晰问题,综合改善暗部阴影细节,增强空间的层次感、真实感。

而通过环境光遮蔽贴图,我们可以预先计算环境光屏蔽结果,并将计算结果存放于贴图中。等着色的时候便可以通过采样环境光遮蔽贴图来得到遮蔽系数,从而增添一些暗部阴影细节。

凹凸映射(Bump Mapping)


所谓凹凸贴图,就是通过改变表面各点的法线,使本来是平的东西看起来有凹凸的效果,是一种欺骗眼睛的技术(因为物体结构并没有发生改变)。例如这个球看起来凹凸不平,实际它的几何结构就是光滑的球,只是通过凹凸贴图改变了各像素点的法线方向,从而导致光照结果不一,产生出凹凸不平的视觉效果:

高度贴图(Height Map)

高度贴图中存储的元素是高度 \(H\)​​​,用于表示局部高度偏移。对输入位置 \(p=(u,v)\)​​ 通过一定公式就能得到其位置上的法线 \(n\)​​。

  1. 先计算出p点上u、v方向的切线值:

\(\frac{dp}{du} = c1* [H(u+1,v)-H(u,v)]\)​​​​

\(\frac{dp}{dv} = c2* [H(u,v+1)-H(u,v)]\)​​​​​

  1. \(p\) 点的法线(切线空间下)为:

\(n_{tangent}(p) = Normalize(-\frac{dp}{du},-\frac{dp}{dv},1)\)

实际上,切线空间下的法线往往还不可以直接使用在光照计算,这是因为光线、视野等方向的向量往往实在模型空间下的,需要将所有这些方向统一坐标空间才可共同参与光照计算。后面切线空间的法线贴图会给出转换切线空间和模型空间的方式。

模型空间的法线贴图(Object-Space Normal Map)

高度贴图的一个问题是它需要经过较复杂的计算才能得出法线(开销略大),而法线贴图的做法则是直接存储表面法线的方向(相当于预先计算好法线)。

不过,在存储法线时需要将法线方向的分量[-1,1]映射到像素分量[0,1]:

\(pixel=(n+1)/2\)

同理,当对法线纹理进行采样后,也需要进行一次反映射,用于得到原先的法线方向:

\(n=pixel∗2−1\)

切线空间的法线贴图(Tangent-Space Normal Map)

切线空间:对每个模型顶点,它都有一个属于自己的切线空间,其原点是顶点本身,z轴就是法线方向(normal),x轴就是顶点的切线方向(tangent),y轴就是法线和切线叉积所得,也就是副切线(bitangent)。

而另一种法线贴图则是记录切线空间下的法线(第一种记录的表面法线是在模型空间下的),换句话说,切线空间的法线本质上就是在记录"相对"的法线信息(而模型空间的法线本质上则是记录"绝对"的法线信息)

将该切线空间下的法线 \(n_{tangent}\) 转换为模型空间下的法线 \(n\) 的方法:

  1. \(B = N \times T\) ,其中 \(N\)\(T\) 分别为顶点上的原始的法线、切线(tangent),\(B\) 则称为副切线(bitangent)。

  2. \(n = [T\ B\ N]\cdot n_{tangent}\)

好处:

  • 自由度高、可重用:模型空间下的Normal Map记录的是绝对法线信息,仅可以用于创建它的那个模型;而切线空间下的Normal Map记录的是相对法线信息,可用于任何朝向的任何模型/面。
  • 可进行UV动画:可以移动一个纹理的UV坐标来实现凹凸移动的效果。UV动画在水或者火山熔岩这种类型的物体长经常用到。
  • 可压缩:由于Tangent-Space Normal Map中法线的Z方向总是正方向的,因此我们可以仅存储XY方向,而推导得到Z方向(通过向量标准化)。

代价:

  • 切线空间的法线贴图需要计算额外的空间变换:在计算光照时,需要统一各种方向向量所在的坐标空间,而法线贴图中存储的法线是切线空间下的方向,因此需要额外计算(模型空间下的法线贴图则不需要)。

光照计算的空间变换有两个选择:

  1. 切线空间下进行光照计算,需要把光照方向、视角方向变换到切线空间下(性能上较优,常用)
  2. 世界空间下进行光照计算,需要把采样得到的法线方向变换到世界空间下,再和世界空间下的光照方向和视角方向进行计算。(通用性上较优)

视差贴图(Parallax Map/Offset Map)

法线贴图的一个问题是与当前的视角无关。因为实际上,从不同的角度去观察一个凹凸不平的物体上的同一位置,看到的法线应该是不一样的。

例如下图,本应该看到的是真实模型B点的法线,而实际上却因为原模型不包含凹凸信息而直接看到了点 T(actual),将该点用于查询法线贴图从而得到了真实模型A点的法线:

Parallax算法的解决方法是:

  1. 从原先直接看到的点 \(T_a\)​​​ 往视角方向 \(V\)​ 位移一定距离(与\(T_a\)​的高度成正比),得到该位移的水平偏移 \(offset\)​ (将是个二维向量)。​​​

\(offset = c\cdot H(u,v)\cdot\frac{[V_x \ V_y]^T}{V_z}\)​​

不过在计算时,需要将视角方向 \(V\)变换成切线空间下的方向。

  1. 那么更加近似正确结果的点应该是:

\(T_b = T_a+offset\)​​​

  1. 将点用于法线贴图的查询,那么 \(NomalMap(T_b)\)​ 即为所见法线。

好处:

  • 无论是哪个角度看向使用Parallax Map的物体,都能有近似正确凹凸的效果

代价:

  • 需要高度贴图,这意味着要不使用高度贴图+法线贴图的组合(占额外显存空间),要不单纯靠高度贴图计算法线(计算量多些)

浮雕贴图(Relief Map)

Relief Map又叫 浮雕纹理 ,是对 Parallax 的进一步精确。如果说Parallax只是根据视角方向在切线空间下的投影和高度图作近似的偏移,那么Relief Map则是以找到正确点B点进行采样为目标,具体分为两步:

  1. 通过步进法(Linear Search)找到交点的大致范围:从观察点朝视角方向步进一定距离(方向不变)进行一次采样,若发现采样的高度高于视角线高度,则意味着该点为范围的一端(观察点为另一端):
  1. 通过二分法(Binary Search)进一步找到交点:在确定大致范围后,步进距离每次减半(方向可变),直到逼近目标值:

好处:

  • 相比与Parallax Map,不同角度看向物体都有更加精确的凹凸效果。

代价:

  • 和Parallax Map一样,需要高度贴图
  • 多轮迭代寻找采样点,计算非常耗时

位移映射(Displacement Mapping)


位移贴图(Displacement Map)则是真正修改模型几何的贴图,它存储了每个像素的位移量(实际上也是一种高度贴图),通过修改顶点位置,从而产生新的模型。

代价(相比于Bump Mapping):

  • 修改顶点位置,需要重新计算由此分出来的新三角形
  • 产生更多数量的三角形,开销大大增加

阴影映射(Shadow Mapping)


之前介绍的光照计算中是不包含阴影的,而阴影映射(Shadow Mapping)则是常见的实现阴影计算的手段。

阴影贴图基本原理:

  1. 额外设置一个摄像机在光源位置,并且看向光照方向,并用一张贴图(称之为阴影贴图Shadow Map)来记录看到的像素深度(每个像素位置只记录所见最近深度,而不做Shading)来作为遮挡深度。

如图,Shadow Map记录了光源摄像机所看到的最近深度图,颜色越深,离摄像机越近:

  1. 主摄像机需要渲染每个像素时,通过光源摄像机的MVP变换,便能得到该像素点在光源摄像机屏幕空间中对应的位置\((x',y',z')\)​;接着深度值 \(z\)​ 和阴影贴图(Shadow Map)用\((x',y')\)​采样得到的遮挡深度做深度比较: 若深度 \(z\)​ 大于对应遮挡深度(意味着该像素的光被遮挡),这时就可以对该像素降低明亮度。

如图,为主摄像机每个像素经过变换后比较深度的结果,其中绿色点意味着深度 \(z\)​​ 约等于阴影贴图对应遮挡深度,非绿色点意味着深度 \(z\)​ 更大(即被遮挡了光照):

以上只是Shadow Mapping的基本解决方案,实际上它仍然有很多不足的地方(例如noise问题、Soft Shadow问题),业界往往采用更加高级的Shadow Mapping解决方案(例如Bias、PCF、PCSS、VSM)。

3D纹理(3D Textures)


3D柏林噪声(3D Perlin Noise)生成纹理

体积渲染(Volume Rendering)

参考


posted @ 2021-08-11 00:17  KillerAery  阅读(1847)  评论(0编辑  收藏  举报