9.4_让人头疼的纹理_上_颜色纹理
在之前的光照模型中,我们看到在给物体加上了光照之后,物体获得了一定的真实感。
可是大家也会发现一个现象,那就是由于表面过于光滑单调,反而显得不是那么真实了。其实,这是因为现实世界中的物体表面,往往还有各种纹理,也就是表面细节。
比如这里的球。加上一定的细节,让我们感觉是地球了。
这里的金属缠绕物加上了纹理,也显得更加逼真了。
所以,我们可以在光照的基础上给物体叠加一个表面细节。比如这里的茶壶原来是这样的。我们增加了这样的一个纹理。给人的感觉就有点青花瓷的效果了。
首先是颜色纹理。比如这里的这个物体,在它的表面给出了一定的花纹和图案。
第二个就是几何纹理,这个时候比较复杂了。他要关注表面微观的几何形状,就像自然界中的橘子,轮胎,这个表面都是。凹凸不平的。
大家看右下角的这幅图,这就是几何纹理的视觉效果,给我们的感觉,就是表面凹凸不平。
怎样得到这些纹理效果?
这就需要进行纹理映射。首先需要预定义纹理模式,然后建立物体表面的点与纹理模式之间的对应关系。
当物体表面的可见点确定之后,以纹理模式的对应点参与光照模型进行计算,就可以把纹理模式附到物体的表面上。这种方法,就称为纹理映射。
其中,纹理模式的定义,有两种方
一个是图像纹理。可以把二维纹理的图像映射到三维物体的表面。绘制物体表面上一个点的时候,可以采用相应的纹理图案中对应点的颜色值
另一个,就是函数纹理,我们可以用数学函数来定义简单的二维纹理图案,比如这里的方格地毯。
定义了纹理模式之后,我们就可以进行纹理映射。
这就需要建立纹理与三维物体之间的对应关系。以及,适当的考虑扰动的法向量。当然,这个扰动法向量的考虑主要是针对几何纹理的。
纹理的定义,其实还有一个纹理空间的概念。纹理空间也就是UV空间,通常是一个单位正方形区域,也就是U和V都在零到一之间。纹理就是定义在纹理空间上的函数。比如这里的棋盘格就是一种比较常见的纹理。
我们可以用一个二维纹理的函数来表示。当然也可以直接用一副纹理图像来表示。
定义了纹理模式之后,就进行模拟映射,这个过程,实际上是建立物体空间、表面和纹理空间之间的对应关系。这就包括两个方面的工作了。
我们需要根据物体空间的表面坐标XYZ计算它的在纹理空间坐标了里面的UV值。
这时候需要对物体表面坐标XYZ用UV来进行参数化,这是第一步。然后,反求出参数UV用物体表面坐标XYZ的表达。其实这才是第二步。那么接下来,就需要根据纹理空间定义的纹理UV得到该处的纹理值,并且,用这个值取代光照明模型中的相应项,实现纹理映射,这就是第三步。
这里,给出了一个半径为R,高为H的圆柱,我们需要把刚才的这个纹理映射在这个圆柱体的表面。
第一步,我们对这个物体的表面用UV进行参数化的表示。
那么进行第二步了,就是反求出参数UV,用物体表面坐标XYZ的表达。如果已知点XYZ,它对应的就有阿尔法和B的值。那么对应的UV值也可以算得,这样一来,找到了一个通过表面点XYZ计算对应UV的方法,这个时候就可以进入第三步了,因为我们找到了它对应的uv值。于是就可以在纹理空间中取出这个UV值对应的纹理直了。之后再进行相应的光照计算,就可以把这个纹理的效果叠加到这个物体上,最终得到右边这幅图的效果了。
在Opengl中,我们提供了设置纹理属性,加载纹理,生成纹理,绑定纹理的相应的方法。这里给出了一些参考。
接下来,还要说一个有点特别的纹理映射方法,那就是立方体贴图。
立方体贴图,其实包括了六个2D的纹理,每个2D的纹理是一个立方体的一个面。
大家之前可能没有考虑过,但是我相信,大家玩的游戏中,一定都用到过这种立方体贴图的方法。那就是天空盒。
立方体贴图有自己特有的属性。可以使用方向向量对他们进行索引和采样。那么有了这样的立方体贴图之后,我们的天空盒会给我们的游戏玩家这样的感觉,就是他身处在一个比较广袤的游戏空间中。
9.4_让人头疼的纹理_下_几何纹理
上次课,我们提到了物体的表面细节,也就是纹理。可是现实中我们常常遇到这样的问题,那就是有些物体外观粗糙,比如这个茶壶。有的,表面凹凸不平,比如这里的墙面。怎么实现这样的细节?这就是几何纹理要考虑的问题了。
几何纹理方法的本质的想法是这样的,对物体的表面,它的几何性质做微小的扰动,产生凹凸不平的细节效果。
那么从而给物体的表面加上一个粗糙的外观。比如,物体表面上的每一个点P,都沿着该处的法向量方向有一个位移。就会有一个新的表面位置。
以此作为出发点,从上个世纪70年代到现在,产生了很多的方法。这里选取几个有代表性的方法给大家进行介绍。
第一个是1978年的BumpMapping凹凸贴图这时候,在计算顶点光强的时候,不是直接使用原始的法向量,而是加上一个扰动。
第二个,是1984年的移位贴图,他这个贴图方法直接作用于顶点了。也就是说,要根据Displacement Mapping中相对应的像素值,使顶点沿着法向移动。产生了真正的凹凸平面。
第三个就是1996年的Normal Mapping法线贴图。就通过一个高度图Height map获得法向量的信息,也就是说这幅图。他表面上存的是RGB值。
但实际上表现的是法向量的XYZ,接下来,就可以利用这样的一个法向量方向计算光强产生凹凸阴影的效果。
第四个,是2001年的视差贴图,这时候他要通过视线和Height map的计算。陡峭的视角,就给顶点更多的位移。平缓的,就给较小的位移。
那么通过视差就可以获得更强的立体感。
最后一个是2005年的浮雕贴图。他更精确的找出观察者视线与高度的交点,实现更精确的位移计算。实际上,这几个算法除了。移位贴图以外。其余的几个表面并没有变得真正的凹凸不平。
但是通过冯模型我们知道,这其中用了一个很重要的一个输入值就是平面的法向量方向,法向量方向不同,光照计算结果就不同了。
那么其余的这四种纹理算法并没有让表面真正的凹凸不平,给不同的片元以不同的法向量方向,那么这些法向量主要就用于以后的光照计算了。
因为它们的法向量不同。光照计算出来的结果就不一样。产生的颜色值,有深有浅,从而就获得了凹凸不平的感觉。
下面给出了这几种算法的效果。这里是凹凸贴图和法线贴图。
这里是视差贴图。右边是浮雕贴图
有人用一句话概括了浮雕贴图,实际上是在shader就是着色器里面做光线追踪
今天,我们就选取在图形编程中非常经典的法线贴图方法为大家进行介绍。
现实中的物体表面,并非是平坦的,而是表现出无数的凹凸不平的细节,每一个fragment或者片元,或者是面片。都使用了自己的法线。我们就可以让光照相信一个表面,是有很多微小的垂直于法线向量的这个平面所组成的。物体表面的细节,就会得到极大的提升,这种每个fragment使用各自的法线替代一个面上所有fragment使用一个法线的技术就叫做法线贴图。
这些乱七八糟的法线是怎样获取的呢?
实际上,他是通过Height_map得来的。
首先我们看第一个问题了,Height_map的使用。
这是一副Height_map高度图。他也是一副彩色图像,跟一般的这个贴图感觉没什么区别,可是,它的颜色值其实对应着片元的法线方向。怎么说,就是每个颜色通道实际上是表面的法线坐标。红色通道对应X方向,绿色通道对应Y方向,蓝色通道对应着Z方向。那么它的RGB值就可以表示法向量的XYZ了。
所以,通过Height_map可以获得片元的法向量信息。
基于这个信息计算光强就可以产生凹凸阴影的效果,也就实现了几何纹理。
我们又遇到一个新的问题,我们来分析一下。一个平面上片源顶点的法向量方向。会根据Heightmap取值,可是如果有多个不同朝向的平面?比如这里的立方体,就六个不同朝向的平面了,也就是平面本身的法向量方向是不同的,可是每个片元通过高度图获得的法向量都是同样的方向。
实际上,这些方向其实是相对于这个平面来说的。我们不转换,直接放到全局中使用,肯定会出错。也就是说,如果不根据各自平面本身的法向量方向进行调整,就无法得到正确的结果。比如右侧的这个面,如果他获得的法向量方向跟正面一样。显然,光照后的效果会发生错误。
看到这两幅图的对比,左边就是刚才这种情况,每个面贴上去感觉是一样的。
而右边,就根据每个平面原本的法向方向进行了修正。所以,这个结果才是正确的。
怎么调整,我们看最右边这幅图了,原本这个顶点对应的方向如果不调整,就是这个红色的。很显然,他需要根据自己所在平面的法向量进行调整。所以,就得到这里亮绿色的方向,显然,这个方向才是正确的。
图形学中的一贯想法还是想通过矩阵运算来实现。
因此,我们给每个平面引入了一个切线空间,大家看这个平面,我们需要三个互相垂直的向量,它们沿着一个表面的法线贴图就对,其余上,右和前。这里,这三个分别对应着法线,切线和副切线。那么每一个平面,都有自己的切线空间,比如这里的立方体,每个平面都有不同的切线空间了
通过这个平面上三个共面但是不共线的点P1,P2和P3,可以分别计算出刚才的TBN的方向量。
当然这三个量,只要算出两个,另外一个,我们可以用右手系的定则,直接计算得到,比如我们算出了法线和切线方向之后,副切线的方向,就可以直接计算得到了。
再获得了TBN的方向以后,就可以得到一个TBN矩阵,把这个矩阵叠加到法向量方向上,也就是这里的TBN乘以这里的normal。当然,其中的normal其实就是之前通过Normalmap,也就是Height_map中获取的法向量方向。
而在这里用了TBN矩阵对他作用以后,就可以把法向量方向根据每一个平面本身的方向进行调整了,也就是将刚才红色的错误方向调整到亮绿色的正确方向上了。此后再进行法线贴图,结果,当然就是正确的。各个面看起来完全不同。
9.5_加入阴影会怎样?
其实在光照效果产生之后,大家就已经有一个感觉,那就是阴影怎么办?因为有光就有影,这是我们生活中的常识。
从概念上来说,阴影是由于物体截断了光线而产生的。所以,如果光源位于物体一侧的话,阴影,总是位于物体的另一侧。也就是与光源相反的一侧。从理论上来说,从视点以及光源看过去都是可见的,面不会落在阴影中,只有那些从视点看过去是可见的,可是从光源看过去,是不可见得面,才落在阴影之内。
基于这样的一个想法,阴影计算算法的思想,大致就是这样的。
首先将视点移到光源的位置,用多边形区域排序相应算法将多边形分成两大类。
向光的多边形和背光的多边形。
向光多边形_是指那些从光源看过去是可见的多边形。
背光多边形_是指那些从光源看过去不可见的多边形,包括被其他面遮挡了多边形和反向面多边形等等。
向光多边形,就不在阴影区之内了,而背光多边形,就在阴影区内。
然后再将视点移到原来的观察位置,对象光多边形和背光多边形进行消隐。选择一种光照模型,计算多边形的亮度,就可以得到有阴影效果的图形了。
如果用之前的冯模型对于背光多边形。因为得不到光源的直接照射,所以只有环境光对其光强有贡献,我们就可以把漫反射和镜面反射关闭掉。
对于向光多边形,那就正常进行光照计算。
那么目前主流的阴影计算方法,主要有两个,分别是是Shadow_Mapping和Shadow_Volumn
Shadow_Mapping的想法,是一个物体之所以会处在阴影当中,是由于在他和光源之间存在着遮蔽物,或者说遮蔽物离光源的距离,比它本身要近。Shadow_Volumn 的想法,是根据光源和遮蔽物的位置关系,可以计算出场景中会产生阴影的区域,然后对所有的物体进行检测,以确定他会不会受到阴影的影响。
Shadow_Mapping的过程基于阴影计算的一般过程,分成两步
第一步以光源为视点,或者说在光源坐标系下对整个场景进行渲染
目的是要得到一幅所有物体相对于光源的Depth_Map,也就是我们说的ShadowMap。
这幅图像中,他的每一个像素值代表这场景里面离光源最近的片元的深度值。当然,这个深度值其实就是这个片元距离光源的距离值。
由于这个部分我们感兴趣的只是像素的深度值,所以这时候我们可以把光照计算先关掉。
第二步,我们就要把视点恢复到原来的正常位置上来渲染整个场景。
对每个像素就计算他和光源的距离,然后将这个值和Depth_Map或者ShadowMap中相应的值比较,从来确定这个像素点是否处在阴影当中。然后根据比较的结果,对Shadow的Fragment就是有阴影的片元和Lighted_Fragment有光照的片元分别进行不同的光照计算。这样就可以得到阴影的效果了,
比如这里V这个顶点对于视点来说,它是可见的。这时候,我们就需要找到它对应在ShadowMap中的深度值。我们就会发现。ShadowMap里面记录的深度值,跟他的这个深度值刚好是相等的,那证明这个顶点就可见,也就是是有光照的片源。
所以对这个点就可以按照冯的模型进行环境光,漫反射镜面反射的计算了。
可是这里的VB这个点,对于视点来说是可见的,我们这时候需要找到它对应在shadowmap中的深度值,我们做这样的一条连线,我们会发现,在shadowmap中记录的深度值,其实是这里绿色的这个点对应的深度值了,显然这个值是小于VB这个点的深度值的,这样就证明这个顶点在阴影中,
它被绿色的这个点给挡住了。因此,在进行光照计算的时候,就只需要考虑环境光就可以了。
阴影失真
在这种方法中,有一个比较典型的问题,那就是阴影失真。大家看这幅图片。是不是感觉有点不对劲,我们放大会发现,地板四边形,会渲染出很大一块交替的这个黑线。这种阴影贴图的不真实感叫做阴影失真。
因为shadowmap其实还有一个解析度的问题,也就是精度的问题。由于解析度的问题,在距离光源比较远的情况下,我们可能会有多个片源,从深度贴图的同一个值里面去取值。
那么看这幅图,每一个斜坡代表深度贴图的一个单独的纹理像素,以看到多个片元从同一个深度值进行采样了。比如这里的片元a和b都从一个shadowmap的一个单元中去取深度值。
虽然很多时候没有问题,但是当光源以一个角度照射表面的时候,可能就会出现问题了。大家还是看刚才的a和b,由于从同一个斜坡的深度纹理单元中去采样,可是a就被认为在地板的上方了。因为它的深度值比这里记录的深度值要小。可是B就反过来,认为在地板的下边,这样A就认为不在阴影中,属于lighted_fragment,我们对应用黄色线条表示他。可是这里的B,就认为是在阴影当中了,它就属于shadow_fragment,我们用紫色线条表示他。很显然,这样的情况就会交替出现,就出现了黄色、紫色线条交替的情况,也就产生了刚才图片中的条纹样式。
我们可以用一个叫做阴影偏移的技巧来解决这个问题,比如我们可以简单地对表面的深度值用一个偏移量。
看这里我们让表面的深度,可以减少一个值。感觉这个地面似乎升到了这个位置上。这个时候,每个部分都会认为在地板以上。当然也就会被照亮,从而得到正确的结果。
这里有两点需要说明
第一个就是这里最终的阴影,还是出现在地板正确的位置,不会真的在地板的上方。
第二个就是这里的阴影偏移的值其实是很小的,而且是需要我们通过实验去寻找的,在不同的场景下,需要通过实验找到最合适的值。否则,最终还是无法得到正确的阴影效果。
好了,现在再看看修正之后的正确结果,很显然,这个阴影的效果就具有一定的真实感。