3评论

纹理四边形插值1--投影映射插值(Projective Interpolation)

ArtStealer 2018-09-07 1.6k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏开发行业精英群711501594

熟悉GLES的同学都属性纹理映射的规则,可能也都遇到过一些纹理映射中的小问题,今天就简单的说一下Projective Texture Mapping(投影映射纹理,有人翻译为透视映射纹理,不要在意这些细节,本文按照 投影映射纹理 来讲)

先列出两个概念:

  • Affine Texture Mapping(仿射纹理映射), 这个是我们平时开发中用的最多的,也是渲染框架默认采用的纹理映射方案.

  • Projective Texture Mapping(投影纹理映射),这个是本文着重介绍的纹理映射方案,来解决实际开发中遇到的纹理映射问题.


开门见山先看一下几张比较经典的示例图:

  1. 原始的贴图纹理

  2. 需要进行纹理映射的四边形:

  3. 预期的的贴图效果(左)和不加透视纹理映射贴出来的实际效果(右)





前面几张示意图应该已经看出来正常的仿射纹理贴图方案确实达不到预期的效果。

下面先从纹理贴图的基本方案开始讲起:

在2D平面上,一条直线可以表示为斜截式:y = Ax + B。这个形式也表示变量x和y是线性关系。也就是说,只要参数A和B定下来,则x和y就有了一个固定的对应关系,有一个x,在直线上就有唯一一个y和它对应。更具体地说,比如x的范围是[X0, X1],则对应的y范围就是[Y0, Y1]。如下图所示

 

线性插值公式:

目前的图形API都可以使用可编程的顶点着色器(Vertex Shader)和像素着色器(Pixel Shader)进行流水线上游(主要包括顶点变换、光照等等功能)和流水线下游(主要包括片元操作等等功能)的数据处理。经过了透视除法的顶点重新组合成图元,进行视口变换后,接下来就要经历光栅化阶段。在这个阶段中,图元被光栅化从而产生片元。

  • 图元是通过顶点定义的图形元素,包括点、线段、多边形、位图等等。

  • 片元是带有一系列属性的图像元素,比如位置、颜色、深度值、纹理坐标等属性。

  • 光栅化就是通过插值把一个图元过滤成能够在屏幕上表示它的一系列离散的片元,并通过片元操作把它们最终以像素的形式显示在帧缓冲中。纹理映射,就是在光栅化阶段进行的。

下图展示了一个三角形在视口中被光栅化的过程,可以看到红色的点表示产生的片元,黑色的箭头表示光栅化的方向。

在实时图形学中,光栅化基本上都是基于对多边形进行扫描线转换(scan-line converting)。把一个三角形的三个顶点所包围的区域转换成和屏幕水平方向平行的由像素组成的一条条扫描线。对三角形进行光栅化,有两种使用比较多的方式:一种是André LaMothe在他那本大而全的《Tricks Of The 3D Game Programming Gurus》(3D游戏编程大师技巧)中所描述的平底或者平顶三角形的方式——把任意一个三角形分成一个平底和一个平顶三角形,然后进行扫描转换。如下图所示:

这样的话,只对平顶和平底三角形进行扫描线转换就可以了,降低了处理难度。另外一个方法就是Chris Hecker在他的震撼性系列文章《Perspective Texture Mapping》中使用的一般性方法——在一般三角形的扫描过程中,当遇到左边或者右边线段斜率变化的时候,比如下面这个三角形的红色线段的扫描线,上面的左线段和下面的左线段不是同一条线段,使用新的左线段来处理下半部分三角形。

实际上两种方法的主要差别在于是否把一个三角形提前分割成两个三角形。扫描线本身的处理都是一样的。现在我们来看一个简单的平底三角形的光栅化方法。

上图是视口中的一个平底三角形,可以看到它有三个顶点P0,P1和P2,分别有相应的x,y和z三个坐标。s和t就是每个顶点的纹理坐标值。现在,我们就要把这个三角形做一个扫描线的转化。我们通过下面的一个简单算法来看看插值过程:

