OpenGL编程(八)3D数学与坐标变换
笛卡尔坐标
一维坐标系
以一个点为原点,选定一个方向为正方向(相反的方向为反方向),以一定的距离为标尺建立一维坐标系。一维坐标系一般应用于描述在一维空间中的距离。
举个例子:一维坐标系好比一条拉直的电线(忽略长度),一只老鼠在电线上,对于这只老鼠来说,这个一维坐标系(电线)就是它的世界,只能沿着电线的方向向前或向后运动(当然也可以不动),这只老鼠这个时候是活在“一维”世界里。二维坐标系
百度百科:二维,两根的坐标轴,有平面直角坐标丶自然坐标丶极坐标等,研究平面运动时用。二维坐标系可以解释为在平面内两条相互垂直且有公共原点的数轴组成的坐标系即二维坐标系。
可以理解为:把两个互相垂直,原点重合的一维坐标系组成的新的坐标系为二维坐标系。
注:二维坐标系有多重情况,这只是二维坐标系中的其中。(以正负方向不同区别)
3. 三维坐标系
百度百科:三维笛卡儿坐标系是在二维笛卡儿坐标系的基础上根据右手定则增加第三维坐标(即Z轴)而形成的。
二维坐标的两个轴分别是x轴和y轴,三维坐标系是在二维坐标系上增加一个同时垂直x、y轴的z轴。三维坐标系分左手坐标系和右手坐标系两种,如下图。
三维坐标系
世界坐标系
百度百科:世界坐标系是系统的绝对坐标系,在没有建立用户坐标系之前画面上所有点的坐标都是以该坐标系的原点来确定各自的位置的。
好比在一个游戏场景中,游戏地图就相当于世界,给地图添加笛卡尔三维坐标系后能够通过坐标点来描述地图上的任何一个点的(相对原点)位置。确定坐标系(左手或右手坐标系)和原定后,能通过坐标来描述在地图上的每个点的位置(坐标)
物体坐标系
地图上的每个物体都有一个独立的坐标系,叫做物体坐标系。也许有同学会有英文,有了世界坐标系为啥还需要物体坐标系呢?物体坐标系有什么用途?小时候我们上体育课的时候,体育老师经常让我们左转、右转、后转等。这个时候我们的世界坐标(相对地球)没变,可是我们每次面对不同的方向都不一样,这个就是我们的物体坐标变了。世界坐标系能确定物体的坐标,有了物体坐标系我们还能确定物体的方向。
摄像机坐标系
摄像机坐标系是和观察者有密切关系的坐标系。通过摄像机坐标系,我们能确定在屏幕上显示的内容。
要把3D场景的事物显示到一个2D的屏幕上,需要一个摄像机,可是某个时刻,我们只能显示一个画面的事物。摄像机的位置,方向不同,所拍取的景也就不一样。
惯性坐标系
引入惯性坐标系的概念是为了辅助物体坐标系到世界坐标系的转换。物体坐标系和世界坐标系一样也是一个三维坐标系,而且有左手坐标系、右手坐标系之分。
当物体的方向不是正方向的时候,我们可以通过旋转把物体调为正方向。如果物体的坐标系原点不是在世界坐标系的原点,我们可以通过移动物体的位置来把物体移到原点。
惯性坐标系就是表示当物体的方向是正方向但是坐标不是在世界坐标系原点时成为惯性坐标系。
简单的线性变换
- 平移
通过改变物体的世界坐标系的x、y或者z值,使物体在世界坐标系上的位置发生变化成为平移。如下图:
其实在OpenGL上,是先确定物体的位置再在改位置绘制物体。
如上图,在绘制立方体之前先把它的y轴向上移动10个单位,可以执行以下代码:
glTraslatef(0.0f, 10.0f, 0.0f);
然后再绘制立方体:
glutWireCube(10.0f);
=====================
2. 旋转
绕着某个旋转轴对物体选择0-360度,使物体的物体坐标系旋转。旋转函数:
glRotatef(angle, x, y, z);
与平移一样,在OpenGL中,也是先通过旋转再绘图。如上图的代码为:
glRotatef(90.0f, 1.0f, 1.0f, 1.0);
glutWireCube(10.0f);
=====================
3. 缩放
缩放变换根据指定的因子,将所有顶点在三根轴的方向上都展宽,由此来增加对象的大小。缩放函数:
glScalef(GLfloat x, GLfloat y, GLfloat z);
实现代码:
glScalef(2.0f, 1.0f, 2.0f);
glutWireCube(10.0f);
=====================
4. 矩阵堆栈
在我们使用前面的平移、旋转、伸缩函数时,调用这些函数后,如果没有复原的话,这些函数的效果是会累积的。例如要实现以下效果:
绘制两个球体,一个的坐标是x轴移动10个像素,另外一个球体的坐标是y轴移动10个像素。正常情况下,认为可以通过以下代码实现:
//y轴移动10个单位
glTranslatef(0.0f, 10.0f, 0.0f);
//绘制第一个球体
glutSolidSphere(1.0f);
//x轴移动10个单位
glTranslatef(10.0f, 0.f, 0.0f);
//绘制第二个球体
glutSolidSphere(1.0f);
如果使用以上的代码,实现的效果如下:
因为我们在绘制第一个球体后,原来锚点的坐标y轴移动了10个单位,我们没有实现复原操作。绘制第二个球体前,移动x轴坐标10个单位,两个操作叠加的效果是,x轴正方向移动10个单位,y轴正方向移动10个单位。那应该怎样才能实现我们想要的效果呢?
绘制完第一个球体后,我们应该调用函数实现复位坐标轴。
glLoadIdentity()
实现代码如下:
//y轴移动10个单位
glTranslatef(0.0f, 10.0f, 0.0f);
//绘制第一个球体
glutSolidSphere(1.0f);
glLoadIdentity()
//x轴移动10个单位
glTranslatef(10.0f, 0.f, 0.0f);
//绘制第二个球体
glutSolidSphere(1.0f);
某些情况下需要用到线性变换的叠加,可是有的时候不需要。为了管理好线性变换的,于是引入了矩阵堆栈的概念。
如上图,当我们要执行某个变换前,可以先执行glPushMatrix操作,实现入栈,执行完后,如果需要保留这个线性变换(需要保留这个累积线性变换)即不需要执行其他操作;否则(不需要保留这个累积效果)执行glPopMatrix操作把该线性变换抛出变换矩阵。
附:3D数学与坐标变换这块涉及到的数学知识点比较多(向量、各种变换数学原理及变换矩阵)。感兴趣的可以学习《3D数学基础图形与游戏开发》这本书,书上讲得很详细。
示例
这次的实验代码也是在前面的实验代码框架基础上添加新的内容。
1. 绘制太阳
在坐标系中心(原点)绘制一个红色的太阳。首先设置为红色,绘制一个半径为10的球体。代码如下:
glColor3ub(255, 0, 0);
glutSolidSphere(10.0f, 15, 15);
2. 绘制火星
火星在相对太阳偏移90个单位的距离:
glRotatef(saturnRotAngle, 0.0f, 1.0f, 1.0f);
设置火星的颜色:
glColor3ub(255, 200, 100);
绘制火星,假设火星的半径为5:
glutSolidSphere(5.0f, 15, 15);
由于后面我们还需要绘制地球,而地球的位置时相对于太阳的位置,所以不需要上面的位置偏移积累。所以绘制完火星后要返回上一步的位置,分别在绘制火星的代码前面与后面添加函数glPushMatrix()与glPopMatrix()。完整代码如下:
glColor3ub(255, 200, 100);
glPushMatrix();
glTranslatef(90.0f, 0.0f, 0.0f);
glutSolidSphere(5.0f, 15, 15);
glPopMatrix();
3. 绘制地球
以相同于绘制火星的原理绘制地球,代码如下:
glColor3ub(0, 0, 200);
glPushMatrix();
glTranslatef(60.0f, 0.0f, 0.0f);
glutSolidSphere(7.0f, 15, 15);
glPopMatrix();
4. 绘制月亮
由于月球是地球的卫星,绕着地球转,所以月球的位置时相对于地球的位置。所以绘制月球跟绘制地球的代码要被glPushMatrix()与glPopMatrix()函数包着:
//地球
glColor3ub(0, 0, 200);
glPushMatrix();
glTranslatef(60.0f, 0.0f, 0.0f);
glutSolidSphere(7.0f, 15, 15);
//月亮
glColor3ub(88, 88, 88);
glTranslatef(20.0f, 0.0f, 0.0f);
glutSolidSphere(2.0f, 15, 15);
glPopMatrix();
5. 让天体动起来
每次重新绘制时要给火星、地球一个旋转的角度,每次以相同方向一定的角速度旋转。同样的原理去实现月球绕地球转。
static float earthRotAngle = 0.0f;
static float saturnRotAngle = 0.0f;
static float moonRotAngle = 0.0f;
当旋转角度累加到大于360度时,控制角度0
moonRotAngle += 15.0f;
if(moonRotAngle > 360.0f)
moonRotAngle -= 360.0f;
glutSolidSphere(2.0f, 15, 15);
glPopMatrix();
earthRotAngle += 10.0f;
if(earthRotAngle > 360.0f)
earthRotAngle -= 360.0f;
saturnRotAngle += 6.0f;
if(saturnRotAngle > 360.0f)
saturnRotAngle -= 360.0f;
注册一个回调函数,定时刷新屏幕:
void TimerFunc(int value)
{
glutPostRedisplay();
glutTimerFunc(100, TimerFunc, 1);
}
总结
这次实验涉及到的主要知识点有平移、旋转等线性变换以及矩阵堆栈的工作原理。三维世界里的平移、旋转、缩放涉及到的数学很多,每个线性操作都有对于的矩阵去处理(本科学的线性代数就很有帮助咯),OpenGL都封装好了底层的数学原理,提供了调用接口给我们直接调用。如果有兴趣,可以去了解一下,对掌握底层知识很有帮助。推荐这本书《3D数学基础图形与游戏开发》。
完整代码
#include <windows.h>
#include <gl/glut.h>
#include<math.h>
const GLfloat PI = 3.1415f;
GLfloat xRot = 0.0f;
GLfloat yRot = 0.0f;
void rendererScene(void);
void changeWindowSize(GLsizei w, GLsizei h);
void setupRC(void);
void rotateMode(int key, int x, int y);
//定时刷新显示
void TimerFunc(int value)
{
glutPostRedisplay();//刷新
glutTimerFunc(100, TimerFunc, 1);//定时器
}
int main(int argc, char* argv[])
{
glutInit(&argc, argv);
//设置显示模式
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
//设置窗口大小
glutInitWindowSize(300, 300);
//设置窗口在屏幕上的位置
glutInitWindowPosition(200, 200);
//创建窗口标题
glutCreateWindow("三角形绘3D模型");
glOrtho(-100.0f, 100.0f, -100.0f, 100.0f, -100.0f, 100.0f);
//注册窗口大小改变时回调函数
glutReshapeFunc(changeWindowSize);
//注册点击上下左右方向按钮时回调rotateMode函数
glutSpecialFunc(rotateMode);
//注册显示窗口时回调渲染函数
glutDisplayFunc(rendererScene);
glutTimerFunc(500, TimerFunc, 1);
setupRC();
//消息循环(处理操作系统等的消息,例如键盘、鼠标事件等)
glutMainLoop();
return 0;
}
/**
渲染函数
*/
void rendererScene(void)
{
static float earthRotAngle = 0.0f;
static float saturnRotAngle = 0.0f;
static float moonRotAngle = 0.0f;
BOOL bCull = TRUE; //是否开启回溯剔除
BOOL bDepth = TRUE; //是否开启深度检测
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if(bCull) glEnable(GL_CULL_FACE);
else glDisable(GL_CULL_FACE);
if(bDepth) glEnable(GL_DEPTH_TEST);
else glDisable(GL_DEPTH_TEST);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
//glTranslatef(0.0f, 0.0f, -100.0f);
//太阳
glColor3ub(255, 0, 0);
glutSolidSphere(10.0f, 15, 15);
//火星
glColor3ub(255, 200, 100);
glPushMatrix();
glRotatef(saturnRotAngle, 0.0f, 1.0f, 1.0f);
glTranslatef(90.0f, 0.0f, 0.0f);
glutSolidSphere(5.0f, 15, 15);
glPopMatrix();
//地球
glColor3ub(0, 0, 200);
glPushMatrix();
glRotatef(earthRotAngle,0.0f, 1.0f, 1.0f);
glTranslatef(50.0f, 0.0f, 0.0f);
glutSolidSphere(7.0f, 15, 15);
//月亮
glColor3ub(88, 88, 88);
glRotatef(moonRotAngle, 0.0f, 1.0f, 1.0f);
glTranslatef(20.0f, 0.0f, 0.0f);
moonRotAngle += 15.0f;
if(moonRotAngle > 360.0f)
moonRotAngle -= 360.0f;
glutSolidSphere(2.0f, 15, 15);
glPopMatrix();
earthRotAngle += 10.0f;
if(earthRotAngle > 360.0f)
earthRotAngle -= 360.0f;
saturnRotAngle += 6.0f;
if(saturnRotAngle > 360.0f)
saturnRotAngle -= 360.0f;
glutSwapBuffers();
}
/**
改变窗口大小时回调函数
*/
void changeWindowSize(GLsizei w, GLsizei h)
{
GLfloat length = 100.0f;
if(h == 0) h = 1;
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if(w <= h) glOrtho(-length, length, -length * h / w, length * h / w, -length*2.0f, length*2.0f);
else glOrtho(-length * w / h, length * w / h, -length, length, -length*2.0f, length*2.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
/**
设置
*/
void setupRC(void)
{
//背景颜色
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glShadeModel(GL_FLAT);
}
/**
旋转
*/
void rotateMode(int key, int x, int y)
{
if(key == GLUT_KEY_UP) xRot -= 5.0f;
else if(key == GLUT_KEY_DOWN) xRot += 5.0f;
else if(key == GLUT_KEY_LEFT) yRot -= 5.0f;
else if(key == GLUT_KEY_RIGHT) yRot += 5.0f;
if(xRot < 0) xRot = 355.0f;
if(xRot > 360.0f) xRot = 0.0f;
if(yRot < 0) yRot = 355.0f;
if(yRot > 360.0f) yRot = 0.0f;
glutPostRedisplay();
}