素材中有四个.bmp格式的纹理文件和一个.txt的模型参数文件
文件格式说明:
纹理文件数量
纹理文件1(字符串)//.bmp
纹理文件2(字符串)
纹理文件3(字符串)
.
.
.
材质数量
ambient(float[4])
diffuse(float[4])
specular(float[4]])
emission(float[4])
shininess(float[1])
纹理文件索引(int[1])//0表示无
ambient(float[4])
diffuse(float[4])
specular(float[4]])
emission(float[4])
shininess(float[1])
纹理文件索引(int[1])//0表示无
ambient(float[4])
diffuse(float[4])
specular(float[4]])
emission(float[4])
shininess(float[1])
纹理文件索引(int[1])//0表示无
.
.
顶点数量
v1(float[3])
v2(float[3])
v3(float[3])
.
.
贴图坐标数量
t1(float[2])
t2(float[2])
t3(float[2])
.
.
法线数量
n1(float[3])
n2(float[3])
n3(float[3])
.
.
模型分组数量
缩放系数(float[3])
submodel1 三角形数量
材质索引(int)//0表示无
vi1 ti1 ni1 vi2 ti2 ni2 vi3 ti3 ni3 (unsigned int[9])
vi1 ti1 ni1 vi2 ti2 ni2 vi3 ti3 ni3 (unsigned int[9])
vi1 ti1 ni1 vi2 ti2 ni2 vi3 ti3 ni3 (unsigned int[9])
.
.
submodel2 三角形数量
材质索引(int)//0表示无
vi1 ti1 ni1 vi2 ti2 ni2 vi3 ti3 ni3 (unsigned int[9])
vi1 ti1 ni1 vi2 ti2 ni2 vi3 ti3 ni3 (unsigned int[9])
vi1 ti1 ni1 vi2 ti2 ni2 vi3 ti3 ni3 (unsigned int[9])
.
.
.
人物分为四个子模型,分别有不同的顶点坐标,纹理文件,材质。
纹理文件名用来打开纹理文件
材质列表是用来设置每个子模型的材质,opengl中设置材质的函数将会用到这些参数,下面会详细说明
顶点列表给出了需要使用的所有顶点,这里不分子模型,不直接绘出(当然直接画出来也没关系,只是没有必要),而是在连线时引用这些参数
贴图坐标给出了二维的纹理文件到三维的人物模型表面的映射,每个三维顶点将会对应一个二维坐标,具体的对应关系会在文件的其他部分给出
法线给出了光照的参数,光照在生成时会依据三维物体表面的法线属性,一个三位顶点对应一个法线,具体的对应关系 会在文件的其他部分给出
每个子模型由一个三角形阵列表示,每个三角形由九个参数给出,三个vi表示三个顶点的索引,三个ti表示三个贴图坐标的索引,三个ni表示三个法线向量的索引。(索引为上面读入的数组,注意这里的索引是从1开始的)
人物绘制
模型是由多个三角形面片组合在表面形成的,每个三角形由三个顶点坐标指定,并且每个顶点有对应的法线参数,对应的纹理坐标,在OpenGL中指定后程序会自动的使用这些参数绘制。
将人物的绘制分为三步:画出线框图-->添加界面交互-->添加纹理
这里先画出线框图是为了先看到效果,实际在一步绘制时用的是三角形绘制而不是线,但是三角形绘制的结果是一片白,线框可以看到细节
界面交互是用来指定三维变换,三维观察,投影等参数
绘制线框图
这里只是用到了txt中的顶点坐标和三角形索引中的ti值
main函数中进行初始化:
int main(int argc, char *argv[]) //主函数: 参数数量&参数值
{
glutInit(&argc, argv); //初始化glut: 接收主函数的参数
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE); //显示模式:颜色&缓冲
glutInitWindowPosition(0, 0); //窗口相对屏幕位置
glutInitWindowSize(720, 720); //窗口大小
glutCreateWindow("luweiqi"); //创建窗口: 标题
glutDisplayFunc(&display); //指定显示函数,这个函数由自己实现
gluLookAt(0.1, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0); //这个是三维观察函数,这里不详细说明,如果不指定这些参数人物的角度会很奇怪
glutMainLoop(); //主循环,程序执行到这里将循环调用指定的display函数
return 0;
}
display函数进行绘制:
void display(void)
{
glClear(GL_COLOR_BUFFER_BIT); //当前背景色填充窗口
//画线
glBegin(GL_LINE_LOOP); //参数指定绘制类型,将会根据类型确定每次取几个点绘制
for (int i = 0; i < allTriangleNum; i++)
{
glVertex3f(vertex[triangle[i][0]][0], vertex[triangle[i][0]][1], vertex[triangle[i][0]][2]); //三个顶点坐标:
glVertex3f(vertex[triangle[i][3]][0], vertex[triangle[i][3]][1], vertex[triangle[i][3]][2]);
glVertex3f(vertex[triangle[i][6]][0], vertex[triangle[i][6]][1], vertex[triangle[i][6]][2]);
}
glEnd(); //结束绘制,一旦缺失就可能会导致人物的某部分缺失
glFlush(); //输出缓冲区
}
效果:
有些线比较奇怪是因为本来这些索引是用于三角形(GL_TRIANGLES)绘制的,这里为了显示细节用了(GL_LINE_LOOP)
添加界面交互
三维变换的实现:
OpenGL中给了三维变换函数:
glTranslatef (GLfloat x, GLfloat y, GLfloat z); //平移
glRotatef (GLfloat angle, GLfloat x, GLfloat y, GLfloat z);//旋转
glScalef (GLfloat x, GLfloat y, GLfloat z); //缩放
但是这里我们手动实现:
为了使平移旋转缩放都统一为矩阵乘法运算,使用齐次坐标
即将三维坐标扩展第四维,但是第四维恒为1,这样变换矩阵就变为:
| 1 0 0 dx | //平移
| 0 1 0 dy |
| 0 0 1 dz |
| 0 0 0 1 |
| sx 0 0 0 | //缩放
| 0 sy 0 0 |
| 0 0 sz 0 |
| 0 0 0 1 |
旋转矩阵需推导,不详述
所以就是将上一步的内容增加一步将所有的顶点坐标乘以指定的变换矩阵,在根据变换后的顶点绘制
void matrixMul(GLfloat matrix[4][4]){
for (int i = 0; i < NumVertexs; i++)
for (int j = 0; j < 4; j++)
for (int k = 0; k < 4; k++)vertexChanged[i][j] += matrix[j][k] * vertex[i][k];
}
我将新旧图形画在一起了,这样方便看出效果:(旋转变换)
三维观察:
gluLookAt (
GLdouble eyex, GLdouble eyey, GLdouble eyez, //视点
GLdouble centerx, GLdouble centery, GLdouble centerz, //观察中心
GLdouble upx, GLdouble upy, GLdouble upz); //观察正向
透视投影:将构建出一个观察椎体
gluPerspective (//对称观察椎体
GLdouble fovy, //竖直张角
GLdouble aspect, //水平张角
GLdouble zNear, //近观察平面
GLdouble zFar);//远观察平面
glFrustum (//非对称观察椎体
GLdouble left,
GLdouble right,
GLdouble bottom,
GLdouble top,
GLdouble zNear,
GLdouble zFar
);
平行投影:(观察立方体)
gluOrtho2D (
GLdouble left,
GLdouble right,
GLdouble bottom,
GLdouble top);
交互:
在main函数中添加键盘事件:
glutKeyboardFunc(void (*func)(unsigned char key, int x, int y));
指定一个当发生键盘输入事件时要调用的函数func
func的第一个参数为按键类型,用于说明按得是哪个键,xy说明了按下按键时鼠标相对于屏幕左上角的坐标
在main函数中添加鼠标事件:
glutMouseFunc(void (*func)(int button, int state, int x, int y));
指定一个当发生鼠标点击/移动事件时要调用的函数func
func的第一个参数说明按的是哪个键,第二个说明了是按下还是松开等状态,xy说明了鼠标相对于屏幕左上角的坐标
添加菜单:
我实在发生鼠标点击事件时调用下面的函数,用于实现点击弹出菜单
glutCreateMenu(void (*)(int));
参数指定一个函数,函数的参数是选择的条目索引,用于根据索引采取不同的操作
glutAddMenuEntry(const char *label, int value);
用于给菜单添加条目,参数分别为菜单条目名,条目索引值
我实现交互的方式就是通过鼠标点击弹出菜单,根据菜单的选择指定当前修改的参数是哪些,通过键盘修改这些参数,实现对图像的改变。键盘修改的是恒定的指针中的内容,选择菜单就是指定这些指针指向了那些参数,参数设置为全局变量。
全局变量:
GLdouble translateX, translateY, translateZ;//平移
GLdouble rotateAngle, rotateX, rotateY, rotateZ;//旋转
GLdouble scaleX = 1.0, scaleY = 1.0, scaleZ = 1.0;//缩放
GLdouble fovy = 0, aspect = 0, zFar = 0, zNear = 0;//投影
GLdouble eyex = 0.1, eyey = 0.2, eyez = 0, centerx = 0, centery = 0, centerz = 0, upx = 0, upy = 0, upz = 1.0;//三维观察
GLdouble* indexA = NULL, *indexB = NULL, *indexC = NULL, *indexD = NULL;
鼠标点击函数:
void mouseFunc(GLint button, GLint action, GLint xMouse, GLint yMouse)
{
glutCreateMenu(chooseMode);
glutAddMenuEntry("translate", 0);
glutAddMenuEntry("scale", 1);
glutAddMenuEntry("rotate", 2);
glutAddMenuEntry("perspective", 3);
glutAddMenuEntry("glLookAt eye", 4);
glutAddMenuEntry("glLookAt center", 5);
glutAddMenuEntry("glLookAt up", 6);
glutAttachMenu(GLUT_RIGHT_BUTTON);
}
菜单选择函数:
void chooseMode(GLint menuIteemNum)
{
switch (menuIteemNum)
{
case 0:
indexA = &translateX; indexB = &translateY; indexC = &translateZ; indexD = NULL; break;
case 1:
indexA = &scaleX; indexB = &scaleY; indexC = &scaleZ; indexD = NULL; break;
case 2:
indexA = &rotateAngle; indexB = &rotateX; indexC = &rotateY; indexD = &rotateZ; break;
case 3:
indexA = &fovy; indexB = &aspect; indexC = &zFar; indexD = &zNear; break;
case 4:
indexA = &eyex; indexB = &eyey; indexC = &eyez; indexD = NULL; break;
case 5:
indexA = ¢erx; indexB = ¢ery; indexC = ¢erz; indexD = NULL; break;
case 6:
indexA = &upx; indexB = &upy; indexC = &upz; indexD = NULL; break;
default:
indexA = NULL; indexB = NULL; indexC = NULL; indexD = NULL; break;
}
}
键盘输入函数:
void keyBoardFunc(unsigned char key, int x, int y)
{
if (key == 'q')
if (indexA)*indexA += 0.05;
if (key == 'a')
if (indexA)*indexA -= 0.05;
if (key == 'w')
if (indexB)*indexB += 0.05;
if (key == 's')
if (indexB)*indexB -= 0.05;
if (key == 'e')
if (indexC)*indexC += 0.05;
if (key == 'd')
if (indexC)*indexC -= 0.05;
if (key == 'r')
if (indexD)*indexD += 0.05;
if (key == 'f')
if (indexD)*indexD -= 0.05;
//输出参数方便调整
cout << "glScale:\t" << scaleX << '\t' << scaleY << '\t' << scaleZ << '\t' << endl;
cout << "glRotate:\t" << rotateAngle * 20 << '\t' << rotateX << '\t' << rotateY << '\t' << rotateZ << endl;
cout << "glTranslate:\t" << translateX << '\t' << translateY << '\t' << translateZ << endl;
cout << "glLookAt eye:\t" << eyex << '\t' << eyey << '\t' << eyez << endl;
cout << "glLookAtCenter:\t" << centerx << '\t' << centery << '\t' << centerz << endl;
cout << "glLookAt up:\t" << upx << '\t' << upy << '\t' << upz << endl;
cout << "glperspective:\t" << fovy * 20 << '\t' << aspect << '\t' << zFar << '\t' << zNear << '\t' << endl << endl;
display();
}
效果:
添加纹理
大致流程:先根据.bmp文件读出纹理,再绑定纹理,在绘制的时候就可以用txt中的贴图坐标指定纹理了。
读纹理有多种方法
方法一:glaux库(库文件添加是另一个问题,假设你的库文件添加好了)
AUX_RGBImageRec * APIENTRY auxDIBImageLoadA(LPCSTR);
参数是指定格式的纹理文件名,返回一个指定格式的文件,该格式的文件在下面的函数中使用,之后就可以直接在画点是使用贴图了
glTexImage2D (GLenum target, GLint level, GLint internalformat,
GLsizei width, GLsizei height,
GLint border, GLenum format, GLenum type,
const GLvoid *pixels);
方法二:这个方法不用使用glaux库
bool LoadTexture(LPCSTR szFileName) // Creates Texture From A Bitmap File
{
HBITMAP hBMP; // Handle Of The Bitmap
BMP; // Bitmap Structure
GLuint *texid=new GLuint; //生成后直接使用,不必放在外面
glGenTextures(1, &texid); // Create The Texture
hBMP = (HBITMAP)LoadImageA(GetModuleHandle(NULL), szFileName, IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION | LR_LOADFROMFILE);
if (!hBMP) {
cout<<"load bit map error"<<endl;
return FALSE; // If Not Return False
}
GetObject(hBMP, sizeof(BMP), &BMP);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glBindTexture(GL_TEXTURE_2D, texid); // Bind To The Texture ID
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // Linear Min Filter
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Linear Mag Filter
glTexImage2D(GL_TEXTURE_2D, 0, 3, BMP.bmWidth, BMP.bmHeight, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, BMP.bmBits);
DeleteObject(hBMP); // Delete The Object
return TRUE; // Loading Was Successful
}
在画图之前调用这个函数就可以了
方法三:使用SOIL库,简易OpenGL图像库(SimpleOpenGL Image Library)
前两种方法对bmp文件格式有要求,一旦格式不符合就会出错,如果想用的话就要将bmp文件转化为24位深的,但是这样会让最终的效果有偏差
但是这个库中的
unsigned int
SOIL_load_OGL_texture
(
const char *filename,
int force_channels,
unsigned int reuse_texture_ID,
unsigned int flags
);
函数会自动根据文件的格式做不同的反应,返回值是一个纹理数据,好像并不需要再次调用glBindTexture函数绑定
texture[i] = SOIL_load_OGL_texture
(
texture_file_name[index].c_str(),
SOIL_LOAD_AUTO,
SOIL_CREATE_NEW_ID,
SOIL_FLAG_INVERT_Y
);
画图部分:
for (size_t j = 0; j < riangle_num; j++)//每次循环,三角形的三个顶点
{
glNormal3fv("法线向量");//顶点一
glTexCoord2f("贴图的二维坐标");
glVertex3fv("顶点的三维坐标");
glNormal3fv("法线向量");//顶点二
glTexCoord2f("贴图的二维坐标");
glVertex3fv("顶点的三维坐标");
glNormal3fv("法线向量");//顶点三
glTexCoord2f("贴图的二维坐标");
glVertex3fv("顶点的三维坐标");
}
这里有很重要的一点:实际上的纹理贴图是要反转y轴的,即glTexCoord2f的第二个参数是(1-贴图y坐标)
材质添加:
使用函数:
glMaterialfv (GLenum face, GLenum pname, const GLfloat *params);
其中的pname是用来指定设置的是那种属性:如GL_AMBIENT/GL_DIFFUSE/GL_SPECULAR/GL_EMISSION/GL_SHININESS,设置镜面反射/漫反射等属性,具体的属性参数在parmsa中指定
在每次画图之前指定材质属性,画图是根据最后一次指定的材质属性绘制的
添加光照:
只有光照才能反映材质
在display之前,glEnable(GL_LIGHT0)来开启光源0,一共有八个光源
通过函数设置光源属性:
glLightfv (GLenum light, GLenum pname, const GLfloat *params);
pname的含义和上面设置材质时一致,而且还有一个GL_POSITION来设置光源位置
最后要开启光照:glEnable(GL_LIGHTING)
但是开启了光照好像没什么变化,但是其实是有变化的,关掉纹理,只画出白色的三角形面片就可以看出: