总是在关键的时候迷乱不已胡乱的写一些变换代码得到.docx
《总是在关键的时候迷乱不已胡乱的写一些变换代码得到.docx》由会员分享,可在线阅读,更多相关《总是在关键的时候迷乱不已胡乱的写一些变换代码得到.docx(19页珍藏版)》请在冰豆网上搜索。
总是在关键的时候迷乱不已胡乱的写一些变换代码得到
想来编程也有一段时间,什么都很明白就是对于坐标变换不是很理解,总是在关键的时候迷乱不已,胡乱的写一些变换代码,得到的结果当然让自己云里雾里。
仔细的看了一下好几本书关于3D变换的篇章,总结了一下,希望对大家有帮助。
末了声明以下,可能我说得也有错误的地方,敬请局内人明鉴指正,我只是一个在校学生没有实际的工作经验。
恳请大家提出宝贵的意见,打造一个MatrixBible,让更多的初学者不要走弯路。
谢谢大家。
矩阵变换是个相当重要的要点,难度应该仅次于数据结构部分。
倒不是因为本身掌握知识对能力的要求有多么高,而是因为从来没有人说明白过在实际情况中如何应用。
在现代游戏的制作过程中,肯定是先由美工制作好要用到的模型,比如人物车辆地形等等,我们称之为基本模型。
而诸如3dsmaxmaya等等建模工具产生的二进制文件是Application特有的格式,所以一般需要导出,各大论坛上无数人曾经提问过如何载入3ds模型。
成熟的3D引擎都有自己的一套Util工具用来把模型导出为引擎特有的数据格式,比如Doom3引擎开源论坛上就提供3dsmaxmaya等使用的导出插件,用来输出为MD5格式的模型文件。
其中会用到一种叫做DataChunk的概念,不再多说。
当美工制作模型的时候,肯定以建模工具提供的那个坐标系为基本坐标系进行建模。
模型的顶点都是相对于各自基本的坐标系,我们称之为Local坐标系统或者Object坐标系。
美工把这些数据交给程序员。
程序员需要在场景中安放这些模型,比如在地图上放置建筑车辆人物等等。
可是程序员面对的这些模型的坐标,数字可能是一样的,因为都是相对于Local坐标系统,就这样一股脑的载入,肯定都是在“世界”的中心位置进行绘制,根本不可能分开。
于是我们需要对各个物体,也就是各个独立的坐标系统进行Transform(包括TranslateRotateScale操作)。
这里我们以GL为概念。
当我们输入glutSolidCube(4)的时候,它会产生这样的数据:
glVertex3f(2,2,2),glVertex3f(-2,-2,-2)等等,也就是长度为4的一个立方体。
注意我们画的这个立方体的位置,肯定是出现在“世界”的中心位置。
如果我们希望它移动到其他的位置呢?
只能先glTranslatef(),再glutSolidCube。
这个glTranslatef作用在MODELVIEW_MATRIX上,具体的形式请到OpenGLWiki上看,那里连载了RedBook。
比如我们输入glTranslatef(1.0,0,0);glutSolidCube(4),其实它产生的“真正顶点”是,(3,2,2),(-1,-2,-2),统统向x方向移动了一个位置。
如果你在自己的范例程序里看不到是因为perspective中的farnearplanes没有设置好。
这样我们就仿佛实现了平移以及旋转等等操作,注意,是仿佛。
我们把变换顶点的矩阵一般称为MV(ModelView)矩阵,把和在一起一步到位的矩阵称为MVP矩阵,在GLSL中就有gl_ModelViewMatirx和gl_ModelViewProjectionMatrix这两个UniformMatirx。
我们输入一个顶点,希望把它放到这个世界的正确位置上,就需要乘以适合它自己的MV,因为不一样的模型当然需要不一样的世界位置。
矩阵乘法就可以完成这项神奇的工作。
可是向量的概念则很大不同。
一个正方体,只要它在我们的映像中从头到尾都是方方正正的立在场景中,它的向量,无论朝上朝下都应该是相同的,比如(1,0,0)左边的面,只要我们不旋转这个正方体,它在Local坐标系还是变换后也应该是(1,0,0),这个时候我们用哪个矩阵呢?
用MV显然不同,就需要用MV的InverseTranspose。
在线性代数中,求一个矩阵的Inverse然后Transpose,与先求Transpose再求Inverse,这两边是完全相等的。
在GLSL中,其实gl_Normal*gl_NormalMatrix等同于gl_Normal*gl_ModelViewInverseTransposeMatrix。
REDBOOK是这样说的:
Inotherwords,normalvectorsaretransformedbytheinversetransposeofthetransformationthattransformspoints.
为什么有这样的变化呢?
我们用V(x,y,z,w)代表顶点,P(a,b,c,d)代表一个平面。
相应的平面方程可以写作,PV=0,也就是ax+by+cz=0,有个向量垂直于顶点所在的那个面。
这是个万用公式么?
还早呢?
如果我们要把这个模型的位置改变掉,我们就一定需要把顶点乘以ModelView矩阵,为了方便我就用M代表MV。
这里写作:
PMV=?
可是这个式子右边等于什么呢?
我也不知道。
为了这个式子依旧让右边等于0,符合基本的几何代数式,我们需要再给左边乘以M-1,就是M的逆矩阵。
PM-1MV=0
有一个向量垂直于这个平面。
于是引入n一个我们真正意义上的面向量,和平面内的任意一个向量都应该是正交的。
那个任意向量如何获得呢?
最简单的就是,那个顶点和原点构成的向量——因为在我们最初的式子里面,默认这个平面就是通过原点的。
我们想让等式依旧成立,式子变成:
NTV=0
注意上式的T。
如果单纯的N矩阵乘以V,得到的结果还是一个向量而不是数字,所以需要一个Transpose变换。
综合后,式子变换为:
NTM-1MV=0(如果我没有理解错的话,V应该隐含着用了2次)
好的,我们开始用矩阵运算法则去分解上述的式子。
MV不变,剩下的也就是(M-1)TN,也就是需要ModelView矩阵的InverseTranspose矩阵乘以向量。
只要这些明白了,高级变换也就没有什么难得了。
UseCase
古老的bumpmapping
在BumpMapping里面有个很重要的过程,就是把光源位置转换到以每个顶点处的向量为Z轴的空间中去,求向量的方法我不多说,为什么这样做也没有必要讲,最关键的就是可能很多人不明白为什么要乘以以NBT为元素的矩阵。
其实这里很多书籍要么没有解释,要么一笔带过。
我来尝试的解释通透,可能有错误,希望大家指正。
GL的Matrix是Column-Major的形势。
我们以NBT为元素的矩阵为例。
NxBxTx
NyByTy
NzBzTz
这里隐含的意思是:
把顶点变换到以NBT为3个坐标轴的空间中,无论这个坐标系是不是和世界坐标系统“倾斜”的。
再次写成4x4的形式:
NxBxTx0
NyByTy0
NzBzTz0
0001
注意第四列的连续三个0。
代表的意义是:
每个顶点变换到以这个NBT为坐标轴的坐标系后,需要Translate到的位置。
因为在BumpMapping中我们不需要对顶点的位置变换,所以隐含着写成3x3的Matrix就足够了。
这里的这个3x3矩阵,其实就是对于每个顶点来说的MV矩阵,转换的就是那个LightPosition。
可是这里又有一个问题,如果转换的不止一个LightPosition,还有Normal怎么办?
因为这是个“斜”的坐标系,原来的(1,0,0)可不是变换后的(1,0,0)。
记起来了么?
InverseTranspose!
我们只要把原来的向量乘以这个以NBT为正交坐标轴向量的MV的InverseTranspose,就可以得到正确的结果了。
(我说得对么?
)其实,因为这个矩阵3个向量都已经normalized,它的Inverse=Transpose,所以这个NBT矩阵的IT矩阵就是它自己!
微软DirectXSDKOct里面有个ShadowMapping的Sample,代码中有一部分详细的说明了这个过程。
它需要变换光源的向量,如果当我们把光源绑定到车上,就需要更改矩阵中w行的元素的。
有兴趣的朋友可以看看。
gl_LightPosition提供的光源参数是针对EyeSpace,也就是所有的顶点已经经过MV变换的空间中的那个点。
如果你有自己的光源安排一定要在空间中互相转换,头疼。
GLSL用Uniform3f自己指定变换后空间中光源位置,比较方便,适合完成以场景为单位的光照计算。
不再新潮的ShadowMapping
Shadowmapping,包括后来的VarianceSM,PCF等,有个关键的步骤,就是把场景转换到以光源为摄像机的空间LightCamera中,获得深度。
这里,场景中所有的顶点需要变换到以LightCamera的NBT为坐标轴的坐标系中,向量的正确变换则需要乘以NBT的InverseTranspose矩阵。
(我推测的,希望大家指正)。
接下来的事情么,在Shader中爱做什么做什么。
那么如何传入所需要的矩阵呢?
其实相当简单。
功夫厉害的,直接把数组通过glUniform4fmatrix(),或者cgSetParameter传入。
功夫弱一些的,老老实实的gl_MatrixModel(GL_MODELVIEW);glLoadIndentity();glMultMatrix();//乘以需要的矩阵到单位矩阵上,然后后再传入Shader。
有的时候我们需要自己独立求逆矩阵,如何办到呢?
这可不是纸上的线性代数考试可以用初等变换计算。
矩阵的变换和逆变换就那么3种,Translate,Rotate,Scale。
我们知道MV=T*R。
(TR代表为了实现Translate和Rotate相应的矩阵)
则Inv(MV)=Inv(T)*Inv(R)
也就是说,假设MV是
(RRRP)
(RRRP)
(RRRP)
(0001)
则它所代表的RT矩阵就是
(RRR0)
(RRR0)
(RRR0)
(0001)
和
(100P)
(010P)
(001P)
(0001)
计算相应子矩阵的逆矩阵,Inv(T)就是
(100-P)
(010-P)
(001-P)
(0001)
逆旋转矩阵可能复杂一些,不过依旧可以计算出来,也就是它的Transpose。
然后乘一下,逆矩阵就出来了。
目前我所想到的关键就这么多,更多的恳请大家添加,谢谢。
解析NVIDIA中的HWShadowMappingDemo
既然是SM的DEMO,在LightSpace和CameraSpace之间进行变换肯定是少不了的。
当然,也应用到了多通道的思想。
quad.new_list(GL_COMPILE);
glPushMatrix();
glRotatef(-90,1,0,0);
glScalef(4,4,4);
glBegin(GL_QUADS);
glNormal3f(0,0,1);
glVertex2f(-1,-1);
glVertex2f(-1,1);
glVertex2f(1,1);
glVertex2f(1,-1);
glEnd();
glPopMatrix();
quad.end_list();
wirecube.new_list(GL_COMPILE);
glutWireCube
(2);
wirecube.end_list();
geometry.new_list(GL_COMPILE);
glPushMatrix();
glTranslatef(0,.4f,0);
glutSolidTeapot(.5f);
glPopMatrix();
geometry.end_list();
首选我们新建了3个显示列表,可以看出,quad的意义是,处在世界平面的xz平面的尺寸为4x4的一个平面(先画xy平面内的点,不过又旋转了90度)。
geometry么,就是那个著名的nurbs茶壶,我们想象为在世界平面y向上的0.4f处。
注意每次绘制前都会调用glPushMatrix把MV矩阵推入Stack,这个步骤相当重要,因为我们还不知道前面的坐标系,究竟在哪里,不过后面我们又看到了如何解决这个问题。
voidrender_scene(glut_simple_mouse_interactor&view)
{
glColor3f(1,1,1);
glPushMatrix();
view.apply_inverse_transform();
glPushMatrix();
object.apply_transform();
render_quad();
glEnable(GL_LIGHTING);
geometry.call_list();
glDisable(GL_LIGHTING);
glPopMatrix();
glPopMatrix();
}
通篇代码阅读完毕,发现这个函数最重要。
参数view,我的理解是,它是View变换矩阵,也就是储存了3个正交单位向量,有可能包括眼睛的位置(注意是有可能),无论这个眼睛是摄像机,还是光源。
不过这个view.apply_inverse_transform(),它究竟代表了哪些操作呢?
让我们在nvidia自己写的glh文件里面探寻一下吧。
voidapply_transform()
{
translator.apply_transform();
trackball.apply_transform();
}
voidapply_inverse_transform()
{
trackball.apply_inverse_transform();
translator.apply_inverse_transform();
}
如果要调用apply_transform()进行坐标变换,那么是先位移,再旋转。
如果要返回到最初的坐标系,那么就应该是先旋转回来,再位移回去。
知道为什么么?
我们默认的位移其实应该是相对于WorldCoordinate,也就是说,我们意义上的向xyz方向移动几个单位其实是在那个最初的平面世界中的,而不是应该在摄像机空间中的位移——因为最初世界坐标系里面的三个正交方向向量其实也已经旋转过了,也就是说,如果我们先旋转再位移,得到的轨迹相对于我们脑海中的世界坐标系是一条斜直线——虽然说它对于摄像机坐标系来说是坐标轴直线。
如果用线性代数的性质也很好解释,本来正确的transform顺序(原因在上面)就是I*T*R,如果要回到I,就必须I*T*R*R-1*T-1=I。
OpenGL的matrix操作是右结合的。
这里的view.apply_inverse_transform()就好理解了。
不管我渲染什么,我总是要先把坐标系放回到世界坐标系中的原点处,保存好当前矩阵,然后再调用显示列表。
不过我们又发现那个render_quad(),好,我们再把它揪出来。
voidrender_quad()
{
glActiveTextureARB(GL_TEXTURE0_ARB);
obj_linear_texgen();
texgen(true);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glScalef(4,4,1);
glMatrixMode(GL_MODELVIEW);
glDisable(GL_LIGHTING);
decal.bind();
decal.enable();
quad.call_list();
decal.disable();
glEnable(GL_LIGHTING);
texgen(false);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
}
激活第一个纹理单元,自动生成纹理,调整纹理矩阵,准备好纹理,绘制桌面。
这里绘制的是,以光源为视点的场景,应该是这个样子,全面的内容解析看注释。
voidrender_scene_from_light_view()
{
//放置灯光
glPushMatrix();
glLoadIdentity();
glLightfv(GL_LIGHT0,GL_POSITION,&vec4f(0,0,0,1)[0]);
glPopMatrix();
//为什么这里光源是(0,0,0)呢?
gl的光源坐标是在objectcoordinates中,也就是它要被I矩阵转换,结果依旧是EyeSpace中的(0,0,0)
//spotimage
glActiveTextureARB(GL_TEXTURE1_ARB);
glPushMatrix();
eye_linear_texgen();
texgen(true);
glPopMatrix();
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glTranslatef(.5f,.5f,.5f);
glScalef(.5f,.5f,.5f);
gluPerspective(lightshaper.fovy,1,lightshaper.zNear,lightshaper.zFar);
//这里生成的是一个生成纹理坐标的矩阵,它的形式是I*T*S*P,提供给处于以光源为原点的场景坐标使用。
glMatrixMode(GL_MODELVIEW);
light_image.bind();
light_image.enable();
glTexEnvi(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE);
glActiveTextureARB(GL_TEXTURE0_ARB);
lightshaper.apply();
if(display_funcs[current_display_func]==render_scene_from_light_view)
largest_square_power_of_two_viewport();
render_scene(spotlight);//让思路回到上面的那个函数,仔细体会
glActiveTextureARB(GL_TEXTURE1_ARB);
light_image.disable();
glActiveTextureARB(GL_TEXTURE0_ARB);
}
voidrender_scene_from_camera_view()
{
//placelight
glPushMatrix();
glLoadIdentity();
camera.apply_inverse_transform();
spotlight.apply_transform();
glLightfv(GL_LIGHT0,GL_POSITION,&vec4f(0,0,0,1)[0]);
glPopMatrix();
//spotimage
glActiveTextureARB(GL_TEXTURE1_ARB);
glPushMatrix();
camera.apply_inverse_transform();
eye_linear_texgen();
texgen(true);
glPopMatrix();
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glTranslatef(.5f,.5f,.5f);
glScalef(.5f,.5f,.5f);
gluPerspective(lightshaper.fovy,1,lightshaper.zNear,lightshaper.zFar);
spotlight.apply_inverse_transform();
glMatrixMode(GL_MODELVIEW);
light_image.bind();
light_image.enable();
glTexEnvi(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_MODULATE);
glActiveTextureARB(GL_TEXTURE0_ARB);
reshaper.apply();
render_scene(camera);
glActiveTextureARB(GL_TEXTURE1_ARB);
light_image.disable();
glActiveTextureARB(GL_TEXTURE0_ARB);
render_light_frustum();
}
看哪,天梯!
说了这么多的东西,贴了这么多代码,我们究竟应该把握住哪些东西呢?
1、计算出自己需要的View变换矩阵,从此告别gluLookAt或者D3DXMatrixLookAtLH
首先选择Eye所在世界中的位置,比如说在(4,4,4)处。
选择目光所看的点,比如原点O(0,0,0),或者一个方向向量D(-4,-4,-4)。
选择一个世界坐标系中Up向量,在GL中就是UpTmp(0,1,0)。
得到一个新向量C=cross(D,UpTmp)。
注意是D叉乘UpTmp。
仍掉那个UpTmp。
U(Up)=cross(C,D)。
完成了大半工作了!
让我们继续。
D.normalize();C.normalize();D.normalize();把向量缩放为单位长度。
构造这个矩阵。
你可以理解为一个定义在原点的旋转矩阵:
matrix4fv(c[0],c[1],c[2],0,
u[0],u[1],u[2],0,
-d[0],-d[1],-d[2],0,
0,0,0,1
);
再次引用Eye的位置(4,4,4),构造一个translate矩阵:
matrix4ft(1,0,0,-4,
0,1,0,-4,
0,0,1,-4,
0,0,0,1
);//注意是负的,因为这是用center-eyepos得到的
有了这两个矩阵,一切就都好办了。
我们就可以得到一个ViewTransform的完整矩阵:
matrix4fViewTransformMatrix=v.mult_right(t);注意是右乘,它的效果等同于:
glMatrixMode(GL_MODELVIEW);
glLoadIndentity();
glMultMatrixf(v);//这里只是比喻一下
glTranslatef(-4,-4,-4);
有了这个变换矩阵后,我们还需要它的逆矩阵。
matrix4fViewTransformInverseMatrix=ViewTransformMatrix.inverse()
接下来把数据放到2