3D物体建模和渲染的跟踪球交互设计和实现

1. 前言
整理电脑时发现自己本科选了一些有意思的选修课,写了一些小项目,马上就要毕业了,不想就这么删了,决定花点时间整理出来,跟大家分享一下。
2. 实验目的
综合应用计算机图形学老师所教授的理论知识,本实验采用openGL编程,实现一个基于3D物体建模和渲染的跟踪球交互应用程序,应用程序主要实现以下功能:
① 画出两个图形,其中一个是可以递归细分的球面,一个是立方体;
① 在WC坐标原点处添加一个光源,物体表面将会有光照的颜色;
② 除此之外,程序能够给物体表面贴图,因此物体表面还混有纹理贴图的颜色;
程序还应能够对物体进行交互,包括:
③ 键盘交互,能够用键盘的0~8控制球面的递归细分的次数。用键盘+,-控制物体透明程度,A在0..1之间变化,其中初始为不透明;
④ 鼠标交互,程序采用模拟跟踪球交互技术实现景物观察,通过控制鼠标左键、中键、右键进而控制物体的拖动旋转、转动、投影变换。
通过此实验,我们将实现以上功能,并且巩固掌握的知识和增强应用的能力。本文档的主要目的是设计说明跟踪球的建模与渲染及其交互的实现。
3. 实验环境
Windows 7/8/10
Visual Studio 2013 C++
4. 实验内容
实验内容分为以下三个方面:
4.1 建模
要在计算机中表示一个三维物体,首先要有它的几何模型表达。因此,三维模型的建模是计算机图形学的基础,是其他内容的前提。程序的第一步是确定基本图形从而进行建模。在本实验中,首先设计两个3D图形,一个是能够细分的球面,一个是立方体。
4.1.1 细分球面
对于球面是由多个四面体组成的,所以首先初始化四面体,定义一个长度为4的point矩阵表示四面体的顶点坐标:

/初始化四面体
point v[] = { { 0.0, 0.0, 1.0 }, { 0.0, 0.942809, -0.33333 },
{ -0.816497, -0.471405, -0.333333 }, { 0.816497, -0.471405, -0.333333 } };

为了实现细分,可以将三角形的细分应用于四面体的每一个面,所以一共需要调用四次细分函数divide_triangle。函数divide_triangle是一个递归函数,使用顶点编号右手规则应用于创建向外指向的面,将四面体面的相邻顶点向量相加、归一化,再用处理后的顶点与相邻的两个顶点作为参数调用函数本身以实现进一步的细分,在递归结束部分调用函数triangle画出三角形。从而,四面体的每一面实现了细分,进而实现了球面的细分。其中divide_triangle的核心算法如下:

 if (m>0)
    {
        for (j = 0; j<3; j++) v1[j] = a[j] + b[j];
        normal(v1);
        for (j = 0; j<3; j++) v2[j] = a[j] + c[j];
        normal(v2);
        for (j = 0; j<3; j++) v3[j] = b[j] + c[j];
        normal(v3);
        divide_triangle(a, v1, v2, m - 1);
        divide_triangle(c, v2, v3, m - 1);
        divide_triangle(b, v3, v1, m - 1);
        divide_triangle(v1, v3, v2, m - 1);
    }
    else(triangle(a, b, c)); /* draw triangle at end of recursion */

4.1.2 立方体
对于立方体,首先定义一个长度为8的二维的顶点数组vertices来表示立方体的顶点坐标,使用了浮点数坐标:

//立方体点
GLfloat vertices[][3] = {
    { -0.5, -0.5, -0.5 }, { 0.5, -0.5, -0.5 }, 
    { 0.5, 0.5, -0.5 }, { -0.5, 0.5, -0.5 },
    { -0.5, -0.5, 0.5 }, { 0.5, -0.5, 0.5 }, 
    { 0.5, 0.5, 0.5 }, { -0.5, 0.5, 0.5 }
};

