本文仅仅是对个人在学习OpenGL的过程中,用到的3D数学,向量和矩阵,在此做一个记录。
1.OpenGL中的矩阵、向量
在OpenGL的三维坐标中,3个值(x,y,z)组合起来就表示2个重要的值:方向和数量-->向量。
M3DVector3f
可以表示⼀个三 维向量(x,y,z),而
M3DVector4f
则可以表示一个四维向量(x,y,z,w)。在典型情况下,w 坐标设为1.0。x,y,z值通过除以w,来进行缩放。而除以1.0则本质上不改变x,y,z值。
typedef float M3DVector3f[3];
typedef float M3DVector4f[4];
声明⼀个三分量向量操作:
M3DVector3f vVector;
类似,声明⼀个四分量的操作:
M3DVector4f vVectro= {0.0f,0.0f,1.0f,1.0f};
声明一个三分量顶点数组,例如⽣成⼀个三⻆形
M3DVector3f vVerts[] = {
-1.0f,0.0f,0.0f,
0.5f,0.0f,0.0f,
0.0f,1.0f,0.0f
};
复制代码
2.点乘:
点乘运算返回2个向量之间的夹角
//实现点乘方法:
//⽅法1: 返回的是-1,1之间的值。它代表这个2个向量量的余弦值。
float m3dDotProduct3(const M3DVector3f u,const
M3DVector3f v);
//方法2:返回2个向量之间的弧度值。
float m3dGetAngleBetweenVector3(const M3DVector3f u,const M3DVector3f v);
复制代码
3.叉乘:
叉乘运算结果返回一个新的向量,这个新的向量与原来的2个向量垂直
void m3dCrossProduct3(M3DVector3f result,const M3DVector3f u ,const M3DVector3f v);
复制代码
4.模型变换
先来看一下OpenGL中的变换:
变换 | 应用 | 描述说明 |
---|---|---|
视图 | 制定观察者或摄像机的位置 | 视图变换允许我们把观察点放在所希望的任何位置,并允许在任何方向上观察场景。确定视图变换就像在场景中放置照相机并让它指向某个方向。 |
模型 | 在场景中移动物体 | 模型变换用于操纵模型和其中的特定对象。这些变换将对象移动到需要的位置,然后再对它们进行旋转和缩放。 |
视图模型 | 描述视图和模型变换的二元性 | 其实不必通过移动摄像机(视图变换)来观察物体,而是移动这个物体(模型变换),这两个方式都能实现同样地效果,有时使用其中一种变换比使用另一种变换要方便得多。这也是把视图变换和模型变换都统一为模型视图矩阵的原因。 |
投影 | 改变视景体的大小或重新设置它的形状,将3维坐标投影为2维屏幕坐标,位于视景体之外的东西将被裁剪掉 | 投影变换将在模型视图变换之后应用到顶点上,它将指定一个完成的场景(所有模型变换都已完成)是如何投影到屏幕上的最终图像。 正投影:所有多边形都是精确地按照指定的相对大小来在屏幕上绘制的。 透视投影:透视投影的特点是透视缩短,这种特性使得远处的物体看起来比进出同样大小的物体更小一些。 |
视口 | 这是一种伪变换,只是对窗口上的最终输出进行缩放 | 当所有变换完成后,就得到了一个场景的二维投影,它将被映射到屏幕上某处的窗口上。这种到物理创口标的映射是我们最后要做的变换,称为视口变换。 |
5.矩阵
声明矩阵
声明一个三维矩阵:
typedef float M3DMatrix33f[9];
声明一个四维矩阵:
typedef float M3DMatrix44f[16];
复制代码
OpenGL并不是将一个4X4矩阵表示为一个浮点值的二维数组,而是将它表示为一个由16个浮点值组成的单个数组。
6.矩阵的使用
模型视图矩阵 模型视图矩阵是一个4X4矩阵,它表示一个变换后的坐标系,我们可以用来放置对象和确定对象的方向。一个包含单个顶点数据的矩阵乘以模型视图矩阵后会得到新的视觉坐标。我们调用math3d库中的m3dTranslationMatrix44函数来使用模型视图变换矩阵。
1.平移
void m3dTranslationMatrix44(M3DMatrix44f m,float x,float y,float z);
2.旋转
m3dRotationMatrix44(M3DMatrix44f m,float angle,float x,float y,float z);
3.缩放
void m3dScaleMatrix44(M3DMatrix44f m,float xScale,float yScale,float zScale);
4.综合(math3d库函数m3dMatrixMultiply44用来将两个矩阵相乘并返回运算结果)
void m3dMatrixMultiply44(M3DMatrix44f product,const M3DMatrix44f a,const M3DMatrix44f b);
eg:
M3DMatrix44f mFinalTransform, mTranslationMatrix, mRotationMatrix;
//平移 xPos,yPos
m3dTranslationMatrix44(mTranslationMatrix, xPos, yPos, 0.0f);
// 每次重绘时,旋转5度
static float yRot = 0.0f;
yRot += 5.0f;
m3dRotationMatrix44(mRotationMatrix, m3dDegToRad(yRot), 0.0f, 0.0f, 1.0f);
//将旋转和移动的结果合并到mFinalTransform 中
m3dMatrixMultiply44(mFinalTransform, mTranslationMatrix, mRotationMatrix);
复制代码
投影视图矩阵
1.正投影
GLFrustum::SetOrthographic(GLfloat xMin,GLfloat xMax,GLfloat yMin,GLfloat yMax,GLfloat zMin,GLfloat zMax);
2.透视投影
GLFrustum::SetPerspective(float fFov,float fAspect,float fNear,float fFar);
// 参数1:从顶点方向看去的视场角度值
// 参数2:宽和高的纵横比
// 参数3:近裁剪面的距离
// 参数4:远裁剪面的距离
复制代码
矩阵堆栈 GLMatrixStack类的构造函数允许指定堆栈的最大深度,默认的堆栈深度为64.这个矩阵堆栈在初始化时已经在堆栈中包含了单位矩阵。(矩阵堆栈先进后出)
GLMatrixStack::GLMatrixStack(int iStackDepth=64);
复制代码
矩阵堆栈的使用:
1.矩阵堆栈顶部载入单元矩阵
void GLMatrixStack::LoadIdentity(void);
2.矩阵堆栈顶部载入任何矩阵
void GLMatrixStack::LoadMatrix(const M3DMatrix44f m);
3.用一个矩阵乘以矩阵堆栈的顶部矩阵,相乘得到的结果随后将存储在堆栈的顶部
void GLMatrixStack::MultMatrix(const M3DMatrix44f);
4.获取矩阵堆栈顶部的值
const M3DMatrix44f& GLMatrixStack::GetMatrix(void);
void GLMatrixStack::GetMatrix(M3DMatrix44f mMatrix);
5.压栈和出栈
void GLMatrixStack::PushMatrix(void);
void PushMatrix(const M3DMatrix44f mMatrix);
void PushMatrix(GLFrame& frame);
void GLMatrixStack::PopMatrix(void);
6.仿射变换
GLMatrixStack类也有对创建旋转、平移和缩放矩阵的支持。
void MatrixStack::Rotate(GLfloat angle,GLfloat x,GLfloat y,GLfloat z);
void MatrixStack::Translate(GLfloat x,GLfloat y,GLfloat z);
void MatrixStack::Scale(GLfloat x,GLfloat y,GLfloat z);
复制代码
我们通过一份绘制多个图形的代码来看一下矩阵和矩阵堆栈的使用: 首先导入头文件:
#include "GLTools.h" // OpenGL toolkit
#include "GLMatrixStack.h" //矩阵堆栈工具类
#include "GLFrame.h" //矩阵位置工具类
#include "GLFrustum.h" // 透视投影矩阵类
#include "GLBatch.h" //三角形批次类
#include "GLGeometryTransform.h" //变换管道
#include "StopWatch.h"
#include <math.h> // 数学类
#ifdef __APPLE__
#include <glut/glut.h>
#else
#define FREEGLUT_STATIC
#include <GL/glut.h>
#endif
复制代码
声明全局变量:
GLShaderManager shaderManager; //存储着色器管理
GLMatrixStack modelViewMatrix;//模型视图矩阵堆栈
GLMatrixStack projectionMatrix;// 投影视图矩阵堆栈
GLFrame cameraFrame;//观察者位置
GLFrame objectFrame;//世界坐标位置
GLFrustum viewFrustum;//视景体,用来构造投影矩阵
GLTriangleBatch CC_Triangle;//三角形批次类
//每一种图形都需要一个单独的三角形批次类:
GLTriangleBatch sphereBatch;//球
GLTriangleBatch torusBatch;//环
GLTriangleBatch cylinderBatch;//圆柱
GLTriangleBatch coneBatch;//锥
GLTriangleBatch diskBatch;//磁盘
GLGeometryTransform transformPipeline;//变换管道,用来管理模型,投影矩阵
M3DMatrix44f shadowMatrix;
GLfloat vGreen[] = { 0.0f, 1.0f, 0.0f, 1.0f }; //定义颜色:绿色
GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };//定义颜色:黑色
int nStep = 0; //定义第几个图形
复制代码
程序入口main()函数:
int main(int argc, char* argv[])
{
gltSetWorkingDirectory(argv[0]);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
//设置GLUT窗口大小、窗口标题
glutInitWindowSize(800, 600);
glutCreateWindow("Sphere");
//注册重塑函数---设置视口
glutReshapeFunc(ChangeSize);
//注册空格函数---切换图形
glutKeyboardFunc(KeyPressFunc);
//注册移动函数---上下左右移动图形
glutSpecialFunc(SpecialKeys);
//注册显示函数
glutDisplayFunc(RenderScene);
GLenum err = glewInit();
if (GLEW_OK != err) {
fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
return 1;
}
//设置渲染环境
SetupRC();
glutMainLoop();
return 0;
}
复制代码
重塑函数ChangeSize(): 当窗口第一次显示或者在改变窗口的大小的时候会重新调用重塑函数,接收新的w,h值。
void ChangeSize(int w, int h)
{
//1.视口
if(h == 0){ // 防止h变为0
h = 1;
}
glViewport(0, 0, w, h); //设置视口坐标,大小,一般情况下视口的坐标都是窗口的左下角,故设置为(0,0);
//2.设置透视投影
//创建透视投影矩阵
viewFrustum.SetPerspective(35.0f, float(w) / float(h), 1.0f, 500.0f);
//projectionMatrix 投影视图矩阵堆栈---加载透视投影矩阵
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
//3.modelViewMatrix 模型视图矩阵堆栈 ----加载一个单元矩阵
modelViewMatrix.LoadIdentity();
//4.通过GLGeometryTransform管理矩阵堆栈
//使用transformPipeline 管道管理模型视图矩阵堆栈 和 投影视图矩阵堆栈
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
}
复制代码
设置渲染环境SetupRC()函数:
// 将上下文中,进行必要的初始化
void SetupRC()
{
//1.设置背景颜色值
glClearColor(0.7f, 0.7f, 0.7f, 1.0f );
//2.初始化存储着色器管理
shaderManager.InitializeStockShaders();
//3.开启深度测试(无论绘制什么图形,都开启深度测试)
glEnable(GL_DEPTH_TEST);
//4.将观察者坐标位置Z移动往屏幕里移动15个单位位置
//表示离屏幕之间的距离。
//负数,是往屏幕后面移动
//正数,是往屏幕前面移动
cameraFrame.MoveForward(-15.0f);
//5.利用三角形批次类构造图形对象
// 球
/*
gltMakeSphere(GLTriangleBatch& sphereBatch, GLfloat fRadius, GLint iSlices, GLint iStacks);
参数1:sphereBatch,三角形批次类对象
参数2:fRadius,球体半径
参数3:iSlices,从球体底部堆叠到顶部的三角形带的数量;其实球体是一圈一圈三角形带组成
参数4:iStacks,围绕球体一圈排列的三角形对数
建议:一个对称较好的球体的片段数量是堆叠数量的2倍,就是iStacks = 2 * iSlices;
绘制球体都是围绕Z轴,这样+z就是球体的顶点,-z就是球体的底部。
*/
gltMakeSphere(sphereBatch, 3.0, 10, 20);
// 环面
/*
gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
参数1:torusBatch,三角形批次类对象
参数2:majorRadius,甜甜圈中心到外边缘的半径
参数3:minorRadius,甜甜圈中心到内边缘的半径
参数4:numMajor,沿着主半径的三角形数量
参数5:numMinor,沿着内部较小半径的三角形数量
*/
gltMakeTorus(torusBatch, 3.0f, 0.75f, 15, 15);
// 圆柱
/*
void gltMakeCylinder(GLTriangleBatch& cylinderBatch, GLfloat baseRadius, GLfloat topRadius, GLfloat fLength, GLint numSlices, GLint numStacks);
参数1:cylinderBatch,三角形批次类对象
参数2:baseRadius,底部半径
参数3:topRadius,头部半径
参数4:fLength,圆形长度
参数5:numSlices,围绕Z轴的三角形对的数量
参数6:numStacks,圆柱底部堆叠到顶部圆环的三角形数量
*/
gltMakeCylinder(cylinderBatch, 2.0f, 2.0f, 3.0f, 15, 2);
//锥
/*
void gltMakeCylinder(GLTriangleBatch& cylinderBatch, GLfloat baseRadius, GLfloat topRadius, GLfloat fLength, GLint numSlices, GLint numStacks);
参数1:cylinderBatch,三角形批次类对象
参数2:baseRadius,底部半径
参数3:topRadius,头部半径
参数4:fLength,圆形长度
参数5:numSlices,围绕Z轴的三角形对的数量
参数6:numStacks,圆柱底部堆叠到顶部圆环的三角形数量
*/
//圆柱体,从0开始向Z轴正方向延伸。
//圆锥体,是一端的半径为0,另一端半径可指定。
gltMakeCylinder(coneBatch, 2.0f, 0.0f, 3.0f, 13, 2);
// 磁盘
/*
void gltMakeDisk(GLTriangleBatch& diskBatch, GLfloat innerRadius, GLfloat outerRadius, GLint nSlices, GLint nStacks);
参数1:diskBatch,三角形批次类对象
参数2:innerRadius,内圆半径
参数3:outerRadius,外圆半径
参数4:nSlices,圆盘围绕Z轴的三角形对的数量
参数5:nStacks,圆盘外网到内围的三角形数量
*/
gltMakeDisk(diskBatch, 1.5f, 3.0f, 13, 3);
}
复制代码
显示场景RenderScene()函数:
//召唤场景
void RenderScene(void)
{
//1.清除当前缓存,防止在渲染过程中出现错误
//GL_COLOR_BUFFER_BIT :指当前激活的用来进行颜色写入缓冲区
//GL_DEPTH_BUFFER_BIT :指深度缓存区
//GL_STENCIL_BUFFER_BIT:指模板缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
//2.模型视图矩阵栈堆,压栈
modelViewMatrix.PushMatrix();
//3.创建摄像头矩阵
M3DMatrix44f mCamera;
//从camereaFrame中获取矩阵到mCamera
cameraFrame.GetCameraMatrix(mCamera);
//模型视图堆栈的 矩阵与mCamera矩阵 相乘之后,存储到modelViewMatrix矩阵堆栈中
modelViewMatrix.MultMatrix(mCamera);
//4.创建矩阵mObjectFrame
M3DMatrix44f mObjectFrame;
//从ObjectFrame 获取矩阵到mOjectFrame中
objectFrame.GetMatrix(mObjectFrame);
//将modelViewMatrix 的堆栈中的矩阵 与 mOjbectFrame 矩阵相乘,存储到modelViewMatrix矩阵堆栈中
modelViewMatrix.MultMatrix(mObjectFrame);
//5.判断你目前是绘制第几个图形
switch(nStep) {
case 0:
DrawWireFramedBatch(&sphereBatch);
break;
case 1:
DrawWireFramedBatch(&torusBatch);
break;
case 2:
DrawWireFramedBatch(&cylinderBatch);
break;
case 3:
DrawWireFramedBatch(&coneBatch);
break;
case 4:
DrawWireFramedBatch(&diskBatch);
break;
}
//6. pop绘制完成之后,把栈顶矩阵pop出去,不要影响下一次的绘制。(还原到以前的模型视图矩阵 (单位矩阵))
modelViewMatrix.PopMatrix();
//7. 进行缓冲区交换
glutSwapBuffers();
}
复制代码
下面重点标记一下这段代码:
- modelViewMatrix.PushMatrix(); 表示让模型视图压栈,这里的PushMatrix()括号中是空的,并不是表示压栈的内容是空的,而是把矩阵堆栈的栈顶元素复制一份进行压栈,压到栈顶。如果括号不是空的,就代表压入一个矩阵到矩阵堆栈的栈顶。
- M3DMatrix44f mCamera; 声明一个观察者矩阵; cameraFrame.GetCameraMatrix(mCamera);意思是从 cameraFrame 这个观察者坐标系中获取矩阵,然后赋值给 mCamera。
- modelViewMatrix.MultMatrix(mCamera);意思是把矩阵堆栈的栈顶矩阵和观察者矩阵(mCamera)相乘,得到的结果矩阵,重新存入到矩阵堆栈的栈顶(也就是替换掉刚刚矩阵堆栈的栈顶矩阵)。
- M3DMatrix44f mObjectFrame; 声明一个物体世界坐标位置的矩阵。 objectFrame.GetMatrix(mObjectFrame);意思是从 objectFrame 这个世界坐标系中获取矩阵,然后赋值给 mObjectFrame。
- modelViewMatrix.MultMatrix(mObjectFrame); 将modelViewMatrix 的堆栈中的栈顶矩阵复制一份与 mOjbectFrame矩阵相乘,得到的结果矩阵存储到modelViewMatrix矩阵堆栈中的栈顶。(此处注意:矩阵相乘并不满足交换律,所以在栈顶矩阵相乘的时候,并不能先是mObjectFrame,再是mCamera)
- modelViewMatrix.PopMatrix(); 意思是把栈顶的矩阵出栈,恢复为原始的矩阵堆栈,才不会影响后续绘制。
在这里记录一下: 在SetupRC()这个方法中,我们将观察者坐标位置Z移动往屏幕里移动15个单位位置,cameraFrame.MoveForward(-15.0f); 但是其实我们也可以不移动观察者,让物体向观察者移动 objectFrame.MoveForward(15.0f); 此时在RenderScene()这个方法中,
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);
modelViewMatrix.MultMatrix(mCamera);
M3DMatrix44f mObjectFrame;
objectFrame.GetMatrix(mObjectFrame);
modelViewMatrix.MultMatrix(mObjectFrame);
复制代码
这几句代码就可以不需要添加,然后模型视图矩阵堆栈的压栈操作,modelViewMatrix.PushMatrix(); 压入的就是objectFrame modelViewMatrix.PushMatrix(objectFrame); 为什么这样做呢?是因为我们在SetupRC()方法中,objectFrame记录了物体向观察者移动的状态,同时在SpecialKeys()方法中,(objectFrame.RotateWorld(m3dDegToRad(-5.0f), 1.0f, 0.0f, 0.0f);)objectFrame同样记录了随着世界坐标系移动的状态,最后在渲染的时候(RenderScene()),modelViewMatrix.PushMatrix(objectFrame); 就是把移动的变化和旋转的变化结合起来放在矩阵堆栈中。
下图是,绘制了一个关于堆栈栈顶矩阵的理解流程图。
绘制图形函数DrawWireFramedBatch():
void DrawWireFramedBatch(GLTriangleBatch* pBatch)
{
//----绘制图形----
//1.平面着色器,绘制三角形
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vGreen);
//参数1:着色器类型
//参数2:通过transformPipeline获取模型视图投影矩阵
//参数3:颜色
//传过来的参数,对应不同的图形Batch
pBatch->Draw();
//2.开启多边形偏移
glEnable(GL_POLYGON_OFFSET_LINE);
//多边形模型(背面、线) 将多边形背面设为线框模式
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//开启多边形偏移(设置偏移数量)
glPolygonOffset(-1.0f, -1.0f);
//线条宽度
glLineWidth(2.5f);
//3.开启混合功能(颜色混合&抗锯齿功能)
glEnable(GL_BLEND);
//开启处理线段抗锯齿功能
glEnable(GL_LINE_SMOOTH);
//设置颜色混合因子
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
//4.平面着色器绘制线条
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
pBatch->Draw();
//5.恢复多边形模式和深度测试
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glDisable(GL_POLYGON_OFFSET_LINE);
glLineWidth(1.0f);
glDisable(GL_BLEND);
glDisable(GL_LINE_SMOOTH);
}
复制代码
移动图形函数SpecialKeys():
//点击键盘的上下左右,来移动世界坐标系从而移动图形
void SpecialKeys(int key, int x, int y)
{
if(key == GLUT_KEY_UP)
//移动世界坐标系,而不是去移动物体。
//将世界坐标系在X方向移动-5.0
objectFrame.RotateWorld(m3dDegToRad(-5.0f), 1.0f, 0.0f, 0.0f);
if(key == GLUT_KEY_DOWN)
objectFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f);
if(key == GLUT_KEY_LEFT)
objectFrame.RotateWorld(m3dDegToRad(-5.0f), 0.0f, 1.0f, 0.0f);
if(key == GLUT_KEY_RIGHT)
objectFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f);
// 通知GLUT在进行一次同样操作
glutPostRedisplay();
}
复制代码
切换图形函数KeyPressFunc():
//点击键盘空格,切换渲染的图形
void KeyPressFunc(unsigned char key, int x, int y)
{
if(key == 32)
{
nStep++;
if(nStep > 4)
nStep = 0;
}
switch(nStep)
{
case 0:
glutSetWindowTitle("Sphere");
break;
case 1:
glutSetWindowTitle("Torus");
break;
case 2:
glutSetWindowTitle("Cylinder");
break;
case 3:
glutSetWindowTitle("Cone");
break;
case 4:
glutSetWindowTitle("Disk");
break;
}
glutPostRedisplay();
}
复制代码
然后来看一下代码执行的图形结果: