想来编程也有一段时间,什么都很明白就是对于坐标变换不是很理解,总是在关键的时候迷乱不已,胡乱的写一些变换代码,得到的结果当然让自己云里雾里。仔细的看了一下好几本书关于3D变换的篇章,总结了一下,希望对大家有帮助。末了声明以下,可能我说得也有错误的地方,敬请局内人明鉴指正,我只是一个在校学生没有实际的工作经验。恳请大家提出宝贵的意见,打造一个Matrix Bible,让更多的初学者不要走弯路。谢谢大家。
矩阵变换是个相当重要的要点,难度应该仅次于数据结构部分。倒不是因为本身掌握知识对能力的要求有多么高,而是因为从来没有人说明白过在实际情况中如何应用。 在现代游戏的制作过程中,肯定是先由美工制作好要用到的模型,比如人物车辆地形等等,我们称之为基本模型。而诸如3dsmax maya等等建模工具产生的二进制文件是Application特有的格式,所以一般需要导出,各大论坛上无数人曾经提问过如何载入3ds模型。成熟的3D引擎都有自己的一套Util工具用来把模型导出为引擎特有的数据格式,比如Doom3引擎开源论坛上就提供3dsmax maya等使用的导出插件,用来输出为MD5格式的模型文件。其中会用到一种叫做Data Chunk的概念,不再多说。 为什么有这样的变化呢?我们用V(x,y,z,w)代表顶点,P(a,b,c,d)代表一个平面。相应的平面方程可以写作,PV = 0,也就是ax + by + cz = 0,有个向量垂直于顶点所在的那个面。 微软DirectX SDK Oct里面有个Shadow Mapping的Sample,代码中有一部分详细的说明了这个过程。它需要变换光源的向量,如果当我们把光源绑定到车上,就需要更改矩阵中w行的元素的。有兴趣的朋友可以看看。 既然是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的意义是,处在世界平面的x z平面的尺寸为4x4的一个平面(先画xy平面内的点,不过又旋转了90度)。geometry么,就是那个著名的nurbs茶壶,我们想象为在世界平面y向上的0.4f处。注意每次绘制前都会调用glPushMatrix把MV矩阵推入Stack,这个步骤相当重要,因为我们还不知道前面的坐标系,究竟在哪里,不过后面我们又看到了如何解决这个问题。 void render_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文件里面探寻一下吧。 void apply_transform()
{ translator.apply_transform(); trackball.apply_transform(); } void apply_inverse_transform() { trackball.apply_inverse_transform(); translator.apply_inverse_transform(); } 如果要调用apply_transform()进行坐标变换,那么是先位移,再旋转。如果要返回到最初的坐标系,那么就应该是先旋转回来,再位移回去。知道为什么么? 我们默认的位移其实应该是相对于World Coordinate,也就是说,我们意义上的向xyz方向移动几个单位其实是在那个最初的平面世界中的,而不是应该在摄像机空间中的位移 —— 因为最初世界坐标系里面的三个正交方向向量其实也已经旋转过了,也就是说,如果我们先旋转再位移,得到的轨迹相对于我们脑海中的世界坐标系是一条斜直线 —— 虽然说它对于摄像机坐标系来说是坐标轴直线。 如果用线性代数的性质也很好解释,本来正确的transform顺序(原因在上面)就是I*T*R,如果要回到I,就必须I*T*R*R-1*T-1 = I。OpenGL的matrix操作是右结合的。 这里的 view.apply_inverse_transform()就好理解了。不管我渲染什么,我总是要先把坐标系放回到世界坐标系中的原点处,保存好当前矩阵,然后再调用显示列表。不过我们又发现那个render_quad(),好,我们再把它揪出来。 void render_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); } 激活第一个纹理单元,自动生成纹理,调整纹理矩阵,准备好纹理,绘制桌面。这里绘制的是,以光源为视点的场景,应该是这个样子,全面的内容解析看注释。
void render_scene_from_light_view()
{ //放置灯光 glPushMatrix(); glLoadIdentity(); glLightfv(GL_LIGHT0, GL_POSITION, & vec4f( 0 , 0 , 0 , 1 )[ 0 ]); glPopMatrix(); //为什么这里光源是(0,0,0)呢?gl的光源坐标是在object coordinates中,也就是它要被I矩阵转换,结果依旧是EyeSpace中的(0,0,0) // spot image 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); } 再把这个函数贴出来,请自己仔细推敲变换过程。 void render_scene_from_camera_view()
{ // place light glPushMatrix(); glLoadIdentity(); camera.apply_inverse_transform(); spotlight.apply_transform(); glLightfv(GL_LIGHT0, GL_POSITION, & vec4f( 0 , 0 , 0 , 1 )[ 0 ]); glPopMatrix(); // spot image 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();把向量缩放为单位长度。 构造这个矩阵。你可以理解为一个定义在原点的旋转矩阵: matrix4f v( 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矩阵: matrix4f t(1,0,0,-4, 0,1,0,-4, 0,0,1,-4, 0,0,0,1 );//注意是负的,因为这是用center - eyepos得到的 有了这两个矩阵,一切就都好办了。我们就可以得到一个View Transform的完整矩阵: matrix4f ViewTransformMatrix = v.mult_right(t);注意是右乘,它的效果等同于: glMatrixMode(GL_MODELVIEW); glLoadIndentity(); glMultMatrixf(v);//这里只是比喻一下 glTranslatef(-4,-4,-4); 有了这个变换矩阵后,我们还需要它的逆矩阵。 matrix4f ViewTransformInverseMatrix = ViewTransformMatrix.inverse(); 接下来把数据放到2个数组中去。 for( i = 0;i<4;i++ )
for( j=0;j<4;j++){ ViewTransformMatrixArray[i*4+j] =ViewTransformMatrix.element(j,i); ViewTransformInverseMatrixArray[i*4+j] =ViewTransformInverseMatrix.element(j,i); } 注意,OpenGL的矩阵是Colunm - Major的顺序,所以载入数组的时候需要把i j位置替换下。 static void
display(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //gluLookAt(4,4,4,0,0,0,0,1,0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glMultMatrixf(ViewTransformMatrixArray); glMultMatrixf(ViewTransformInverseMatrixArray); glMultMatrixf(LightViewTransformMatrix);//我生成了2套矩阵,分别用于Eye和Camera /* glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(4,4,4,0,0,0,0,1,0); */ glPushMatrix(); glPointSize(4.0f); glBegin(GL_LINES); glColor3f(0,1.0,0); glVertex3f(0,0,0); glVertex3f(1,0,0); glVertex3f(0,0,0); glVertex3f(0,0,1); glVertex3f(0,0,0); glVertex3f(0,1,0); glEnd(); glPopMatrix(); glPushMatrix(); glTranslatef(0,ypos,0); glutSolidSphere(0.5,32,32); glPopMatrix(); glutSwapBuffers(); }
The params parameter contains four integer or floating-point values that specify the position of the light in homogeneous object coordinates. Both integer and floating-point values are mapped directly. Neither integer nor floating-point values are clamped.
The position is transformed by the modelview matrix when glLight is called (just as if it were a point), and it is stored in eye coordinates. If the w component of the position is 0.0, the light is treated as a directional source. Diffuse and specular lighting calculations take the lights direction, but not its actual position, into account, and attenuation is disabled. Otherwise, diffuse and specular lighting calculations are based on the actual location of the light in eye coordinates, and attenuation is enabled. The default position is (0,0,1,0); thus, the default light source is directional, parallel to, and in the direction of the –z axis. 意思是,我们指定的坐标是Object Space空间的坐标,然后被MV转换。W是作为齐次缩放系数使用的,0代表无限远好象太阳光束。 我们上面已经提到光源的位置在(-2,4,2)。这里我们写成无限远的(-2,4,2,0)。为了测试起见,我的显示函数写成了切换视点的模式。 static void
display(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); switch(InWhichSpace){ case 0: glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glMultMatrixf(ViewTransformMatrix); glLightfv(GL_LIGHT0, GL_POSITION, & vec4f(-2,4,2,0)[0]); break; case 1: glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glMultMatrixf(LightViewTransformMatrix); glLightfv(GL_LIGHT0, GL_POSITION, & vec4f(-2,4,2,0)[0]); break; } glPushMatrix(); glPointSize(12.0f); glScalef(4,4,4); glBegin(GL_LINES); glColor3f(1,1,1); glVertex3f(0,0,0); glVertex3f(-2,4,2); glColor3f(1,0,0); glVertex3f(0,0,0); glVertex3f(1,0,0); glColor3f(0,1,0); glVertex3f(0,0,0); glVertex3f(0,0,1); glColor3f(0,0,1); glVertex3f(0,0,0); glVertex3f(0,1,0); glEnd(); glPopMatrix(); glPushMatrix(); glTranslatef(0,zviewpos,0); glutSolidSphere(0.5,32,32); glPopMatrix(); glutSwapBuffers(); }
switch(InWhichSpace){
case 0: glutSetWindowTitle("From Camera View"); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glMultMatrixf(ViewTransformMatrix); glPushMatrix(); //glLoadIdentity(); //glMultMatrixf(ViewTransformMatrix); glMultMatrixf(LightViewTransformInverseMatrix); glLightfv(GL_LIGHT0, GL_POSITION, & vec4f(0,0,0,1)[0]); glPopMatrix();
总结: |
|