OpenGL的透视投影

坐标系统

在所有顶点着色器运行后,所有我们可见的顶点都变为标准化设备坐标(Normalized Device Coordinate, NDC)。每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。投影是一个将坐标转换为标准化设备坐标的过程。在对象坐标转换到屏幕空间之前会先将其转换到多个坐标系统。将对象的坐标转换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中进行一些操作或运算更加方便和容易。
对我们来说比较重要的总共有5个不同的坐标系统:

  1. 物体空间(Object Space), 或称为局部空间(Local Space)
  2. 世界空间(World Space)
  3. 观察空间(View Space,或者称为视觉空间(Eye Space))
  4. 裁剪空间(Clip Space)
  5. 屏幕空间(Screen Space)

为了将坐标从一个坐标系转换到另一个坐标系,我们需要用到几个转换矩阵,最重要的几个分别是模型(Model)、视图(View)、投影(Projection)三个矩阵。首先,顶点坐标开始于局部空间(Local Space),称为局部坐标(Local Coordinate),然后经过世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)结束。

坐标系统的转换
  1. 局部坐标是对象相对于局部原点的坐标;也是对象开始的坐标。
  2. 将局部坐标转换为世界坐标,世界坐标是作为一个更大空间范围的坐标系统。这些坐标是相对于世界的原点的。
  3. 接下来我们将世界坐标转换为观察坐标,观察坐标是指以摄像机或观察者的角度观察的坐标。
  4. 在将坐标处理到观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标是处理-1.0到1.0范围内并判断哪些顶点将会出现在屏幕上。
  5. 最后,我们需要将裁剪坐标转换为屏幕坐标,我们将这一过程成为视口变换(Viewport Transform)。视口变换将位于-1.0到1.0范围的坐标转换到由glViewport函数所定义的坐标范围内。最后转换的坐标将会送到光栅器,由光栅器将其转化为片段。
    物体变换到的最终空间就是世界坐标系,并且你会想让这些物体分散开来摆放(从而显得更真实)。对象的坐标将会从局部坐标转换到世界坐标;该转换是由模型矩阵(Model Matrix)实现的。
    观察空间(View Space)经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间就是将对象的世界空间的坐标转换为观察者视野前面的坐标。因此观察空间就是从摄像机的角度观察到的空间。观察(视图)矩阵(View Matrix)里,用来将世界坐标转换到观察空间。
    在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个给定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就被忽略了,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。
    为了将顶点坐标从观察空间转换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix),它指定了坐标的范围,例如,每个维度都是从-1000到1000。投影矩阵接着会将在它指定的范围内的坐标转换到标准化设备坐标系中(-1.0,1.0)。
    投影矩阵将观察坐标转换为裁剪坐标的过程采用两种不同的方式,每种方式分别定义自己的平截头体。我们可以创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。
投影方式
  • 正射投影,即正投影,多用于三维健模
  • 透视投影,则由于和人的视觉系统相似,多用于在二维平面中对三维世界的呈现。透视投影具有消失感、距离感、相同大小的形体呈现出有规律的变化等一系列的透视特性,能逼真地反映形体的空间形象。透视投影通常用于动画、视觉仿真以及其它许多具有真实性反映的方面。
    可以将透视投影变换看作是调整照相机的焦距,它模拟了为照相机选择镜头的过程。投影变换是所有变换中最复杂的一个。

    视锥体是一个三维体,他的位置和摄像机相关,视锥体的形状决定了模型如何从camera space投影到屏幕上。最常见的投影类型-透视投影,使得离摄像机近的物体投影后较大,而离摄像机较远的物体投影后较小。透视投影使用棱锥作为视锥体,摄像机位于棱锥的椎顶。该棱锥被前后两个平面截断,形成一个棱台,叫做View Frustum,只有位于Frustum内部的模型才是可见的。
    object-projection.png
透视投影

我们以OpenGL绘制3D图形为例来了解透视投影。

  1. 定义物体、camera的GLframe:cameraFrame、objectFrame
    GLFrame叫参考帧,其中存储了1个世界坐标点和2个世界坐标下的方向向量,也就是9个GLfloat值,分别用来表示:当前位置点,向前方向向量,向上方向向量。我们分别创建物体和camera的GLFrame的表示。
    一般来说,物体坐标系X轴永远平行于视口的水平方向,+X的方向根据右手定则由+Y与+Z得出;Y轴永远平行于视口的竖直方向,竖直向上为+Y;Z轴永远平行于视口的垂直纸面向里方向,正前方为+Z。也就是说,在世界坐标系中,物体坐标系的Y轴看上去就是GLFrame的向上方向向量,Z轴看上去就是GLFrame的向前方向向量,而X轴由Y轴方向向量与Z轴方向向量根据右手定则可得出。
    举例来说,比如相机,默认状态下,其物体坐标系为:+Y竖直向上;相机的视野中心轴即Z轴,+Z指向相机看到的方向,也就是垂直纸面向里;根据+Y与+Z可得出+X是水平向左。所以对于默认的相机,修改其vOrigin.y,若增大,则相机上移,看到的景物下移,减小同理;修改其vOrigin.z,若增大,则相机向前移动,看到的景物更加靠近,减小同理;修改其vOrigin.x,若增大,则相机向左移动,看到的景物右移,减小同理。

  2. 在改变的窗口的时候定义一个View Frustum,创建View Frustum完成可以通过GetProjectionMatrix()得到一个投影矩阵
    SetPerspective(float fFov, float fAspect, float fNear, float fFar)
    fFov,这个可以理解为视角的大小
    fAspect,是实际窗口的纵横比,即x/y
    fNear,这个呢,表示你近处,的裁面,必须为正数
    fFar表示远处的裁面,必须为正数
    创建GLMatrixStack projectionMatrix;投影矩阵堆栈,把投影矩阵压入投影矩阵堆栈,然后加载模型堆栈的单位矩阵

