OpenGL学习脚印: 静止的物体

2014-10-28  方海龙的...

Face Culling 背面剔除

然而,在初始化例程中,这里有一个值得注意的代码改动。有几个函数需要讨论。

  1. void init()  
  2. {  
  3.     InitializeProgram();  
  4.     InitializeVertexBuffer();  
  5.       
  6.     glGenVertexArrays(1, &vao);  
  7.     glBindVertexArray(vao);  
  8.       
  9.     glEnable(GL_CULL_FACE);  
  10.     glCullFace(GL_BACK);  
  11.     glFrontFace(GL_CW);  
  12. }  

最后的3行是新添加的。

glEnalbe函数是一个多功能的工具。OpenGL里有许多二进制的开关位,它们作为OpenGL状态的一部分。glEnable用于启用这些标志位。类似的函数glDisable用于关闭这些标志位。

启用GL_CULL_FACE标志,就告诉了OpenGL激活背面剔除功能。到目前为止,我们在渲染时都让背面剔除功能关闭着。

背面剔除是一个提高性能的有用特性。拿我们的长方体来说。你拿起一个遥控器,他们通常就是长方体的。不管你如何看它和摆放他们的朝向,你一次看到的面永远不会超过3个。那么我们何必花费片元处理时间来绘制另外3个面呢?

背面剔除用来告诉OpenGL不要绘制物体的那些你看不到的面。这个真的很简单。

NDC坐标转换后,在屏幕坐标下,你有一个三角形。这个三角形的每个顶点都以特定顺序呈现给OpenGL。这样给定了三角形顶点标序号的方式。

不管三角形大小或者形状,你有两种指定三角形顺序的方法:顺时针或者逆时针。

这就是说 ,如果顶点1-2-3,相对三角形的中心,在一个圆中按顺时针方向来数的话,那么这个三角形对观察者来说就是顺时针方向。反之,则是逆时针方向。这种顺序被称为绕序(winding order)。解释如下图所示:

上图中,左边三角形是顺时针绕序;右边则是逆时针绕序。

背面剔除的工作依赖于此绕序。设置这个绕序,需要两步,也就是上述初始化函数中代码中最后两行完成的。

glFrontFace定义了顺时针和逆时针那个绕序被当做三角形的“前面”('front')。这个函数可以指定GL_CW或者GL_CCW,即分别是顺时针和逆时针顺序。

glCullFace函数,定义了前面或者后面哪一面会被剔除。可以设置为GL_BACK、GL_FRONT或者GL_FRONT_AND_BACK。后者剔除所有东西,因此没有三角形被渲染。这对于测试顶点着色器的性能有用,但对实际的绘制却没那么有用。

本节中的顶点被特别指定,因此那些顺时针的面将会被剔除,这样避免绘制后面的面(rear-facing faces)。

补充:如何指定的顶点绕序