接着需定义立方体对象的六个面,加入颜色描述和其他参数,为此在函数DrawCube分别六次调用glBegin(GL_QUADS),必须明确每一个面的顶点顺序符合从立方体外部对其观察时为逆时针次序的要求。而此实验的立方体表面带有纹理贴图,将在下一部分进行设计实现讨论。
4.2 渲染
有了三维模型或场景,为了把这些三维几何模型画出来以产生令人赏心悦目的真实感图像,将应用到多种渲染技术。在本实验中,将采用光照和纹理贴图。
4.2.1 光照
本实验在WC坐标原点处添加一个光源GL_LIGHT0,首先指定一个光源的位置和类型。光源类型和光源位置坐标用一个四元素的浮点数向量来指定,该向量的前三个元素给出世界坐标位置,因为我们要设在原点,所以前三个元素皆为0.0;本实验中对该位置向量的第四个元素赋值为1.0,即设置的光源是一个局部光源,光源位置被光照子程序用来确定对场景中每一个对象的光照方向。最后,利用glLightfv(GL_LIGHT0, GL_POSITION, sun_light_position)指定了一个位置在原点的局部光源。
还应指定光源的颜色,我们使用符号颜色特性常量GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR来指定场景环境光、漫反射光照、镜面反射光照的颜色。其中每一个通过指定四元素浮点值集来赋值,分别为RGBA。在本实验中,通过以下代码:


    //设置光源,光源颜色为红色
    GLfloat light_ambient[] = { 0.0, 0.0, 0.0, 1.0 };
    GLfloat light_diffuse[] = { 1.0, 0.0, 0.0, 1.0 };
    GLfloat light_specular[] = { 1.0, 1.0, 1.0, 1.0 };

    glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);//GL_AMBIENT表示各种光线照射到该材质上,经过很多次反射后最终遗留在环境中的光线强度(颜色)  
    glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);//漫反射后
    glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular);//镜面反射后
    glLightfv(GL_LIGHT0, GL_POSITION, sun_light_position);//指定第0号光源的位置 

将光源0的默认颜色设置为环境光为黑色,漫反射光为红色而镜面反射光为白色。
除了设置光源属性外,还要指定物体表面的特性,表面的反射系数和其他可选特性可用函数glMaterialfv(surFace, surProperty, propertyValue)设定,并且将参数surFace赋以GL_FRONT_AND_BACK;surProperty用来标识表面参数;propertyValue用来设定相应的值,除了镜面反射指数外所有其他特性均用向量值指定。本实验中,surProperty的符号常量有GL_SPECULAR、GL_AMBIENT、GL_DIFFUSE、GL_EMISSION和GL_SHININESS,用来设定镜面反射系数、环境光系数、漫反射系数、散射颜色和镜面反射指数:

//设置球体的球面材质
    GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };//镜面光的反射系数
    GLfloat mat_diffuse[] = { 1.0, 1.0, 1.0, 1.0 };//漫反射的反射系数
    GLfloat mat_ambient[] = { 1.0, 1.0, 1.0, 1.0 };//环境光的反射系数
    GLfloat sun_mat_emission[] = { 0.0f, 0.3f, 1.0f, 1.0f };//发射光颜色
    GLfloat mat_shininess =  30.0 ;//镜面反射指数

    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, mat_specular);
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, mat_ambient);
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, mat_diffuse);
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, mat_shininess);
    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, sun_mat_emission);

在光源与材质属性设定完毕之后,应该使用glEnable(GL_LIGHT0)点亮光源0,并用glEnable(GL_LIGHTING)激活光源。除此,还应使用glShadeModel(GL_SMOOTH)平滑阴影,并且通过glEnable(GL_DEPTH_TEST)启用深度测试,这样在后面的物体将会被挡着。
4.2.2 纹理贴图
本实验,为立方体表面纹理贴图,首先定义一个6元素的纹理数组texture用于存放纹理名。因为立方体有六个表面,所以需要载入表面纹理六次,所以需定义一个长度为6的AUX_RGBImageRec型数组TextureImage用于存放读取的图片。对于一次的载入表面纹理贴图函数实现如下:
① 首先得将图片载入,利用TextureImage[0] = auxDIBImageLoad(“a.bmp”)可读取bmp格式的图案存放入TextureImage[0]中;
② 使用glGenTextures(1, &texture[0]); glBindTexture(GL_TEXTURE_2D, texture[0]);来申请一个纹理名字来将它用于某个图案,并激活一个命名的图案。所以最终,glGenTextures生成了一个纹理名,并存放在了texture[0]。
③ 接下来便是建立二维RGBA纹理空间的参数:

glTexImage2D(GL_TEXTURE_2D, 0,4, TextureImage[0]->sizeX,
TextureImage[0]->sizeY,0, GL_RGB, GL_UNSIGNED_BYTE,
TextureImage[0]->data);