void changeSize(int w, int h) {
    glViewport(0, 0, w, h);
    // 创建投影矩阵 并将它载入投影矩阵堆栈中
    viewFrustum.SetPerspective(50.f, float(w)/float(h), 1.f, 500.f);
    // projectionMatrix 、modelViewMatrix 投影、模型堆栈为全局变量
    // 把投影矩阵压入投影矩阵堆栈
    projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
    // 调用模型堆栈,载入单元矩阵 ,这一步可以省略,因为modelViewMatrix堆栈底部默认就是一个单元矩阵
    modelViewMatrix.LoadIdentity();
}
projection.jpg
  1. setupRC函数的操作
  • 定义全局变量GLGeometryTransform transformPipeline; GLGeometryTransform类型的变换管道是专门用来管理投影和模型矩阵的 。其实就是把两个矩阵堆栈都存到他这个管道里,方便我们用的时候拿出来使用。调用SetMatrixStacks(GLMatrixStack& mModelView, GLMatrixStack& mProjection) 给transformPipeline设置堆栈transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
  • 定义顶点坐标,设置图元类型,并提交批次类
void setupRC(){
    /// 设置背景颜色,颜色缓存区
    glClearColor(1.f, 1.f, 1.f, 1.f);
    /// 初始化着色管理器
    shaderManager.InitializeStockShaders();
    // 设置变换管线
    transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
    // 为了让效果明显,将观察者坐标位置Z移动往屏幕里移动15个单位位置
    // 参数:表示离屏幕之间的距离。 负数,是往屏幕后面移动;正数,往屏幕前面移动
    cameraFrame.MoveForward(-15.f);
    
    GLfloat vCoast[9] = {
        3,3,0,
        0,3,0,
        3,0,0
    };
    // 用点的方式
    pointBatch.Begin(GL_POINTS, 3);
    pointBatch.CopyVertexData3f(vCoast);
    pointBatch.End();
}
  1. 渲染
  • 根据cameraFrame、objectFrame创建观察者矩阵和object矩阵
    // 观测者矩阵
    M3DMatrix44f mCamera;
    cameraFrame.GetCameraMatrix(mCamera);
    // 矩阵相乘,观察者矩阵乘模型视图矩阵顶部的矩阵,结果存储在模型视图矩阵堆栈的顶部
    modelViewMatrix.MultMatrix(mCamera);
    // object矩阵
    M3DMatrix44f mObjectFrame;
    objectFrame.GetMatrix(mObjectFrame);
    modelViewMatrix.MultMatrix(mObjectFrame);
void renderScene(void) {
    /// 清除一个或一组特定的缓冲区
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    
    /// 设置一组浮点数表示红色
    GLfloat vRed[] = {1.f, 0.f, 0.f, 1.f};
    // 如果 PushMatix() 括号里是空的,就代表是把栈顶的矩阵复制一份,再压栈到它的顶部。如果不是空的,比如是括号里是单元矩阵,那么就代表压入一个单元矩阵到栈顶了。
    modelViewMatrix.PushMatrix();
    // 观测者矩阵
    M3DMatrix44f mCamera;
    cameraFrame.GetCameraMatrix(mCamera);
    // 矩阵相乘,观察者矩阵乘模型视图矩阵顶部的矩阵,结果存储在模型视图矩阵堆栈的顶部
    modelViewMatrix.MultMatrix(mCamera);
    // 获取object矩阵
    M3DMatrix44f mObjectFrame;
    objectFrame.GetMatrix(mObjectFrame);
    // object矩阵乘模型视图矩阵顶部的矩阵,结果存储在模型视图矩阵矩阵的顶部
    modelViewMatrix.MultMatrix(mObjectFrame);
    // 调用平面着色器,使用transformPipeline传入模型矩阵堆栈和投影矩阵堆栈顶部矩阵
    shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
    
    // 设置渲染时点的大小
    glPointSize(8.f);
    pointBatch.Draw();
    glPointSize(1.f);
        
    // 还原单元矩阵
    modelViewMatrix.PopMatrix();
    
    // 交换缓冲区
    glutSwapBuffers();
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,348评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,122评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,936评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,427评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,467评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,785评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,931评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,696评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,141评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,483评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,625评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,291评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,892评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,492评论 2 348