OpenGL课程设计 三维图形交互程序 bunny兔+飞机模型

链接: 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,并没有通过计算来实现。
这次实验使我对图形学的兴趣更加浓厚,做出完整的模型也使我充满了自豪感,今后的学习中我会更加努力。

  • 10
    点赞
  • 84
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 好的,我可以提供一些参考,你可以使用Three.js这个JavaScript库来创建一个基于WebGL(一种图渲染API)的3D轮播图框架。Three.js提供了许多API来创建3D场景、模型、材质等,并且也有许多现成的轮播图组件可以使用。你可以先了解一下Three.js的基本用法和概念,再尝试用Three.js创建一个基本的3D场景和模型,最后将轮播图组件集成进去。希望能对你有所帮助。 ### 回答2: 当然可以帮您设计一个简单的3D轮播图框架。首先,我们需要引入所需的库和技术。 第一步是引入HTML结构。我们可以创建一个包含图片和控制按钮的容器。每个图片都有一个唯一的标识符,以便我们可以在轮播过程中进行切换。 第二步是使用CSS来布局和样式化轮播图容器。我们可以使用CSS3的3D转换来创建3D旋转效果。通过设置透视投影,我们可以调整旋转的视觉效果。我们还可以添加过渡效果和动画来实现平滑的外观。 第三步是使用JavaScript来实现轮播图的功能。我们可以首先获取容器和图像对象,并将其存储在变量中。然后,我们可以通过更改元素的样式属性来实现旋转和动画效果。我们还可以添加事件监听器来处理按钮点击事件并切换到下一张或上一张图片。 最后,我们需要调用一个初始化函数,该函数将设置初始状态和开始轮播。我们可以设置一个定时器,每隔一段时间切换到下一张图片。还可以添加其他选项,例如自动播放的暂停和重新开始功能。 综上所述,以上是一个简单的3D轮播图框架的大致实现步骤。根据您的需求和技术水平,您可以进一步扩展和改进该框架,以实现更多高级功能和自定义选项。祝您成功实现您的3D轮播图框架! ### 回答3: 要帮你写一个3D轮播图框架,首先需要明确该框架的功能和设计思路。一个基本的3D轮播图框架应该能够展示多张图片,并提供前进、后退和自动播放等功能。以下是一个简单的示例: 首先,我们需要一个HTML容器来放置轮播图,例如一个div元素,并为其设置一个适当的宽度和高度。然后,我们可以使用CSS来创建一个适合的3D转换效果,比如使用transform属性进行旋转或平移。 接下来,我们需要一组图片元素,可以放在一个ul元素中,并使用CSS样式来隐藏除第一张以外的图片,只显示当前展示的图片。 在JavaScript中,我们需要编写代码来处理用户的交互和自动播放功能。我们可以为前进和后退按钮添加事件监听器,当点击按钮时,通过更改ul元素的transform属性值来实现切换图片。同时,我们可以设置一个计时器,每隔一段时间自动进行图片切换,以达到自动播放的效果。 另外,可以为轮播图添加一些额外的功能,比如导航指示器,显示当前图片的索引,或者添加动画过渡效果等等。具体实现方式可以根据个人需求和技术水平进行调整。 在编写轮播图框架时,可以考虑使用现有的JavaScript库或框架来简化开发过程,比如jQuery等。这些工具可以提供丰富的功能和文档支持,帮助我们更快速地完成轮播图的实现。 总结来说,编写一个3D轮播图框架需要确定功能和设计思路,通过HTML、CSS和JavaScript来实现图片展示、交互和自动播放等功能,并可以借助现有的工具来简化开发过程。希望这个简单的指南对你有帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值