图形学矩阵变换(二)_shoping & programing_百度空间

接下来来看viewMatrix。

视图变换实际上是坐标系的变换,将世界坐标系变换到眼坐标系(view ),这样做可以方便后面的投影(Projection)操作,因为投影是在眼坐标系中进行的。

建立眼坐标系用gluLookAt函数来实现,我们看看函数原型:
gluLookAt(double eyex, double eyey, double eyez, double centerx, double centery, double centerz, double upx, double upy, double upz)

这些参数中包括三种信息:眼睛的位置eye,视点中心位置center和向上的向量up。注意up不是y轴,只是指明了y轴的大致方向。

设眼睛的位置为原点,向量(eye-center)所得的向量为Z轴,y叉乘z得到x轴,再由x轴和z轴叉乘,反算出y轴。这就是眼坐标系的三个方向,设其单位向量为u、v、n。

然后就是坐标系之间的变换,这一点可以通过两种思路来完成:
1. 将眼坐标系变换到同世界坐标系重合。
2.代数的方法。

对于1而言,又有两种思路:
1. 代数上已经证明的公式:先移动eye的位置到世界坐标系的原点,再进行正交变换;
2. 由基本的变换合成,如下:
1)移动眼睛的位置到世界坐标系原点;
2)将眼坐标系绕世界坐标系x轴旋转,使得眼坐标系的z轴位于世界坐标系xoz平面;
3)将眼坐标系绕世界坐标系y轴旋转,使得眼坐标系的z轴和世界坐标系z轴重合;
4)将眼坐标系绕世界坐标系z轴旋转,使得眼坐标系的x、y轴和世界坐标系x、y轴重合;
这个步骤也需要很多的草稿纸,O(∩_∩)O

对于2,代数的方法,显得更简洁一些,看下图:


图中,对于任意的P点,向量减OP-Oeye= eyeP。再考虑eyeP向量,将其对眼坐标系的x/y/z轴进行投影,呵呵,就能得到P点在新坐标系中的坐标。投影很简单,用点乘即可。
于是,对于P点的新坐标P'(x',y',z'):
x' =(OP-Oeye).u = OP.u - Oeye.u =(x,y,z).(u[0],u[1],u[2]) -Oeye.(u[0],u[1],u[2]);
其中,u为眼坐标x轴方向的单位向量, x为P点的x坐标,u[0]为u向量的x值;其他类似。

y、z类似,我想你应该能写出矩阵来了吧,此处矩阵略过,直接看代码吧。

/* Build a row-major (C-style) 4x4 matrix transform based on the parameters for gluLookAt. */
static void buildLookAtMatrix(double eyex, double eyey, double eyez,
double centerx, double centery, double centerz,
double upx, double upy, double upz,float m[16])
{
double x[3], y[3], z[3], mag;

/* Difference eye and center vectors to make Z vector. */
z[0] = eyex - centerx;
z[1] = eyey - centery;
z[2] = eyez - centerz;
/* Normalize Z. */
mag = sqrt(z[0]*z[0] + z[1]*z[1] + z[2]*z[2]);
if (mag) {
z[0] /= mag;
z[1] /= mag;
z[2] /= mag;
}

/* Up vector makes Y vector. */
y[0] = upx;
y[1] = upy;
y[2] = upz;

/* X vector = Y cross Z. */
x[0] = y[1]*z[2] - y[2]*z[1];
x[1] = -y[0]*z[2] + y[2]*z[0];
x[2] = y[0]*z[1] - y[1]*z[0];

/* Recompute Y = Z cross X. */
y[0] = z[1]*x[2] - z[2]*x[1];
y[1] = -z[0]*x[2] + z[2]*x[0];
y[2] = z[0]*x[1] - z[1]*x[0];

/* Normalize X. */
mag = sqrt(x[0]*x[0] + x[1]*x[1] + x[2]*x[2]);
if (mag) {
x[0] /= mag;
x[1] /= mag;
x[2] /= mag;
}

/* Normalize Y. */
mag = sqrt(y[0]*y[0] + y[1]*y[1] + y[2]*y[2]);
if (mag) {
y[0] /= mag;
y[1] /= mag;
y[2] /= mag;
}

/* Build resulting view matrix. */
m[0*4+0] = x[0]; m[0*4+1] = x[1];
m[0*4+2] = x[2]; m[0*4+3] = -x[0]*eyex + -x[1]*eyey + -x[2]*eyez;

m[1*4+0] = y[0]; m[1*4+1] = y[1];
m[1*4+2] = y[2]; m[1*4+3] = -y[0]*eyex + -y[1]*eyey + -y[2]*eyez;

m[2*4+0] = z[0]; m[2*4+1] = z[1];
m[2*4+2] = z[2]; m[2*4+3] = -z[0]*eyex + -z[1]*eyey + -z[2]*eyez;

m[3*4+0] = 0.0; m[3*4+1] = 0.0; m[3*4+2] = 0.0; m[3*4+3] = 1.0;
}

到此,view矩阵已经OK了。

{zh1}看一下投影矩阵,这是最复杂的一个变换。我们只看xx投影矩阵,平行投影会简单很多。

在前面Cg Studying()一文中,我们已经看到了xx投影变换的过程:将平头视锥体变换了边长为2,中心在原点的立方体,以便于后面的裁剪和消隐工作。过程如下:


下面将变换设定在常用的情况上,就是投影平面的中心和xy平面的中心重合,即在上面右图的Z轴上。OpenGL中构建投影矩阵的函数是gluPerspective,其原型是:

void gluPerspective(
  GLdouble fovy, //角度
  GLdouble aspect,//视景体的宽高比
  GLdouble zNear,//沿z轴方向的两裁面之间的距离的近处
  GLdouble zFar //沿z轴方向的两裁面之间的距离的远处
  )

