精华内容
下载资源
问答
  • 投影矩阵推导

    2019-02-23 15:33:00
    3D矩阵变换中,投影矩阵是最复杂的。位移和缩放变换一目了然,旋转...计算机显示屏是二维平面,所以如果你想显示三维物体,需要找到把三维物体渲染成二维图像的方法。这正是投影要做的。最简单的做法:直接丢掉三维...

    原文:https://www.cnblogs.com/davelink/p/5623760.html

    3D矩阵变换中,投影矩阵是最复杂的。位移和缩放变换一目了然,旋转变换只要基本的三角函数就能想象出来,投影矩阵则很难凭借直觉想象出来。

    总述:什么是投影

    计算机显示屏是二维平面,所以如果你想显示三维物体,需要找到把三维物体渲染成二维图像的方法。这正是投影要做的。最简单的做法:直接丢掉三维物体各顶点的Z坐标。对于一个立方体,看起来像图1:

    image

    图1 通过丢掉Z坐标方法投影到XY平面

    这种投影简单且不实用。所以,一开始就不应该投影到“面”(plane)上,而应该投影到一个“体”(volume)内,即所谓的“规范视域体”(canonical view volume)。规范视域体的顶点坐标在不同的API(DirectX/OpenGL)中有所不同。这里就用D3D的标准,从 (-1,-1,0)到(1,1,1)。当所有的顶点映射到规范视域体中后,XY坐标用来再映射到屏幕上。Z坐标看起来无用,不过通常用来表示深度信息。这也是为什么会投影到一个“体”,而不是“面”的原因。

    下面将讲述两种常见变换:正交变换、透视变换。

    1、正交变换

    “正交”的由来是投影线与显示平面垂直,是一种相对简单的投影技术。“视体”——眼睛可见的所有几何体的空间——是个各边与坐标轴平行的盒子,现在要把这个盒子映射到规范视域体中,如图2所示:

    image

    图2 正交投影

    正如你所见到的,视体(view volume)可以由6个面定义:

    Left: x = l

    Right: x = r

    Bottom: y = b

    Top: y = t

    Near: z = n

    Far: z = f

    因为视体(view volume)和规范视域体(canonical view volume)的轴都与坐标轴平行,所以在这种投影中,对距离没有修正(correction)(这里指的是近大远小)。实际上,它非常像 图1 那种,直接扔掉各点Z坐标的做法。在3D空间中,同样大小的物体,投影后,在投影空间也是一样大,即使其中一个距离摄像机比另一个远很多也不会有不同。3D空间中平行的线在投影后依然平行。这种投影不可能用于第一人称射击游戏,因为它不能区分出物体的远近。但是它确实有自己的用途。你可以把它用在基于格子的游戏中,特别是当摄像机处于固定角度时,正如 图3所示的那样:

    image

    图3 正交投影的例子

    下面来看看它(正交投影)是怎么工作的。简单地,可以将三个坐标轴分开考虑,分别计算如何将各点(在某个轴上)的坐标(3D空间中),映射到规范视域体中(投影空间)。先算X轴。视体中的坐标处于 [l, r] 区间,现在要把它映射到 [-1, 1] 区间:

    image

    在进行缩放前,要将区间的左边归 0。简单地,将 X 坐标减去l:

    image

    现在区间的左边已经是 0 了,可以将它缩放到需要的尺寸。我们想要的投影后的区间长度为 2(-1 到 1),所以将 X 坐标乘 2/(r-l)。因为 r – l 就是视体的宽度,所以它总为正数,不用担心它会改变不等号方向:

    image

    接下来,将结果减去 1,让它落在 [-1, 1] 区间:

    image

    整理一下:

    image

    好了,下面要把得到的结果分解成 px + q 这样的形式,因为这样方便转换成矩阵形式:

    image

    上面式子的中间部分,就是将3D空间中的 X 转换到规范视域体的算式:

    image

    对于Y轴也应用同样的过程:

    image

    类似的过程也应用于Z轴。这会有一点点的不同,因为Z的范围是 [0, 1],而不是 [-1, 1]。但它们看起来会很相似。

    开始:

    image

    接着:

    image

    再接着:

    image

    再接着:

    image

    最后:

    image

    现在,我们已经做好准备,能写出正交变换矩阵了。这里有三个算式:

    image

    把它们写成矩阵:

    image

    这就是我们要得到的结果!

    D3D提供了一个叫 D3DXMatrixOrthoOffCenterLH() 的函数来构建正交变换矩阵。LH表示这个函数使用的是左手坐标系。那么,OffCenter 是什么鬼?

    这个问题涉及一个形式更简单的正交变换矩阵。

    考虑这样一些点:

    1. 在眼睛所在的空间里,摄像机处于原点;

    2. 通常,我们希望视场(Field of View)相对于Z轴左右对称,上下对称。

    如此的话,Z轴就直接从视体(View Volume)的中间穿过。所以有 r = -l,t = -b。换句话说,可以不去管 r,l,t和b,只需要视体(view volume)的宽w和高h,加上裁剪平面f和n,就可以定义视体了。如果使用新的定义,将得到一个更简单的投影矩阵版本:

    image

    D3DXMatrixOrthoLH() 方法实现了这个矩阵的算法。大多数情况下可以直接使用这个版本,而不是那个更通用的“off center”版本。

    在结束这个小节前,我们再深入一点点。这个正交变换矩阵可以表示为两个稍微简单点的矩阵的乘积:平移矩阵和缩放矩阵。不难理解,因为正交变换相当于把点从一个坐标轴平行的盒子里变换到另一个坐标轴平行的盒子里。视体(Viewing Volume)并没有改变物体的形状,只改变了位置和大小。更准确地说:

    image

    上面的乘积更直观。首先,视体沿着Z轴平移,让它的近平面与原点重合;接着,对它进行缩放。让它的大小与规范视域体(Canonical View Volume)相同。这个很好理解吧?Off Center版本的投影矩阵也可以被表示为一个平移和缩放矩阵的乘积。

    以上是正交变换矩阵的所有内容,接下来试试点挑战。

    2、透视投影

    透视投影相对复杂一点,也更常用。因为它给人一种近大远小的错觉,所以感觉上更真实。

    从几何上说,这种投影与正交投影的不同在于:透视投影的视域(View Volume)是一个截头锥体,就是一个截掉头部的金字塔形状,而不是一个各边与坐标轴平行的盒子。如图4:

    image

    图 4 透视投影

    如你所见,视体的近平面是一个左下角(l,b,n)到右上角(r,t,n)的长方形。远平面则是从原点到近平面的四个顶点作射线,与平面z = f相交,得到的四个点构成的长方形。因为视锥(View Frustum)距离原点越远越宽,又因为要把这个视锥体变换到规范视域体(canonical view volume)中。而规范视域体是一个各边与坐标轴平行的盒子。所以视锥的远端(比较粗的一头),相对于近端(比较细的一头)压缩的程序更大。这也是“近大远小”的原因。

    因为视锥在变换的过程中形状发生了变化,所以透视投影不能简单地分解成平移和绽放变换。不过这并不意味着在正交投影下的工作白费。数学上常用的方法就是把一个复杂问题分解成简单问题嘛。上次(正交变换),我们一次考虑一个坐标轴。而这次,将把x,y坐标一起考虑,最后再处理z坐标。X,y坐标的处理分2步进行:

    1. 给定一个点视锥内的点(x,y,z),将它投影到近平面 z = n上。因为抽影在近平面上,所以x落在区间 [l,r] 内,y落在 [b, t] 内。

    2. 类似正交投影矩阵中的那样,将新的x坐标(投影到近平面上的)从 [l,r] 映射到 [-1,1],新的y坐标从 [b,t] 映射到 [-1,1]。

    看起来不错?见图 5:

    image

    图 5:利用相似三角形将点投影到 z = n 近平面上

    如图 5,从点(x,y,z)向原点作直线,直线与 z = n平面(近平面)相交于图中黑色的点。从这些点向z轴作两条垂线,突然你就得到了两个相似三角形:D。如果想不起相似三角形是啥,翻翻高中课本就知道了。

    根据勾股定理,从(x,y,z)向z轴作的垂线长度为:

    image

    从近平面上的交点(黑色点)到z轴作的垂线的长度:

    image

    投影点的x坐标为 x * n / z,y坐标为 y * n / z。(其实不用勾股定理,直接由相似三角形就得到结果了。)

    这就是第1步的内容。

    第2步类似正交变换中的,将x,y坐标映射到规范视域体:

    image

    公式是一样的,只是要把x用x * n / z代替,y用 y * n / z 代替:

    image

    两边同乘z:

    image

    这个结果有点奇怪。想要把它写成矩阵的话,需要写成这种格式:

    image

    不过上面的式子不是这样的,看起来似乎不对:p。

    怎么办呢?我们已经得到了x’z,y’z的表达式,如果同样能获得类似的z’z的表达式,就能获得将(x,y,z)映射到(x’z,y’z,z’z)的矩阵。将它们同除z,就能获得(x’,y’,z’)了。

    Z到z’的变换不依赖x,y。因为是线性变换,我们知道大概的变换公式是这样的:z’z = pz + q,其中p,q是常数。要确定这两个常数也容易,因为有两个特殊点。要映射 [n,f] 到 [0, 1],所以当 z = n时,z’ = 0,当 z = f 时 z’ = 1。将这两组值代入 z’z = pz + q,可以解出 p,q。

    第 1 组值:

    image

    第 2 组值:

    image

    结果:

    image

    image

    这样就获得了z’z的表达式:

    image

    快完成了。不过因为结果需要用齐次坐标来表示。原来,我们直接把齐次坐标的第四个分量写成 1,即 w = 1,现在我们要的是(x’z,y’z,z’z,w’z),w’ = 1,所以,可以写成 w’z = z。

    image

    将它写成矩阵形式:

    image

    当我们把这个矩阵应用于点(x,y,z,1)时,我们获得的是(x’z,y’z,z’z,z)。接着我们会做齐次坐标常做的处理:同除第四个分量。这样就得到了(x’,y’,z’,1)。

    这就是投影矩阵了。D3DXMatrixPerspectiveOffCenterLH()函数实现了上面的公式。

    如果视锥是以z轴为中心对称的(r = -1,t = -b)。投影矩阵可以用视锥的宽和高来表示:

    image

    D3D也有一个这种形式的函数:D3DXMatrixPerspectiveLH()。

    最后,还有一种表示也很方便。在这种形式中,并不考虑视锥参数,而是使用摄像机的参数。图6展示了这个概念:

    image

    图6 用相机张角表示视锥的高

    视锥的高度用相机的视角来表示。相机的视角用α表示,则α被z轴平分。α与视锥的宽和高的关系如下:

    image

    这样可以替换掉公式中的高度。更进一步,还可以将宽度替换为(屏幕的)高宽比r。

    image

    这样就获得了一个用视角和高宽比表示的投影矩阵:

    image

    D3D中,可以用D3DXMatrixPerspectiveFovLH()来获得这个形式的投影矩阵。这种形式的矩阵比较方便,因为只要设置好屏幕高宽比,和相机视角(一般90度),剩下的就只要关心视锥在z轴上的位置就行了。

    这就是投影矩阵的推导过程。如果你用的是右手坐标系,可能会有所不同。

    展开全文
  • 投影矩阵推导过程

    千次阅读 2014-03-24 08:48:36
    3d程序经常要用到矩阵算法, 比较常见的如旋转矩阵,平移矩阵,以及投影矩阵 opengl与d3d均有对应的api进行相应的操作。 本文主要介绍一下投影矩阵, ...程序内存中保存的是x y z这样的三维数据,最终
    3d程序经常要用到矩阵算法,
    
    比较常见的如旋转矩阵,平移矩阵,以及投影矩阵
    opengl与d3d均有对应的api进行相应的操作。

    本文主要介绍一下投影矩阵,
    (在阅读irricht与ogre代码时碰到了一些问题,发现视截体是根据投影矩阵计算出来的,

    其实也可以根据视角与摄影机的位置与朝向,计算出视截体的,quake就是这么做的,

    另外处理纹理阴影时,也需要对投影矩阵有一定程度的了解)


    程序内存中保存的是x y z这样的三维数据,最终的显示结果,却只有x y这两个维度。

    将三维空间的点转换成屏幕(显存)的二维数据,就是通过投影矩阵来完成的.
    投影矩阵其实很简单。

    想像一下,近平面与远平面,与原点组成了一个金字塔,

    所有的视截体内在三维点,全部投影到近平面上,得到一个二维点
    (z变成深度值,对点投影在二维平面上没有影响,这点很重要,在后继的阐述中将会发现,z的值是非线性的,
    越接近近平面,值被放大,接近远平面的值则放大系数变小)

    再来理一下三维点转换成二维点的过程:
    1 世界坐标转换成观察坐标
    2 投影变换
    3 从坐标中去除齐次坐标的成份(表达的不清楚,实际上是除以点的z坐标值,后面会阐述)

    经过旋转平移,我们可以直接假定摄影机就位于0,0,0 对着 0,0,-1(即z轴负向)
    而视截体则为一个缺顶的金字塔,由6个平面构成(上下左右,远平面z=n,近平面z=f)
    我们假设视角为a(在计算中,这个好像没什么用), 近平面宽w 高 h (远平面不用管,只要知道远平面的z=f即可)
    则位于视截体内部的一个点 (x, y, z) 它在近平面上的投影 (x1, y1)
    可以表示如下(根据相似三角形)
    x1 = x * (n/z)
    y1 = y * (n/z)

    然后x1 y1 和 z(没有z1) 再转换成标准视空间(一个从 -1,-1,0 延伸到 1,1,1的轴对齐矩形)
    即最终的数据(x2, y2, z2) x2 y2取值一定是从-1,1之间 z2的取值则为0,1
    按比例变换即可
    x2 = x1 / w
    y2 = y1 / h


    x2 = (x * (n/z))/w
    y2 = (y * (n/z))/h
    所以投影矩阵其实是一个缩放矩阵,
    这里分母包含z,也就是上文提到的齐次坐标的问题,投影矩阵里都是常数(w,h给定情况下)
    改写上面的的等式
    x2 * z = (x*n)/w
    y2 * z = (y*n)/w

    然后是z2
    因为引入了齐次坐标,自然也希望来个 z2 * z = p*z + q,以便投影矩阵就是一个缩放矩阵
    由于 z2取值是 0,1
    即 z=n 时 z2等于 0
    即 z=f 时 z2等于 1
    我们假设这样的一个等式
    z2 * z = p * z + q (p q是常数)
    分别将z2 = 0, z = n  与 z2 = 1, z = f 代入,即可解出这个方程p 与 q 的值
    可以得到
    p = f / (f - n)
    q = - (f * n) / (f - n)

    上面提到过 z2是非线性的,即,接近近平面,被放大,接近远平面,放大倍数变小,在远平面上的点无放大(z2=1)
    如果按严格的比例变换,z2的真实值应该是
    z2_real = (z - n)/(f - n)  (z2_real严格处在0,1之间)
    再来看我们假设求得的z2
    p q代入
    z2 = (f/z) *( (z-n)/ (f-n) )  ---- 这里笔者做了一些变换,以便说明问题

    f/z就是放大的倍数(就是上文反复提到的) 而且 z2也一定处于(0,1)之间,并且单调递增
    因为z2并不影响投影结果,它只表征深度,所以这样的变换满足3d绘图需求的。

    于是有

    x2 * z = (x*n)/w
    y2 * z = (y*n)/h
    z2 * z = pz + q  (p,q已经在上文求出) ---- <标记1>

    则点(x,y,z,1) 这里加上w,因为要解决齐次坐标问题,经过这样一个投影矩阵,变成

    (x2*z,  y2*z, z2*z,  z),  去除齐次坐标后 (x2, y2, z2, 1) 就变换到标准视空间了(也就是最终绘图的依据)

    现在给出投影矩阵

      n/w, 0,   0, 0    --- 此处的w是宽除2
      0,   n/h, 0, 0     ----  此处h同上
      0,   0,   p, q
      0,   0,   1, 0
     
    前面的三行都很好理解,根据<标记1>处的三个等式得来的,
    最后一行的1,就是为了引入齐次坐标z.
    以便能opengl能正确地去除z

    这样的一个矩阵,最后计算结果x2 y2受z影响,就能表现出透视的效果,z2也正确地在0,1之间单调递增(非线性)。

    ps:
    d3d的标准视空间是 -1,-1,0 1,1,1
    opengl的标准的视空间好像是 -1,-1,-1 到 1,1,1 (z的取值好像是-1,1)但这不影响相应的公式推导

    w, h的值可以用a与n组合得出(还有纵横比),这也是opengl与d3d那个计算投影矩阵函数要传的参数 a, 纵横比, n, f


    附上代码片断与注释

    void matrix4::gen_pers_proj_matrix(ant_f32 fovy, ant_f32 aspect, ant_f32 z_near, ant_f32 z_far, matrix4& out_mtx)
    {
    	/*
    	* 假设视截体内的点(x, y, z) 投影到标准视空间里的(x_p, y_p, z_p)
    	* 需要把视截体里的点,映身到标准视空间里(-1, -1, 0) (1, 1, 1)
    	* 即投影之后 x_p y_p取值为(-1, 1) z_p的取值为(0, 1)
    	* 根据相似三角形,投影形成的 x_p y_p肯定与z有关,所以矩阵中需要包含一个齐次空间因子,以便做矩阵乘法后w取值为z
    	*
    	* 2n/w, 0,    0, 0
    	* 0,    2n/h, 0, 0   --- x y只是单纯的缩放(根据相似三角形)
    	* 0,    0,    p, q   --- 这两个参数纯粹为了矩阵运算是造出来的,在几何上不存在意义,它们能保证变换后的z_p在(0,1)区间单调递增,并且z会与比例值放大(f/z)倍,这也是很多3d编程书籍中提到的放大倍数
    	* 0,    0,    1, 0   --- 1添加齐次空间因子,最终(x, y, z)经过矩阵运算会变成(x_p * z, y_p * z, z_p * z, z),去除齐次坐标因子后得到(x_p, y_p, z_p, 1)
    	*
    	* p = f / (f - n)
    	* q = - (f * n) / (f - n)
    	*
    	* 以上是d3d的情况,opengl的标准视空间是 (-1, -1, -1) (1, 1, 1),即 z_p的取值范围是(-1, 1)
    	* 并且z是负值,因为opengl默认是位于(0, 0, 0)并且看向z负方向,相应的缩放系数会变成
    	*  x_p * (-z) = (2n/w) * x
    	*  y_p * (-z) = (2n/h) * y
    	*  z_p * (-z) = p_o * z + q_o   --- 这里要做一个变通 z虽然是负的,但解的时候,要假设 z在(n,f) 然后z_p对应(-1,1)
    	*
    	* 解得 p_o = (f+n)/(f-n)  q_o=(-2fn)(f-n),  然后考虑z实际是处于(-f, -n)这个区间,则 p_o要取 -(f+n)/(f-n)
    	* 对应的矩阵
    	*
    	*  2n/w, 0,    0,   0
    	*  0,    2n/h, 0,   0
    	*  0,    0,    p_o, -1  --- 这个取-1,是因为齐次坐标因子此时是 -z;
    	*  0,    0,    q_o, 0   
    	*/
    
    	/* 2n/h 实际上就是tan(fovy/2) */
    	ant_f32 h = tan(fovy * ANT_AR_CONVERT * 0.5f);
    	ant_f32 w = h / aspect;//aspect不可为零
    	ant_f32 p = (z_far + z_near) / (z_near - z_far);//分母不可为零
    	ant_f32 q = (2 * z_far * z_near) / (z_near - z_far);//分母不可为零
    
    	out_mtx.m_[0] = w;
    	out_mtx.m_[1] = 0;
    	out_mtx.m_[2] = 0;
    	out_mtx.m_[3] = 0;
    
    	out_mtx.m_[4] = 0;
    	out_mtx.m_[5] = h;
    	out_mtx.m_[6] = 0;
    	out_mtx.m_[7] = 0;
    
    	/*  */
    	out_mtx.m_[8] = 0;
    	out_mtx.m_[9] = 0;
    	out_mtx.m_[10] = p;
    	out_mtx.m_[11] = -1;
    
    	out_mtx.m_[12] = 0;
    	out_mtx.m_[13] = 0;
    	out_mtx.m_[14] = q;
    	out_mtx.m_[15] = 0;
    }
    
















    展开全文
  • 三维坐标旋转矩阵推导

    万次阅读 2018-08-08 16:08:00
    三维坐标旋转矩阵 毫不避讳的说的我的这份博文就是全部借鉴小李的专栏的三维坐标旋转矩阵的。之所以这样做是为了方便我以后查阅方便。若有人要转载我的博文,请和我一样,说明这篇博文的原创是小李(附上网址:...

         在这里首先感谢博主:博客首页 > 小李的专栏 > 三维坐标旋转矩阵    毫不避讳的说的我的这份博文就是全部借鉴小李的专栏的三维坐标旋转矩阵的。之所以这样做是为了方便我以后查阅方便。若有人要转载我的博文,请和我一样,说明这篇博文的原创是小李(附上网址:https://blog.csdn.net/lz20120808/article/details/50809397)在这里再次感谢博主。

       

    1.三维坐标旋转矩阵的推导过程

    任何维的旋转可以表述为向量与合适尺寸的方阵的乘积。最终一个旋转等价于在另一个不同坐标系下对点位置的重新表述。 坐标系旋转角度θ则等同于将目标点围绕坐标原点反方向旋转同样的角度θ。

    若以坐标系的三个坐标轴X、Y、Z分别作为旋转轴,则点实际上只在垂直坐标轴的平面上作二维旋转。

    假设三维坐标系(右手坐标系,拇指即指向X轴的正方向。伸出食指和中指,如右图所示,食指指向Y轴的正方向,中指所指示的方向即是Z轴的正方向。要确定轴的正旋转方向,用右手的大拇指指向轴的正方向,弯曲手指。那么手指所指示的方向即是轴的正旋转方向)中的某一向量,其在直角坐标系中的图如图1所示。其中点P在XY平面、XZ平面、YZ平面的投影分别为点M、点Q、点N。

    这里写图片描述

    一、绕Z轴逆时针旋转θ角
    绕Z轴旋转,相当于在XY平面的投影OM绕原点逆时针旋转,如下图所示,OM旋转θ角到OM’。
    这里写图片描述

    设旋转前的坐标为,旋转后的坐标为,则点M的坐标为,点M’的坐标为。由此可得:

              
          
       

    对于和进行三角展开可得:

    且有;可得绕Z轴旋转角的旋转矩阵为:

    由此可得:
    二. 绕X轴逆时针旋转θ角

    三. 绕Y轴逆时针旋转θ角

    以上旋转矩阵都是在右手坐标系下计算的。三维旋转矩阵就可由以上三个矩阵相乘得到。
    这里的旋转矩阵是需要左乘的,而且以逆时针为正。R是一个旋转矩阵,X是一个三维列向量[x,y,z]’。
    RX就是把X旋转。

    参考:
    1. http://blog.csdn.net/qiuqchen/article/details/21980731

    展开全文
  • 深入理解OpenGL之投影矩阵推导

    千次阅读 2019-08-14 23:49:01
    图形学惯例下的平行投影矩阵推导 首先,确定我们使用以下惯例: 将视图坐标系中的顶点Pe变换到NDC坐标系中的顶点Pn。 视图坐标系使用右手坐标系,NDC也使用右手坐标系。 NDC范围为-1<= x <=1, -1<= y...

    深入理解OpenGL之投影矩阵推导

    OpenGL流水线中的投影矩阵以及坐标变换

    OpenGL中,投影矩阵在Vertex shader中使用,用于变换顶点。一般和Model, View矩阵结合成MVP矩阵后使用。Vertex shader的输出gl_Position是一个处于Clip Space中的齐次坐标。之所以叫做Clip Space,是因为OpenGL会在此空间中对图元进行裁剪(所谓图元就是三角形,线,点)。再这之后,进行透视除法,将通过clip的顶点从clip space的齐次坐标变换成一个3D坐标,这个坐标被称为归一化设备坐标(NDC: normalized device coordinates)。之所以叫归一化,因为这个坐标系的范围对于x,y,z都是从-1到+1,另外这个坐标系形成的几何体被称为规则观察体(CVV: canonical view volume)。再这之后,进行viewport transform将3D的NDC坐标转换成2D的屏幕坐标。

    投影矩阵的作用,投影究竟是什么操作?

    之所以在上面把经过投影之后的坐标的变换复习一遍,是因为我们需要从最终的目标出发理解投影矩阵的作用。因为如果仅仅从投影这个名词出发,是不能理解为何要变换到Clip Space再变换到NDC,然后最终变换到屏幕坐标。因为毕竟对于透视投影,将x,y坐标除以z就是从3D投影到2D,z越大,x和y越小,近大远小的效果就有了;而对于平行投影,直接将z值舍弃就完成了3D到2D的转换。OpenGL搞了那么多事情,都是为了最终能正确高效的进行渲染。
    首先,在观察空间中,图元有可能在视景体内部也可能在外部,对于完全在外部的图元,没有必要进行渲染,所以需要丢弃这些图元,而完全在内部的图元需要保留;部分在内部的图元则需要进行剪裁,对于三角形,需要找出边和视景体边界的交点,将视景体内的部分生成一个或多个新的三角形图元,而在视景体外的顶点进行抛弃。但是直接在观察空间进行裁剪计算起来很麻烦,因为视景体形状和范围各不相同,需要比较复杂的计算才能完成裁剪。因此OpenGL将观察空间变换到规则观察体CVV,这样所有的坐标范围都是-1到+1,就比较容易计算了。需要指出的是,实际进行剪裁不是在CVV中,而是在裁剪空间(Clip Space)中。CVV中的NDC坐标范围是-1到+1,而Clip space中的x,y,z坐标满足 − W c < = X c < = W c -W_c<=X_c<=W_c Wc<=Xc<=Wc, − W c < = Y c < = W c -W_c<=Y_c<=W_c Wc<=Yc<=Wc, − W c < = Z c < = W c -W_c<=Z_c<=W_c Wc<=Zc<=Wc,ClipSpace的齐次坐标经过透视除法将 X c , Y c , Z c X_c,Y_c,Z_c Xc,Yc,Zc都除以 W c W_c Wc就转换到了CVV中的NDC三维坐标。对于透视投影,我们将会看到, W c W_c Wc的值是 − Z e -Z_e Ze ( X c , Y c , Z c ) (X_c,Y_c,Z_c) (Xc,Yc,Zc)除以 − Z e -Z_e Ze后得到了投影后的坐标,因此除以Wc被称为透视除法。
    其次,对于投影来说,从3D转换到2D减少了一个维度,屏幕坐标只需要x,y值。但是为了进行深度测试以及裁剪,需要保留Z值。而且在后面的光栅化阶段,需要对顶点进行插值,得到中间的像素,除了对x,y插值,z也要插值。所以除了要保留Z值,还要保证插值后Z值的正确性。
    再次,对于透视投影,还需要让生成的x,y坐标和z坐标成反比,以达到近大远小的效果。
    所以,要完成以上这些目标,投影矩阵就需要多考虑一些事情,实际上有些图形学教材上例举的投影矩阵比OpenGL的简单一些,比如只是把x,y坐标按透视效果投影到投影面(OpenGL实际是投影到CVV),而投影前的z值没有保留;或者在此基础上保留了z值且插值后的z值是透视正确的,但是没转换到Clip Space,即对x,y坐标没有进行范围的映射。这些矩阵往往只是出于教学目的,OpenGL投影矩阵可以说是这些矩阵的超集。

    小结一下,OpenGL坐标转换的过程:
    【模型坐标 ----> [Vertex Shader] —> 裁剪坐标 ---->[透视除法]---->NDC—>[Viewport变换]---->窗口坐标】
    1-1)ModelView矩阵将模型顶点从模型坐标变换到View Space。
    1-2)投影矩阵变换View space的顶点,得到的是Clip Space中的裁剪坐标(齐次坐标)。
    2)在Clip Space中进行剪裁
    3)进行透视除法,得到的是CVV中的NDC坐标
    4)进行viewport变换,得到屏幕坐标。进行depthRange变换,得到定点数深度值。
    5)光栅化阶段对顶点的屏幕坐标和深度值进行插值,得到图元所覆盖的像素(片段)的坐标和深度。
    其中1-1,1-2在vertex shader中经常合并成MVP矩阵。而本文要讨论的投影矩阵就是将顶点从View space变换到Clip space。
    展开一点讨论:上面的模型空间,视图空间,NDC都是3D坐标空间,尽管计算时顶点使用齐次坐标表示,但顶点的w值为1,直接提取x,y,z即得到3D坐标。而裁剪空间很特殊,其中的点也用齐次坐标表示,但w值通常不为1。(例如通过透视投影变换得到的裁剪空间坐标,w值为-Ze)。这样的一个裁剪空间不能简单的提取x,y,z得到一个对应的3D坐标空间,为了得到3D坐标,需要除以w,而除以w得到的就是NDC这个3D坐标空间。一般没法用图示表示裁剪空间,他真的不是一个立方体,因为它就不是3D空间。其实模型空间,视图空间从数学上说也是齐次坐标空间,因为你运算的时候使用的都是齐次坐标表示的顶点。只不过由于w为1,所以这些齐次点对应的3D点构成了3D空间的模型,视图空间。而裁剪空间也是一个齐次坐标空间,它对应的3D空间就是NDC,所以不精确的你也可以说裁剪空间是个立方体。

    OpenGL的一些重要约定

    理解了投影究竟是干什么的,我们就可以开始推导投影矩阵了。但在这之前先让我们明确OpenGL的一些重要约定。
    在投影之前,顶点处于View Space观察空间中,对于OpenGL,观察空间是+x向右,+y向上,+z向屏幕外的一个右手坐标系,观察方向沿着-z轴,即看向屏幕内部。也就是说如果我们没有模型和视图变换,vertex shader中指定顶点坐标默认使用的坐标系就是这样的一个右手坐标系。
    通过投影(以及透视除法),顶点被变换到CVV中,在OpenGL中,CVV是一个坐标范围从 ( − 1 , − 1 , − 1 ) (-1,-1,-1) (1,1,1) ( 1 , 1 , 1 ) (1,1,1) (1,1,1)的轴对齐立方体。而且重要的是,OpenGL的CVV是左手坐标系。这其实也好理解,因为OpenGL的视景体中,near plane被映射到NDC的 z = − 1 z=-1 z=1平面,far plane被映射到 z = 1 z=1 z=1平面,而near pane离眼睛更近,因此NDC的+z轴就是指向屏幕内(+x, +y方向和观察空间相同),因此可以看出观察空间是右手坐标系,CVV(NDC)是左手坐标系。

    两种投影矩阵

    没错,我们要分别推导透视投影矩阵和平行投影矩阵。这两种投影使用的视景体的形状不同。对于透视投影采用frustum(平截头体),而平行投影采用一个轴对齐六面体。但是两种投影都是要变换(映射)到相同的CVV中。

    推导OpenGL透视投影矩阵

    目标:将视图坐标系中的顶点 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)变换到NDC坐标系中的顶点 P n = ( X n , Y n , Z n ) Pn=(X_n,Y_n,Z_n) Pn=(Xn,Yn,Zn),其中投影矩阵完成从 P e P_e Pe到裁剪空间顶点 P c = ( X c , Y c , Z c , W c ) P_c=(X_c,Y_c,Z_c,W_c) Pc=(Xc,Yc,Zc,Wc)的变换,然后 P c P_c Pc进行透视除法得到 P n P_n Pn
    结合上面的讨论,我们使用以下惯例和约定:
    • 视图坐标系使用右手坐标系,NDC使用左手坐标系。NDC范围为 − 1 < = x < = 1 , − 1 < = y < = 1 , − 1 < = z < = 1 -1<= x <=1, -1<= y <=1, -1<= z <=1 1<=x<=1,1<=y<=1,1<=z<=1
    • 透视投影的视景体(frustum)由六个参数定义,对应了OpenGL的传统函数glFrustum(left, right, bottom, top, nearVal, farVal)。其中 l e f t , r i g h t , b o t t o m , t o p left,right,bottom,top leftrightbottomtop为frustum的四个边平面在近视截面上所截出的矩形区域的 左 边 x = l e f t , 右 边 x = r i g h t , 底 边 y = b o t t o m 和 顶 边 y = t o p 左边x=left,右边x=right,底边y=bottom和顶边y=top x=leftx=righty=bottomy=top n e a r V a l 和 f a r V a l nearVal和farVal nearValfarVal则为距离观察点的最近和最远距离,这两个是距离值必须为正(而由于观察空间中视线是看向负Z轴的,因此近远剪裁面的坐标为 z = − n e a r V a l z=-nearVal z=nearVal z = − f a r V a l z=-farVal z=farVal)。为了书写方便,下面这六个参数简写为 l , r , b , t , n , f l,r,b,t,n,f lrbtnf
    • NDC和屏幕的对应关系为: x = 1 x=1 x=1的点在屏幕右边, x = − 1 x=-1 x=1在左边; y = 1 y=1 y=1在顶部, y = − 1 y=-1 y=1在底部; z = − 1 z=-1 z=1的点距离观察者最近, z = 1 z=1 z=1的点距离观察者最远。
      透视投影视景体和NDC
      约定很重要,因为约定不一样,推导出的矩阵不一样,比如n和f,OpenGL的约定为不含符号的正数距离值,而有些文章推导时n和f是包含符号的坐标值。再如OpenGL约定 z = − n z=-n z=n 映射到 z = − 1 z=-1 z=1; z = − f z=-f z=f映射到 z = 1 z=1 z=1,而有些图形学教材是将 n n n映射到 z = 1 z=1 z=1, f f f映射到 z = − 1 z=-1 z=1,这样矩阵的第三行符号就是反的。
    推导过程

    视锥体俯视图和侧视图
    首先,在视图空间中,我们以近裁剪面为投影面,计算视图空间中的一个点 ( X e , Y e , Z e ) (X_e,Y_e,Z_e) (Xe,Ye,Ze)在投影面上的坐标 ( X p , Y p , Z p ) (X_p,Y_p,Z_p) (Xp,Yp,Zp),从俯视图可看出,根据相似三角形的比例关系:
    X p X e = Z p Z e \frac{X_p}{X_e} = \frac{Z_p}{Z_e} XeXp=ZeZp,而 Z p = − n Z_p=-n Zp=n
    因此 X p X e = − n Z e \frac{X_p}{X_e} = \frac{-n}{Z_e} XeXp=Zen X p = − n X e Z e = n X e − Z e X_p = \frac{-nX_e}{Z_e}=\frac{nX_e}{-Z_e} Xp=ZenXe=ZenXe
    同样,根据侧视图,可计算得到 Y p = n Y e − Z e Y_p = \frac{nY_e}{-Z_e} Yp=ZenYe
    P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)被投影到 P p = ( n X e − Z e , n Y e − Z e , − n ) P_p=( \frac{nX_e}{-Z_e}, \frac{nY_e}{-Z_e}, -n) Pp=(ZenXe,ZenYe,n)。注意投影后的z坐标总是 − n -n n,但是我们想在投影后仍然保留投影前z坐标的信息以便进行深度测试等工作。如果我们直接保留 Z e Z_e Ze行不行呢?即 P p = ( n X e − Z e , n Y e − Z e , Z e ) P_p=( \frac{nX_e}{-Z_e}, \frac{nY_e}{-Z_e}, Z_e) Pp=(ZenXe,ZenYe,Ze)。看上去没毛病,但是这是不行的。因为投影之后的光栅化阶段,需要在屏幕空间对顶点属性进行插值,以得到每个像素的深度值和其他属性如纹理坐标光照亮度等。而光栅化时在屏幕空间从点A到点B均匀的遍历像素,并根据像素到AB的距离对Z坐标进行线性插值,得到在屏幕空间均匀分布的Z值,可是每个像素逆投射回视图空间就会发现,这些像素在视图空间对应的Z值并不是均匀分布。具体请参考图形学基础之透视校正插值。实际上,光栅化时应该对Z坐标的倒数进行插值,因此需要建立关于1/Z的映射函数: Z p = A Z e + B Z_p = \frac{A}{Z_e}+B Zp=ZeA+B。综上所述,投影后得到的顶点为:
    P p = ( n X e − Z e , n Y e − Z e , A Z e + B ) P_p = (\frac{nX_e}{-Z_e}, \frac{nY_e}{-Z_e}, \frac{A}{Z_e}+B) Pp=(ZenXe,ZenYe,ZeA+B)
    而投影面上(近视截面)的顶点满足 l ≤ X p ≤ r l \leq X_p \leq r lXpr b ≤ Y p ≤ t b \leq Y_p \leq t bYpt
    如上所说,视锥体通过投影矩阵(以及透视除法)最终变换为CVV,即 ( X p , Y p , Z p ) (X_p,Y_p,Z_p) (Xp,Yp,Zp)变换为NDC坐标 ( X n , Y n , Z n ) (X_n,Y_n,Z_n) (Xn,Yn,Zn)。而 X n , Y n , Z n X_n,Y_n,Z_n Xn,Yn,Zn的范围都是 [ − 1 , 1 ] [-1,1] [1,1]。首先我们处理x,y坐标,将 X p , Y p X_p,Y_p Xp,Yp映射到 X n , Y n X_n,Y_n Xn,Yn,即将 [ l , n ] [l,n] [l,n] [ b , t ] [b,t] [b,t]映射到 [ − 1 , 1 ] [-1,1] [1,1]的范围,这通过简单的线性函数就可以实现:
    X n = 2 ( X p − l ) r − l − 1 X_n = \frac{2(Xp-l)}{r-l}-1 Xn=rl2(Xpl)1
    Y n = 2 ( Y p − b ) t − b − 1 Y_n = \frac{2(Yp-b)}{t-b}-1 Yn=tb2(Ypb)1
    代入上面关于 X p , Y p X_p,Y_p Xp,Yp的表达式:
    X n = 2 ( n X e − Z e − l ) r − l − 1 = 2 n r − l ( X e − Z e ) − 2 l r − l − 1 X_n = \frac{2(\frac{nX_e}{-Z_e}-l)}{r-l}-1 = \frac{2n}{r-l}(\frac{X_e}{-Z_e})-\frac{2l}{r-l}-1 Xn=rl2(ZenXel)1=rl2n(ZeXe)rl2l1
    X n = 2 n r − l ( X e − Z e ) − r + l r − l X_n = \frac{2n}{r-l}(\frac{X_e}{-Z_e})-\frac{r+l}{r-l} Xn=rl2n(ZeXe)rlr+l
    同样可得
    Y n = 2 n t − b ( Y e − Z e ) − t + b t − b Y_n=\frac{2n}{t-b}(\frac{Y_e}{-Z_e})-\frac{t+b}{t-b} Yn=tb2n(ZeYe)tbt+b
    这就得到了从视图坐标的xy到NDC坐标的xy的映射关系,下面找一下z坐标的映射关系 Z n = f ( Z e ) Z_n=f(Z_e) Zn=f(Ze),即视图空间Z坐标和NDC的Z坐标的函数。
    由于我们将视图空间投影后的z坐标设置为 A Z e + B \frac{A}{Z_e}+B ZeA+B的形式,而从投影坐标到NDC坐标是线性映射,因此可将NDC坐标 Z n Z_n Zn也记为 A Z e + B \frac{A}{Z_e}+B ZeA+B,只是相对于 Z p Z_p Zp其A,B值不同。
    已知视图空间z坐标 Z e Z_e Ze的范围是 [ − f , − n ] [-f,-n] [f,n],对应了NDC中的z坐标范围 [ − 1 , 1 ] [-1,1] [1,1],且 − n -n n映射到 − 1 -1 1 − f -f f映射到 1 1 1,因此将 − n , − f -n,-f n,f分别代入 Z n = A Z e + B Zn=\frac{A}{Ze}+B Zn=ZeA+B得:
    − 1 = A − n + B -1 = \frac{A}{-n}+B 1=nA+B
    1 = A − f + B 1 = \frac{A}{-f}+B 1=fA+B
    可解出A,B为:
    A = 2 n f f − n A=\frac{2nf}{f-n} A=fn2nf
    B = f + n f − n B=\frac{f+n}{f-n} B=fnf+n
    将A,B代入 Z n = A Z e + B Zn=\frac{A}{Ze}+B Zn=ZeA+B的表达式后,即可得到 Z e 和 Z n Z_e和Z_n ZeZn的关系式:
    Z n = 2 n f f − n Z e + f + n f − n Z_n=\frac{\frac{2nf}{f-n}}{Ze}+\frac{f+n}{f-n} Zn=Zefn2nf+fnf+n,即:
    Z n = − 2 n f f − n ( 1 − Z e ) + f + n f − n Z_n = \frac{-2nf}{f-n}(\frac{1}{-Z_e})+\frac{f+n}{f-n} Zn=fn2nf(Ze1)+fnf+n
    至此,我们已经得到了视图空间坐标 ( X e , Y e , Z e ) (X_e,Y_e,Z_e) (Xe,Ye,Ze)到NDC坐标 ( Z n , Y n , Z n ) (Z_n,Y_n,Z_n) (Zn,Yn,Zn)的函数:
    X n = 2 n r − l ( X e − Z e ) − r + l r − l X_n = \frac{2n}{r-l}(\frac{X_e}{-Z_e})-\frac{r+l}{r-l} Xn=rl2n(ZeXe)rlr+l
    Y n = 2 n t − b ( Y e − Z e ) − t + b t − b Y_n=\frac{2n}{t-b}(\frac{Y_e}{-Z_e})-\frac{t+b}{t-b} Yn=tb2n(ZeYe)tbt+b
    Z n = − 2 n f f − n ( 1 − Z e ) + f + n f − n Z_n = \frac{-2nf}{f-n}(\frac{1}{-Z_e})+\frac{f+n}{f-n} Zn=fn2nf(Ze1)+fnf+n
    上文说过,从视图坐标到NDC坐标的变换分为两个过程,即先通过投影矩阵变换得到裁剪空间的齐次坐标,然后经过透视除法得到NDC坐标。我们已经得到了NDC坐标 ( X n , Y n , Z n ) (X_n,Y_n,Z_n) (Xn,Yn,Zn),为了得到投影矩阵,需要得到裁剪空间的齐次坐标 ( X c , Y c , Z c , W c ) (X_c,Y_c,Z_c,W_c) (Xc,Yc,Zc,Wc)。由于 X n = X c W c X_n = \frac{X_c}{W_c} Xn=WcXc, Y n = Y c W c Y_n = \frac{Y_c}{W_c} Yn=WcYc, Z n = Z c W c Z_n = \frac{Z_c}{W_c} Zn=WcZc,且上面的 X n , Y n , Z n X_n,Y_n,Z_n Xn,Yn,Zn的表达式中,都有 − 1 Z e -\frac{1}{Z_e} Ze1,显然可以令 W c = − Z e W_c=-Z_e Wc=Ze X n , Y n , Z n X_n,Y_n,Z_n Xn,Yn,Zn分别乘以 − Z e -Z_e Ze得到 ( X c , Y c , Z c , W c ) (X_c,Y_c,Z_c,W_c) (Xc,Yc,Zc,Wc)为:
    X c = 2 n r − l X e + r + l r − l Z e X_c = \frac{2n}{r-l}X_e+\frac{r+l}{r-l}Z_e Xc=rl2nXe+rlr+lZe
    Y c = 2 n t − b Y e + t + b t − b Z e Y_c=\frac{2n}{t-b}Y_e+\frac{t+b}{t-b}Z_e Yc=tb2nYe+tbt+bZe
    Z c = − f + n f − n Z e − 2 n f f − n Z_c = -\frac{f+n}{f-n}Z_e-\frac{2nf}{f-n} Zc=fnf+nZefn2nf
    W c = − Z e W_c=-Z_e Wc=Ze
    以上都是关于 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)的线性函数,可以用矩阵表示为:
    P p r o j = [ 2 n r − l 0 r + l r − l 0 0 2 n t − b t + b t − b 0 0 0 − f + n f − n − 2 n f f − n 0 0 − 1 0 ] P_{proj} = \left[\begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2nf}{f-n} \\ 0 & 0 & -1 & 0 \end{matrix}\right] Pproj=rl2n0000tb2n00rlr+ltbt+bfnf+n100fn2nf0
    即得到了OpenGL的透视投影矩阵

    关于Z值插值的一点补充

    上文说到,为了对 1 Z e \frac{1}{Z_e} Ze1进行插值,我们将 Z n Z_n Zn定义成 A Z e + B \frac{A}{Z_e}+B ZeA+B的形式,然后在光栅化时经过glDepthRange的映射,将 [ − 1 , 1 ] [-1,1] [1,1] Z n Z_n Zn映射为 [ 0 , 1 ] [0,1] [0,1]的Z值,这个Z值被写到Z Buffer中。按理说插值Z应该就是用这个将写入Z Buffer的Z值了。但是我在某本书上看到,使用clip space的W值的倒数进行插值。clip space顶点是vertex shader的输出,其顶点的W值就是 − Z e -Z_e Ze,因此感觉也是挺科学的。具体什么情况,等我弄清楚了再补充。

    gluPerspective风格的透视投影矩阵

    OpenGL固定流水线的传统函数

    void gluPerspective(	GLdouble fovy,
     	GLdouble aspect,
     	GLdouble zNear,
     	GLdouble zFar);
    

    这其实是另外一种定义frustum视截体的方式,不同的是这种方式定义的视截体的中心在Z轴,也就是说,glFrustum矩阵中当 l = − r , b = − t l=-r, b=-t l=r,b=t时的情况。
    fovy为视截体在yz平面上的夹角,aspect为裁剪面的宽高比。因为左右上下对称,因此可知对于glFrustum矩阵中的 l , r , b , t l,r,b,t l,r,b,t, l , b l,b l,b为负值, r , t r,t r,t为正值,因此可计算得到:
    t a n ( f o v y / 2 ) = t n tan(fovy/2) = \frac{t}{n} tan(fovy/2)=nt
    t = n ∗ t a n ( f o v y / 2 ) t = n*tan(fovy/2) t=ntan(fovy/2)
    b = − t = − n ∗ t a n ( f o v y / 2 ) b=-t = -n*tan(fovy/2) b=t=ntan(fovy/2)
    r = a s p e c t ∗ t = n ∗ a s p e c t ∗ t a n ( f o v y / 2 ) r = aspect * t = n*aspect*tan(fovy/2) r=aspectt=naspecttan(fovy/2)
    l = − r = − n ∗ a s p e c t ∗ t a n ( f o v y / 2 ) l = -r = -n*aspect*tan(fovy/2) l=r=naspecttan(fovy/2)
    l , r , b , t l,r,b,t l,r,b,t代入上面的glFrustum矩阵中,可得gluPerspective矩阵:
    P g l u P e r s p e c t i v e = [ 1 a s p e c t ∗ t a n ( f o v y / 2 ) 0 0 0 0 1 t a n ( f o v y / 2 ) 0 0 0 0 − f + n f − n − 2 n f f − n 0 0 − 1 0 ] P_{gluPerspective} = \left[\begin{matrix} \frac{1}{aspect*tan(fovy/2)} & 0 &0 & 0 \\ 0 & \frac{1}{tan(fovy/2)} & 0 & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2nf}{f-n} \\ 0 & 0 & -1 & 0 \end{matrix}\right] PgluPerspective=aspecttan(fovy/2)10000tan(fovy/2)10000fnf+n100fn2nf0

    推导OpenGL平行投影矩阵

    平行投影视景体和NDC
    如图所示,平行投影的视景体是一个轴对齐六面体,由于没有透视效果,我们只需要将视景体映射到NDC。

    目标:将平行投影视图坐标系中的顶点 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)变换到NDC坐标系中的顶点 P n = ( X n , Y n , Z n ) P_n=(X_n,Y_n,Z_n) Pn=(Xn,Yn,Zn)
    约定:

    NDC的约定同透视投影,视景体的定义同传统OpenGL函数 g l O r t h o ( l e f t , r i g h t , t o p , b o t t o m . n e a r , f a r ) glOrtho(left, right, top, bottom. near, far) glOrtho(left,right,top,bottom.near,far)。前4个参数分别定义了视景体的左右上下四个面。near, far是近裁剪面和远裁剪面相对于视点的距离,但是和透视投影不同,near, far不一定是正数。如果near或far小于0,则表示位于视点后面(视点位于 ( 0 , 0 , 0 ) (0,0,0) (0,0,0))。同样为了书写方便,这六个参数简写为 l , r , t , b , n , f l, r, t, b, n, f l,r,t,b,n,f。这样 ( r , t , − n ) (r,t,-n) (r,t,n)表示的是近裁剪面的右上角。

    推导过程

    如上所述,由于平行投影的视景体是一个轴对称六面体,而NDC是一个立方体,也是轴对称的。因此只需要简单的线性映射,即可将视景体中的顶点 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)变换到NDC中的顶点 P n = ( X n , Y n , Z n ) P_n=(X_n,Y_n,Z_n) Pn=(Xn,Yn,Zn)。这只需要先将六面体的长宽高缩放到2,然后将中心点移动到立方体中心即可。
    以X坐标为例,我们需要将 X e X_e Xe映射到 X n X_n Xn,其实这和上面透视投影将 X p X_p Xp映射到 X n X_n Xn是一样的,但是之前没有具体推导,一笔带过了。这儿稍微详细推导一下:
    由于 X e X_e Xe的范围是 [ l , r ] [l,r] [l,r] X n X_n Xn的范围是 [ − 1 , 1 ] [-1,1] [1,1],因此通过
    1 − ( − 1 ) r − l . X e \frac{1-(-1)}{r-l}.X_e rl1(1).Xe即可把 X e X_e Xe缩放到 [ − 1 , 1 ] [-1,1] [1,1],然后再进行一个偏移将中心点移动到原点,假设偏移量为 B B B,则可得:
    X n = 1 − ( − 1 ) r − l X e + B X_n = \frac{1-(-1)}{r-l}X_e+B Xn=rl1(1)Xe+B
    为了计算出 B B B,我们将 X e = r 和 X n = 1 X_e=r和X_n=1 Xe=rXn=1带入上式
    1 = 2 r − l r + B 1 = \frac{2}{r-l}r+B 1=rl2r+B,可得
    B = − r + l r − l B=-\frac{r+l}{r-l} B=rlr+l,将其代入上式,可得:
    X n = 2 r − l X e − r + l r − l X_n = \frac{2}{r-l}X_e-\frac{r+l}{r-l} Xn=rl2Xerlr+l
    同样可得
    Y n = 2 t − b Y e − t + b t − b Y_n = \frac{2}{t-b}Y_e-\frac{t+b}{t-b} Yn=tb2Yetbt+b

    Z n Z_n Zn的推导过程一样,只是由于 n , f n,f n,f是距离值,因此其坐标表示为 − n , − f -n,-f n,f,不失一般性在上图所示的情况下, − f 映 射 到 1 , − n 映 射 到 − 1 -f映射到1,-n映射到-1 f1n1,因此:
    Z n = 1 − ( − 1 ) − f − ( − n ) Z e + B Z_n = \frac{1-(-1)}{-f-(-n)}Z_e+B Zn=f(n)1(1)Ze+B
    代人 Z n = 1 , Z e = − f Z_n=1, Z_e=-f Zn=1,Ze=f
    1 = 2 n − f ( − f ) + B 1 = \frac{2}{n-f}(-f)+B 1=nf2(f)+B,得
    B = 2 f n − f + 1 = n + f n − f B = \frac{2f}{n-f}+1=\frac{n+f}{n-f} B=nf2f+1=nfn+f,因此:
    Z n = 2 n − f Z e + n + f n − f Z_n = \frac{2}{n-f}Z_e+\frac{n+f}{n-f} Zn=nf2Ze+nfn+f
    Z n = − 2 f − n Z e − f + n f − n Z_n = \frac{-2}{f-n}Z_e-\frac{f+n}{f-n} Zn=fn2Zefnf+n
    由此,我们得到了 P e P_e Pe P n P_n Pn的线性映射关系,我们实际需要的是 P e P_e Pe P c P_c Pc的线性关系,因为投影矩阵变换后得到的是Clip Space的顶点。但对于平行投影,w值没有意义,因此可以任意指定,这样我们指定w=1,即可直接将 P c P_c Pc P n P_n Pn表示,最终我们得到如下表达式:

    X c = 2 r − l X e − r + l r − l X_c = \frac{2}{r-l}X_e-\frac{r+l}{r-l} Xc=rl2Xerlr+l
    Y c = 2 t − b Y e − t + b t − b Y_c= \frac{2}{t-b}Y_e-\frac{t+b}{t-b} Yc=tb2Yetbt+b
    Z c = − 2 f − n Z e − f + n f − n Z_c = \frac{-2}{f-n}Z_e-\frac{f+n}{f-n} Zc=fn2Zefnf+n
    W c = 1 W_c= 1 Wc=1
    以上都是关于 P e = ( X e , Y e , Z e ) P_e=(X_e,Y_e,Z_e) Pe=(Xe,Ye,Ze)的线性函数,可以用矩阵表示为:
    P p r o j = [ 2 r − l 0 0 − r + l r − l 0 2 t − b 0 − t + b t − b 0 0 − 2 f − n − f + n f − n 0 0 0 1 ] P_{proj} = \left[\begin{matrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & \frac{-2}{f-n} & -\frac{f+n}{f-n} \\ 0 & 0 & 0 & 1 \end{matrix}\right] Pproj=rl20000tb20000fn20rlr+ltbt+bfnf+n1
    即得到了OpenGL的平行投影矩阵

    补充

    最近学习了GAMES101课程,闫令琪老师讲解了图形学约定下投影矩阵的推导,非常值得一看:
    https://www.bilibili.com/video/BV1X7411F744?p=4&t=3007
    其中的约定和OpenGL稍微有些不同,一是OpenGL中NDC空间是左手坐标系,而闫老师推导的是右手坐标系,即和视图坐标系一致。二是关于n和f,OpenGL是距离值,而闫老师使用的是坐标值。
    推导的过程非常好,比如平行投影矩阵,只是先将frustum平移到原点,然后坐一个缩放,直接将两个矩阵相乘就得到投影矩阵。由于约定的不同,在闫老师的矩阵中将n和f取反,并且将z乘以-1,最终得到的矩阵和OpenGL就是一样的了。

    展开全文
  • 投影矩阵推导(翻译)

    2016-06-28 15:51:00
    投影矩阵推导(翻译) 原网址:http://www.codeguru.com/cpp/misc/misc/graphics/article.php/c10123/Deriving-Projection-Matrices.htm 3D矩阵变换中,投影矩阵是最复杂的。位移和缩放变换一目了然,旋转变换只要...
  • 推导投影矩阵

    2019-11-26 15:15:42
    参考:添加链接描述 在任何3D图形程序员工具...但是,我还没有在网上看到很多资源,它们仅描述了如何推导投影矩阵。这就是我将在本文中讨论的主题。 对于刚开始使用3D图形的人们,我应该提到了解投影矩阵的来源可能是...
  • OpenGL学习脚印: 投影矩阵... Projection Matrix》内容,以供自己和初学者熟悉投影矩阵推导过程。 通过本节,你可以了解到:  投影矩阵计算的阶段  透视投影和正交投影的矩阵推导 本节的要点就在于: 阅读时
  • opoengl 投影矩阵推导

    千次阅读 2015-06-03 16:09:54
    原文:... OpenGL学习脚印: 投影矩阵的推导 ... 本节内容翻译和整理自http://www.songho.ca songho的博客《OpenGL Projection Matrix》内容,以供自己和初学者熟悉投影矩阵推导
  • 写在前面 本节内容翻译和整理自http://www.songho.ca songho的博客《OpenGL Projection Matrix》内容,以供自己和初学者熟悉投影矩阵推导过程。通过本节,你可以了解到: 投影矩阵计算的阶段 透视投影和正交投影的...
  • 齐次坐标概念&&透视投影矩阵推导

    千次阅读 2012-05-07 21:34:36
      透视投影是3D固定流水线的重要组成部分,是将相机空间中的点从视锥体(frustum)变换到规则观察体...在算法中它是通过透视矩阵乘法和透视除法两步完成的。 透视投影变换是令很多刚刚进入3D图形领域的开发
  • 文章目录前言一、视点和视线二、投影矩阵 前言 本篇继续前面文章来讲解投影矩阵相关内容。 一、视点和视线 二、投影矩阵
  • 如图所示,我们通过2推导。 已知:向量V,投影到的平面n,n是单位向量。求投影向量V2。 我们可以根据向量的投影公式求得,v在n上的投影长度,也就是V2的模。 v2 = n * (v * n) 我们通过矩阵的x基坐标求得x...
  • 写在前面 本节内容翻译和整理自http://www.songho.ca songho的博客《OpenGL Projection Matrix》内容,以供自己和初学者熟悉投影矩阵推导过程。通过本节,你可以了解到: 投影矩阵计算的阶段 透视投影和正交投影的...
  • 文章目录顶点数据分析如何变换到...并且本文不对基础的线性代数进行介绍,比如向量,矩阵,逆,转置的概念就不介绍了(本小白比较懒==)。 顶点数据分析 想从变换一个立方体的坐标开始着手。每一个物体都有自己的loc
  • 投影矩阵推导

    千次阅读 2016-08-01 11:35:21
    这个就得到正交投影矩阵了。 3、透视投影 透视投影与正交投影的不同点在于,透视投影的视锥体是一个削去尖的椎体,如下图所示: 利用相似三角形来推导透视之后的点: 上图中L的距离等于: 然后我们因为L2这个线段与...
  • 旋转矩阵可以推广至更高维度的空间,以三维空间为例: 设有向量,那么当向量绕轴逆时针旋转度时,相当于在向量在一个平行于平面的的二维空间中,绕原点旋转,旋转后坐标保持不变,那么旋转矩阵可以改为: 向量绕...
  • 【转】投影矩阵推导 原文:https://www.cnblogs.com/wonderKK/p/5695116.html 博主: 这篇文章写得非常好,对投影矩阵推导清晰明了,但有个错误:推导的全程是基于列矩阵,右手坐标系的,而不是作者文中说...
  • 投影矩阵推导(Deriving Projection Matrices)

    万次阅读 多人点赞 2018-11-10 11:15:26
    投影矩阵推导&gt;译文,原文地址为:  http://www.codeguru.com/cpp/misc/misc/math/article.php/c10123__1/Deriving-Projection-Matrices.htm,由于本人能力有限,有译的不明白的地方大家可以参考原文,...
  • 用于游戏中模型顶点到任意空间平面的投影, 求得的的矩阵可用作shader之矩阵参数, 完全原创, 有图示, 推导过程一目了然, 每步推导, 有助于大家掌握其他线性关系的矩阵推导
  • 正交投影矩阵-原理及推导

    万次阅读 2019-02-15 15:51:42
    二维投影 上图表示的是,向量b在向量a上的投影。显然有例如以下表达式: 当中,P为投影矩阵,由P... 三维投影,就是将一个向量投影到一个平面上。同上面一样,如果是将b向量投影到平面上的p向量,则有表达式:...
  • 三维坐标旋转矩阵及其推导过程

    千次阅读 2019-11-15 09:24:58
    一.绕坐标轴逆时针旋转 任何的旋转可以表述为向量与合适尺寸的方阵的乘积。最终一个旋转等价于在另一个不同坐标系下对点位置的重新表述。...若以坐标系的个坐标轴X、Y、Z分别作为旋转轴,则点实际上...
  • 在3D图形程序的基本矩阵变换中,投影矩阵是其中...而且,我在网上还未看到许多关于如何推导投影矩阵的教程资源。本文的话题就是如何推导投影矩阵。  对于刚刚开始接触3D图形的人,我应该指出,理解投影矩阵如何推

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 7,781
精华内容 3,112
关键字:

三维投影矩阵推导