本节中给定的顶点为:

  1. const float vertexData[] = {  
  2.      0.25f,  0.25f, 0.75f, 1.0f,  
  3.      0.25f, -0.25f, 0.75f, 1.0f,  
  4.     -0.25f,  0.25f, 0.75f, 1.0f,  
  5.   
  6.      0.25f, -0.25f, 0.75f, 1.0f,  
  7.     -0.25f, -0.25f, 0.75f, 1.0f,  
  8.     -0.25f,  0.25f, 0.75f, 1.0f,  
  9.   
  10.      0.25f,  0.25f, -0.75f, 1.0f,  
  11.     -0.25f,  0.25f, -0.75f, 1.0f,  
  12.      0.25f, -0.25f, -0.75f, 1.0f,  
  13.   
  14.      0.25f, -0.25f, -0.75f, 1.0f,  
  15.     -0.25f,  0.25f, -0.75f, 1.0f,  
  16.     -0.25f, -0.25f, -0.75f, 1.0f,  
  17.   
  18.     -0.25f,  0.25f,  0.75f, 1.0f,  
  19.     -0.25f, -0.25f,  0.75f, 1.0f,  
  20.     -0.25f, -0.25f, -0.75f, 1.0f,  
  21.   
  22.     -0.25f,  0.25f,  0.75f, 1.0f,  
  23.     -0.25f, -0.25f, -0.75f, 1.0f,  
  24.     -0.25f,  0.25f, -0.75f, 1.0f,  
  25.   
  26.      0.25f,  0.25f,  0.75f, 1.0f,  
  27.      0.25f, -0.25f, -0.75f, 1.0f,  
  28.      0.25f, -0.25f,  0.75f, 1.0f,  
  29.   
  30.      0.25f,  0.25f,  0.75f, 1.0f,  
  31.      0.25f,  0.25f, -0.75f, 1.0f,  
  32.      0.25f, -0.25f, -0.75f, 1.0f,  
  33.   
  34.      0.25f,  0.25f, -0.75f, 1.0f,  
  35.      0.25f,  0.25f,  0.75f, 1.0f,  
  36.     -0.25f,  0.25f,  0.75f, 1.0f,  
  37.   
  38.      0.25f,  0.25f, -0.75f, 1.0f,  
  39.     -0.25f,  0.25f,  0.75f, 1.0f,  
  40.     -0.25f,  0.25f, -0.75f, 1.0f,  
  41.   
  42.      0.25f, -0.25f, -0.75f, 1.0f,  
  43.     -0.25f, -0.25f,  0.75f, 1.0f,  
  44.      0.25f, -0.25f,  0.75f, 1.0f,  
  45.   
  46.      0.25f, -0.25f, -0.75f, 1.0f,  
  47.     -0.25f, -0.25f, -0.75f, 1.0f,  
  48.     -0.25f, -0.25f,  0.75f, 1.0f,  
  49.   
  50.   
  51.   
  52.     0.0f, 0.0f, 1.0f, 1.0f,  
  53.     0.0f, 0.0f, 1.0f, 1.0f,  
  54.     0.0f, 0.0f, 1.0f, 1.0f,  
  55.   
  56.     0.0f, 0.0f, 1.0f, 1.0f,  
  57.     0.0f, 0.0f, 1.0f, 1.0f,  
  58.     0.0f, 0.0f, 1.0f, 1.0f,  
  59.   
  60.     0.8f, 0.8f, 0.8f, 1.0f,  
  61.     0.8f, 0.8f, 0.8f, 1.0f,  
  62.     0.8f, 0.8f, 0.8f, 1.0f,  
  63.   
  64.     0.8f, 0.8f, 0.8f, 1.0f,  
  65.     0.8f, 0.8f, 0.8f, 1.0f,  
  66.     0.8f, 0.8f, 0.8f, 1.0f,  
  67.   
  68.     0.0f, 1.0f, 0.0f, 1.0f,  
  69.     0.0f, 1.0f, 0.0f, 1.0f,  
  70.     0.0f, 1.0f, 0.0f, 1.0f,  
  71.   
  72.     0.0f, 1.0f, 0.0f, 1.0f,  
  73.     0.0f, 1.0f, 0.0f, 1.0f,  
  74.     0.0f, 1.0f, 0.0f, 1.0f,  
  75.   
  76.     0.5f, 0.5f, 0.0f, 1.0f,  
  77.     0.5f, 0.5f, 0.0f, 1.0f,  
  78.     0.5f, 0.5f, 0.0f, 1.0f,  
  79.   
  80.     0.5f, 0.5f, 0.0f, 1.0f,  
  81.     0.5f, 0.5f, 0.0f, 1.0f,  
  82.     0.5f, 0.5f, 0.0f, 1.0f,  
  83.   
  84.     1.0f, 0.0f, 0.0f, 1.0f,  
  85.     1.0f, 0.0f, 0.0f, 1.0f,  
  86.     1.0f, 0.0f, 0.0f, 1.0f,  
  87.   
  88.     1.0f, 0.0f, 0.0f, 1.0f,  
  89.     1.0f, 0.0f, 0.0f, 1.0f,  
  90.     1.0f, 0.0f, 0.0f, 1.0f,  
  91.   
  92.     0.0f, 1.0f, 1.0f, 1.0f,  
  93.     0.0f, 1.0f, 1.0f, 1.0f,  
  94.     0.0f, 1.0f, 1.0f, 1.0f,  
  95.   
  96.     0.0f, 1.0f, 1.0f, 1.0f,  
  97.     0.0f, 1.0f, 1.0f, 1.0f,  
  98.     0.0f, 1.0f, 1.0f, 1.0f,  
  99.   
  100. };  

