想来编程也有一段时间,什么都很明白就是对于坐标变换不是很理解,总是在关键的时候迷乱不已,胡乱的写一些变换代码,得到的结果当然让自己云里雾里。仔细的看了一下好几本书关于3D变换的篇章,总结了一下,希望对大家有帮助。末了声明以下,可能我说得也有错误的地方,敬请局内人明鉴指正,我只是一个在校学生没有实际的工作经验。恳请大家提出宝贵的意见,打造一个Matrix Bible,让更多的初学者不要走弯路。谢谢大家。
矩阵变换是个相当重要的要点,难度应该仅次于数据结构部分。倒不是因为本身掌握知识对能力的要求有多么高,而是因为从来没有人说明白过在实际情况中如何应用。
在现代游戏的制作过程中,肯定是先由美工制作好要用到的模型,比如人物车辆地形等等,我们称之为基本模型。而诸如3dsmax maya等等建模工具产生的二进制文件是Application特有的格式,所以一般需要导出,各大论坛上无数人曾经提问过如何载入3ds模型。成熟的3D引擎都有自己的一套Util工具用来把模型导出为引擎特有的数据格式,比如Doom3引擎开源论坛上就提供3dsmax maya等使用的导出插件,用来输出为MD5格式的模型文件。其中会用到一种叫做Data Chunk的概念,不再多说。
当美工制作模型的时候,肯定以建模工具提供的那个坐标系为基本坐标系进行建模。模型的顶点都是相对于各自基本的坐标系,我们称之为Local坐标系统或者Object坐标系。
美工把这些数据交给程序员。程序员需要在场景中安放这些模型,比如在地图上放置建筑车辆人物等等。可是程序员面对的这些模型的坐标,数字可能是一样的,因为都是相对于Local坐标系统,就这样一股脑的载入,肯定都是在“世界”的中心位置进行绘制,根本不可能分开。于是我们需要对各个物体,也就是各个独立的坐标系统进行Transform(包括Translate Rotate Scale操作)。
这里我们以GL为概念。当我们输入glutSolidCube(4)的时候,它会产生这样的数据:glVertex3f(2,2,2),glVertex3f(-2,-2,-2)等等,也就是长度为4的一个立方体。注意我们画的这个立方体的位置,肯定是出现在“世界”的中心位置。如果我们希望它移动到其他的位置呢?只能先glTranslatef(),再glutSolidCube。这个glTranslatef作用在MODELVIEW_MATRIX上,具体的形式请到OpenGL Wiki上看,那里连载了RedBook。
比如我们输入glTranslatef(1.0,0,0);glutSolidCube(4),其实它产生的“真正顶点”是,(3,2,2),(-1,-2,-2),统统向x方向移动了一个位置。如果你在自己的范例程序里看不到是因为perspective中的far near planes没有设置好。这样我们就仿佛实现了平移以及旋转等等操作,注意,是仿佛。
我们把变换顶点的矩阵一般称为MV(Model View)矩阵,把和在一起一步到位的矩阵称为MVP矩阵,在GLSL中就有gl_ModelViewMatirx和gl_ModelViewProjectionMatrix这两个Uniform Matirx。我们输入一个顶点,希望把它放到这个世界的正确位置上,就需要乘以适合它自己的MV,因为不一样的模型当然需要不一样的世界位置。矩阵乘法就可以完成这项神奇的工作。可是向量的概念则很大不同。
一个正方体,只要它在我们的映像中从头到尾都是方方正正的立在场景中,它的向量,无论朝上朝下都应该是相同的,比如(1,0,0)左边的面,只要我们不旋转这个正方体,它在Local坐标系还是变换后也应该是(1,0,0),这个时候我们用哪个矩阵呢?用MV显然不同,就需要用MV的Inverse Transpose。在线性代数中,求一个矩阵的Inverse然后Transpose,与先求Transpose再求Inverse,这两边是完全相等的。在GLSL中,其实gl_Normal*gl_NormalMatrix等同于gl_Normal*gl_ModelViewInverseTransposeMatrix。REDBOOK是这样说的:
In other words, normal vectors are transformed by the inverse transpose of the transformation that transforms points.
为什么有这样的变化呢?我们用V(x,y,z,w)代表顶点,P(a,b,c,d)代表一个平面。相应的平面方程可以写作,PV = 0,也就是ax + by + cz = 0,有个向量垂直于顶点所在的那个面。
这是个万用公式么?还早呢?如果我们要把这个模型的位置改变掉,我们就一定需要把顶点乘以ModelView矩阵,为了方便我就用M代表MV。这里写作:
PMV = ?
可是这个式子右边等于什么呢?我也不知道。为了这个式子依旧让右边等于0,符合基本的几何代数式,我们需要再给左边乘以M-1,就是M的逆矩阵。
P M-1 M V = 0
有一个向量垂直于这个平面。于是引入n一个我们真正意义上的面向量,和平面内的任意一个向量都应该是正交的。那个任意向量如何获得呢?最简单的就是,那个顶点和原点构成的向量 —— 因为在我们最初的式子里面,默认这个平面就是通过原点的。我们想让等式依旧成立,式子变成:
N T V = 0
注意上式的T。如果单纯的N矩阵乘以V,得到的结果还是一个向量而不是数字,所以需要一个Transpose变换。综合后,式子变换为:
N T M-1 M V = 0
(如果我没有理解错的话,V应该隐含着用了2次)
好的,我们开始用矩阵运算法则去分解上述的式子。MV不变,剩下的也就是(M-1)T N,也就是需要ModelView矩阵的Inverse Transpose矩阵乘以向量。
只要这些明白了,高级变换也就没有什么难得了。
Use Case
古老的bump mapping
在BumpMapping里面有个很重要的过程,就是把光源位置转换到以每个顶点处的向量为Z轴的空间中去,求向量的方法我不多说,为什么这样做也没有必要讲,最关键的就是可能很多人不明白为什么要乘以以N B T为元素的矩阵。
其实这里很多书籍要么没有解释,要么一笔带过。我来尝试的解释通透,可能有错误,希望大家指正。
GL的Matrix是Column-Major的形势。我们以N B T为元素的矩阵为例。
Nx Bx Tx
Ny By Ty
Nz Bz Tz
这里隐含的意思是:把顶点变换到以N B T为3个坐标轴的空间中,无论这个坐标系是不是和世界坐标系统“倾斜”的。
再次写成4x4的形式:
Nx Bx Tx 0
Ny By Ty 0
Nz Bz Tz 0
0 0 0 1
注意第四列的连续三个0。代表的意义是:每个顶点变换到以这个N B T为坐标轴的坐标系后,需要Translate到的位置。因为在Bump Mapping中我们不需要对顶点的位置变换,所以隐含着写成3x3的Matrix就足够了。
这里的这个3x3矩阵,其实就是对于每个顶点来说的MV矩阵,转换的就是那个LightPosition。可是这里又有一个问题,如果转换的不止一个LightPosition,还有Normal怎么办?因为这是个“斜”的坐标系,原来的(1,0,0)可不是变换后的(1,0,0)。记起来了么?Inverse Transpose!我们只要把原来的向量乘以这个以NBT为正交坐标轴向量的MV的Inverse Transpose,就可以得到正确的结果了。(我说得对么?)其实,因为这个矩阵3个向量都已经normalized,它的Inverse = Transpose,所以这个NBT矩阵的IT矩阵就是它自己!
微软DirectX SDK Oct里面有个Shadow Mapping的Sample,代码中有一部分详细的说明了这个过程。它需要变换光源的向量,如果当我们把光源绑定到车上,就需要更改矩阵中w行的元素的。有兴趣的朋友可以看看。
gl_LightPosition提供的光源参数是针对Eye Space,也就是所有的顶点已经经过MV变换的空间中的那个点。如果你有自己的光源安排一定要在空间中互相转换,头疼。GLSL用Uniform3f自己指定变换后空间中光源位置,比较方便,适合完成以场景为单位的光照计算。
不再新潮的Shadow Mapping
Shadow mapping,包括后来的Variance SM,PCF等,有个关键的步骤,就是把场景转换到以光源为摄像机的空间LightCamera中,获得深度。这里,场景中所有的顶点需要变换到以LightCamera的NBT为坐标轴的坐标系中,向量的正确变换则需要乘以NBT的Inverse Transpose矩阵。(我推测的,希望大家指正)。接下来的事情么,在Shader中爱做什么做什么。
那么如何传入所需要的矩阵呢?其实相当简单。功夫厉害的,直接把数组通过glUniform4fmatrix(),或者cgSetParameter传入。功夫弱一些的,老老实实的gl_MatrixModel(GL_MODELVIEW);glLoadIndentity();glMultMatrix();//乘以需要的矩阵到单位矩阵上,然后后再传入Shader。
有的时候我们需要自己独立求逆矩阵,如何办到呢?这可不是纸上的线性代数考试可以用初等变换计算。
矩阵的变换和逆变换就那么3种,Translate,Rotate,Scale。
我们知道MV = T * R。(T R代表为了实现Translate和Rotate相应的矩阵)
则Inv(MV) = Inv(T) * Inv(R)
也就是说,假设MV是
( R R R P)
( R R R P)
( R R R P)
( 0001)
则它所代表的 R T矩阵就是
( R R R 0)
(R R R 0)
( R R R 0)
( 0001)
和
( 100 P)
( 010 P)
( 001 P)
( 0001)
计算相应子矩阵的逆矩阵,Inv(T)就是
( 100 -P)
( 010 -P)
( 001 -P)
( 000 1)
逆旋转矩阵可能复杂一些,不过依旧可以计算出来,也就是它的Transpose。然后乘一下,逆矩阵就出来了。
目前我所想到的关键就这么多,更多的恳请大家添加,谢谢。
既然是SM的DEMO,在LightSpace和CameraSpace之间进行变换肯定是少不了的。当然,也应用到了多通道的思想。
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,这个步骤相当重要,因为我们还不知道前面的坐标系,究竟在哪里,不过后面我们又看到了如何解决这个问题。
{
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文件里面探寻一下吧。
{
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(),好,我们再把它揪出来。
{
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);
}
{
//放置灯光
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);
}
再把这个函数贴出来,请自己仔细推敲变换过程。
{
// 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( 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位置替换下。
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();
}
是不是觉得我多此一举?为什么要乘来乘去的,不就是回到单位矩阵么?事实上我曾经调试了很多次,通过比较输出gluLookAt(4,4,4,0,0,0,0,1,0)生成的矩阵和自己生成的矩阵是否相同,结果正确的变换到了LightView空间。
对光源位置的转换
这个问题讨论已久,仿佛久久没有标准,总是有初学者不断提问,而我们回答的也总是一个子集,治标不治本。
在上文中,我们已经生成了用于转换Object Space Coordinates的2个MV矩阵以及相应的逆矩阵。我们先从固定管线的Phone光照模型的GL入手,看看如何正确的转换光源。我们先看看gl manual怎么定义那个GL_POSITION的。
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)。为了测试起见,我的显示函数写成了切换视点的模式。
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开关。如果我切换到Camera,我将看到这样。
如果切换到光源视图,是这样的。
下面让我们来看看为什么,还有注释掉的矩阵乘法代码。
第一个case:我们用载入ViewTransformMatrix,下面声明LightPosition,是(-2,4,2,0),这个坐标是Object Space的坐标,在我们的想象中,就是相对于世界坐标系的位置,也就是每次我绘制一个Sphere所产生的位置。
第二个case:载入LightViewTransformMatrix,依旧传入(-2,4,2,0),得到的结果依旧正确。
最好自己向自己复述一遍,注意一定要联系我们上面计算矩阵的算式。
然后我们把case0代码改一下。
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();
我们要好好剖析第二个PushMatrix,LoadIndentity后的那两个连续的矩阵乘法,还有为什么光源成了(0,0,0,1)。NVIDIA的那个render_scene_from_camera也是这样放置光源的。让我们看看为什么。
这个V(0,0,0,1)是Object Space中的点。我们先用Mvt代表ViewTransformMatrix,再用Mlvti代表LightViewTransformInverseMatrix。写成完整的算式应该是
Mvt(Mlvti * V)
想起来了么?矩阵乘法的结合形式,意思是,“ vertex V under transformed by Matrix Mlvt”。这里产生光源的过程如下:
Object Space中的(0,0,0,1)被Mlvti转换到Object空间,是多少呢?(-2,4,2,1),就是光源的相对于世界的位置。其实你也可以通过vec4f new = LightViewTransformInverseMatrix.mult_matrix_vec(vec4f(0,0,0,1))自己验证。
由于转换到LightView空间后,产生的是,世界空间和模型空间中的(-2,4,2,1) —— GL没有世界坐标,而且我们一般认为Object Space是和世界空间重合的。即使在D3D中,一般情况下初始化世界矩阵也都是载入单位矩阵。
(-2,4,2,1)再乘以Mlvt,又被转换到了 —— 其实我不知道它在哪里!相对于转换后的CAMERA坐标系,它的位置我可以手动求出来,得到的是光栅化坐标。但是它的位置的确是正确的,效果等同于直接在glLightv中传入(-2,4,2,1)
总结:
对于一个成熟的3D引擎来说,矩阵都是自己计算出来的,绝非调用API自己的指令。在NVIDIA SDK的DEMO中包含了大量成熟的基础代码,在不侵犯原作者权益的情况下应该合理的采用,省下诸多开发调试时间。我引用的HEADER文件和代码。
这里下载