中山大学数据科学与计算机学院本科生实验报告
(2019年春季学期)
课程名称 | PAOGD | 任课老师 | 郑贵锋 |
---|---|---|---|
年级 | 16 | 专业(方向) | 软件工程(计算机应用方向) |
学号 | 16340132 | 姓名 | 梁颖霖 |
电话 | 13680473185 | dic0k@qq.com | |
开始日期 | 2019/4/27 | 完成日期 | 2019/5/2 |
一、实验题目
HW3 OpenGL初步
二、实现内容
开发环境要求
- Ubuntu 16.04
- OpenGL 3.3
- GLFW
- GLAD
安装教程
apt-get install g++ cmake git
apt-get install libsoil-dev libglm-dev libassimp-dev libglew-dev libglfw3-dev libxinerama-dev libxcursor-dev libxi-dev
git clone https://github.com/JoeyDeVries/LearnOpenGL.git
cd LearnOpenGL
mkdir build
cd build
cmake ..
make -j8
编译完成后,各章节Demo可执行文件会在build/bin 目录下
任务
- 在model loading的Demo中, 示范了如何在OpenGL中导入一个.obj文件
- 运行范例程序,我们会看到如下画面.同时,我们可以利用鼠标和键盘(WASD),控制镜头的移动
- 试编写一个程序,实现模型自身的旋转,平移,缩放
- 功能需求:
- 按下J键, 模型随着时间绕着自身Z轴旋转
- 按下K键, 模型沿着水平方向往复移动
- 按下L键, 模型在一定范围内不断放大缩小
- 功能需求:
- 注意
- 不允许提交所有的范例程序,请务必新建工程并重新编写CMakeList.txt
- 确保在你的个人目录下,代码能够独立地编译和执行
- 确保只保留必要的代码和资源等文件(可以复用范例代码中的文件)
- 不要上传build目录下的内容
- 报告要求
- 你是如何利用CMake生成你的可执行程序的
- .obj格式的文件是怎么保存模型信息的
- OpenGL与Blender的联系?
- OpenGL中的坐标系统是怎么样的
- 你是如何实现功能需求的(简述思路)
- 你的工程中包含了哪些文件,这些文件的作用是什么
三、实验结果
1. 你是如何利用CMake生成你的可执行程序的
-
下载安装CMake
-
打开到文件的目录下,这里我以加载模型的demo作为尝试
-
查看并重新编写CMakeList.txt,修改成编译单个工程的
-
在vs17中打开vs解决方案,编译完成后,生成了bin文件夹,里面就放着我们所运行的demo可执行文件
-
这里提示缺少assimp.dll,故需要下载assimp源码来生成一下。这里同样用到cmake生成build文件,再在vs17中编译生成。
-
执行exe文件即可,查看demo的运作
2. .obj格式的文件是怎么保存模型信息的
obj文件是3D模型文件格式。OBJ文件是一种文本文件,可以直接打开进行查看和编辑修改。
obj格式有4种数据,分别以一下字母开头:
- v 顶点
- 格式:v x y z
- 意义:每个顶点的坐标
- vt 纹理坐标
- 格式:vt u v w
- 意义:绘制模型的三角面片时,每个顶点取像素点时对应的纹理图片上的坐标。纹理图片的坐标指的是,纹理图片如果被放在屏幕上显示时,以屏幕左下角为原点的坐标。
- 注意:w一般用于形容三维纹理,大部分是用不到的,基本都为0。
- vn 顶点法向量
- 格式:vn x y z
- 意义:绘制模型三角面片时,需要确定三角面片的朝向,整个面的朝向,是由构成每个面的顶点对应的顶点法向量的做矢量和决定的(xyz的坐标分别相加再除以3得到的)。
- f 面
- 格式 :f v/vt/vn v/vt/vn v/vt/vn(f 顶点索引 / 纹理坐标索引 / 顶点法向量索引)
- 意义:绘制三角面片的依据,每个三角面片由三个f构成,由f可以确定顶点、顶点的对应的纹理坐标(提取纹理图片对应该坐标的像素点)、通过三个顶点对应的顶点法向量可以确定三角面的方向。
3. OpenGL与Blender的联系?
这里,我们使用Blender所制造的模型导出成.obj文件,再在OpenGL中来控制这个模型的动作逻辑等。Blender偏向于可视化构造模型动画,而OpenGL则是用编码的手段来控制模型的动作,两者之间可以互相转换。
- Blender是跨平台支持,它基于 OpenGL 的图形界面在任何平台上都是一样的。
- OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口。
4. OpenGL中的坐标系统是怎么样的?
我们的顶点坐标起始于局部空间(Local Space),在这里它称为局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)的形式结束。
- 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
- 下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
- 接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
- 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
- 最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
局部空间
局部空间是指物体所在的坐标空间,即对象最开始所在的地方。想象你在一个建模软件(比如说Blender)中创建了一个立方体。你创建的立方体的原点有可能位于(0, 0, 0),即便它有可能最后在程序中处于完全不同的位置。甚至有可能你创建的所有模型都以(0, 0, 0)为初始位置。所以,你的模型的所有顶点都是在局部空间中:它们相对于你的物体来说都是局部的。
世界空间
我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。如果你希望将物体分散在世界上摆放(特别是非常真实的那样),这就是你希望物体变换到的空间。物体的坐标将会从局部变换到世界空间;该变换是由模型矩阵(Model Matrix)实现的。
观察空间
观察空间经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)里,它被用来将世界坐标变换到观察空间。
裁剪空间
在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。因为将所有可见的坐标都指定在-1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它变换回标准化设备坐标系,就像OpenGL期望的那样。
5. 你是如何实现功能需求的(简述思路)
- 按下J键, 模型随着时间绕着自身Z轴旋转
- 按下K键, 模型沿着水平方向往复移动
- 按下L键, 模型在一定范围内不断放大缩小
OpenGL变换实际上是通过矩阵乘法来实现。不管是移动、旋转还是缩放大小,都是通过在当前矩阵的基础上乘以一个新的矩阵来达到目的。首先通过glMatrixMode()方法设置投影矩阵和模型观察矩阵,而后面的绘制过程,就是在模型矩阵中产生不同的点,形成各种图形,然后通过选择的投影矩阵投影到屏幕上,被我们观察到。所以对图形的各种操作也能够看成是对坐标系的变换。
a. 获取键盘的输入来进行处理
在main函数中,glfwGetKey()
来进行获取
GLFW_PRESS表示当按键按下即进入if判断条件
if (glfwGetKey(window, GLFW_KEY_J) == GLFW_PRESS) {
// do sth
}
else if (glfwGetKey(window, GLFW_KEY_K) == GLFW_PRESS) {
// do sth
}
else if (glfwGetKey(window, GLFW_KEY_L) == GLFW_PRESS) {
// do sth
}
b. 在原矩阵的基础上进行变换
-
绕z轴旋转
这里利用的是glm::rotate(),
model是初始的单位矩阵,来对其做变换,可以绕x,y,z轴来进行变换,第二个参数是旋转的角度。
这里我根据时间来对旋转的角度来做增减操作,从而控制模型的旋转。
if (glfwGetKey(window, GLFW_KEY_J) == GLFW_PRESS) { int count = 1; while (!glfwWindowShouldClose(window)) { glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, glm::vec3(0.0f, -1.25f, 0.0f)); model = glm::scale(model, glm::vec3(0.10f, 0.10f, 0.10f)); // 计数 count = (count + 1)%370; Sleep(500); printf("%f\n", 1.0f * count); model = glm::rotate(model, 1.0f * count, glm::vec3(0.0f, 1.0f, 0.0f)); ourShader.setMat4("model", model); ourModel.Draw(ourShader); glfwSwapBuffers(window); glfwPollEvents(); // clear glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) break; } }
-
沿着水平方向往复移动
这里利用的是glm::translate(),
model是初始的单位矩阵,来对其做变换,根据后面的向量来进行平移变换。
这里我根据时间来对x坐标来做增减操作,从而控制模型的移动
else if (glfwGetKey(window, GLFW_KEY_K) == GLFW_PRESS) { float length = 0; int flag = 1; while (!glfwWindowShouldClose(window)) { if (length > 1.4) { flag = -1; } if (length < -1.4) { flag = 1; } length += flag * 0.1f; Sleep(500); printf("%f\n", length); glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, glm::vec3(length, -1.25f, 0.0f)); model = glm::scale(model, glm::vec3(0.1f, 0.1f, 0.1f)); ourShader.setMat4("model", model); ourModel.Draw(ourShader); glfwSwapBuffers(window); glfwPollEvents(); // clear glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) break; } }
-
在一定范围内不断放大缩小
这里利用的是glm::scale(),
model是初始的单位矩阵,来对其做变换,根据后面的向量来进行大小变换。
这里我根据时间来对放大系数来做增减操作,从而控制模型的缩放
else if (glfwGetKey(window, GLFW_KEY_L) == GLFW_PRESS) { float scale_q = 0.1; int flag = 1; while (!glfwWindowShouldClose(window)) { if (scale_q >= 0.15) { flag = -1; } if (scale_q <= 0.05) { flag = 1; } scale_q += flag * 0.01f; Sleep(500); printf("%f\n", scale_q); glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, glm::vec3(0.0f, -1.25f, 0.0f)); model = glm::scale(model, glm::vec3(scale_q, scale_q, scale_q)); ourShader.setMat4("model", model); ourModel.Draw(ourShader); glfwSwapBuffers(window); glfwPollEvents(); // clear glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) break; } }
c. 变换矩阵后,变换模型shader
ourShader.setMat4("model", model);
ourModel.Draw(ourShader);
glfwSwapBuffers(window);
glfwPollEvents();
d. 由于是连续运动,需要清除前面的动画
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
e. 添加退出键
为了方便我们的操作调试,我加入了Q键来中止上述的动画
if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) break;
效果截图
- 旋转
- 平移运动
- 缩小放大
6. 你的工程中包含了哪些文件,这些文件的作用是什么
- bin 可执行文件,里面放的是.exe运行文件
- configuration 我的vs用户配置文件
- dlls 存放动态链接库的,里面放的是assimp.dll,用于读取模型
- lib 编译所用到的库文件
- includes 存放头文件
- resources 存放资源,包括模型与蒙皮等
- src 存放代码源文件,里面保护main函数
- CMakeLists.txt 用于camke生成build文件