这里面包含12个三角形和其顶点颜色,实际上指定的长方体如下图所示:


三角形依次为:

ABD、BCD、EHG、FHG、DCG、DGH、AFB、AEF、EAD、EDH、FCB、FGC共12个三角形。

以ABD为例,其为顺时针方向,作为被显示面;以EHG为例,其为逆时针方向,作为背面被剔除。

因此代码中使用:

  1. glEnable(GL_CULL_FACE);  
  2. glCullFace(GL_BACK);  
  3. glFrontFace(GL_CW);  
即指定前面为顺时针方向,剔除背面。

Lack of Perspective 缺少透视

因此图片看起来是这样:

这里有些错误。那就是,它看起来像个正方形。

再次拿起遥控器。直接把它拿到你的眼睛位置,放在你视野的中心处。你应该只能看到遥控器的前面。

现在,向右上方向移动遥控器,移到类似于方块所在位置。你应该可以看到遥控器的底面和左侧面面。

那么我们应该能够看到长方体的底面和左侧面,但是为什么没有呢?

回想下渲染的时候发生了什么。在裁剪坐标系,位于长方体后面的顶点在前面顶点的后面。当我们将其转换到屏幕坐标下时,在后面的顶点仍然在前面顶点的后面。这就是光栅器看到的,也就是它所渲染的。
这里肯定有些事情在现实中发生,但我们却没做的。那就是“投影”。


Perspective Projection 透视投影

回想下,我们的目标图像,就是屏幕,仅仅是一个二维的像素数组。我们使用的3D绘制流水线定义了把顶点从裁剪坐标系变换到屏幕坐标系的转换。一旦顶点位置在屏幕坐标下,2D三角形就被绘制了。

投影,从绘制管线来说,是把空间从一个维度转换到另一个的一种方式。我们初始的空间是3维的,因此,绘制管线定义了从3D空间到我们所看到的2D空间的投影。实际上,三角形是在2D空间里绘制的。

有限的投影,使我们所感兴趣的一类,仅仅把物体投影到有限的低维空间里。对于3D到2D投影,这里有一个,高维空间被投影到的有限的平面。对于2D到1D投影,投影的界限是是一条直线,这就是投影的结果。

正交投影是很简单的一种。当投影到轴对齐的表面时,如下图所示,投影仅仅涉及到丢弃与表面正交的那个坐标分量。

2D到1D正交投影如下图所示:

场景正交地投影到黑色线条上。灰色的盒子代表了对于投影可见的部分,有一部分场景位于这个盒子之外则不可见了。

当投影到任意直线上时,涉及到的数学要稍微复杂些。但是,与投影面正交的那一维度统一被丢弃了( negated uniformly)才真正产生了正交投影。The fact that it is a projection in the direction of the perpendicular and that it is uniform is what makes it orthographic.

人眼并不以蒸饺方式看东西,否则我们看到的区域就近近是瞳孔大小的区域。因为我们不适用正交投影来看东西,因此正交投影看起来并不太真实。对于2D到1D的透视投影看起来像这样:

正如你看到的,这个投影是放射状的,基于一个特殊的点。这个店就是投影的视点或者相机。

从形状上来看,透视投影可以支持更大几何区域的投影。正交投影仅仅捕获在投影平面前的长方体。透视投影捕获更大的空间。

2D形式的透视投影中,投影行政是一个等腰梯形(a quadrilateral that has only one pair of parallel sides, and the other pair of sides have the same slope);3D形式的投影形状称作视见体(frustum),就是金字塔除去塔尖的那种截锥体,如下图所示:

Mathematical Perspective 投影的数学

现在我们知道我们想要做什么,剩下的就是如何去做了。

我们作出以下假设:

  • 投影平面轴对齐且朝向-Z轴的。这样,-Z远离投影平面

  • 视点在原点 (0, 0, 0).

  • 投影平面大小为[-1,1],所有投影到此区域外的点都不会被绘制

是的,这看起来有点像NDC空间。这并不是一个巧合。但是现在我们不要太超前。

我们对投影结果如何工作知之甚少。透视投影本质上基于顶点位置,将顶点转移到视点。在Z轴远处点比近处的点转移的少(Vertices farther in Z from the front of the projection are shifted less than those closer to the eye. )。并且,这个转移也取决于顶点在XY方向上离投影平面的中心有多远。

这个问题其实就是一个简单的集合问题。下图是2D到1D透视投影等价形式:

P点投影到投影平面上。相对于位于原点处的视点,投影平面在Ez位置,R是投影后的点。

细节这里略去,利用相似三角形可以计算得出:

因为这是一个向量操作的函数,这个公式也可应用到2D、3D形式。这样,透视投影的就是对顶点着色器接受的每个顶点简单地应用这个简单的公式。

The Perspective Divide 透视除法

基本的透视投影函数很简单。事实上,它如此简单以至于在早期3Dfx卡甚至图形卡之前已经内置到图形硬件里面去了。

你可能注意到缩放可以表达为除法(乘以倒数)。你可能会议其裁剪坐标系和NDC坐标系之前的区别就在于除以W分量。因此,不再着色器里面执行,我们可以简单的为每个顶点设置正确的W分量,然后让硬件来处理它。

这个步骤,从裁剪坐标系到NDC坐标系,有一个特别的名字:透视除法。这样命名是因为,它通常用于透视投影;正交投影通常将W设为1.0,这样透视除法就是一个空操作。

注意 

你可能会好奇为什么这个独有的除以W步骤存在。你也可能好奇,在着色器能够执行向量除法非常快速的今天,我们为什么要使用硬件来执行透视除法。这里有许多原因。其中一个我们在将矩阵时会提到。更重要的原因我们后面会讲到。一言以蔽之,这么做是有好的原因的。

Camera Perspective  相机视角

在我们能实际实现透视投影之前,还有一些问题要考虑。正交投影的变换本质上上一个空操作。OpenGL天生就是这么实用从顶点着色器输出的裁剪坐标系顶点,这是自动的。

透视投影却复杂些。事实上,它从根本上改变了空间的自然特性。

我们的顶点目前都是直接以裁剪坐标存储的。有时我们会添加一个偏移量来移动他们。但是不管出于什么意图和目的,存储在缓存对象中的位置值,就是我们顶点着色器的输出: 裁剪坐标系下位置。

回想一下,除以W操作是OpenGL定义的从裁剪坐标空间变换到NDC空间的变换的一部分。透视投影定义了一个把位置变换到裁剪坐标下的处理过程,以至于这些裁剪坐标下位置看起来像是3D空间的投影。这个变换有一个定义良好的输出:裁剪坐标系下位置。但是这些输入从哪儿来?

这样我们为顶点位置定义了一个新的空间:我们乘坐照相机坐标系(camera space)。这不是OpenGL所能识别的空间(不像裁剪坐标系由OpenGL显式定义)。事实上纯粹是用户构造。照相机坐标系的定义影响到透视投影的处理,因为投影必须产生合适的裁剪坐标输出。因此,局域我们知道的投影处理过程,定义照相机坐标系是有用的。这样可以最小化照相机坐标空间和裁剪坐标空间的区别,简化透视投影的逻辑。

照相机坐标系的视见体在所有方向上范围从正无穷到负无穷。正X轴向右延伸,正Y轴向上延伸,正Z轴向前延伸。最后一个与裁剪坐标系不同,裁剪坐标下正Z轴向远处。

我们的又是投影变换就是在这个空间进行。如前所述,投影平面XY轴范围在[-1,1],在Z轴-1处。投影将会从-Z方向投影到这个平面,具有正Z值得点将位于投影平面后面。