④ 使用glTexParameteri为纹理映射子程序指定参数;
⑤ 重复①~④,创建六个纹理图案;
⑥ 当一个纹理图案使用完毕之后,需要删除它以释放其在OpenGL的纹理内存占用的空间,通过free(TextureImage[i]->data)来实现;当然最后还得记住释放图像结构,free(TextureImage[i])。
当载入纹理贴图之后,就要将这些图案映射到立方体的表面,首先在每一个表面映射之前启动2D纹理和选择纹理:

glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture[0]);// 选择纹理

在定义立方体的每一个表面都得调用glBegin(GL_QUADS)。二维纹理空间的一个坐标位置可用这个函数来选择:glTexCoord2f(sCoord, tCoord),对纹理空间进行规范化从而可用0.0到1.0范围内的坐标值来指定图案,因而我们可以使用任意纹理坐标值来表示一个表面上的图案:

glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture[0]);// 选择纹理
glBegin(GL_QUADS);
glNormal3f( 0.0F, 0.0F,-1.0F);  
glTexCoord2f(0.0f, 0.0f); glVertex3fv(vertices[1]); // 纹理和四边形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3fv(vertices[0]); // 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3fv(vertices[3]); // 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3fv(vertices[2]); // 纹理和四边形的左上
glEnd();

其中,glNormal3f()用来设置当前的法向量,这个法向量将被应用到紧接下来的glVertex3fv()所定义的顶点上。但是通常各个顶点的法向是各不相同的,所以我们通常在定义每个顶点之前都为它确定一次法向量。
在六个表面都实现纹理贴图时,最后记得:

glDisable(GL_TEXTURE_2D);
glFlush();

4.3 控制
通过鼠标与键盘的一些交互,使应用程序完成相应的任务。
4.3.1 鼠标交互
(1) 鼠标左键
本实验中,当鼠标左键按下时,物体通过捕捉鼠标移动来改变物体显示的角度,并且跟随鼠标移动来调整旋转方向;当松开鼠标左键的时候,物体能够继续按先前速度和方向继续转动。
所以利用glutMouseFunc(myMouse)绑定点击鼠标的函数myMouse,判断当鼠标左键按下的时候调用函数startMotion开始拖动物体,获取的x、y坐标值作为参数传递;判断当鼠标左键松开时,调用函数stopMotion开始旋转物体,获取的x、y坐标值作为参数传递。
设置一个全局变量redrawContinue进行控制物体是否旋转。
对于函数startMotion,获取到鼠标点击位置的坐标值x、y,并将redrawContinue设为false,即物体停止旋转,以待鼠标移动改变物体显示角度。所以当鼠标按下进行拖动的过程中将会调用函数trackball_ptov(x, y, winWidth, winHeight, lastPos)以更换物体显示角度。物体将会跟踪鼠标拖动的方向来旋转,旋转的函数trackball_ptov如下:

void trackball_ptov(int x, int y, int width, int height, float v[3])
{
    float d, a;

    v[0] = (2.0F*x - width) / width;
    v[1] = (height - 2.0F*y) / height;
    d = (float)sqrt(v[0] * v[0] + v[1] * v[1]);
    v[2] = (float)cos((M_PI / 2.0F) * ((d < 1.0F) ? d : 1.0F));
    a = 1.0F / (float)sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
    v[0] *= a;
    v[1] *= a;
    v[2] *= a;
}

通过glutMotionFunc(mouseMotion)绑定鼠标移动函数mouseMotion以获取鼠标移动方向:

/*通过捕捉鼠标移动来改变物体显示的角度*/
void mouseMotion(int x, int y)
{
    float curPos[3], dx, dy, dz;
    trackball_ptov(x, y, winWidth, winHeight, curPos);

    dx = curPos[0] - lastPos[0];
    dy = curPos[1] - lastPos[1];
    dz = curPos[2] - lastPos[2];

    if (dx || dy || dz) {
        angle = 90.0F * sqrt(dx*dx + dy*dy + dz*dz);

        axis[0] = lastPos[1] * curPos[2] - lastPos[2] * curPos[1];
        axis[1] = lastPos[2] * curPos[0] - lastPos[0] * curPos[2];
        axis[2] = lastPos[0] * curPos[1] - lastPos[1] * curPos[0];

        lastPos[0] = curPos[0];
        lastPos[1] = curPos[1];
        lastPos[2] = curPos[2];
    }

    glutPostRedisplay();
}

在鼠标松开的时候,只要将redrawContinue设为true即可让物体沿着刚鼠标拖动方向和速度旋转,因为glutIdleFunc(myidle)在闲置时将会回调函数myidle使物体继续旋转:

/*定义没有指令输入时执行的函数*/
void myidle()
{
    if (redrawContinue == true)
    {
        angle = 0.2;

        glutPostRedisplay();
    }

}

(2) 鼠标中键
当鼠标中键按下时,物体停止转动。很明显,只要将redrawContinue设为false即可停止物体的旋转。当然,按住鼠标中键也可以拖动物体改变显示角度。
(3) 鼠标右键
本实验在点击鼠标右键将会弹出菜单,以供用户选择投影方式——“正交”或“透视”。通过以下代码创建右键菜单,并且添加“正交”和“透视”两个选项:

glutCreateMenu(Draw_menu);   //创建右键菜单
glutAddMenuEntry("Orthographic", 1);
glutAddMenuEntry("Perspective", 2);
glutAttachMenu(GLUT_RIGHT_BUTTON);

当选择正交投影时,使用glMatrixMode(GL_PROJECTION)引入投影模式,并通过glOrtho函数为正交投影指定裁剪窗口和近、远裁剪平面的参数;
当选择透视投影的时候,使用glMatrixMode(GL_PROJECTION)引入投影模式,并通过glFrustum函数为透视投影指定裁剪窗口和近、远裁剪平面的参数;
4.3.2 键盘交互
(1) 键盘0~8
程序通过键盘的0~8键控制球面的细分次数,通过glutKeyboardFunc(KeyFunction)绑定键盘迭代划分球体的回调函数,当用户进行键盘操作的时候,将会通过KeyFunction判断输入的字符,如果输入的是‘q’或‘Q’,则直接退出程序,如果输入的是0~8的数字,程序将会相应的改变细分控制变量n的值,从而调用glutPostRedisplay()以新的划分次数调用函数tetrahedron,从而重新显示球面图形。
(2) 键盘+、-
通过glutSpecialFunc(ControlBlendKeys)绑定盘调节透视度的回调函数,当用户用键盘输入+的时候,将使透明度d增加0.2,相应代码如下:

if(key == GLUT_KEY_UP || key == GLUT_KEY_RIGHT)
    {
        d += 0.2;
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        LoadGLTextures();            //载入纹理贴图
        glCullFace(GL_BACK);         //面的背面剔除
        glEnable(GL_CULL_FACE);      //启用剔除后向面功能
        //glEnable(GL_LIGHT1); //启用一号光源
        //glEnable(GL_LIGHTING); //开光
        glColor4f(1.0f, 1.0f, 1.0f, d);//颜色0.5 alpha值
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //指定混合函数
        glEnable(GL_BLEND);//启用透明
        glEnable(GL_COLOR_MATERIAL);
        glEnable(GL_TEXTURE_2D);//启用纹理2D
    }

当用户用键盘输入-的时候,将使透明度d减小0.2,相应代码如下:

if(key == GLUT_KEY_DOWN || key == GLUT_KEY_LEFT)
    {
        d -= 0.2;
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        LoadGLTextures();            //载入纹理贴图
        glCullFace(GL_BACK);         //面的背面剔除
        glEnable(GL_CULL_FACE);      //启用剔除后向面功能
        //glEnable(GL_LIGHT1); //启用一号光源
        ////glEnable(GL_LIGHTING); //开光
        glColor4f(1.0f, 1.0f, 1.0f, d);//颜色0.5 alpha值
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //指定混合函数
        glEnable(GL_BLEND);//启用透明
        glEnable(GL_COLOR_MATERIAL);
        glEnable(GL_TEXTURE_2D);//启用纹理2D
    }

5. 实验结果
经过物体的建模、渲染和交互设计,我们实现了老师要求的基本功能,包括设计一个能够细分的球面和一个有纹理贴图的立方体;添加光源并设置相应参数,使物体表面颜色混合了光照颜色和纹理贴图颜色;能够进行简单的鼠标与键盘交互:通过鼠标左键进行物体的拖动和旋转方向速度控制;通过鼠标中键控制物体是否旋转;通过鼠标右键选择投影方式;通过键盘0~8键控制球面细分次数;通过键盘+、-控制物体透明度。
以下是程序运行效果图。
细分次数为5的球面,正交投影:
这里写图片描述
细分次数为1的球面,透视投影:
这里写图片描述
透明度效果展示:
这里写图片描述
6. 下载链接
http://download.csdn.net/download/qq_22408539/10186050

猜你喜欢

转载自blog.csdn.net/qq_22408539/article/details/78960942
今日推荐