文章目录
- 第五章 视口变换、剪切与反馈
第五章 视口变换、剪切与反馈
5.1 观察视图
- 将三维空间的模型投影到二维的关键方法,就是齐次坐标的应用、矩阵乘法的线性变换方法,以及视口映射
5.1.1 视图模型
5.1.2 相机模型
-
视图变换的操作可以类比为使用照相机拍摄照片的过程:
- 将相机移动到准备拍摄的位置,将它对准某个方向(视图变换,view transform)
- 将准备拍摄的对象移动到场景中必要的位置上(模型变换,model transform)
- 设置相机的焦距,或者调整缩放比例(投影变换,projection transform)
- 拍摄照片(应用变换结果)
- 对结果图像进行拉伸或挤压,将它变换到需要的图片大小(视口变换,viewport transform)
-
值得注意的是,可以认为上边的第1步和第2步做的是同一件事情,只不过方向相反而已。因此,通常将这两个步骤合并为一个模型-视图变换(model-view transform)。然而,这一过程将总包含多级平移、旋转和缩放操作。而这一合并过程中的主要特征,就是构建一个独立的、统一的空间系统,将场景中所有的物体都变换到视图空间,或者人眼空间当中
-
我们传递给OpenGL的坐标因该是已经完成模型视图变换和投影变换的。我们还需要告诉OpenGL如何完成视口变换
-
OpenGL整个处理过程中用到的坐标系统:
-
上图所示的过程中,最后一步是设置OpenGL的视口和深度范围。OpenGL得到的最终坐标是归一化之后的齐次坐标,并且将进行剪切和光栅化的操作。也就是说,最后要绘制的坐标总是在[-1.0, 1.0]的范围内,知道OpenGL对它们进行缩放以匹配视口大小为止
-
上图中用户变换的解释:
视锥体
- 平截锥体 frustum
视锥体的剪切
- 应用程序需要告诉OpenGL当前视锥体的参数值,而着色器将负责变换的应用过程;OpenGL将通过这些参数来完成剪切的操作;着色器中也可以使用用户自定义的平面来进行剪切
5.1.3 正交视图模型
5.2 用户变换
- 渲染管线各阶段中,对于三维坐标的变换情况:
5.2.1 矩阵乘法的回顾
- 矩阵乘法的定义:(一个4x4的矩阵与一个4维向量的乘法,得到一个新的4维向量)
- 对于视图模型来说,我们需要对一个向量进行多次变换,这一过程可以通过矩阵A和矩阵B的乘法来表达:
- 矩阵的乘法不可交换,即AB!=BA
- 矩阵的乘法符合结合律,即C(BA)=(CB)A=CBA
5.2.2 齐次坐标
- 将三维的笛卡尔坐标转换为四维的齐次坐标,有两个主要的好处:
- 可以进一步完成透视变换
- 可以使用线性变换来实现模型的平移
也就是说,如果使用四维坐标系统,就可以通过矩阵乘法完成所有的旋转、平移、缩放和投影变换操作
投影变换是透视效果实现的关键步骤,也是我们必须在着色器中实现的一个步骤
进阶:什么是齐次坐标
-
三维数据可以通过3维向量与3x3矩阵的乘法操作,来完成缩放和旋转的线性变换。但是对3维笛卡尔坐标的平移是无法通过3x3矩阵的乘法操作来完成的
-
只要将三维数据置入到四维坐标空间中,平移操作就回归成为一种简单的线性变换了。举例来说,将坐标(x,y,z)沿y轴移动0.3则有:
-
齐次坐标的第四个分量是用来实现透视投影变换的。齐次坐标总是有一个额外的分量,并且所有的分量都除以一个相同的值,那么将不会改变它所表达的坐标位置。这样的话,齐次坐标所表达的其实是方向而不是位置;对一个方向值的缩放不会改变方向本身
-
透视变换会将w分量修改为1.0以外的值。如果w更大,那么坐标将位于更远的位置。当OpenGL准备显示几何体时,它会使用前三个分量分别除以最后一个分量w,然后舍弃w,从而将齐次坐标重新变换到3维的笛卡尔坐标
5.2.3 线性变换与矩阵
- 为了将数据映射到设备坐标系当中,首先对3维的笛卡尔坐标添加第四个分量,并且设置值为1.0,从而构建了齐次坐标。这些齐次坐标通过与一个或多个4x4矩阵的乘法运算来表达旋转、平移、缩放和透视投影的变换过程
平移
- 物体的平移需要用到齐次坐标的第四个分量w(1.0),以及4x4矩阵的第4列:
- vmath::mat4 vmath::translate(float x, float y, float z)
缩放
- 物体的缩放变换需要矩阵对角线的前三个分量中设置合适的缩放值,然后与顶点v向量相乘:
- 如果物体缩放时中心没有处于(0,0,0)点,那么这个简单的矩阵在缩放的同时也会将物体远离或者靠近(0,0,0)点。如果希望改变一个偏离中心的物体的大小,并且不希望它的位置同时发生改变,那么首先将物体的中心移动到(0,0,0)点,然后再缩放其大小,最后平移回原来的位置
- vmath::mat4 vmath::scale(float s)
旋转
- 旋转物体同样需要一个矩阵R与顶点相乘(沿z轴逆时针旋转50°):
- 如果物体本身偏离原点,那么这一操作也会将物体本身沿z轴整体旋转。如果要不改变物体中心旋转,首先将物体平移到原点,再进行旋转,最后平移回原来的位置
- vmath::mat4 vmath::roate(float x, float y, float z)
- 得到的平移、缩放和旋转矩阵M左乘顶点v,即v’=Mv
透视投影
- vmath::mat4 vmath::frustum(float left, float right, float bottom, float top, float near, float far) 根据给定的视锥体设置返回一个透视投影矩阵
- vmath::mat4 vmath::loolAt(vmath::vec3 eye, vmath::vec3 center, vmath::vec3 up) 根据eye朝向center的视线,以及up定义的上方向,返回一个视图矩阵
正交投影
- vmath::mat4 vmath::ortho(…) 返回一个正交投影变换矩阵
5.2.4 法线变换
- 除了顶点的变换之外,还需要对表面法线进行变换
- 法线是需要进行归一化的,也就是它的长度必须为1.0;目的是为了进行光照计算,只有这样,才会知道物体表面的哪个方向会反射光线
- 物体表面的平移不会影响到法线的值,因此法线不用考虑平移操作,这也是法线不用齐次坐标的原因;由于法线的主要作用是光照计算,而这一步通常是在透视变换之前完成的,因此这就是我们不使用齐次坐标的另一个原因
-
法线变换:首先令M为一个3x3矩阵,它已经包含必要的旋转和缩放信息,可以将物体从模型坐标系变换到人眼坐标系,但是不包含透视变换的信息。也就是之前4x4变换矩阵的左上3x3子矩阵,没有包含平移或者透视变换的运算。然后使用下面的方程式来完成法线的变换
也就是,用M的逆矩阵的转置来完成法线的变换
5.2.5 OpenGL矩阵
OpenGL中的矩阵行与列
- 本书中使用传统的矩阵表示方法,列总是指垂直方向的一组数据
- 在着色器中,可以使用数组的语法来获取矩阵中的数据,它返回的是矩阵某一列数据的向量
- 矩阵的列主序或者行主序指的是矩阵中数据的内存排列关系;而内存的排列关系与矩阵传统表示方法,或者GLSL中的语法操作符都是无关的;其实我们并不需要知道矩阵内部到底是按照列主序还是行主序的方式记录的
- 只有一种情况需要考虑矩阵的列主序或者行主序,那就是将GLSL矩阵放入自定义的内存块时;当将矩阵传递到uniform块中时,就需要考虑这个问题;当管理uniform块时,为了通知GLSL从内存中正确加载矩阵数据,需要使用布局限定符row_major和column_major
- 如果在矩阵乘法运算中将一个GLSL向量置于乘法的左侧或者右侧,那么它会自动处理为行向量或者列向量的形式。这时候,它不遵循一个单列矩阵或者单行矩阵的特性
5.3 OpenGL变换
- void glDepthRange(GLclampd near, GLclampd far)
void glDepthRangef(GLclamdf near, GLclampf far)
设置z轴上的*面位于near,远平面位于far;这个函数定义了视口变换过程中z坐标的变换范围; *面和远平面的值也就是深度缓存中所保存的最小值和最大值;默认情况下他们分别是0.0和1.0;这个函数的参数设置范围必须是[0,1]之间的数值
视口
- void glViewport(GLint x, GLint y, GLint width, GLint height) 在程序窗口中定义一个矩形的像素区域,并且将最终渲染的图像映射到其中;x和y参数设置了视口的左下角坐标,width和height设置了视口矩形的像素大小;默认情况下视口设置为打开窗口的整个像素区域
多视口
z的精度
- 计算过程中,硬件的浮点数精度支持是有限的;这会造成深度缓存中的隐藏面计算结果不正确,由于这个现象可能对多个像素都有影响,因而导致相互距离较为接近的物体会发生闪烁交叠的情形;经过透视变换之后,z的精度问题可能会恶化。如果要避免这个问题,需要尽量将远平面与*面靠近,并且尽可能不要在一个很小的区域内绘制过多的z值
5.3.1 用户剪切
- OpenGL的用户剪切操作需要用到一个内置在顶点着色器中的数组gl_ClipDistace,我们需要自行设置它的内容,这个变量允许我们控制剪切平面与顶点的关系;它的值经过插值之后设置给顶点之间的各个片元
- gl_ClipDistance[x]这个变量的含义是,距离为0表示顶点落在平面上,正数值表示顶点在剪切平面的内测(保留这个顶点),负数值表示顶点在剪切平面的外侧(裁剪这个顶点);在图元中剪切距离是线性插值的,而OpenGL会直接抛弃所有距离小于0的片元
- gl_ClipDistance数组的每个元素都对应一个平面;剪切平面的数量是有限的,所有声明或者使用了这个变量的着色器都必须将这个数组设置为同样的大小
- glEnable(GL_CLIP_PLANEi) 启用第i个剪切平面
- 着色器中必须写入所有启用的平面距离值,否则可能会得到奇怪的剪切结果
5.4 transform feedback
- transform feedback是OpenGL管线中,顶点处理阶段结束之后,图元装配和光栅化之前的一个步骤。这个过程可以重新捕获即将装配为图元(点、线段、三角形)的顶点,然后将他们的部分或者全部属性传递到缓存对象中。用户程序可以回读这些缓存对象的内容,或者OpenGL将它们用于后续的渲染工作
5.4.1 transform feedback对象
- transform feedback状态是封装在一个transform feedback对象中的;这个状态包含所有用于记录顶点数据的缓存对象、用于标识缓存对象的充满程度的计数器,以及用于识别transform feedback当前是否启用的状态量
- transform feedback对象的创建需要一个对象名称,然后将它绑定到当前环境的transform feedback对象绑定点上
- void glGenTransformFeedbacks(GLsizei n, GLuint* ids) 为transform feedback对象生成n个名称,并将生成的名称记录到数据ids中
- void glBindTransformFeedback(GLenum target, GLuint id) 将一个名称为id的transform feedback对象绑定到目标target上,target的值必须是GL_TRANSFORM_FEEDBACK;这个对象在第一次进行绑定时创建,而在此之前时不存在的;如果不是第一次绑定,那么这个对象的状态将被重新设置为当前状态
- GLboolean glIsTransformFeedback(GLuint id) 判断id是否是一个transform feedback对象的名称
- 系统会内置一个默认的transform feedback对象,它的id是0
- void glDeletTransformFeedback(GLsizei n, const GLuint* ids) 删除n个transform feedback对象;删除操作会延迟到所有相关的操作结束之后才进行
5.4.2 transform feedback缓存
-
将整个缓存对象(记录顶点数据的)绑定到某个transform feedback缓存绑定点上
void glBindBufferBase(GLenum target, GLuint index, GLuint buffer)
将名称为bufffer的缓存对象(记录顶点数据的)绑定到目标target=GL_TRANSFORM_FEEDBACK_BUFFER的某个绑定点上,这个绑定点是target的一般缓存绑定点,这个target的索引值是index -
将一个缓存对象的一部分绑定到某个transform feedback缓存绑定点上
void glBindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size)
将缓存对象buffer的一部分绑定到目标traget的绑定点索引index上;offset和size的单位为字节,表示要绑定的缓存对象的范围;也将buffer绑定到target所指定的一般缓存绑定点上
这个函数可以用来将同一个缓存对象的不同区域绑定到不同的transform feedback绑定点上,但我们需要保证这些区域是不交叠的,否则将出现不确定的结果 -
glBindBufferBase()和glBindBufferRange()都不会主动创建缓存对象,与glBindBuffer()不同,glBindBuffer()可以创建缓存对象,因此不能使用glBindBufferBase/Range()和glBufferData()的序列来创建和分配transform feedback缓存的空间
5.4.3 配置transform feedback的变量
-
void glTransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar** varyings, GLenum bufferMode)
设置transform feedback过程中要记录哪些变量;program设置了transform feedback所用的程序对象;varyings是一个字符串数组,其中记录所有输出到片元(或者几何)着色器中的,同时需要通过transform feedback获取的变化量;count设置的是varyings中字符串的数量;bufferMode是一个标记量,它标识transform feedback中捕获的变量是如何分配的;如果bufferMode设置为GL_INTERLEAVED_ATTRIBS,那么所有的变量是一个接着一个记录在绑定到当前transform feedback对象的第一个绑定点的缓存对象里的;如果bufferMode为GL_SEPARATE_ATTRIBS,那么每个变量都会记录到一个单独的缓存对象中 -
在调用glTransformFeedbackVaryings()之后一定要重新链接程序对象,即glLinkProgram(program),这是因为glTransformFeedbackVaryings()中所选择的变量只有程序对象再一次被链接的时候才会起作用
-
如果OpenGL遇到了它的内置变量gl_SkipComponents1~4,就会在transform feedback缓存中留出一个指定数量的空隙;只有bufferMode设置为GL_INTERLEAVED_ATTRIBS时,才可以使用这些变量
-
如果OpenGL遇到内置变量gl_NextBuffer,那么它会将变量传递到当前绑定的下一个transform feedback缓存中;这样就可以将多个变量保存到单一的缓存对象中
5.4.4 transform feedback的启动和停止
-
transform feedback可以随时启动、停止和暂停;如果启动时,并没有处于暂停的状态,那么它会重新开始将数据记录到当前绑定的transform feedback缓存中;如果正处于暂停状态,那么再次启动它将会从之前暂停的位置开始记录
-
void glBeginTransformFeedback(GLenum primitiveMode)
启动transform feedback,设置它准备记录的图元类型;primitiveMode必须是GL_POINTS、GL_LINES或者GL_TRIANGLES -
void glPauseTransformFeedback(void)
暂停transform feedback对变量的记录;暂停模式下有三点限制条件:- 当前绑定的transform feedback对象不可改变
- 不允许将其他的缓存绑定到GL_TRANSFORM_FEEDBACK_BUFFER的绑定点
- 当前的程序对象不可改变
-
void glResumeTransformFeedback(void)
重新启用一个之前暂停的transform feedback过程 -
如果已经完成了所有的transform feedback图元的渲染,可以重新切换到正常的渲染模式,方法是直接调用glEndTransformFeedback()