现在我们做一个更简单的假设:透视投影的平面的重心位于照相机坐标系下(0,0,-1)。因此,既然投影平面朝向-Z轴,视点位置相对它就是(0,0,-1)。那么,Ez,投影屏幕到视点的偏移量就是-1,这意味着我们上述公式中,以除法形式表达时就是 Pz/-1。

有一个固定的视点和投影平面让放大缩小操作变得困难。这通常是通过将投影平面相对视点移动来完成。然而,还是有一种方式。当从照相机坐标系变换到裁剪坐标系时,将XY坐标以一个常量进行缩放来完成。这样,从相机来看,XY方向就变小或变大了。这回事视见体变宽或窄。

作为比较,以2D透视投影来表现照相机和NDC坐标系空间:

注意,照相机坐标系和NDC坐标系的Z轴翻转了。这是因为,他们有着不同的观察方向。在照相机坐标系下,照相机朝向-Z轴;Z的赋值月小离得越远。在NDC空间中,相机朝着+Z轴,Z正值越大离得越远。

如果你从右边的NDC坐标下执行正交投影,那么你得到的就是左侧的透视投影。实际上,我们所做的就是讲物体变换到一个3D空间,在这个空间,正交投影看起来像透视投影。

Perspective in Depth  投影深度

我们已经知道如何处理XY坐标了,那么Z值对于透视投影是什么意思?

知道下节,我们将会忽略Z值。我们简单了解下。如果一个顶点在任何轴上超出[-1,1]的范围在NDC坐标下,那么他将位于可是区域之外。因为Z坐标也像XY一样将进行透视除法,所以我们需要将其考虑在内。
我们的W坐标将基于Z坐标。我们需要将照相机坐标系下 [0, -∞) 转换到NDC下[-1,1]。因为照相机坐标空间无限,而我们将映射到一个有限空间,我们需要作出界限。视见体在XY方向上作出界限,我们需要添加Z轴界限。
顶点能位于可见范围内的最大距离被称作zFar,最小距离称作zNear,这样为我们的照相机坐标系下视点创建了一个有限的视见体。
注意  记住zNear和zFar是在照相机坐标系下很重要。下一节我们将会介绍一个深度范围,同样适用名字zNear和zFar,他们相关但是是不同范围。

照相机下zNear看起来有效地决定了视点和投影平面之间的偏移量。但是,不是这样的。即使zNear小于1,这样会将近裁剪面放到投影平面的后面,你仍然可以获取一个合法的投影。在投影平面后面的物体也可以被投影到平面上,就像其前面的一样。这仍然是一个透视投影,丛数学上看,它工作。
如果你移动投影平面,那么它就不如你想得那样工作。因为投影平面具有固定大小(范围[-1,1]),移动平面将会改变点投到平面上位置。改变zNear不会影响投影的XY值。

将一个有限范围投影到另一个有多种方法。一个容易混淆的问题是透视除法本身。

在两个有限空间之间进行线性映射很简单。但是要在执行透视除法后仍然保持线性就另当别论。因为我们将除以-Z本身(照相机坐标下的z,而不是裁剪坐标系下的Z),数学比你想象的复杂。

原因下节介绍,我们会使用这个适度复杂的公式计算裁剪坐标Z:

关于这个公式重要的几点。首先,zNear,zFar是正值,在执行变换时这个公式考虑到这一点;另外,zNear不可以为0,它可以很接近0,但是永远不能恰好为0.

回过去看下2D形式照相机坐标系和NDC坐标系下的对比。

这个例子中使用这个公式来计算Z值。注意Z值是如何匹配的。在照相机坐标系下Z距离是均匀映射的,在NDC中却不是线性分布的。同时,在照相机坐标系下共线的点在NDC中保持共线。这个特性我们下节探讨。

Drawing in Perspective 透视画法

把上面的都考虑进来,我们有一系列步骤来将顶点从照相机坐标系转换到裁剪坐标系。步骤如下:

  1. 视锥调整:将照相机坐标系下XY值乘以一个常量
  2. 深度调整:        将照相机坐标下Z值修改到裁剪坐标下
  3. 透视除法分量: 计算W值,这里 Ez等于-1

我们讲述完了原理,下面在ShaderPerspective节中人工构造透视如下:

  1. version 330  
  2.   
  3. layout(location = 0) in vec4 position;  
  4. layout(location = 1) in vec4 color;  
  5.   
  6. smooth out vec4 theColor;  
  7.   
  8. uniform vec2 offset;  
  9. uniform float zNear;  
  10. uniform float zFar;  
  11. uniform float frustumScale;  
  12.   
  13. void main()  
  14. {  
  15.     vec4 cameraPos = position + vec4(offset.x, offset.y, 0.0, 0.0);  
  16.     vec4 clipPos;  
  17.       
  18.     clipPos.xy = cameraPos.xy * frustumScale;  
  19.       
  20.     clipPos.z = cameraPos.z * (zNear + zFar) / (zNear - zFar);  
  21.     clipPos.z += 2 * zNear * zFar / (zNear - zFar);  
  22.       
  23.     clipPos.w = -cameraPos.z;  
  24.       
  25.     gl_Position = clipPos;  
  26.     theColor = color;  
  27. }  

程序初始化:

  1. offsetUniform = glGetUniformLocation(theProgram, "offset");  
  2.   
  3. frustumScaleUnif = glGetUniformLocation(theProgram, "frustumScale");  
  4. zNearUnif = glGetUniformLocation(theProgram, "zNear");  
  5. zFarUnif = glGetUniformLocation(theProgram, "zFar");  
  6.   
  7. glUseProgram(theProgram);  
  8. glUniform1f(frustumScaleUnif, 1.0f);  
  9. glUniform1f(zNearUnif, 1.0f);  
  10. glUniform1f(zFarUnif, 3.0f);  
  11. glUseProgram(0);  
效果如下图所示:


注意这里一种称作swizzle selection的操作。例如:

clipPos.xy = cameraPos.xy * frustumScale;

这里使用分量xy表达来获取一个vec2变量,其第一分量来自x,另一分量来自y,并保持顺序。

后文太长,略去,仅列出方法。

利用公式


可以使用矩阵方式来进行投影,着色器代码如下

  1. #version 330  
  2.   
  3. layout(location = 0) in vec4 position;  
  4. layout(location = 1) in vec4 color;  
  5.   
  6. smooth out vec4 theColor;  
  7.   
  8. uniform vec2 offset;  
  9. uniform mat4 perspectiveMatrix;  
  10.   
  11. void main()  
  12. {  
  13.     vec4 cameraPos = position + vec4(offset.x, offset.y, 0.0, 0.0);  
  14.       
  15.     gl_Position = perspectiveMatrix * cameraPos;  
  16.     theColor = color;  
  17. }  
程序中构造投影矩阵如下:

  1. offsetUniform = glGetUniformLocation(theProgram, "offset");  
  2.   
  3. perspectiveMatrixUnif = glGetUniformLocation(theProgram, "perspectiveMatrix");  
  4.   
  5. float fFrustumScale = 1.0f; float fzNear = 0.5f; float fzFar = 3.0f;  
  6.   
  7. float theMatrix[16];  
  8. memset(theMatrix, 0, sizeof(float) * 16);  
  9.   
  10. theMatrix[0] = fFrustumScale;  
  11. theMatrix[5] = fFrustumScale;  
  12. theMatrix[10] = (fzFar + fzNear) / (fzNear - fzFar);  
  13. theMatrix[14] = (2 * fzFar * fzNear) / (fzNear - fzFar);  
  14. theMatrix[11] = -1.0f;  
  15.   
  16. glUseProgram(theProgram);  
  17. glUniformMatrix4fv(perspectiveMatrixUnif, 1, GL_FALSE, theMatrix);  
  18. glUseProgram(0);  

效果同上图。

    猜你喜欢
    发表评论评论公约
    喜欢该文的人也喜欢 更多