链接: https://pan.baidu.com/s/1cBTTbbzRCVBCX_H4jf6qMA 提取码: kj8w
一、实验要求和内容
1.1 实验内容
(1)实验描述
实现一个三维图形交互程序,能够读入三维obj文件、绘制并打上光照,并且实现基本的三维交互—包含平移、旋转和缩放。要求能够成功读取发给大家的obj文件模型(bunny.obj),也可以自行增加另外的三维模型文件。此作业要求每人单独完成。
(2)实验环境
在Clion平台下结合OpenGL开发
操作系统:macOS Monterey 12.0 Beta版(21A5248p)
处理器:Apple M1
内存:16.00GB
系统类型:64位操作系统
1.2 实验要求
(1)读入三维Obj文件
设计一定的数据结构,从而实现从实验资料bunny.obj文件中,将三维模型的必要信息进行读取收集。
(2)绘制三维模型并打光
在上述操作后,结合OpenGL的数据接口,绘制三维模型,给场景添加光照,使得最终效果良好。
(3)实现三维交互
实现基本的三维交互,包括平移,旋转,缩放。
二、实验步骤
2.1 数据结构设计
(1)OpenGL绘制要求
三维模型事实上是由许多的三角面拼接而成的,而OpenGL的接口可以绘制小三角面,通过绘制许多三角面,最终拼接成兔子模型。
要绘制三维模型,OpenGL需要知道三维模型每个三角面的顶点的坐标(每个坐标有x、y、z这3个坐标值),以及每个点或者每个面的法向量;读出obj文件中的顶点坐标v,顶点向量值vn,然后根据每个三角形面的点坐标和法向的索引值,把点和法向按照顺序存储在triangleVerts和normals中,再三个点为一组面进行渲染,得到bunny模型。
(2)设计数据结构
设计存储模型的类Imported
class Imported
{
private:
int numVertices{};//存放点的个数
std::vector<glm::vec3> vertices;//存放顶点集
std::vector<glm::vec3> normalVecs;//存放法向集
std::vector<float> vertVals;//存放读入的点的值
std::vector<float> triangleVerts;//存放按面的索引排序后的顶点集
std::vector<float> fnormals;//存放按面的索引排序后的法向集
std::vector<float> normVals;//存放法向的值
public:
Imported();//无参构造
Imported(const char *filePath);//文件名构造,传输本地模型
int getNumVertices() const;//返回顶点个数的函数
std::vector<glm::vec3> getVertices();//返回顶点集vertices的函数
std::vector<glm::vec3> getNormals();//返回法向集normalVecs的函数
};
内置返回顶点个数、顶点集、法向集的函数,Imported利用parseOBJ函数实现获取有顺序意义的点集和法向集,以此来返回正确的顶点个数、顶点集、法向集。
2.2 OBJ文件读取
(1)Bunny.obj文件内容格式
将bunny.obj文件用记事本打开如下(截取部分):
对于该文件内容有如下说明:
v:代表顶点。格式为v、x、y、z,v后面的x、y、z表示三个顶点坐标。
vn:法向量。三角形的三个顶点都要指定法向量。格式为vn,nx、ny、nz。
f: 面。后面的整型值分别是属于这个面的顶点、法向量的索引。
(2)文件读取
在该文件中,顶点数据所在行由“v”字符开头,法向量数据所在行由“vn”字符开头,而面数据所在行由“f”字符开头。由此,根据每行开头字符,可以通过C++文件流将obj文件中不同类型的数据装入特定的数据结构中。
其中对使用的特殊的文件流对象stringstream,通过stringstream对象,我们可以简单的将一行以空格(或其他字符)为分界的字符串进行值分离。在本实验中,以点为例
1.v 0.1102022 0.74011 1.132398
用该对象可以轻松的将三个值读取出来。
然而在读取面时,则先将“//”替换成空格,最后通过子串函数获取相应值。
2.f 6830//6830 10332//10332 15543//15543
2.3 程序模块介绍
(1)读取文件模块
通过Imported读取文件模块,可以将Obj文件的点、法向量、面的数据转移到内存中,通过一定数据结构进行存储。上述内容已经给出了文件模块的具体代码,其主要思想是:先读取模型中的点和点的法向,存储起来,然后将模型的面部分逐行读取,通过面的索引把顶点数据和法向数据按照面的顺序重新储存一遍。
#include <fstream>
#include <sstream>
#include "glm/glm.hpp"
#include "Model.h"
using namespace std;
Imported::Imported() = default;
Imported::Imported(const char *filePath) {
float x, y, z;
ifstream fileStream(filePath, ios::in);
string line;
while (!fileStream.eof()) {
getline(fileStream, line);
if (line.compare(0, 2, "v ") == 0) {
stringstream ss(line.erase(0, 1));
ss >> x; ss >> y; ss >> z;
vertVals.push_back(x);
vertVals.push_back(y);
vertVals.push_back(z);
}
if (line.compare(0, 2, "vn") == 0) {
stringstream ss(line.erase(0, 2));
ss >> x; ss >> y; ss >> z;
normVals.push_back(x);
normVals.push_back(y);
normVals.push_back(z);
}
if (line.compare(0, 2, "f ") == 0) {
string oneCorner, v, t, n;
stringstream ss(line.erase(0, 2));
for (int i = 0; i < 3; i++) {
getline(ss, oneCorner, ' ');
stringstream oneCornerSS(oneCorner);
getline(oneCornerSS, v, '/');//顶点索引值
getline(oneCornerSS, t, '/');//纹理坐标索引值
getline(oneCornerSS, n, '/');//顶点法线索引值
int vertRef = (stoi(v) - 1) * 3; //第v个顶点
int normRef = (stoi(n) - 1) * 3;
triangleVerts.push_back(vertVals[vertRef]);//x
triangleVerts.push_back(vertVals[vertRef + 1]);//y
triangleVerts.push_back(vertVals[vertRef + 2]);//z
fnormals.push_back(normVals[normRef]);//nx
fnormals.push_back(normVals[normRef + 1]);//ny
fnormals.push_back(normVals[normRef + 2]);//nz
}
}
}
numVertices = (int)triangleVerts.size()/3;//获取顶点的个数
for (int i = 0; i < numVertices; i++) {//将点和法向加入点集和法向集
vertices.emplace_back(triangleVerts[i*3], triangleVerts[i*3+1], triangleVerts[i*3+2]);
normalVecs.emplace_back(fnormals[i*3], fnormals[i*3+1], fnormals[i*3+2]);
}
}
//返回类中参数的各个函数
int Imported::getNumVertices() const { return numVertices; }
std::vector<glm::vec3> Imported::getVertices() { return vertices; }
std::vector<glm::vec3> Imported::getNormals() { return normalVecs; }
(2)绘制模型模块
通过绘制模型模块,可以将vector中存储的数据,调用OpenGL的接口,绘制一个个小的三角形面片,最后绘制成三维模型。
(3)函数、参数定义模块
这里再主函数外声明了需要用到的参数和函数等等,比如鼠标和键盘的监听函数等等,以及模型几何变换的平移、缩放、旋转等参数。
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods);
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
//缩放参数
float size= 0.3f;//大小
//平移参数
float front_back = 0;
float front_back2 = 0;
float left_right = 0;
float left_right2 = 2.0;
float up_down = 0;
float up_down2 = 0;
//线框图转换标志
int line_flag = 0;
// camera视角变化类的引入
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;
float deltaTime = 0.0f;
float lastFrame = 0.0f;
(4)主函数初始化模块
主函数开始先进行glfw的初始化。然后建立着色器、调用输入设备监听函数等等,为后面的模型渲染做铺垫。
glfwInit();//初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//OpenGL主版本号 3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//OpenGL副版本号 .3
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//OpenGL模式 OpenGL核心模式
#ifdef __APPLE__//MacOS 下必须的调用
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// glfw window creation
// --------------------
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", nullptr, nullptr);//窗口宽、高、标题
if (window == nullptr)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
//各类主进程函数的调用,比如鼠标和键盘监听等等。
glfwMakeContextCurrent(window);//让当前窗口的环境在当前线程上成为当前环境
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//监听窗口大小变化
glfwSetCursorPosCallback(window, mouse_callback);//鼠标监听
glfwSetScrollCallback(window, scroll_callback);//滚轮
glfwSetKeyCallback(window, key_callback);//键盘监听
// tell GLFW to capture our mouse
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader zprogram
// ------------------------------------
Shader lightingShader("/Users/bloodsvery/Desktop/学习/大二下/计算机图形学/Curriculumdesign1/1.colors.vert", "/Users/bloodsvery/Desktop/学习/大二下/计算机图形学/Curriculumdesign1/1.colors.frag");
(5)几何变换以及光源设置模块
该模块设定好缩放平移旋转、视角移动的变换矩阵,以及光照模型需要的参数向量,利用定义好的set方法传入着色器lightingShader中。
glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
auto timeControl = (float)glfwGetTime();//定义timeControl系统时间函数
float radius = 10.0f;
float camX = sin(timeControl) * radius;
float camZ = cos(timeControl) * radius;//设置X、Z轴随时间的三角函数,实现光源的旋转
glm::vec3 lightPos(camX, 0.0f, camZ);//设置光源三维向量的旋转
glm::vec3 lightPos_pro(-camX, 0.0f, -camZ);//设置光源三维向量的旋转
lightingShader.setVec3("objectColor", 0.788f, 0.459f, 0.525f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
lightingShader.setVec3("lightPos", lightPos);
lightingShader.setVec3("lightPos_pro", lightPos_pro);
lightingShader.setVec3("viewPos", camera.Position);
// view/projection transformations
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
lightingShader.setMat4("projection", projection);
lightingShader.setMat4("view", view);
// world transformation
glm::mat4 model = glm::mat4(1.0f);
//缩放
model = glm::scale(model, glm::vec3(size,size,size));
//平移
model = glm::translate(model, glm::vec3(left_right, up_down, front_back));
//旋转
lightingShader.setMat4("model", model);
glBindVertexArray(vao[0]);
glDrawArrays(GL_TRIANGLES, 0, myModel.getNumVertices());
glm::mat4 projection2 = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.3f, 100.0f);
glm::mat4 view2 = camera.GetViewMatrix();
lightingShader.setMat4("projection2", projection2);
lightingShader.setMat4("view2", view2);
// world transformation
glm::mat4 model2 = glm::mat4(3.0f);
//缩放
model2 = glm::scale(model2, glm::vec3(size,size,size));
//平移
model2 = glm::translate(model2, glm::vec3(left_right2, up_down2, front_back2));
//旋转
lightingShader.setMat4("model", model2);
(6)视角变化监听模块
鼠标移动可以视点不变,操控视角变化。
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
if (firstMouse)
{
lastX = (float)xpos;
lastY = (float)ypos;
firstMouse = false;
}
float xoffset = (float)xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
lastX = (float)xpos;
lastY = (float)ypos;
camera.ProcessMouseMovement(xoffset, yoffset);
}
键盘的WSADQE可以控制视点的左、右、前、后、上、下六个方位变化,封装在processInput函数中,这样就可以控制视角和视点的变化了。
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
camera.ProcessKeyboard(FORWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
camera.ProcessKeyboard(BACKWARD, deltaTime);
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
camera.ProcessKeyboard(LEFT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
camera.ProcessKeyboard(RIGHT, deltaTime);
if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS)
camera.ProcessKeyboard(UP, deltaTime);
if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS)
camera.ProcessKeyboard(DOWN, deltaTime);
}
(7)几何变换监听模块
该模块主要负责键盘事件控制几何变换,以及线框图与面图转换的监听。
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
if (key == GLFW_KEY_Z) size += 0.03;//模型放大
if (key == GLFW_KEY_X) size -= 0.03;//模型缩小
if (key == GLFW_KEY_F) front_back += 0.3;//模型前移
if (key == GLFW_KEY_G) front_back -= 0.3;//模型后移
if (key == GLFW_KEY_J) left_right += 0.3;//模型右移
if (key == GLFW_KEY_H) left_right -= 0.3;//模型左移
if (key == GLFW_KEY_K) up_down += 0.3;//模型上移
if (key == GLFW_KEY_L) up_down -= 0.3;//模型下移
if (key == GLFW_KEY_C) line_flag = 0;//转换为面图
if (key == GLFW_KEY_V) line_flag = 1;//转换为线框图
}
(8)着色器模块
其中lightPos和lightPos_pro是主函数中定义的可以随系统时间旋转的两个光源的位置向量。
1.colors.vert
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
gl_Position = projection * view * vec4(FragPos, 1.0);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
1.colors.frag
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 FragPos;
uniform vec3 lightPos;
uniform vec3 lightPos_pro;
uniform vec3 lightColor;
uniform vec3 viewPos;
uniform vec3 objectColor;
void main()
{
// ambient
float ambientStrength = 0.12;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);//入射方向
vec3 lightDir_pro = normalize(lightPos_pro - FragPos);//入射方向
float diff = max(dot(norm, lightDir), 0.0);
float diff_pro = max(dot(norm, lightDir_pro), 0.0);
vec3 diffuse = diff * lightColor;
vec3 diffuse_pro = diff_pro * lightColor;
// specular
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
vec3 reflectDir_pro = reflect(-lightDir_pro, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 128);
float spec_pro = pow(max(dot(viewDir, reflectDir_pro), 0.0), 128);
vec3 specular = specularStrength * lightColor * spec;
vec3 specular_pro = specularStrength * lightColor * spec_pro;
vec3 result = (ambient + diffuse + specular + diffuse_pro + specular_pro) * objectColor;
FragColor = vec4(result, 1.0);
}
(9)纹理模块
三、实验结果展示与分析
(1)功能整体概述
本次实验实现了双光照模型,通过两个光源旋转对bunny和shuttle进行打光,程序也可以实现对模型的线框转换、旋转、平移、缩放操作,占用22个键盘快捷键,实现对模型几何状态的完全操控。
(2)截图展示
初始模型
视角变换
前后平移
上下平移
左右平移
模型缩放
线框转换
四、实验体会
这次3D图形的课程设计,我成功地用OpenGL的GLFW绘制出了三维的兔子模型以及航天飞机模型,这也加深了我对三角形面片构成三维模型的理解和认识。同时,这也让我复习了C++的文件流输入输出和字符串处理,也对obj文件也有了一定的认识。
实验过程中,我遇到了很多各种各样的问题,比如如何同时存放两个模型,依照之前的太阳系实验代码,成功做出来了。以及模型读取那里卡了很久,后来完成了还是很有成就感的。
唯一可惜的就是我使用的是现成的vn,并没有通过计算来实现。
这次实验使我对图形学的兴趣更加浓厚,做出完整的模型也使我充满了自豪感,今后的学习中我会更加努力。