double x, y, xleft, xright;
double s, t, sleft, sright, tleft, tright, sstep, tstep;
for(y = y0; y < y1; ++y)
{
       xleft = 用y和左边的直线方程来求出左边的x;
       xright = 用y和右边的直线方程来求出右边的x;
       sleft = (y – y0) * (s1 – s0) / (y1 – y0) + s0;
       sright = (y – y0) * (s2 – s0) / (y2 – y0) + s0;
       tleft = (y – y0) * (t1 – t0) / (y1 – y0) + t0;
       tright = (y – y0) * (t2 – t0) / (y2 – y0) + t0;
       sstep = (sright – sleft) / (xright – xleft);
       tstep = (tright – tleft) / ( xright – xleft);
       for(x = xleft, s = sleft, t = tleft; x < xright;++x, s += sstep, t += tstep)
       {
               帧缓冲像素[x, y] = 纹理[s, t];
       }
}


以上看完,仿射纹理映射(Affine Texture Mapping ) 的基本方案应该是心中有数了,下面来看看投影纹理映射(Projective Texture Mapping)

上面仿射的投影映射中纹理坐标s和t的变化和y的变化是按照线性、均匀的方式处理的。但投影平面上的线性关系,还原回物体坐标空间中所对应的就不是简单的线性映射关系了(不平行于投影平面),如下图所示:

如上图 底部焦点为眼睛,视觉的起点,平面为投影平面,蓝色的投影平面上的均匀等长线段(蓝色),还原回真实的线段(红色)所对应的长度是不相等的,也就是我们常识中的近大远小。因此,纹理坐标ST与投影平面的XY已经不再是线性关系。


透视纹理映射的数学推导

上图为相机空间俯视图,eye是眼睛的位置(原点)。np和fp分别是近、远裁剪平面,N和F分别是z=0到两个裁剪平面的距离。pq是一个三角形pqr在xz平面上的两个点(y轴为垂直于屏幕的轴,r理解为不再xz平面上的点),p的坐标为(x, y, z),p’ 是p投影之后的点,坐标为(x’, y’, z’),则有:

上图(2)代入(1) 推导得(3)

通过这个式子推出了投影之后的x’和原始z之间的关系——x’和1/z是线性关系,y’和1/z也是线形关系。因此我们可以在投影面上通过x’和y’对1/z进行线性插值。在投影平面上通过x’和y’对1/z线性插值,计算出1/z后,通过上面的(1)式计算出原始的x和y,然后在3D空间中通过x和y计算出s和t。这样就找到了投影面上一个点所对应的纹理坐标的正确值了。

算法修改如下:

double x, y, xleft, xright; // 插值x和y,左右线段x
double oneoverz_left, oneoverz_right; // 左右线段1/z
double oneoverz_top, oneoverz_bottom; // 上下顶点1/z
double oneoverz, oneoverz_step;   // 插值1/z以及扫描线1/z步长
double originalx, originaly, originalz; // 空间中的原始x、y和z
double s, t; // 要求的原始s和t
for(y = y0; y < y1; ++y)
{
       xleft = 用y和左边的直线方程来求出左边的x
       xright = 用y和右边的直线方程来求出右边的x
       oneoverz_top = 1.0 / z0;
       oneoverz_bottom = 1.0 / z1;
       oneoverz_left = (y – y0) * (oneoverz_bottom – oneoverz_top) / (y1 – y0) + oneoverz_top;
       oneoverz_bottom = 1.0 / z2;
       oneoverz_right = (y – y0) * (oneoverz_bottom – oneoverz_top) / (y2 – y0) + oneoverz_top;
       oneoverz_step = (oneoverz_right – oneoverz_left) / (xright – xleft);
       for(x = xleft, oneoverz = oneoverz_left; x < xright;++x, oneoverz += oneoverz_step)
       {//公式(1)计算出 x,y,z
              originalz = 1.0 / oneoverz;
              originalx = -x * originalz / N;
              originaly = -y * originalz / N;
              用originalx、originaly以及originalz在空间中通过线性插值找到相应的s和t
              帧缓冲像素[x, y] = 纹理[s, t];
       }
}

好,到此基本的投影映射纹理的实现原理就已经介绍完毕了。当然实际的生产环境并不像上面写的这么简单粗暴,光看伪代码都看得出来,上面的算法在性能上是有很大的问题的,下面介绍一下升级版本:

在空间中,x、y和s、t都是线性的,所以:

把(4)带入(1),有

把(3)带入上式的中间项:

我们发现s/z、t/z和x’、y’也是线性关系。而我们之前知道1/z和x’、y’是线性关系。对1/z关于x’、y’插值得到1/z’,然后对s/z、t/z关于x’、y’进行插值得到s’/z’、t’/z’,然后用s’/z’和t’/z’分别除以1/z’,就得到了插值s’和t’。

改进算法:

double x, y, xleft, xright; // 插值x和y,左右线段x
double oneoverz_left, oneoverz_right; // 左右线段1/z
double oneoverz_top, oneoverz_bottom; // 上下顶点1/z
double oneoverz, oneoverz_step;   // 插值1/z以及扫描线步长
double soverz_top, soverz_bottom; // 上下顶点s/z
double toverz_top, toverz_bottom; // 上下顶点t/z
double soverz_left, soverz_right; // 左右线段s/z
double toverz_left, toverz_right; // 左右线段t/z
double soverz, soverz_step; // 插值s/z以及扫描线步长
double toverz, toverz_step; // 插值t/z以及扫描线步长
double s, t; // 要求的原始s和t
for(y = y0; y < y1; ++y)
{
       xleft = 用y和左边的直线方程来求出左边的x
       xright = 用y和右边的直线方程来求出右边的x
       oneoverz_top = 1.0 / z0;
       oneoverz_bottom = 1.0 / z1;
       oneoverz_left = (y – y0) * (oneoverz_bottom – oneoverz_top) / (y1 – y0) + oneoverz_top;
       oneoverz_bottom = 1.0 / z2;
       oneoverz_right = (y – y0) * (oneoverz_bottom – oneoverz_top) / (y2 – y0) + oneoverz_top;
       oneoverz_step = (oneoverz_right – oneoverz_left) / (xright – xleft);
       soverz_top = s0 / z0;
       soverz_bottom = s1 / z1;
       soverz_left = (y – y0) * (soverz_bottom – soverz_top) / (y1 – y0) + soverz_top;
       soverz_bottom = s2 / z2;
       soverz_right = (y – y0) * (soverz_bottom – soverz_top) / (y2 – y0) + soverz_top;
       soverz_step = (soverz_right – soverz_left) / (xright – xleft);
       toverz_top = t0 / z0;
       toverz_bottom = t1 / z1;
       toverz_left = (y – y0) * (toverz_bottom – toverz_top) / (y1 – y0) + toverz_top;
       toverz_bottom = t2 / z2;
       toverz_right = (y – y0) * (toverz_bottom – toverz_top) / (y2 – y0) + toverz_top;
       toverz_step = (toverz_right – toverz_left) / (xright – xleft);
       for(x = xleft, oneoverz = oneoverz_left,
              soverz = soverz_left, toverz = toverz_lef,t
              x < xright; ++x, oneoverz += oneoverz_step,
              soverz += soverz_step, toverz += toverz_step)
       {
              s = soverz / oneoverz;
              t = toverz / oneoverz;
              帧缓冲像素[x, y] = 纹理[s, t];
       }
}

好了,基本的原理差不多到这了.

下面看一个实际应用场景(当然这个连续的四边形带还有些问题,此处只看投影映射部分的效果,先不纠结连接处平滑的问题)。

最初想实现的效果时一段连续的四边形组成的带状体。

完成基本需求后,扩展一下,希望带状体内的宽度产生变化(其实上图中的四边形已经导致了纹理贴图产生了错误的效果,只不过是不够明显),下面看看传入不同的宽度参数后产生的新效果:

看上面的辅助白线 和 红色的箭头,可以明显的看出,纹理效果已经不是我们预期的在一个四边形内正常的透视成一个四边形的贴图效果了。

这里就需要用到透视投影映射,见下图处理后的效果

如上图,单个四边形内的纹理映射已经符合了我们的预期效果,两个三角形对接处的纹理是能衔接上的,并且也保持了直线效果。但是四边形与四边形的衔接处因为不同的四边形产生的不同透视系数导致纹理无法正常衔接。这个本文暂不进行讨论。


欢迎关注我的公众号(ArtStealer)进行深入探讨交流:



参考文章