向x轴负方向看过去,这两个变换空间如下图所示。


考虑上边界情况,对于眼坐标系来说:y=-ztan(fovy/2), 而对于投影坐标系来说:y'=1.

很显然,y的映射为:y'=-y/(ztan(fovy/2)=-ycot(fovy/2)/z。 考虑z相同的情况,此时y‘是y的单调增函数(考虑y>0情况)。所以这个映射也符合视锥内部的点。

对于y的负边界来说,情况类似。对于x来说,只需额外除以aspect这个值即可,因为:

aspect = width/height = x宽度/y高度

所以:x' = -xcot(fovy/2)/z/aspect.

由于这是非线性变换,要解决除以z,可以借助齐次坐标w来进行。令w'=-z,于是:

x' = xcot(fovy/2)/aspect.

y' = ycot(fovy/2)

w'=-z

那么z呢?其实在屏幕上显示后,z值对于颜色没有任何贡献。但是,消隐少不了z值,也就是所谓的z缓冲。对于z变换来说,一定要保留两个点的z值大小顺序不变,另外,为了归一化处理,将其映射到[-1,1]之间。而{zh1}我们还可以在OpenGL中用glDepthRange调节深度值,这一点说明从这里算出来的Z值到最终的深度值之间还会有一个映射调整。好,话说远了,回来看看z的变换。

那么所需的Z变换有以下约束:

1)z=-znear--->z'=-1;

2)z=-zfar---->z'=+1;

3)映射具有单调减性质(因为有负号,所以单调减)

4)最终z'还要除以w'=-z

根据以上条件,构造映射:

f(z) = -z*(Zfar+Znear) / ( Zfar – Znear ) – 2* Zfar*Znear / ( Zfar – Znear )

z'= f(z)/w' = (Zfar+Znear) / ( Zfar – Znear ) + [2* Zfar*Znear / ( Zfar – Znear )]/z

验证一下,上述几个条件都满足。你要是想正向推导这个公式,也是可以的。可以参考后面的文献。

还是直接给出代码,一看就清楚:

static const double myPi = 3.14159265358979323846;

static void buildPerspectiveMatrix(double fieldOfView,
double aspectRatio,
double zNear, double zFar,
float m[16])
{
double sine, cotangent, deltaZ;
double radians = fieldOfView / 2.0 * myPi / 180.0;

deltaZ = zFar - zNear;
sine = sin(radians);
/* Should be non-zero to avoid division by zero. */
assert(deltaZ);
assert(sine);
assert(aspectRatio);
cotangent = cos(radians) / sine;

m[0*4+0] = cotangent / aspectRatio;
m[0*4+1] = 0.0;
m[0*4+2] = 0.0;
m[0*4+3] = 0.0;

m[1*4+0] = 0.0;
m[1*4+1] = cotangent;
m[1*4+2] = 0.0;
m[1*4+3] = 0.0;

m[2*4+0] = 0.0;
m[2*4+1] = 0.0;
m[2*4+2] = -(zFar + zNear) / deltaZ;
m[2*4+3] = -2 * zNear * zFar / deltaZ;

m[3*4+0] = 0.0;
m[3*4+1] = 0.0;
m[3*4+2] = -1;
m[3*4+3] = 0;
}

至此,几个矩阵都推导完成了。

题外话:其实网上有很介绍矩阵的推导过程,包括很多教材上也有。但要真正理解,只有一种方法:实践出真知,自己动手。

Reference:
1. ;
2.http://blog.csdn.net/popy007/archive/2007/09/23/1797121.aspx;
3.http://blog.csdn.net/popy007/archive/2009/04/19/4091967.aspx。
补充:


在投影空间中,z的范围是[-1,+1]。在OpenGL中,默认情况下,最接近眼睛的片断(在近截面上)被映射到0.0,离眼睛最远的片断(在远截面上)映射到1.0。这个映射调整应该是线性的(没有查到正式文献)。当然,还可用glDepthRange调节深度值范围。


在正交投影中距离和Z值的关系是线性的,但在xx投影中却不是的。在xx投影中,情况发生了变化,还是看看映射公式:


z'= f(z)/w' = (Zfar+Znear) / ( Zfar – Znear ) + [2* Zfar*Znear / ( Zfar – Znear )]/z
这个映射是个双曲线:


下面来理解()给出的这几句话:
1. 在xx投影中这种关系是非线性的,而且非线性的程度与Frustum函数中的far/near(或gluPerspective函数中的zFar/zNear)成比例——也不是严格成比例,因为有一个Zfar*Znear 在。
2. 这种非线性在靠近近截面时增加了Z值的精度,而且增加了深度缓存的效率;但是在视见体的其它部分则降低了精度,也就减少了深度缓存的xx性——在上图,将[-Zfar,-Znear]之间等分,呵呵,靠近-Znear得到的Z’的范围显然大得多。
3. 根据经验,far/near比值大于1000会有这种不好的效果。所以一般far/near比值应小于1000。要想解决这个问题,最简单的方法是通过将近截面远离眼睛来降低far/near比值,其{wy}的副作用是离眼睛很近的物体可能会被裁减掉,但在特定的应用程序中这很少是个问题,近截面的位置对X、Y坐标的投影没有影响,因此这对图像的影响很小——这个要牢记,呵呵。



郑重声明:资讯 【图形学矩阵变换(二)_shoping & programing_百度空间】由 发布,版权归原作者及其所在单位,其原创性以及文中陈述文字和内容未经(企业库qiyeku.com)证实,请读者仅作参考,并请自行核实相关内容。若本文有侵犯到您的版权, 请你提供相关证明及申请并与我们联系(qiyeku # qq.com)或【在线投诉】,我们审核后将会尽快处理。
—— 相关资讯 ——